线上 Demo: flow.houmq.cn/
前言
DevFlow Harness 是我打算长期做的一个 Web 研发工作台——PM 写需求、前后端改代码、QA 点测,都在浏览器里完成,不是 IDE 插件。参考了 Pi Agent 的 Session + Tools,以及 DeepSeek Harness 的插件化思路。
M1 还没上插件、Agent、文件工具这些。这次就先把 能聊天的 Web 壳子 搭出来,并且 部署到公网,后面所有能力都往这个壳子里挂。
具体要完成这几件事:
- FastAPI 后端:
/health、/chat、/chat/stream(SSE 流式) - React 前端:流式打字机、Markdown 渲染、停止生成、清空对话
- 对话历史存
localStorage,刷新不丢 - Docker 双容器 + GitHub Actions 自动部署到
flow.houmq.cn
一、技术选型
| 层面 | 技术 | 选择理由 |
|---|---|---|
| 后端 | FastAPI + OpenAI SDK | Python AI 生态顺手,自带 OpenAPI,M1 体量够轻 |
| 前端 | React 18 + TypeScript + Vite | 我日常主力栈,热更新快 |
| 流式协议 | SSE | AI 回复是单向流,HTTP 就能搞定,调试也方便 |
| Markdown | react-markdown | assistant 消息直接渲染,不用自己写 parser |
| 本地联调 | Vite proxy | 前端始终请求 /api/*,和生产路径一致 |
| 部署 | Docker Compose + Nginx | 前后端各一个容器,本地和生产同一套 compose |
| 模型 | OpenAI 兼容 API | DeepSeek / OpenAI 换 .env 就行 |
M1 没做用户登录、没做数据库存会话——那些放到 M8 角色协作再说。现在对话历史只存浏览器本地。
二、项目架构
devflow-harness/
├── backend/
│ ├── main.py # FastAPI:/health、/chat、/chat/stream
│ ├── requirements.txt
│ ├── Dockerfile.server
│ └── .env.example
├── frontend/
│ ├── src/
│ │ ├── pages/ChatPage.tsx # 聊天页(消息列表 + 输入框 + 停止/清空)
│ │ ├── lib/
│ │ │ ├── streamChat.ts # fetch + ReadableStream 读 SSE
│ │ │ └── storage.ts # localStorage 读写
│ │ └── types/chat.ts
│ ├── nginx.conf # 容器内:/api/ 反代 backend
│ ├── vite.config.ts # 开发态 /api proxy
│ └── Dockerfile.server
├── deploy/
│ ├── nginx-flow.houmq.cn.conf # 主机 Nginx → 127.0.0.1:8082
│ └── README.md
├── docker-compose.yml # 本地 / 生产通用
├── docker-compose.server.yml # 服务器用(只拉镜像不 build)
├── .github/workflows/deploy.yml
└── docs/architecture.md
本地开发: 浏览器 :5173 → Vite 把 /api 转到 FastAPI :8000 → LLM API
生产: 浏览器 → 主机 Nginx :80 → frontend 容器 :8082 → 容器内 Nginx 把 /api/ 转到 backend :8000
三、核心功能实现
3.1 后端:SSE 流式接口
/chat/stream 接收 message + history,调 OpenAI SDK 的 stream=True,逐 chunk 往外推。
OpenAI Python SDK 的 stream 返回的是 同步迭代器,所以我用普通的 def generate() 包一层,没有硬上 async——混用 async/sync 容易出 chunk 缓冲,字一块一块蹦出来。
# backend/main.py
def generate() -> Iterator[str]:
for chunk in stream:
delta = chunk.choices[0].delta.content
if delta:
yield f"data: {json.dumps({'content': delta}, ensure_ascii=False)}\n\n"
yield "data: [DONE]\n\n"
return StreamingResponse(
generate(),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"X-Accel-Buffering": "no", # 告诉 Nginx 别缓冲
},
)
/chat 是非流式接口,调试用;线上主要走 /chat/stream。
3.2 前端:fetch 读 SSE,不用 EventSource
EventSource 只能 GET,带不了 history 的 POST body。所以前端用 fetch('/api/chat/stream') + response.body.getReader(),自己按 \n\n 切 SSE 帧。
// frontend/src/lib/streamChat.ts
const response = await fetch('/api/chat/stream', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message, history }),
signal, // 绑 AbortController,用于停止生成
})
const reader = response.body.getReader()
// ... 按 data: {...}\n\n 解析,onChunk 增量更新 UI
Vite 开发态的 proxy 配置:
// frontend/vite.config.ts
proxy: {
'/api': {
target: 'http://127.0.0.1:8000',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
},
},
前端请求 /api/chat/stream,到后端实际是 /chat/stream。
3.3 聊天页:流式渲染 + 停止 + 持久化
ChatPage.tsx 里,用户发送后立刻插入一条空的 assistant 消息,每收到 chunk 就 append 到这条消息的 content 上,配合 react-markdown 实时渲染。
停止生成:AbortController 绑到 fetch 的 signal,点「停止」就 abort()。catch 到 AbortError 时 保留已经输出的内容,不 rollback。
持久化:useEffect 监听 messages 变化,写入 localStorage(key: devflow-harness-m1-chat)。M1 不做服务端存储,够用了。
四、Docker 容器化部署
4.1 双容器结构
# docker-compose.yml(节选)
services:
backend:
build: ./backend
expose: ["8000"]
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; ..."]
frontend:
build: ./frontend
ports: ["8082:80"]
depends_on:
backend:
condition: service_healthy
frontend 镜像多阶段构建:Node 打包 → Nginx 托管静态文件 + 反代 API。
4.2 Nginx 两处配置
容器内(frontend/nginx.conf)—— /api/ 转发到 backend:
location /api/ {
proxy_pass http://backend:8000/; # 注意末尾斜杠,/api/chat → /chat
proxy_buffering off;
proxy_read_timeout 3600s;
}
主机上(deploy/nginx-flow.houmq.cn.conf)—— 域名指到容器:
location / {
proxy_pass http://127.0.0.1:8082;
proxy_buffering off;
proxy_read_timeout 3600s;
}
SSE 流式如果一坨一坨出来,先查 proxy_buffering off 有没有开——主机 Nginx 和容器 Nginx 都要关。
4.3 GitHub Actions 自动部署
main push 触发:
- Ubuntu runner 构建 backend / frontend 镜像,打成 tar 包
- Mac self-hosted runner 加载镜像,SSH 到服务器
- 同步
docker-compose.server.yml,执行docker compose up -d --no-build
服务器目录:/home/ubuntu/devflow-harness,.env 里配好 OPENAI_API_KEY、OPENAI_BASE_URL、MODEL。
本地一键:
cp .env.docker.example .env
chmod +x scripts/docker-deploy.sh
./scripts/docker-deploy.sh
# → http://localhost:8082
五、项目成果
功能清单
- ✅ FastAPI 后端
/health、/chat、/chat/stream - ✅ React 流式打字机 + Markdown 渲染
- ✅ 停止生成(AbortController)
- ✅ 清空对话
- ✅ localStorage 持久化
- ✅ Docker 双容器 + docker-compose
- ✅ GitHub Actions 自动部署
- ✅ 公网 Demo:flow.houmq.cn/
技术亮点
- 开发 / 生产 API 路径统一 — 都走
/api/*,Vite proxy 和容器 Nginx 各管一段 - SSE 而非 WebSocket — M1 单向流够用,WebSocket 留给后面 QA 实时事件
- 同步 generate 包 SSE — 跟着 OpenAI SDK 的同步 stream 走,少踩 async 坑
- 双 Nginx + 关 buffering — 公网流式不卡顿的关键配置
项目截图
六、下一里程碑计划(M2)
M1 把聊天壳子和部署跑通了。M2 开始往 Harness 内核长肉:
- Session 工作区 — 一个项目一个 Session,不再只有单页聊天
- Agent Loop — 多轮推理循环
- 读 / 写文件 — Agent 第一次能碰磁盘
插件协议 deliberately 往后放(M9),等工具都跑真了再收编,避免先搭空框架。详见仓库 docs/roadmap.md。
七、总结
M1 看起来就是个 ChatGPT 网页,但对我这个项目的意义是:前后端 + SSE + Docker + CI/CD + 公网域名 整条线通了。后面加 Session、加文件工具、加预览 iframe,都是在已有壳子上叠功能,不用重新折腾部署。
最大的收获是搞清楚了 SSE 在 Nginx 双层反代下怎么才不缓冲——X-Accel-Buffering: no 和 proxy_buffering off 两处都得配。
教训是 M1 功能砍得够狠才好上线:没登录、没数据库、没插件,但一周之内就能有个能用的公网 Demo,比一上来设计 Plugin Registry 实用多了。
附录:项目地址
- GitHub:github.com/MingQi39/de…
- 线上 Demo:flow.houmq.cn/
- 架构文档:仓库内
docs/architecture.md - 部署文档:仓库内
deploy/README.md