core(/v1/skills + dev SPA): GET /v1/skills + 新建任务弹窗 skill 字段改下拉
- web/app.py: lifespan 启动扫一次 SkillRegistry 挂 app.state;新增 GET /v1/skills(JWT 鉴权) - web/static/dev.html: nt-skill input → select + loadSkillOptions 缓存到 state.skills - PROGRESS: 记录 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f9311b069c
commit
b057c3bd06
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
> 配合 `DESIGN.md`。本文件只记 phase 状态、决策偏差、文件量、下一步。
|
> 配合 `DESIGN.md`。本文件只记 phase 状态、决策偏差、文件量、下一步。
|
||||||
|
|
||||||
最后更新:2026-05-18(dev SPA 全套 UI 中文化:菜单/按钮/状态提示/role 标签/弹窗文案全部本地化;状态码 `active/completed/abandoned` 显示为「进行中/已完成/已废弃」)
|
最后更新:2026-05-18(`GET /v1/skills` 路由 + dev SPA 新建任务弹窗 skill 字段改下拉选择,数据源自 SkillRegistry)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -21,6 +21,7 @@
|
||||||
|
|
||||||
## 已完成关键能力
|
## 已完成关键能力
|
||||||
|
|
||||||
|
- **05-18 / `GET /v1/skills` + dev SPA skill 字段改下拉**:原 `nt-skill` 是自由输入(用户得记住 `coding / ppt / proposal` 拼写),用户反馈"加 skill 接口给前端选"。后端 `web/app.py` lifespan 启动时 `SkillRegistry(ROOT / cfg["skills_dir"])` 扫一次挂到 `app.state.skill_registry`(文件系统静态,运行中不变);新增 `GET /v1/skills` 走 `require_user` JWT 鉴权,返 `{skills:[{name,description}]}` 按 name 升序(registry 已 sorted)。dev SPA(`web/static/dev.html`):`<input id=nt-skill>` 换 `<select>`,首项固定 `(默认 · 不限定)` 空值;`hd-new` 打开 modal 时 `loadSkillOptions()` 与 `loadFolderSuggestions()` 并发(`Promise.all`),首次拉到的列表缓存到 `state.skills`,失败时静默退化为只剩"默认"项不阻塞。option 文案 `name — description`,`title` 也带 description 鼠标悬停看长文。Smoke:`TestClient` 起 app → `/v1/auth/login` 拿 token → `/v1/skills` 返 3 项(coding/ppt/proposal)+ 描述;无 token 401。**未动**:`_build_system_prompt` 注入的 skill discovery block(name + description)和这里渲染的下拉项是同源 registry,改一处不影响另一处;`POST /v1/tasks` body 不校验 `skill ∈ registry`(留空 / 任意串都允许,与 schema 一致 — 真要拦在 UI 层早就拦了)。
|
||||||
- **05-18 / dev SPA 全套 UI 中文化**:用户反馈"web 页面菜单按钮啥的改为中文"。`web/static/dev.html` 静态部分(login overlay / header / 三栏 pane-head label / chat 操作按钮 / new task modal)+ JS 动态部分(状态文案 / role 标签 / confirm/alert 文案 / 状态 badge / SSE 流式提示)全面本地化。**静态文案**:`zcbot dev login → zcbot 登录` / `+ new task → + 新建任务` / `logout → 退出登录` / `tasks/chat/files → 任务/对话/文件` / 状态 select `(all)/active/completed/abandoned → (全部)/进行中/已完成/已废弃` / `export .docx/done/abandon/delete → 导出 docx/完成/废弃/删除` / `stop/send → 停止/发送` / `ready/sending/streaming/cancelling → 就绪/发送中/接收中/停止中` / `(no task selected) → (未选中任务)` / `select a task on the left → 请在左侧选一个任务` / `loading… → 加载中…` / `load failed → 加载失败` / `(no tasks) → (暂无任务)` / `(no messages yet) → (暂无消息 · 在下方输入开始对话)` / `(unnamed) → (未命名)` / `(user root) → (根目录)`。**动态文案**:`renderTaskList` / `renderChatMeta` 加 `statusLabels` map(`active→进行中`等),task list 计数 `msg → 条`;消息卡 role 标签 `user/assistant/error → 我/助手/错误`,`tool · name → 工具调用 · name`,`result (N chars) → 结果(N 字符)`,SSE 流式 `tool_call:/tool_result → 工具调用:/工具结果`;`cancelled` badge `已停止(stopped by user) → 已停止`(更简洁)。**弹窗 / 错文案**:`确认置为 status? → 确认置为「中文 label」?` / `delete failed → 删除失败:` / `download failed → 下载失败:` / `upload failed → 上传失败:` / `export failed → 导出失败:` / 删 task confirm 文案改"任务「项目名」(N 条消息)" / `任务名 必填 → 任务名为必填项`。**modal**:`新建 task → 新建任务` / 各 label "必填"/"可选" 加括号统一 / `留空 fallback 用任务名 → 留空则用任务名` / `N 个 task → N 个任务`。**Smoke**(in-process TestClient 拉 `/static/dev.html`):assert 13 个中文标签全在 + 8 个原英文按钮文案全无残留。**没动**:技术字段(`user_id` / `platform_key` / `UUID` / `tok` token 简称)、CSS class(`badge active` 等仍是英文 class,但显示文本走 statusLabels)、SSE event 名(`text/tool_call/tool_result/done/error/cancelled`)、API 字段名 — 都是 schema 层,不影响 UI 中文。
|
- **05-18 / dev SPA 全套 UI 中文化**:用户反馈"web 页面菜单按钮啥的改为中文"。`web/static/dev.html` 静态部分(login overlay / header / 三栏 pane-head label / chat 操作按钮 / new task modal)+ JS 动态部分(状态文案 / role 标签 / confirm/alert 文案 / 状态 badge / SSE 流式提示)全面本地化。**静态文案**:`zcbot dev login → zcbot 登录` / `+ new task → + 新建任务` / `logout → 退出登录` / `tasks/chat/files → 任务/对话/文件` / 状态 select `(all)/active/completed/abandoned → (全部)/进行中/已完成/已废弃` / `export .docx/done/abandon/delete → 导出 docx/完成/废弃/删除` / `stop/send → 停止/发送` / `ready/sending/streaming/cancelling → 就绪/发送中/接收中/停止中` / `(no task selected) → (未选中任务)` / `select a task on the left → 请在左侧选一个任务` / `loading… → 加载中…` / `load failed → 加载失败` / `(no tasks) → (暂无任务)` / `(no messages yet) → (暂无消息 · 在下方输入开始对话)` / `(unnamed) → (未命名)` / `(user root) → (根目录)`。**动态文案**:`renderTaskList` / `renderChatMeta` 加 `statusLabels` map(`active→进行中`等),task list 计数 `msg → 条`;消息卡 role 标签 `user/assistant/error → 我/助手/错误`,`tool · name → 工具调用 · name`,`result (N chars) → 结果(N 字符)`,SSE 流式 `tool_call:/tool_result → 工具调用:/工具结果`;`cancelled` badge `已停止(stopped by user) → 已停止`(更简洁)。**弹窗 / 错文案**:`确认置为 status? → 确认置为「中文 label」?` / `delete failed → 删除失败:` / `download failed → 下载失败:` / `upload failed → 上传失败:` / `export failed → 导出失败:` / 删 task confirm 文案改"任务「项目名」(N 条消息)" / `任务名 必填 → 任务名为必填项`。**modal**:`新建 task → 新建任务` / 各 label "必填"/"可选" 加括号统一 / `留空 fallback 用任务名 → 留空则用任务名` / `N 个 task → N 个任务`。**Smoke**(in-process TestClient 拉 `/static/dev.html`):assert 13 个中文标签全在 + 8 个原英文按钮文案全无残留。**没动**:技术字段(`user_id` / `platform_key` / `UUID` / `tok` token 简称)、CSS class(`badge active` 等仍是英文 class,但显示文本走 statusLabels)、SSE event 名(`text/tool_call/tool_result/done/error/cancelled`)、API 字段名 — 都是 schema 层,不影响 UI 中文。
|
||||||
- **05-18 / 入口归位:`cli.py` 改名 `main.py`、原 `main.py` 挪 `core/agent_builder.py`,删 CLI REPL `chat/tasks/export`,§7 E 双模式路线撤**:接 0004 schema 大瘦身后又一轮架构清理。用户复盘"§7 E `--remote` 是不是可以移除""有 dev SPA 后 CLI REPL 还需要吗""统一到 main.py 是否合理"——一连串问题指向同一个底层:`cli.py`(CLI 入口)+ `main.py`(装配 lib)+ `chat / tasks / export` REPL 子命令是历史多形态共存遗留,在"UI 由 platform 实现 + dev SPA 是开发主路径"的新形态下都是冗余。**架构判断**:`main.py` 此前混三角色(装配 lib + 路径/验证 utility + 被 cli+web 共同 import 的事实入口),按 §5 Less Scaffolding + SoC 应该拆;直接答案是 `cli.py` 改名 `main.py`(入口),原 `main.py` 改 `core/agent_builder.py`(装配 lib),单一职责对齐 Python 社区惯例(入口叫 main.py,lib 在子模块)。**改动**:① `git mv main.py core/agent_builder.py`;② `git mv cli.py main.py`(覆盖);③ 5 处 `web/app.py::from main import xxx` → `from core.agent_builder import xxx`(`build_agent / sync_task_tokens / working_dir_from_name / resolve_workspace / user_root / InvalidTaskName / validate_task_name`);④ 新 `main.py` 自指 `from main import` 改 `from core.agent_builder import`;⑤ 删 `chat / tasks / export` 三个 click 命令 + REPL 内部 helpers(`_cleanup_if_empty / _delete_task_db_row / _task_has_messages / _list_task_rows` 共 ~110 行)+ REPL 主循环(`/exit /reset /new /resume /id /status /done /abandon /desc /export` 共 ~200 行)+ `--name --working-dir --skill --desc --resume --model` CLI 选项 + `tasks` 列表渲染 + `export` 命令 — 共 ~400 行;新 `main.py` ~180 行(`db {upgrade,downgrade,current}` + `probe` + `web` 三命令组);⑥ `core/agent_builder.py` 顺手清:删 `_resolve_uuid_or_prefix` 函数(web 端只传完整 UUID,前缀匹配无 caller)+ `resolve_task_id` 内 `task_id_arg in (None, "", "last")` 分支(web 不传 "last"),resume 直接 `UUID(task_id_arg)`;模块 docstring "本地 CLI user_id = SENTINEL" → "所有入口走 web `/v1` + JWT;dev SPA 默认填 SENTINEL 走同一条路径"。**Smoke 6 case 全绿**(in-process TestClient + 子进程跑 `python main.py db current`):① `/healthz` 200 ② POST /v1/tasks → GET → POST messages(返 `events_url` 无 run_id)→ cancel → DELETE 全链路 ③ `/v1/folders` 走 `core.agent_builder.user_root` 路径 ④ `/v1/files` 走 `_load_user_root` ⑤ `resolve_task_id` 完整 UUID resume(去前缀匹配后用 `UUID(...)` 直接解析;非 UUID 字符串 ValueError;ghost UUID `empty working_dir` ValueError)⑥ `subprocess.run([sys.executable, "main.py", "db", "current"])` 子进程跑通 + stdout 含 `0004 (head)`(验证 click 入口、alembic config 路径、ROOT 解析都没坏)。**文档同步**:DESIGN §1 形态兼容(删 `--remote`,讲"无 CLI / in-process 分叉")/ §2 目录树(`{main.py, cli.py}` → `core/agent_builder.py + main.py`)/ §3.3 `cli.py probe` → `main.py probe` / §3.6 "REPL 内 task 切换"段改"Task 切换 / 软删 走 dev SPA + /v1" + "入口"段讲 `python main.py web` / §7.0 共享差别表入口列改 `python main.py web` + auth 行讲"dev SPA 填 sentinel + 本地 key" / §7.6 #8 标"已撤" / §7.7 E 阶段标"撤" / §7.8 风险表"CLI 双模式分叉"行融合进"过早抽象" / §7.9 新增"CLI REPL 撤,入口统一 main.py"取舍说明 + 删原"CLI 双模式共存"段;RUN 顶 / 一次性初始化 / 日常命令 / 故障兜底 / 关键路径全部 `cli.py` → `main.py`,且日常命令段重写"只剩 `web / db / probe` + 所有 task 交互走 main.py web 后浏览器或 /v1`";PROGRESS 文件清单 / 状态表 / 下一步候选同步(去掉 E 路线)。**净效果**:总代码 -360 行(`cli.py` 558 行删 → `main.py` 180 行 + `core/agent_builder.py` ~320 行 = ~500;原 `main.py` 337 + `cli.py` 558 = 895;净减 -395);入口文件数 2 → 1;维护面 -1 套 task 切换语义(REPL `/new /resume /done /abandon` 全归到 `/v1/tasks*`);测试面 -1 套(原 cli build_agent 调用链 smoke 全归到 web TestClient)。
|
- **05-18 / 入口归位:`cli.py` 改名 `main.py`、原 `main.py` 挪 `core/agent_builder.py`,删 CLI REPL `chat/tasks/export`,§7 E 双模式路线撤**:接 0004 schema 大瘦身后又一轮架构清理。用户复盘"§7 E `--remote` 是不是可以移除""有 dev SPA 后 CLI REPL 还需要吗""统一到 main.py 是否合理"——一连串问题指向同一个底层:`cli.py`(CLI 入口)+ `main.py`(装配 lib)+ `chat / tasks / export` REPL 子命令是历史多形态共存遗留,在"UI 由 platform 实现 + dev SPA 是开发主路径"的新形态下都是冗余。**架构判断**:`main.py` 此前混三角色(装配 lib + 路径/验证 utility + 被 cli+web 共同 import 的事实入口),按 §5 Less Scaffolding + SoC 应该拆;直接答案是 `cli.py` 改名 `main.py`(入口),原 `main.py` 改 `core/agent_builder.py`(装配 lib),单一职责对齐 Python 社区惯例(入口叫 main.py,lib 在子模块)。**改动**:① `git mv main.py core/agent_builder.py`;② `git mv cli.py main.py`(覆盖);③ 5 处 `web/app.py::from main import xxx` → `from core.agent_builder import xxx`(`build_agent / sync_task_tokens / working_dir_from_name / resolve_workspace / user_root / InvalidTaskName / validate_task_name`);④ 新 `main.py` 自指 `from main import` 改 `from core.agent_builder import`;⑤ 删 `chat / tasks / export` 三个 click 命令 + REPL 内部 helpers(`_cleanup_if_empty / _delete_task_db_row / _task_has_messages / _list_task_rows` 共 ~110 行)+ REPL 主循环(`/exit /reset /new /resume /id /status /done /abandon /desc /export` 共 ~200 行)+ `--name --working-dir --skill --desc --resume --model` CLI 选项 + `tasks` 列表渲染 + `export` 命令 — 共 ~400 行;新 `main.py` ~180 行(`db {upgrade,downgrade,current}` + `probe` + `web` 三命令组);⑥ `core/agent_builder.py` 顺手清:删 `_resolve_uuid_or_prefix` 函数(web 端只传完整 UUID,前缀匹配无 caller)+ `resolve_task_id` 内 `task_id_arg in (None, "", "last")` 分支(web 不传 "last"),resume 直接 `UUID(task_id_arg)`;模块 docstring "本地 CLI user_id = SENTINEL" → "所有入口走 web `/v1` + JWT;dev SPA 默认填 SENTINEL 走同一条路径"。**Smoke 6 case 全绿**(in-process TestClient + 子进程跑 `python main.py db current`):① `/healthz` 200 ② POST /v1/tasks → GET → POST messages(返 `events_url` 无 run_id)→ cancel → DELETE 全链路 ③ `/v1/folders` 走 `core.agent_builder.user_root` 路径 ④ `/v1/files` 走 `_load_user_root` ⑤ `resolve_task_id` 完整 UUID resume(去前缀匹配后用 `UUID(...)` 直接解析;非 UUID 字符串 ValueError;ghost UUID `empty working_dir` ValueError)⑥ `subprocess.run([sys.executable, "main.py", "db", "current"])` 子进程跑通 + stdout 含 `0004 (head)`(验证 click 入口、alembic config 路径、ROOT 解析都没坏)。**文档同步**:DESIGN §1 形态兼容(删 `--remote`,讲"无 CLI / in-process 分叉")/ §2 目录树(`{main.py, cli.py}` → `core/agent_builder.py + main.py`)/ §3.3 `cli.py probe` → `main.py probe` / §3.6 "REPL 内 task 切换"段改"Task 切换 / 软删 走 dev SPA + /v1" + "入口"段讲 `python main.py web` / §7.0 共享差别表入口列改 `python main.py web` + auth 行讲"dev SPA 填 sentinel + 本地 key" / §7.6 #8 标"已撤" / §7.7 E 阶段标"撤" / §7.8 风险表"CLI 双模式分叉"行融合进"过早抽象" / §7.9 新增"CLI REPL 撤,入口统一 main.py"取舍说明 + 删原"CLI 双模式共存"段;RUN 顶 / 一次性初始化 / 日常命令 / 故障兜底 / 关键路径全部 `cli.py` → `main.py`,且日常命令段重写"只剩 `web / db / probe` + 所有 task 交互走 main.py web 后浏览器或 /v1`";PROGRESS 文件清单 / 状态表 / 下一步候选同步(去掉 E 路线)。**净效果**:总代码 -360 行(`cli.py` 558 行删 → `main.py` 180 行 + `core/agent_builder.py` ~320 行 = ~500;原 `main.py` 337 + `cli.py` 558 = 895;净减 -395);入口文件数 2 → 1;维护面 -1 套 task 切换语义(REPL `/new /resume /done /abandon` 全归到 `/v1/tasks*`);测试面 -1 套(原 cli build_agent 调用链 smoke 全归到 web TestClient)。
|
||||||
- **05-18 / 0004 schema 大瘦身:删 runs / usage_events 表,run_status / run_error 合入 tasks;路由从 run_id 维度改 task 维度**:用户复盘"为什么 cancel 接口要带 run_id?现在不是一个 task 一个 run 吗",顺手把 runs / usage_events 表也重新审视 — `usage_events` 全代码库零引用、零写入、零读取,纯死代码(为未来计费预付的架构成本);`runs` 表 `tokens_p/c` 写但从未被读(tokens 累计走 tasks 列),`started_at / finished_at / error` 也只写不读,`run_id` 唯二实用是 broker 频道键 + cancel 参数 — 但 §7.1 已选定**单活 run** 形态,同 task 同时最多 1 个活 run,客户端只需要 task_id(永远有)就够,run_id 完全冗余。按"开发期不写兼容层"心智一把切干净。**alembic 0004**:`DROP TABLE usage_events / runs`,`tasks` 加 `run_status text not null default 'idle'`(idle / running / cancelling / error)+ `run_error text null`。**ORM** `models.py` 删 `Run` / `UsageEvent` 两 class + 删 `BigInteger` import;`Task` 加两列;`storage/__init__.py` 文档示例同步;`Task.run_status` 终态语义:`ok / cancelled` 收尾都回 `idle`(用户视角"跑完 / 停了"等价不留持久标记),只有 `error` 是持久终态,起新 run 时清。**Broker**(`web/broker.py`)全面 task_id 索引:`_subs / _done / _cancel_flags` 三个 dict key 从 run_id 换 task_id;加 `start(task_id)` 入口在新 run 起来前清 `_done` 标记(避免上一轮 done 让新订阅者立刻断流)。**Sink**(`web/sinks.py`)绑 task_id 替代 run_id。**`web/app.py`**:① `_run_agent_bg(task_id, user_id, content)` 去掉 run_id 参数;装 `agent.cancel_check = lambda tid=task_id: broker.is_cancelled(tid)`;终态写 `tasks.run_status = "idle"`(原 `Run.status = "ok"/"cancelled"`)或 `"error"`(`run_error = err`);finally `broker.clear_cancel(tid) + broker.close(tid)`。② `POST /v1/tasks/{tid}/messages` 改:`SELECT Task.run_status … FOR UPDATE` 替代 `select(Run.run_id) … running/cancelling`;同事务 `UPDATE Task SET run_status='running', run_error=NULL`(error 也算可重启视为清);commit 后 `broker.start(tid)` 清 done;返 `{"events_url": "/v1/tasks/{tid}/events"}` 去掉 `run_id`。③ `POST /v1/tasks/{tid}/cancel` 取代 `POST /v1/tasks/{tid}/runs/{rid}/cancel`,只校验 task 归属 user;`run_status != 'running'` → 409。④ `GET /v1/tasks/{tid}/events` 取代 `/runs/{rid}/events`,broker.subscribe(tid)。⑤ lifespan reaper `UPDATE Task SET run_status='error' WHERE run_status IN ('running','cancelling')`,文案不变。⑥ `_task_dict` 暴露 `run_status` / `run_error` 字段给前端。**dev SPA**(`web/static/dev.html`):`state.currentRunId` 改 `state.streaming` bool;`POST /messages` 拿到 `events_url` 直接订阅,不再保存 run_id;cancel 按钮 click → `POST /v1/tasks/{tid}/cancel`(去掉 `/runs/{rid}/`)。**Migration 跑通**:本地 PG `db upgrade 0003 → 0004 (head)` 一把过(用户授权清旧数据,无 backfill)。**Smoke 18 case 全绿**(in-process TestClient + BG mock):POST /messages 返 `events_url` 无 run_id / tasks.run_status='running' / gate when running 409 / POST /cancel 202 + run_status='cancelling' + broker flag set / double cancel 409(状态非 running)/ gate during cancelling 也 409 / cancel idle 409 / cancel error 409 / error 状态可发新消息(error 不挂 gate + 清 run_error) / ghost task 404 / invalid UUID 404 / cross-user 404 / no auth 401 / GET /events 路由注册(SSE 流式跑会挂 30s 心跳,smoke 只验路径 + headers) / GET /tasks 返回 run_status / run_error 字段 / stale reaper 扫 running+cancelling 标 error / broker.start 清 _done / broker.subscribe + emit + close + late subscriber 立刻收 done / broker.request_cancel + is_cancelled + clear_cancel。**净增量**:核心代码 -200 行(删表 ORM + 两路由层简化),broker 加 21 行 start/cancel API,dev.html 几行字段重命名;DB 表 5 → 3,路由 `/runs/{rid}/{events,cancel}` → `/{events,cancel}`,前端 SPA 不再需要先拿 run_id 才能 cancel / 订阅 — 客户端只需 task_id。**文档同步**:DESIGN §7.2 路由表 messages 路由返 `events_url`(去 `run_id`)+ cancel / events 改 task-level + lead-in 注 0004 简化 + SSE schema text event 字段 `delta`(实际就是 delta,文档原 `content` 笔误);§7.4 schema 块 tasks 加两列 + 注 0004 合并;§7.9 hard cascade 行注 "原 usage_events 0004 删" + 加专项取舍说明"0004 删 runs + usage_events 表";§7.7 风险表两行同步 / 改 task-level 路由名;RUN 路由表三路由全改 + 故障兜底 cancel 409 文案改 + db upgrade head 改 0004;PROGRESS 已完成 + 状态表 + 文件清单。
|
- **05-18 / 0004 schema 大瘦身:删 runs / usage_events 表,run_status / run_error 合入 tasks;路由从 run_id 维度改 task 维度**:用户复盘"为什么 cancel 接口要带 run_id?现在不是一个 task 一个 run 吗",顺手把 runs / usage_events 表也重新审视 — `usage_events` 全代码库零引用、零写入、零读取,纯死代码(为未来计费预付的架构成本);`runs` 表 `tokens_p/c` 写但从未被读(tokens 累计走 tasks 列),`started_at / finished_at / error` 也只写不读,`run_id` 唯二实用是 broker 频道键 + cancel 参数 — 但 §7.1 已选定**单活 run** 形态,同 task 同时最多 1 个活 run,客户端只需要 task_id(永远有)就够,run_id 完全冗余。按"开发期不写兼容层"心智一把切干净。**alembic 0004**:`DROP TABLE usage_events / runs`,`tasks` 加 `run_status text not null default 'idle'`(idle / running / cancelling / error)+ `run_error text null`。**ORM** `models.py` 删 `Run` / `UsageEvent` 两 class + 删 `BigInteger` import;`Task` 加两列;`storage/__init__.py` 文档示例同步;`Task.run_status` 终态语义:`ok / cancelled` 收尾都回 `idle`(用户视角"跑完 / 停了"等价不留持久标记),只有 `error` 是持久终态,起新 run 时清。**Broker**(`web/broker.py`)全面 task_id 索引:`_subs / _done / _cancel_flags` 三个 dict key 从 run_id 换 task_id;加 `start(task_id)` 入口在新 run 起来前清 `_done` 标记(避免上一轮 done 让新订阅者立刻断流)。**Sink**(`web/sinks.py`)绑 task_id 替代 run_id。**`web/app.py`**:① `_run_agent_bg(task_id, user_id, content)` 去掉 run_id 参数;装 `agent.cancel_check = lambda tid=task_id: broker.is_cancelled(tid)`;终态写 `tasks.run_status = "idle"`(原 `Run.status = "ok"/"cancelled"`)或 `"error"`(`run_error = err`);finally `broker.clear_cancel(tid) + broker.close(tid)`。② `POST /v1/tasks/{tid}/messages` 改:`SELECT Task.run_status … FOR UPDATE` 替代 `select(Run.run_id) … running/cancelling`;同事务 `UPDATE Task SET run_status='running', run_error=NULL`(error 也算可重启视为清);commit 后 `broker.start(tid)` 清 done;返 `{"events_url": "/v1/tasks/{tid}/events"}` 去掉 `run_id`。③ `POST /v1/tasks/{tid}/cancel` 取代 `POST /v1/tasks/{tid}/runs/{rid}/cancel`,只校验 task 归属 user;`run_status != 'running'` → 409。④ `GET /v1/tasks/{tid}/events` 取代 `/runs/{rid}/events`,broker.subscribe(tid)。⑤ lifespan reaper `UPDATE Task SET run_status='error' WHERE run_status IN ('running','cancelling')`,文案不变。⑥ `_task_dict` 暴露 `run_status` / `run_error` 字段给前端。**dev SPA**(`web/static/dev.html`):`state.currentRunId` 改 `state.streaming` bool;`POST /messages` 拿到 `events_url` 直接订阅,不再保存 run_id;cancel 按钮 click → `POST /v1/tasks/{tid}/cancel`(去掉 `/runs/{rid}/`)。**Migration 跑通**:本地 PG `db upgrade 0003 → 0004 (head)` 一把过(用户授权清旧数据,无 backfill)。**Smoke 18 case 全绿**(in-process TestClient + BG mock):POST /messages 返 `events_url` 无 run_id / tasks.run_status='running' / gate when running 409 / POST /cancel 202 + run_status='cancelling' + broker flag set / double cancel 409(状态非 running)/ gate during cancelling 也 409 / cancel idle 409 / cancel error 409 / error 状态可发新消息(error 不挂 gate + 清 run_error) / ghost task 404 / invalid UUID 404 / cross-user 404 / no auth 401 / GET /events 路由注册(SSE 流式跑会挂 30s 心跳,smoke 只验路径 + headers) / GET /tasks 返回 run_status / run_error 字段 / stale reaper 扫 running+cancelling 标 error / broker.start 清 _done / broker.subscribe + emit + close + late subscriber 立刻收 done / broker.request_cancel + is_cancelled + clear_cancel。**净增量**:核心代码 -200 行(删表 ORM + 两路由层简化),broker 加 21 行 start/cancel API,dev.html 几行字段重命名;DB 表 5 → 3,路由 `/runs/{rid}/{events,cancel}` → `/{events,cancel}`,前端 SPA 不再需要先拿 run_id 才能 cancel / 订阅 — 客户端只需 task_id。**文档同步**:DESIGN §7.2 路由表 messages 路由返 `events_url`(去 `run_id`)+ cancel / events 改 task-level + lead-in 注 0004 简化 + SSE schema text event 字段 `delta`(实际就是 delta,文档原 `content` 笔误);§7.4 schema 块 tasks 加两列 + 注 0004 合并;§7.9 hard cascade 行注 "原 usage_events 0004 删" + 加专项取舍说明"0004 删 runs + usage_events 表";§7.7 风险表两行同步 / 改 task-level 路由名;RUN 路由表三路由全改 + 故障兜底 cancel 409 文案改 + db upgrade head 改 0004;PROGRESS 已完成 + 状态表 + 文件清单。
|
||||||
|
|
|
||||||
24
web/app.py
24
web/app.py
|
|
@ -276,6 +276,12 @@ def create_app() -> FastAPI:
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def lifespan(app: FastAPI):
|
async def lifespan(app: FastAPI):
|
||||||
broker.bind_loop(asyncio.get_running_loop())
|
broker.bind_loop(asyncio.get_running_loop())
|
||||||
|
# Skill 注册表启动时扫一次 — 文件系统静态,运行中不变;/v1/skills 直接读
|
||||||
|
from core.agent_builder import load_config
|
||||||
|
from core.paths import ROOT
|
||||||
|
from core.skills import SkillRegistry
|
||||||
|
_cfg = load_config()
|
||||||
|
app.state.skill_registry = SkillRegistry(ROOT / _cfg.get("skills_dir", "skills"))
|
||||||
# Stale-run reaper:上次进程 crash 留下的 "running" / "cancelling" 已无 BG 线程
|
# Stale-run reaper:上次进程 crash 留下的 "running" / "cancelling" 已无 BG 线程
|
||||||
# 继续,启动时标 error,让对应 task 重新可发消息(否则 gate 永挂)。
|
# 继续,启动时标 error,让对应 task 重新可发消息(否则 gate 永挂)。
|
||||||
# TODO 真生产 multi-worker:换 heartbeat / lease,只 reap 自家 worker 的孤儿。
|
# TODO 真生产 multi-worker:换 heartbeat / lease,只 reap 自家 worker 的孤儿。
|
||||||
|
|
@ -527,6 +533,24 @@ def create_app() -> FastAPI:
|
||||||
folders.sort(key=lambda f: f["last_used"] or "", reverse=True)
|
folders.sort(key=lambda f: f["last_used"] or "", reverse=True)
|
||||||
return {"folders": folders}
|
return {"folders": folders}
|
||||||
|
|
||||||
|
# ───────────── Skills ─────────────
|
||||||
|
|
||||||
|
@app.get("/v1/skills", tags=["skills"])
|
||||||
|
def list_skills(user_id: UUID = Depends(require_user)):
|
||||||
|
"""列出当前可用的 skill(智能体类型),供新建 task 时下拉选择。
|
||||||
|
|
||||||
|
skill registry 在 lifespan 启动时扫一次(`skills/<name>/SKILL.md` frontmatter),
|
||||||
|
运行中不变;返 name + description 给 UI 渲染。排序按 name 升序(registry 已 sorted)。
|
||||||
|
"""
|
||||||
|
reg = getattr(app.state, "skill_registry", None)
|
||||||
|
skills = list(reg.skills.values()) if reg else []
|
||||||
|
return {
|
||||||
|
"skills": [
|
||||||
|
{"name": s.name, "description": s.description}
|
||||||
|
for s in skills
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
@app.delete("/v1/tasks/{task_id}", status_code=204, tags=["tasks"])
|
@app.delete("/v1/tasks/{task_id}", status_code=204, tags=["tasks"])
|
||||||
def delete_task(task_id: str, user_id: UUID = Depends(require_user)):
|
def delete_task(task_id: str, user_id: UUID = Depends(require_user)):
|
||||||
"""硬删除:DELETE DB 行(messages / runs CASCADE)。**FS task_dir 不动**
|
"""硬删除:DELETE DB 行(messages / runs CASCADE)。**FS task_dir 不动**
|
||||||
|
|
|
||||||
|
|
@ -345,8 +345,10 @@
|
||||||
<div class="small muted" id="nt-wd-hint" style="margin-top:4px;min-height:1em;"></div>
|
<div class="small muted" id="nt-wd-hint" style="margin-top:4px;min-height:1em;"></div>
|
||||||
<label for="nt-desc">描述(可选,任务长描述)</label>
|
<label for="nt-desc">描述(可选,任务长描述)</label>
|
||||||
<input id="nt-desc" />
|
<input id="nt-desc" />
|
||||||
<label for="nt-skill">智能体类型(可选,如 coding / ppt / proposal)</label>
|
<label for="nt-skill">智能体类型(可选)</label>
|
||||||
<input id="nt-skill" />
|
<select id="nt-skill">
|
||||||
|
<option value="">(默认 · 不限定)</option>
|
||||||
|
</select>
|
||||||
<div class="err" id="nt-err"></div>
|
<div class="err" id="nt-err"></div>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button id="nt-cancel">取消</button>
|
<button id="nt-cancel">取消</button>
|
||||||
|
|
@ -1065,7 +1067,7 @@ $("hd-new").onclick = async () => {
|
||||||
$("nt-err").textContent = "";
|
$("nt-err").textContent = "";
|
||||||
$("nt-wd-hint").textContent = "";
|
$("nt-wd-hint").textContent = "";
|
||||||
$("new-task-modal").classList.add("show");
|
$("new-task-modal").classList.add("show");
|
||||||
await loadFolderSuggestions(); // 拉已有目录填 datalist
|
await Promise.all([loadFolderSuggestions(), loadSkillOptions()]);
|
||||||
$("nt-name").focus();
|
$("nt-name").focus();
|
||||||
};
|
};
|
||||||
$("nt-cancel").onclick = () => $("new-task-modal").classList.remove("show");
|
$("nt-cancel").onclick = () => $("new-task-modal").classList.remove("show");
|
||||||
|
|
@ -1073,7 +1075,7 @@ $("nt-go").onclick = async () => {
|
||||||
const name = $("nt-name").value.trim();
|
const name = $("nt-name").value.trim();
|
||||||
const working_dir = $("nt-wd").value.trim();
|
const working_dir = $("nt-wd").value.trim();
|
||||||
const desc = $("nt-desc").value.trim();
|
const desc = $("nt-desc").value.trim();
|
||||||
const skill = $("nt-skill").value.trim();
|
const skill = $("nt-skill").value;
|
||||||
$("nt-err").textContent = "";
|
$("nt-err").textContent = "";
|
||||||
if (!name) { $("nt-err").textContent = "任务名为必填项"; return; }
|
if (!name) { $("nt-err").textContent = "任务名为必填项"; return; }
|
||||||
try {
|
try {
|
||||||
|
|
@ -1101,6 +1103,27 @@ async function loadFolderSuggestions() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 智能体类型下拉:skill registry 服务器端静态,首次加载后缓存到 state.skills
|
||||||
|
async function loadSkillOptions() {
|
||||||
|
const sel = $("nt-skill");
|
||||||
|
if (!state.skills) {
|
||||||
|
try {
|
||||||
|
const data = await api("GET", "/v1/skills");
|
||||||
|
state.skills = data.skills || [];
|
||||||
|
} catch (e) {
|
||||||
|
state.skills = []; // 静默兜底,select 仍保留"(默认)"项
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 渲染:第一项固定为"默认"(空 value),其后逐 skill 一项
|
||||||
|
const opts = ['<option value="">(默认 · 不限定)</option>'];
|
||||||
|
for (const s of state.skills) {
|
||||||
|
const label = `${s.name}${s.description ? " — " + s.description : ""}`;
|
||||||
|
opts.push(`<option value="${escapeHtml(s.name)}" title="${escapeHtml(s.description || "")}">${escapeHtml(label)}</option>`);
|
||||||
|
}
|
||||||
|
sel.innerHTML = opts.join("");
|
||||||
|
sel.value = ""; // hd-new 已清空,这里幂等再保一次
|
||||||
|
}
|
||||||
|
|
||||||
$("nt-wd").addEventListener("input", () => {
|
$("nt-wd").addEventListener("input", () => {
|
||||||
const v = $("nt-wd").value.trim();
|
const v = $("nt-wd").value.trim();
|
||||||
const hint = $("nt-wd-hint");
|
const hint = $("nt-wd-hint");
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue