core(§7 Phase G G1): Web UI 脚手架 + cli.py web 子命令
- web/ 新包:app.py FastAPI 工厂(/ + /healthz + /static),Jinja2 base.html / home.html,minimal style.css。HTMX + HTMX-SSE 走 CDN (无 node 链路,与 §5 Less Scaffolding 一致)。 - cli.py 加 web --host --port --reload 子命令,默认 127.0.0.1:8765, 本地形态 sentinel user 无 auth(Phase D 才上 OIDC)。 - requirements: fastapi / uvicorn[standard] / jinja2 / python-multipart (multipart 为 G5 文件上传留)。 - Starlette 新签名踩坑:TemplateResponse(request, name, context), 旧式塞 context 里会让 jinja 用 dict 当 cache key 炸 unhashable,记 RUN.md 故障兜底。 - Linux portability:模板 path 显示约定 .as_posix();SSE 头 G4 上时 带 X-Accel-Buffering: no(nginx 反代友好)。`cli.py web` 在 .venv/Scripts/python.exe(Win)/ .venv/bin/python(Linux)走同一路径。 Smoke 四路径(in-process via Starlette TestClient)全绿:/healthz → "ok" / / → 1063B(title + static + version)/ /static/style.css → 1624B / /nonexistent → 404。`cli.py web --help` 子命令注册 OK。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
e8dbfa57a5
commit
91202b6172
26
PROGRESS.md
26
PROGRESS.md
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
> 配合 `DESIGN.md`。本文件只记 phase 状态、决策偏差、文件量、下一步。
|
> 配合 `DESIGN.md`。本文件只记 phase 状态、决策偏差、文件量、下一步。
|
||||||
|
|
||||||
最后更新:2026-05-14(Step 6)
|
最后更新:2026-05-14(Phase G G1)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
| 5 | Eval Suite | ⏸ 不做 | dogfooding 替代,probe 覆盖健康检查 |
|
| 5 | Eval Suite | ⏸ 不做 | dogfooding 替代,probe 覆盖健康检查 |
|
||||||
| 6 | 长任务工程化 | 🟡 | task + 恢复 ✅;双层记忆 ✅;context 压缩未做 |
|
| 6 | 长任务工程化 | 🟡 | task + 恢复 ✅;双层记忆 ✅;context 压缩未做 |
|
||||||
| 7 | 打磨 | ❌ | Docker 沙盒 / 更多 skill |
|
| 7 | 打磨 | ❌ | Docker 沙盒 / 更多 skill |
|
||||||
| §7 SaaS | DESIGN §7 路线 | 🟡 | A 事件流化 ✅;B 完工(Step 1 基建 ✅;Step 2 Session ORM ✅;Step 3 TaskState ORM ✅;Step 4 task_dir 双形态 ✅;Step 6 no-subtask 校验 ✅;Step 5 migrate-from-fs 取消)。下一阶 C(Executor + sandbox)或 Phase G(Web UI)。 |
|
| §7 SaaS | DESIGN §7 路线 | 🟡 | A 事件流化 ✅;B 完工;**Phase G Web UI 进行中(G1 脚手架 ✅;G2 task list 待;G3 chat 只读;G4 SSE 流式;G5 文件浏览;G6 打磨)**。下一阶 C(Executor) / D(HTTP /v1) 待。 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -34,6 +34,7 @@
|
||||||
- **05-14 / §7 B Step 3 TaskState ORM**:`core/task.py` 重写,TaskState dataclass 保留为内存 DTO 但落地走 PG —— `save()` 调 `upsert_task`(INSERT ON CONFLICT DO UPDATE,显式 set `updated_at=func.now()`),`load(task_id)` 走 SELECT;**字段去掉 `cwd`**(改读 task_dir,§7 SaaS task_dir-as-identity)。`state.json` 文件**全面废除**,task_dir 只承担 skill 产物。`core/storage/utils.py` 加 `upsert_task` / `update_task` 工具。`main.py::sync_task_tokens` 改 `update_task(tokens_p,tokens_c)` 单字段 UPDATE(ORM-level update 自带 onupdate=func.now())。`core/session.py::Session.append` 的 ensure 调用补传 `mode/description/reasoning_effort`,避免首次 INSERT 后 _list_task_rows 看到空 meta。`cli.py` 全字段从 ORM Task 列读;`_cleanup_if_empty` 去 state.json 特例(任何 FS 文件 / 子目录都算实质痕迹);`/done /abandon /desc` 走 PG。`core/export_docx.py` meta 改从 `TaskState.load(tid)` 读(asdict 拿到 dict),去 CWD 字段。端到端 smoke:storage UPSERT/UPDATE round-trip + build_agent 懒创建 + Session.append 自动 INSERT 完整 meta + sync_task_tokens 局部 UPDATE + task_state.save UPSERT 保留 task_dir/tokens + export → .docx 37KB 全绿。
|
- **05-14 / §7 B Step 3 TaskState ORM**:`core/task.py` 重写,TaskState dataclass 保留为内存 DTO 但落地走 PG —— `save()` 调 `upsert_task`(INSERT ON CONFLICT DO UPDATE,显式 set `updated_at=func.now()`),`load(task_id)` 走 SELECT;**字段去掉 `cwd`**(改读 task_dir,§7 SaaS task_dir-as-identity)。`state.json` 文件**全面废除**,task_dir 只承担 skill 产物。`core/storage/utils.py` 加 `upsert_task` / `update_task` 工具。`main.py::sync_task_tokens` 改 `update_task(tokens_p,tokens_c)` 单字段 UPDATE(ORM-level update 自带 onupdate=func.now())。`core/session.py::Session.append` 的 ensure 调用补传 `mode/description/reasoning_effort`,避免首次 INSERT 后 _list_task_rows 看到空 meta。`cli.py` 全字段从 ORM Task 列读;`_cleanup_if_empty` 去 state.json 特例(任何 FS 文件 / 子目录都算实质痕迹);`/done /abandon /desc` 走 PG。`core/export_docx.py` meta 改从 `TaskState.load(tid)` 读(asdict 拿到 dict),去 CWD 字段。端到端 smoke:storage UPSERT/UPDATE round-trip + build_agent 懒创建 + Session.append 自动 INSERT 完整 meta + sync_task_tokens 局部 UPDATE + task_state.save UPSERT 保留 task_dir/tokens + export → .docx 37KB 全绿。
|
||||||
- **05-14 / §7 B Step 4 task_dir 双形态**:CLI `chat --task-dir <path>` 支持用户显式指定项目目录(§7.1 task-primary + dir 副视图心智模型落地)—— 留空走默认派生 `workspace/tasks/<uuid>/`,显式走用户路径(绝对或相对 cwd,Path.resolve())。`main.py::resolve_task_id` 增 `task_dir_arg`;resume 时从 PG `tasks.task_dir` 读(`SELECT task_dir WHERE task_id=?`),空则降级默认派生。新增 `is_managed_task_dir(td, ws)` 判断是否在 `workspace/tasks/<uuid>/` 模板下,作 `_cleanup_if_empty` 保护开关 —— 用户自指定的项目目录**绝不 rmtree**(可能含用户已有文件);DB 行该删还是删。`core/export_docx.py::export_chat_to_docx` 重构:task_id 升一等参数(从 `task_dir.name` 提取改入参传入),task_dir 留空时自动从 PG 读,支持用户目录(非 UUID 命名)正常导出。cli `/export` 与 `cli.py export` 子命令均改走 `_resolve_uuid_or_prefix` + task_id 直传。Smoke 4 路径全绿:default-derived(managed=True, cleanup rmtree)/ --task-dir(managed=False, FS preserved)/ resume reads DB / export 自动 PG 查路径。
|
- **05-14 / §7 B Step 4 task_dir 双形态**:CLI `chat --task-dir <path>` 支持用户显式指定项目目录(§7.1 task-primary + dir 副视图心智模型落地)—— 留空走默认派生 `workspace/tasks/<uuid>/`,显式走用户路径(绝对或相对 cwd,Path.resolve())。`main.py::resolve_task_id` 增 `task_dir_arg`;resume 时从 PG `tasks.task_dir` 读(`SELECT task_dir WHERE task_id=?`),空则降级默认派生。新增 `is_managed_task_dir(td, ws)` 判断是否在 `workspace/tasks/<uuid>/` 模板下,作 `_cleanup_if_empty` 保护开关 —— 用户自指定的项目目录**绝不 rmtree**(可能含用户已有文件);DB 行该删还是删。`core/export_docx.py::export_chat_to_docx` 重构:task_id 升一等参数(从 `task_dir.name` 提取改入参传入),task_dir 留空时自动从 PG 读,支持用户目录(非 UUID 命名)正常导出。cli `/export` 与 `cli.py export` 子命令均改走 `_resolve_uuid_or_prefix` + task_id 直传。Smoke 4 路径全绿:default-derived(managed=True, cleanup rmtree)/ --task-dir(managed=False, FS preserved)/ resume reads DB / export 自动 PG 查路径。
|
||||||
- **05-14 / §7 B Step 6 no-subtask 校验**:`core/storage/utils.py::check_no_subtask(task_dir, user_id=SENTINEL)` —— 同 user 下查 `new LIKE existing||'/%' OR existing LIKE new||'/%'`(`task_dir != new` 过滤掉同 task_dir 同项目多对话场景)。冲突抛 `NoSubtaskError`(`ValueError` 子类),消息带冲突 task 的 UUID 前 8 位 + 它的 task_dir。**分隔符容差**:SQL 里 `replace(task_dir, :bs, '/')` 把存的 Windows `\` 在比较前归一,新值也 `replace('\\', '/')`,跨 OS / 历史数据混合分隔符不漏判;`bs` 通过 bind 参数传(绕开 SQL 字符串转义陷阱)。空 / whitespace `task_dir` 直接 return(legacy / 未绑项目)。`main.py::build_agent` 在 `resolve_task_id` 后、TaskState 构造前调,`if not resume` 单层闸 —— resume 跳过(目录改名走未来 Folder API cascade,这里只拦新建)。`cli.py` 三处 build_agent 调用现有 try/except 直接接住 NoSubtaskError 并友好打印。Smoke 全绿:同 dir 允许 / child 拒 / parent 拒 / sibling 允许 / `proj_a_other` 不误中 `proj_a`(因为用 `/%` 而非 `%`)/ 空跳过 / Win `\` 子目录拒 / 混合分隔符(`\` 存 + `/` 查)仍拒 / build_agent 端到端三分支(child raise / same pass / resume bypass)。
|
- **05-14 / §7 B Step 6 no-subtask 校验**:`core/storage/utils.py::check_no_subtask(task_dir, user_id=SENTINEL)` —— 同 user 下查 `new LIKE existing||'/%' OR existing LIKE new||'/%'`(`task_dir != new` 过滤掉同 task_dir 同项目多对话场景)。冲突抛 `NoSubtaskError`(`ValueError` 子类),消息带冲突 task 的 UUID 前 8 位 + 它的 task_dir。**分隔符容差**:SQL 里 `replace(task_dir, :bs, '/')` 把存的 Windows `\` 在比较前归一,新值也 `replace('\\', '/')`,跨 OS / 历史数据混合分隔符不漏判;`bs` 通过 bind 参数传(绕开 SQL 字符串转义陷阱)。空 / whitespace `task_dir` 直接 return(legacy / 未绑项目)。`main.py::build_agent` 在 `resolve_task_id` 后、TaskState 构造前调,`if not resume` 单层闸 —— resume 跳过(目录改名走未来 Folder API cascade,这里只拦新建)。`cli.py` 三处 build_agent 调用现有 try/except 直接接住 NoSubtaskError 并友好打印。Smoke 全绿:同 dir 允许 / child 拒 / parent 拒 / sibling 允许 / `proj_a_other` 不误中 `proj_a`(因为用 `/%` 而非 `%`)/ 空跳过 / Win `\` 子目录拒 / 混合分隔符(`\` 存 + `/` 查)仍拒 / build_agent 端到端三分支(child raise / same pass / resume bypass)。
|
||||||
|
- **05-14 / §7 Phase G G1 Web UI 脚手架**:新增 `web/` 包(`app.py` FastAPI 工厂 + `templates/{base,home}.html` + `static/style.css`),`cli.py web --host --port --reload` 子命令(默认 127.0.0.1:8765,本地形态 sentinel user 无 auth,Phase D 才上 OIDC)。模板用 Jinja2 + HTMX/HTMX-SSE 走 CDN(无 node 链路),`base.html` 留 `{% block nav %}` 让 G2+ 扩。**Starlette 新版 `TemplateResponse` 签名**:`(request, name, context)`,旧式塞 context 里会让 jinja 用 dict 当 cache key 报 `unhashable type`,踩过修了。requirements 加 `fastapi>=0.111 uvicorn[standard] jinja2>=3.1 python-multipart`(后者为 G5 文件上传留)。Smoke 四路径全绿(in-process via Starlette `TestClient`):`/healthz` → "ok" / `/` → 1063B(title + static link + version) / `/static/style.css` → 1624B / `/nonexistent` → 404。**Linux portability 顺手**:模板里 path 显示约定用 `Path.as_posix()`(G3+ 模板落地);SSE 响应头 G4 上时带 `X-Accel-Buffering: no`(nginx 反代友好)。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -73,12 +74,15 @@ tools/shell.py 94
|
||||||
tools/run_python.py 84
|
tools/run_python.py 84
|
||||||
tools/skill_tool.py 45
|
tools/skill_tool.py 45
|
||||||
main.py 277 ← §7 B Step 4-6: +is_managed_task_dir / task_dir_arg / no-subtask check
|
main.py 277 ← §7 B Step 4-6: +is_managed_task_dir / task_dir_arg / no-subtask check
|
||||||
cli.py 538 ← §7 B Step 4: --task-dir / cleanup 保护用户目录
|
cli.py 558 ← §7 B Step 4 / Phase G G1: --task-dir / web 子命令
|
||||||
db/migrations/env.py 61 ← §7 B Step 1
|
db/migrations/env.py 61 ← §7 B Step 1
|
||||||
db/migrations/versions/
|
db/migrations/versions/
|
||||||
0001_initial_schema.py 125 ← §7 B Step 1
|
0001_initial_schema.py 125 ← §7 B Step 1
|
||||||
|
web/__init__.py 5 ← Phase G G1
|
||||||
|
web/app.py 40 ← Phase G G1: FastAPI 工厂
|
||||||
─────────────────────────────────
|
─────────────────────────────────
|
||||||
Python 合计 ~3101 行
|
Python 合计 ~3146 行
|
||||||
|
+ web/templates/{base,home}.html ~40 行 + web/static/style.css 64 行(不计 Python 主仓库)
|
||||||
```
|
```
|
||||||
|
|
||||||
加 skills/ppt 脚本 ~600 行 + SKILL.md / references / config / prompts + alembic.ini,总仓库约 3500 行。
|
加 skills/ppt 脚本 ~600 行 + SKILL.md / references / config / prompts + alembic.ini,总仓库约 3500 行。
|
||||||
|
|
@ -87,10 +91,12 @@ Python 合计 ~3101 行
|
||||||
|
|
||||||
## 下一步候选(性价比排序)
|
## 下一步候选(性价比排序)
|
||||||
|
|
||||||
1. **§7 Phase G Web UI 简洁版**(~2-3 天)—— FastAPI + Jinja2 + HTMX + SSE,task list / chat / folder tree / 文件上传下载;依赖 D(HTTP /v1)的 SSE 端点,与 E 无强序。Phase G 也可先上 task list + chat(读 PG)再补 folder tree。
|
1. **§7 Phase G G2 task list 页**(~小半天)—— `/` 渲染最近 task,filter by status + 分页,链 chat 页。复用 `_list_task_rows`。
|
||||||
2. **§7 C Executor + sandbox**(~2-3 天)—— `run_python`/`shell` → `Executor.run(...)`;本地保留 subprocess executor,SaaS 走 docker;`api_key_env` → `KeyProvider` 运行时注入。
|
2. **§7 Phase G G3 chat 只读页**(~小半天)—— `/tasks/{id}` 渲染 PG messages,Markdown server-side 渲染。
|
||||||
3. **Phase 6 context 三层压缩**(~1 天)—— 兜底,V4 长上下文一般用不到。
|
3. **§7 Phase G G4 chat 发送 + SSE**(~1 天)—— `WebEventSink` 把 §7 A 的 `sink.emit` 推 text/event-stream,HTMX `sse-swap` 追加 DOM。**核心一步**。
|
||||||
4. **Phase 7 更多 skill / 模型档案**(持续)。
|
4. **§7 Phase G G5 文件浏览 + G6 打磨**(~半天 + 半天)—— task_dir 树 / upload / download / 错误 toast / `/done /abandon` 按钮 / `/export` 链接。
|
||||||
5. **Proposal mermaid 预渲染**(~半天)—— ASCII 透传不够用时再上 `mmdc`。
|
5. **§7 C Executor + sandbox**(~2-3 天)—— Phase G 完后再做,或穿插。
|
||||||
|
6. **Phase 6 context 三层压缩**(~1 天)—— 兜底,V4 长上下文一般用不到。
|
||||||
|
7. **Proposal mermaid 预渲染**(~半天)—— ASCII 透传不够用时再上 `mmdc`。
|
||||||
|
|
||||||
> §7 B 已完工(Step 1-4 + Step 6,Step 5 取消)。剩余路线:C(Executor)/ D(HTTP API)/ E(auth + UI)/ G(Web UI)/ F(deploy / billing)。
|
> §7 B 已完工。Phase G 进行中(G1 ✅)。剩余路线:G2-G6 → C(Executor)→ D(HTTP /v1 + OIDC)→ E(CLI 双模式)→ F(deploy / billing)。
|
||||||
|
|
|
||||||
22
RUN.md
22
RUN.md
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
> 怎么把 zcbot 跑起来。env / 常用命令 / 故障兜底。设计看 `DESIGN.md`,进度看 `PROGRESS.md`。
|
> 怎么把 zcbot 跑起来。env / 常用命令 / 故障兜底。设计看 `DESIGN.md`,进度看 `PROGRESS.md`。
|
||||||
|
|
||||||
最后更新:2026-05-14(Step 6)
|
最后更新:2026-05-14(Phase G G1)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -90,6 +90,21 @@ REPL 内命令:`/exit /reset /new /resume [last|<id>] /id /status /done /abandon
|
||||||
.venv/Scripts/python.exe cli.py db current
|
.venv/Scripts/python.exe cli.py db current
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Web UI(§7 Phase G,本地 sentinel user 无 auth)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 默认 127.0.0.1:8765 启,浏览器开 http://127.0.0.1:8765
|
||||||
|
.venv/Scripts/python.exe cli.py web
|
||||||
|
|
||||||
|
# 自定义端口 / 监听 0.0.0.0(配合 firewall 慎用,本地形态无 auth)
|
||||||
|
.venv/Scripts/python.exe cli.py web --port 9000
|
||||||
|
|
||||||
|
# dev:文件改动自动重启(uvicorn 工厂模式 reload)
|
||||||
|
.venv/Scripts/python.exe cli.py web --reload
|
||||||
|
```
|
||||||
|
|
||||||
|
> G1 阶段只有 / + /healthz + /static;G2-G6 渐进上 task list / chat / SSE / 文件浏览。Linux:`.venv/bin/python cli.py web` 一致。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 故障兜底
|
## 故障兜底
|
||||||
|
|
@ -104,15 +119,18 @@ REPL 内命令:`/exit /reset /new /resume [last|<id>] /id /status /done /abandon
|
||||||
| `--task-dir` 指定后 `/exit` 没清 task_dir | 设计如此 —— 用户路径绝不 rmtree;DB 行该删还是删。要彻底删手动 `rm -rf <dir>` |
|
| `--task-dir` 指定后 `/exit` 没清 task_dir | 设计如此 —— 用户路径绝不 rmtree;DB 行该删还是删。要彻底删手动 `rm -rf <dir>` |
|
||||||
| Export 报 "无可导出内容" | task 没 messages(只 system 不算);先在 REPL 发条消息再 export |
|
| Export 报 "无可导出内容" | task 没 messages(只 system 不算);先在 REPL 发条消息再 export |
|
||||||
| `NoSubtaskError: task_dir ... 与已有 task ... 前缀嵌套` | §7.4 no-subtask:同 user 不允许 task_dir 嵌套(child 或 parent)。**同项目多对话**请传**完全相同**的 `--task-dir`;否则改路径成 sibling(平级) |
|
| `NoSubtaskError: task_dir ... 与已有 task ... 前缀嵌套` | §7.4 no-subtask:同 user 不允许 task_dir 嵌套(child 或 parent)。**同项目多对话**请传**完全相同**的 `--task-dir`;否则改路径成 sibling(平级) |
|
||||||
|
| `cli.py web` 启动后浏览器开不了 | 检查 proxy(`HTTP_PROXY` / `HTTPS_PROXY`):本地形态服务在 127.0.0.1,系统 proxy 拦截会 502。临时 `unset HTTP_PROXY HTTPS_PROXY` 或浏览器配 bypass。`curl` 验通走 `curl --noproxy '*' http://127.0.0.1:8765/healthz`(应返 `ok`) |
|
||||||
|
| `TypeError: unhashable type: 'dict'` from Jinja templating | Starlette 新版签名:`TemplateResponse(request, name, context)`,旧式 `(name, {"request":..., "...":...})` 在 newer Starlette 会把 context dict 当 cache key 炸 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 关键路径与文件
|
## 关键路径与文件
|
||||||
|
|
||||||
- **入口**:`cli.py`(REPL + 子命令)→ `main.py::build_agent`(装配)
|
- **入口**:`cli.py`(REPL + `chat / tasks / probe / db / web` 子命令)→ `main.py::build_agent`(装配)
|
||||||
- **核心**:`core/loop.py`(ReAct)/ `core/session.py`(PG messages)/ `core/task.py`(PG tasks)/ `core/llm.py`(LiteLLM 封装)
|
- **核心**:`core/loop.py`(ReAct)/ `core/session.py`(PG messages)/ `core/task.py`(PG tasks)/ `core/llm.py`(LiteLLM 封装)
|
||||||
- **工具**:`tools/{fs,shell,run_python,skill_tool}.py`
|
- **工具**:`tools/{fs,shell,run_python,skill_tool}.py`
|
||||||
- **存储**:`core/storage/{engine,models,utils}.py`(SQLAlchemy 2.x ORM)+ `db/migrations/`(alembic)
|
- **存储**:`core/storage/{engine,models,utils}.py`(SQLAlchemy 2.x ORM)+ `db/migrations/`(alembic)
|
||||||
|
- **Web**:`web/{app.py, templates/, static/}`(§7 Phase G,FastAPI + Jinja2 + HTMX)
|
||||||
- **配置**:`config/agent.yaml`(全局)/ `config/models/*.yaml`(模型档案,§3.2 Model Profile)
|
- **配置**:`config/agent.yaml`(全局)/ `config/models/*.yaml`(模型档案,§3.2 Model Profile)
|
||||||
- **Skill**:`skills/{coding,ppt,proposal}/SKILL.md`(渐进披露,§3.5)
|
- **Skill**:`skills/{coding,ppt,proposal}/SKILL.md`(渐进披露,§3.5)
|
||||||
- **Workspace**:`workspace/memory/{core.md, extended/}`(跨 task 记忆,FS 永久)/ `workspace/tasks/<uuid>/`(默认派生 task_dir,只放 skill 产物)
|
- **Workspace**:`workspace/memory/{core.md, extended/}`(跨 task 记忆,FS 永久)/ `workspace/tasks/<uuid>/`(默认派生 task_dir,只放 skill 产物)
|
||||||
|
|
|
||||||
20
cli.py
20
cli.py
|
|
@ -534,5 +534,25 @@ def probe(model: str, long_context: bool) -> None:
|
||||||
console.print("\n[ok]全部能力声明与实测一致。[/ok]")
|
console.print("\n[ok]全部能力声明与实测一致。[/ok]")
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command()
|
||||||
|
@click.option("--host", default="127.0.0.1", show_default=True,
|
||||||
|
help="监听地址。本地形态默认 127.0.0.1,不对外暴露")
|
||||||
|
@click.option("--port", default=8765, show_default=True, type=int,
|
||||||
|
help="监听端口")
|
||||||
|
@click.option("--reload/--no-reload", default=False,
|
||||||
|
help="dev:文件改动自动重启(uvicorn 工厂模式)")
|
||||||
|
def web(host: str, port: int, reload: bool) -> None:
|
||||||
|
"""启动 Web UI(§7 Phase G,本地形态 sentinel user 无 auth)。"""
|
||||||
|
import uvicorn
|
||||||
|
|
||||||
|
if reload:
|
||||||
|
# reload 模式需要 import string + factory,uvicorn 才能监听文件
|
||||||
|
uvicorn.run("web.app:create_app", host=host, port=port,
|
||||||
|
reload=True, factory=True, log_level="info")
|
||||||
|
else:
|
||||||
|
from web.app import create_app
|
||||||
|
uvicorn.run(create_app(), host=host, port=port, log_level="info")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
cli()
|
cli()
|
||||||
|
|
|
||||||
|
|
@ -15,3 +15,9 @@ markitdown[pdf,docx,pptx,xlsx]>=0.0.1
|
||||||
sqlalchemy>=2.0.0
|
sqlalchemy>=2.0.0
|
||||||
psycopg[binary]>=3.1.0
|
psycopg[binary]>=3.1.0
|
||||||
alembic>=1.13.0
|
alembic>=1.13.0
|
||||||
|
|
||||||
|
# §7 Phase G: Web UI (FastAPI + Jinja2 + HTMX + 原生 SSE)
|
||||||
|
fastapi>=0.111.0
|
||||||
|
uvicorn[standard]>=0.30.0
|
||||||
|
jinja2>=3.1.0
|
||||||
|
python-multipart>=0.0.9
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
"""§7 Phase G: Web UI 简洁版(FastAPI + Jinja2 + HTMX + 原生 SSE)。
|
||||||
|
|
||||||
|
入口:`cli.py web` → `web.app.create_app()` → uvicorn 起。
|
||||||
|
本地形态固定 sentinel user(无 auth);Phase D 加 OIDC 后才有真正用户态。
|
||||||
|
"""
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
"""FastAPI app 工厂。G1 = 脚手架 + 占位 /;G2 起接 PG。
|
||||||
|
|
||||||
|
设计:
|
||||||
|
- 单 FastAPI 进程,模板走 Jinja2,静态走 StaticFiles
|
||||||
|
- 模板里 path 显示一律 `Path.as_posix()`,Win / Linux 看到统一形态
|
||||||
|
- SSE 在 G4 加,响应头会带 `X-Accel-Buffering: no`(nginx 反代友好)
|
||||||
|
- 本地形态 sentinel user 固定;Phase D 加 OIDC 之后才有真正 user 态
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from fastapi import FastAPI, Request
|
||||||
|
from fastapi.responses import HTMLResponse
|
||||||
|
from fastapi.staticfiles import StaticFiles
|
||||||
|
from fastapi.templating import Jinja2Templates
|
||||||
|
|
||||||
|
WEB_ROOT = Path(__file__).resolve().parent
|
||||||
|
TEMPLATES_DIR = WEB_ROOT / "templates"
|
||||||
|
STATIC_DIR = WEB_ROOT / "static"
|
||||||
|
|
||||||
|
|
||||||
|
def create_app() -> FastAPI:
|
||||||
|
"""FastAPI 工厂。uvicorn --reload 模式需要工厂签名(factory=True)。"""
|
||||||
|
app = FastAPI(title="zcbot web", version="0.1")
|
||||||
|
templates = Jinja2Templates(directory=str(TEMPLATES_DIR))
|
||||||
|
app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")
|
||||||
|
|
||||||
|
@app.get("/", response_class=HTMLResponse)
|
||||||
|
def home(request: Request):
|
||||||
|
# Starlette 新签名:request 升一等位置参数,context 不再带 request
|
||||||
|
return templates.TemplateResponse(
|
||||||
|
request, "home.html", {"version": app.version}
|
||||||
|
)
|
||||||
|
|
||||||
|
@app.get("/healthz", response_class=HTMLResponse)
|
||||||
|
def healthz():
|
||||||
|
return HTMLResponse("ok")
|
||||||
|
|
||||||
|
return app
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
/* zcbot web — minimal sane defaults. Phase G 渐进扩。 */
|
||||||
|
:root {
|
||||||
|
--bg: #fafafa;
|
||||||
|
--surface: #ffffff;
|
||||||
|
--fg: #1a1a1a;
|
||||||
|
--muted: #888;
|
||||||
|
--border: #e5e5e5;
|
||||||
|
--accent: #c00; /* 商务红,延续 ppt skill 配色 */
|
||||||
|
--accent-soft: #fceaea;
|
||||||
|
--link: #0a58ca;
|
||||||
|
--mono: ui-monospace, "SF Mono", Menlo, Consolas, "Courier New", monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
html, body { margin: 0; padding: 0; }
|
||||||
|
body {
|
||||||
|
font: 14px/1.5 -apple-system, "Segoe UI", Roboto, "Helvetica Neue", system-ui, "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--fg);
|
||||||
|
}
|
||||||
|
a { color: var(--link); text-decoration: none; }
|
||||||
|
a:hover { text-decoration: underline; }
|
||||||
|
code { font-family: var(--mono); background: var(--accent-soft); padding: 0 .25em; border-radius: 3px; color: var(--accent); }
|
||||||
|
small.muted, .muted { color: var(--muted); font-weight: normal; }
|
||||||
|
|
||||||
|
.topbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1.25rem;
|
||||||
|
padding: .65rem 1.25rem;
|
||||||
|
background: var(--surface);
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
.brand {
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--accent);
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
.brand:hover { text-decoration: none; }
|
||||||
|
.navlinks { display: flex; gap: 1rem; flex: 1; }
|
||||||
|
.user-tag {
|
||||||
|
font-size: .75rem;
|
||||||
|
color: var(--muted);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
padding: .1em .5em;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 960px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 1.5rem 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 { font-size: 1.5rem; margin: 0 0 .5rem; }
|
||||||
|
h2 { font-size: 1.1rem; margin: 1.5rem 0 .5rem; }
|
||||||
|
.lead { font-size: 1rem; color: #444; }
|
||||||
|
.status ul { padding-left: 1.25rem; }
|
||||||
|
.status li { margin: .25rem 0; }
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>{% block title %}zcbot{% endblock %}</title>
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', path='style.css') }}">
|
||||||
|
<script src="https://unpkg.com/htmx.org@2.0.4" defer></script>
|
||||||
|
<script src="https://unpkg.com/htmx-ext-sse@2.2.2" defer></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header class="topbar">
|
||||||
|
<a class="brand" href="/">zcbot</a>
|
||||||
|
<nav class="navlinks">
|
||||||
|
{% block nav %}{% endblock %}
|
||||||
|
</nav>
|
||||||
|
<span class="user-tag" title="本地 sentinel user — Phase D 加 OIDC 之前固定">local</span>
|
||||||
|
</header>
|
||||||
|
<main class="container">
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}zcbot · home{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<h1>zcbot web <small class="muted">v{{ version }}</small></h1>
|
||||||
|
<p class="lead">
|
||||||
|
§7 Phase G 脚手架已就位。task list / chat / SSE 流式回复 / 文件上传下载将在 G2-G6 上线。
|
||||||
|
</p>
|
||||||
|
<section class="status">
|
||||||
|
<h2>状态</h2>
|
||||||
|
<ul>
|
||||||
|
<li>G1 ✅ 脚手架 + <code>cli.py web</code></li>
|
||||||
|
<li>G2 🚧 task list 页</li>
|
||||||
|
<li>G3 🚧 chat 只读页</li>
|
||||||
|
<li>G4 🚧 chat 发送 + SSE 流式</li>
|
||||||
|
<li>G5 🚧 文件浏览 / 上传 / 下载</li>
|
||||||
|
<li>G6 🚧 打磨 + export</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
{% endblock %}
|
||||||
Loading…
Reference in New Issue