api+ui(dev SPA): POST /v1/tasks/{id}/clear 清空对话 + 顶栏「清空对话」按钮
- 后端: 同事务 SELECT FOR UPDATE 锁 + active run 检查(running/cancelling → 409) + DELETE messages + reset tasks.tokens_prompt/completion/cost_usd=0 + run_status='idle' - usage_events 完全不动 — 用户级账单 source of truth 与对话清空解耦; message_id FK 是 ondelete=SET NULL,task_id/units/cost_usd 全保留可重建累计 - dev SPA 顶栏在导出后插「清空对话」(紫色 hover,介于完成绿/废弃橙/删除红), running||n_messages==0 → disabled,confirm 二次确认 + 同步刷新 chat-meta / 消息 / 任务列表 - FS 文件保留(沿用 task delete 的"FS 视图可重生"心智) - RUN.md API 表 + 故障兜底加 409 case;DESIGN.md 不动(无架构 / schema 字段语义变化) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
ecff1d7858
commit
d1a2961bf4
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
> 配合 `DESIGN.md`。本文件只记 phase 状态、决策偏差、文件量、下一步。每条 2-4 句:做了啥 + 关键判断 + 没动什么;细节查 `git log` / `git diff`。
|
> 配合 `DESIGN.md`。本文件只记 phase 状态、决策偏差、文件量、下一步。每条 2-4 句:做了啥 + 关键判断 + 没动什么;细节查 `git log` / `git diff`。
|
||||||
|
|
||||||
最后更新:2026-05-20(task 级「宪法」文件命名约定 + `spec_lock` → `spec` 简化 — 解决同 working_dir 多 task 的 spec 文件冲突)
|
最后更新:2026-05-20(`POST /v1/tasks/{id}/clear` 清空对话 + dev SPA 清空对话按钮)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -23,6 +23,7 @@
|
||||||
|
|
||||||
### 2026-05-20
|
### 2026-05-20
|
||||||
|
|
||||||
|
- **`POST /v1/tasks/{id}/clear` 清空对话 + dev SPA「清空对话」按钮**:用户要在同一 task 内重新开始对话。后端新路由:同事务 `SELECT … FOR UPDATE` 锁 + `run_status in (running, cancelling)` → 409(先 cancel)+ `DELETE FROM messages WHERE task_id=tid` + reset `tasks.tokens_prompt/completion/cost_usd=0` + `run_status='idle'` + `run_error=None`,返回新 task dict(`n_messages=0`)。**`usage_events` 表完全不动** — 那是用户级账户账单的 source of truth,清空对话不该影响计费;`usage_events.message_id` FK 是 `ondelete=SET NULL`(models.py:128),message_id 列变 NULL,但 task_id/model_profile/units(tokens_in/out)/cost_usd 全保留,按 task_id 聚合可重建历史累计。**reset task 三列累计 vs 保留累计**:选 reset,因为顶栏「N 条 · M tok」显示"0 条 vs 50k tok"会视觉矛盾;真正账单数据在 usage_events 完整无损。dev SPA 顶栏在「导出对话记录」后插「清空对话」按钮(紫色 hover #8e44ad,区别于完成绿/废弃橙/删除红),`renderChatMeta` 里 `running||n_messages==0 → disabled`,confirm 二次确认(显示任务名 + 消息数),clear 后 `renderMessages([])` + `renderChatMeta()` + `loadTaskList()` 同步列表。**没动**:DESIGN(无架构/schema 字段语义变化)、其他 task 写路径、FS 文件(沿用 task delete 的"FS 视图可重生"心智 — 中间产物保留,模型重起对话可继续基于已有素材推进)、SSE 协议。
|
||||||
- **dev SPA 对话内 tool_call/result 加 artifact chip(复用文件预览 modal)**:用户反馈"中间产物只能在右栏点,对话里不能直接预览/下载"。`web/static/dev.html` 新加两个 helper:`extractArtifactRels(text, workingDir)` 把文本里 `\` 一律归 `/`,正则锚定 `<working_dir>/...`(lead 边界字符类 `[\s"'\`/=:,()<>\[\]{}|]` 避免 `multi_proj_x` 误匹配,末段必须含 `.` 把目录滤掉),Set 去重;`renderArtifactBarHtml(rels)` 渲一行 `.art-chip` 小药丸(`📄 文件名`,前缀 emoji + hover 翻品牌红)。四个渲染点都插入 chip 条:① `renderMessages` 的 `role==="tool"` 历史卡;② `renderMessages` 的 assistant `tool_calls` 历史;③ `handleSseEvent` 的 `tool_call` 流式;④ `handleSseEvent` 的 `tool_result` 流式。`chat-stream` 上加点击委托 → `openFilePreview(rel)`,modal 内已带"下载"按钮所以 chip 不另开二级图标。**取舍**:路径识别限定 `working_dir/` 前缀(skill 脚本 `cd` 后只 print 纯相对路径的情况会漏抓,v1 误判控制代价);纯目录(末段无 `.`)直接跳过。**没动**:右栏文件面板、`openFilePreview` / `downloadFile` 接口(纯复用)、后端、DESIGN、RUN(对外行为零变化,纯 UI 增量)。
|
- **dev SPA 对话内 tool_call/result 加 artifact chip(复用文件预览 modal)**:用户反馈"中间产物只能在右栏点,对话里不能直接预览/下载"。`web/static/dev.html` 新加两个 helper:`extractArtifactRels(text, workingDir)` 把文本里 `\` 一律归 `/`,正则锚定 `<working_dir>/...`(lead 边界字符类 `[\s"'\`/=:,()<>\[\]{}|]` 避免 `multi_proj_x` 误匹配,末段必须含 `.` 把目录滤掉),Set 去重;`renderArtifactBarHtml(rels)` 渲一行 `.art-chip` 小药丸(`📄 文件名`,前缀 emoji + hover 翻品牌红)。四个渲染点都插入 chip 条:① `renderMessages` 的 `role==="tool"` 历史卡;② `renderMessages` 的 assistant `tool_calls` 历史;③ `handleSseEvent` 的 `tool_call` 流式;④ `handleSseEvent` 的 `tool_result` 流式。`chat-stream` 上加点击委托 → `openFilePreview(rel)`,modal 内已带"下载"按钮所以 chip 不另开二级图标。**取舍**:路径识别限定 `working_dir/` 前缀(skill 脚本 `cd` 后只 print 纯相对路径的情况会漏抓,v1 误判控制代价);纯目录(末段无 `.`)直接跳过。**没动**:右栏文件面板、`openFilePreview` / `downloadFile` 接口(纯复用)、后端、DESIGN、RUN(对外行为零变化,纯 UI 增量)。
|
||||||
- **task 级「宪法」文件 (spec) 命名约定 + `spec_lock` → `spec` 简化**:同 working_dir 多 task 共享中间产物(`source/` / `sections/` / `figures/` 跨本子复用)是设计意图,但 spec 这种 task 1:1 宪法文件必须隔离 — 两本子 spec 直接撞。文件名约定 `<YYYY-MM-DD>-<task_short_id>-<task_name>.spec.md`:`task_short_id`(`task_id.hex[:8]`,永不变)作主锚,glob `*-<short_id>-*.spec.md` 字典序最大 = current;`<YYYY-MM-DD>` 让"重定调"写新文件而非 edit 覆盖,旧版自然成历史快照;`<task_name>` 写入作建时元数据,改 task.name 不 cascade(由 short_id 兜底定位)。`core/agent_builder.py::_build_system_prompt` 加 `task_id` / `today` 注入 + 命名约定段 — 所有 skill 共享一份约定文本,SKILL.md 不再重复;proposal / ppt SKILL.md 阶段一加"先 glob 检测已有 spec → 询问沿用/重定调"分支。`_lock` 后缀无信息量去掉(`templates/spec_lock.md` → `templates/spec.md` git mv 保历史)。**没动**:DB schema(无新字段)、`PATCH /v1/tasks/{id}` 改 name 入口(免 cascade)、其他中间产物扁平共享、quality_check.py(`--spec` 接路径,SKILL.md 拼对参数即可)。**反方案**(cascade rename / spec 入 PG / 物理 task 子目录)及"何时升级到 DB 化"信号见 DESIGN §7.9 取舍说明。
|
- **task 级「宪法」文件 (spec) 命名约定 + `spec_lock` → `spec` 简化**:同 working_dir 多 task 共享中间产物(`source/` / `sections/` / `figures/` 跨本子复用)是设计意图,但 spec 这种 task 1:1 宪法文件必须隔离 — 两本子 spec 直接撞。文件名约定 `<YYYY-MM-DD>-<task_short_id>-<task_name>.spec.md`:`task_short_id`(`task_id.hex[:8]`,永不变)作主锚,glob `*-<short_id>-*.spec.md` 字典序最大 = current;`<YYYY-MM-DD>` 让"重定调"写新文件而非 edit 覆盖,旧版自然成历史快照;`<task_name>` 写入作建时元数据,改 task.name 不 cascade(由 short_id 兜底定位)。`core/agent_builder.py::_build_system_prompt` 加 `task_id` / `today` 注入 + 命名约定段 — 所有 skill 共享一份约定文本,SKILL.md 不再重复;proposal / ppt SKILL.md 阶段一加"先 glob 检测已有 spec → 询问沿用/重定调"分支。`_lock` 后缀无信息量去掉(`templates/spec_lock.md` → `templates/spec.md` git mv 保历史)。**没动**:DB schema(无新字段)、`PATCH /v1/tasks/{id}` 改 name 入口(免 cascade)、其他中间产物扁平共享、quality_check.py(`--spec` 接路径,SKILL.md 拼对参数即可)。**反方案**(cascade rename / spec 入 PG / 物理 task 子目录)及"何时升级到 DB 化"信号见 DESIGN §7.9 取舍说明。
|
||||||
- **dev SPA 左 pane 折叠改 rail 模式 + 删 header 冗余按钮 + time-ago 锁宽完成跨行对齐**:用户反馈 ① "原来 zcbot 旁的折叠按钮不要了,没用处" + ② "数字对齐那块现在是不是每块内容左侧对齐?"(实际是右对齐但因 time-ago 宽度变化导致 N 条/N tok 右边界也跟着抖,跨行没真对齐)。两件套:① 折叠模式从「pane display:none」改 VS Code 范式 rail —— `body.left-collapsed #app.ready { grid-template-columns: 40px 1fr 320px }` + `#pane-left > * { display: none }`(藏全部直接子) + override 第一行 pane-head 重显且只留 `#pane-toggle-left`(`> *:not(#pane-toggle-left) { display: none }`,选择器特异性 2 ids 压 1 id);pane-head 第一行用 `position: static` 取消 sticky / `border-bottom: none` / `background: transparent` 看起来更像 rail 非"卡片"。按钮符号根据 `body.left-collapsed` 在 `applyLeftCollapsed` 里翻向(展开态 `‹` 折叠态 `›`)。彻底删 `#hd-toggle-left` + `header .icon-btn` CSS 块,header 不再背 expand 入口的债。② time-ago 加 `flex-shrink: 0; text-align: right; min-width: 64px` 锁宽,**这才是真正解决跨行对齐的关键**:此前 `.num.right-group` 用 `margin-left: auto` 把 [N 条][N tok][time] 整组推右,但 time 自身宽度浮动 30~70px(刚刚 / 10 小时前 / 2025-12-05)→ time 左边界抖 → N tok 右边界抖 → N 条 右边界抖,逐级传染。锁 time 宽后整组位置稳定,槽内 `text-align: right` 才能让"条/tok"后缀跨行真正垂直对齐。删 `.badge .time-ago { flex-shrink: 0 }` 合并里的 time-ago(已独立给规则)。**没动**:fmtTokens / 桶分级 / tabular-nums / `.num min-width: 44px`(上一轮已正确)、右 pane / chat 中列。
|
- **dev SPA 左 pane 折叠改 rail 模式 + 删 header 冗余按钮 + time-ago 锁宽完成跨行对齐**:用户反馈 ① "原来 zcbot 旁的折叠按钮不要了,没用处" + ② "数字对齐那块现在是不是每块内容左侧对齐?"(实际是右对齐但因 time-ago 宽度变化导致 N 条/N tok 右边界也跟着抖,跨行没真对齐)。两件套:① 折叠模式从「pane display:none」改 VS Code 范式 rail —— `body.left-collapsed #app.ready { grid-template-columns: 40px 1fr 320px }` + `#pane-left > * { display: none }`(藏全部直接子) + override 第一行 pane-head 重显且只留 `#pane-toggle-left`(`> *:not(#pane-toggle-left) { display: none }`,选择器特异性 2 ids 压 1 id);pane-head 第一行用 `position: static` 取消 sticky / `border-bottom: none` / `background: transparent` 看起来更像 rail 非"卡片"。按钮符号根据 `body.left-collapsed` 在 `applyLeftCollapsed` 里翻向(展开态 `‹` 折叠态 `›`)。彻底删 `#hd-toggle-left` + `header .icon-btn` CSS 块,header 不再背 expand 入口的债。② time-ago 加 `flex-shrink: 0; text-align: right; min-width: 64px` 锁宽,**这才是真正解决跨行对齐的关键**:此前 `.num.right-group` 用 `margin-left: auto` 把 [N 条][N tok][time] 整组推右,但 time 自身宽度浮动 30~70px(刚刚 / 10 小时前 / 2025-12-05)→ time 左边界抖 → N tok 右边界抖 → N 条 右边界抖,逐级传染。锁 time 宽后整组位置稳定,槽内 `text-align: right` 才能让"条/tok"后缀跨行真正垂直对齐。删 `.badge .time-ago { flex-shrink: 0 }` 合并里的 time-ago(已独立给规则)。**没动**:fmtTokens / 桶分级 / tabular-nums / `.num min-width: 44px`(上一轮已正确)、右 pane / chat 中列。
|
||||||
|
|
|
||||||
4
RUN.md
4
RUN.md
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
> 怎么把 zcbot 跑起来。env / 常用命令 / 故障兜底。设计看 `DESIGN.md`,进度看 `PROGRESS.md`。
|
> 怎么把 zcbot 跑起来。env / 常用命令 / 故障兜底。设计看 `DESIGN.md`,进度看 `PROGRESS.md`。
|
||||||
|
|
||||||
最后更新:2026-05-20(加 GLM 5.1 模型档案:`.env` 加 `ZHIPUAI_API_KEY`,probe 示例加 `glm.pro`)
|
最后更新:2026-05-20(加 `POST /v1/tasks/{id}/clear` 清空对话路由)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -126,6 +126,7 @@ curl --noproxy '*' -H "Authorization: Bearer $TOKEN" http://127.0.0.1:8765/v1/ta
|
||||||
| `POST /v1/tasks/{id}/messages` | `{content}` 发消息;返 `{events_url}`;**`run_status` 是 running/cancelling → 409**(单活 run;error 起新 run 时清);UI 应 disable send 直到 SSE `done` | 必填 |
|
| `POST /v1/tasks/{id}/messages` | `{content}` 发消息;返 `{events_url}`;**`run_status` 是 running/cancelling → 409**(单活 run;error 起新 run 时清);UI 应 disable send 直到 SSE `done` | 必填 |
|
||||||
| `GET /v1/tasks/{id}/events` | SSE 流(`event: <type>` + `data: <json>`);订阅 task 当前活动 | 必填 |
|
| `GET /v1/tasks/{id}/events` | SSE 流(`event: <type>` + `data: <json>`);订阅 task 当前活动 | 必填 |
|
||||||
| `POST /v1/tasks/{id}/cancel` | 协作式 cancel;`run_status != running` → 409;LLM 同步 call 不可中断,最坏等当前一轮跑完 | 必填 |
|
| `POST /v1/tasks/{id}/cancel` | 协作式 cancel;`run_status != running` → 409;LLM 同步 call 不可中断,最坏等当前一轮跑完 | 必填 |
|
||||||
|
| `POST /v1/tasks/{id}/clear` | 清空当前 task 全部 messages + reset `tasks.tokens_prompt/completion/cost_usd` 三列累计 + `run_status='idle'`;`usage_events`(账单记账)**不动**,只 `message_id` 列变 NULL;run 活跃中(running/cancelling)→ 409(先 cancel);FS 文件保留 | 必填 |
|
||||||
| `GET /v1/files?path=` | 列 user_root 下条目 + 面包屑;dotfile 隐藏 | 必填 |
|
| `GET /v1/files?path=` | 列 user_root 下条目 + 面包屑;dotfile 隐藏 | 必填 |
|
||||||
| `GET /v1/files/download?path=` | 下单文件 | 必填 |
|
| `GET /v1/files/download?path=` | 下单文件 | 必填 |
|
||||||
| `POST /v1/files/upload` | multipart 上传到 `<user_root>/<path>/`;路径不存在自动 mkdir,重名覆盖 | 必填 |
|
| `POST /v1/files/upload` | multipart 上传到 `<user_root>/<path>/`;路径不存在自动 mkdir,重名覆盖 | 必填 |
|
||||||
|
|
@ -251,6 +252,7 @@ sudo journalctl -u zcbot -n 50 # 看新进程起没起干
|
||||||
| platform CORS preflight 失败 | 本地 dev `allow_origins=["*"]` 应该没事;部署后看是否按 platform 域名收紧过头 |
|
| platform CORS preflight 失败 | 本地 dev `allow_origins=["*"]` 应该没事;部署后看是否按 platform 域名收紧过头 |
|
||||||
| `POST /v1/tasks/{id}/messages` 返 409 `task already has an active run` | 上一条消息的 BG run 还没跑完;等流式 done 或点 stop / `POST .../cancel`;服务异常下 `run_status` 卡 `running`/`cancelling`,启动 reaper 会清 |
|
| `POST /v1/tasks/{id}/messages` 返 409 `task already has an active run` | 上一条消息的 BG run 还没跑完;等流式 done 或点 stop / `POST .../cancel`;服务异常下 `run_status` 卡 `running`/`cancelling`,启动 reaper 会清 |
|
||||||
| `POST /v1/tasks/{id}/cancel` 返 409 `task not running` | `run_status` 不是 `running`(idle / cancelling / error 都不能 cancel);dev SPA 自动忽略不报错 |
|
| `POST /v1/tasks/{id}/cancel` 返 409 `task not running` | `run_status` 不是 `running`(idle / cancelling / error 都不能 cancel);dev SPA 自动忽略不报错 |
|
||||||
|
| `POST /v1/tasks/{id}/clear` 返 409 `task has an active run` | 当前 run 还没跑完;先点停止 / `POST .../cancel` 等流式 done 再清空 |
|
||||||
| 点 stop 后流式没立刻停 | LLM 同步 call 不可中断,最坏等当前一轮跑完(几十秒);loop 进入下个 check 点(每轮 LLM 前 / 每个 tool_call 前)就退,emit `cancelled` → SSE `done` → UI 收回 stop |
|
| 点 stop 后流式没立刻停 | LLM 同步 call 不可中断,最坏等当前一轮跑完(几十秒);loop 进入下个 check 点(每轮 LLM 前 / 每个 tool_call 前)就退,emit `cancelled` → SSE `done` → UI 收回 stop |
|
||||||
| `[startup] reaped N stale active run(s)` | 上次 web 进程未正常 finish 留下 N 个孤儿 run,启动 lifespan 自动标 error。info 级,无需处理 |
|
| `[startup] reaped N stale active run(s)` | 上次 web 进程未正常 finish 留下 N 个孤儿 run,启动 lifespan 自动标 error。info 级,无需处理 |
|
||||||
| `kill -HUP <pid>` 后 `/openapi.json` 没新接口 | uvicorn **不响应 SIGHUP**(没装 handler,落 Python 默认终止;Windows 上信号本身无效)。Ubuntu 上用 `systemctl restart zcbot`,或 unit 加 `--reload` 让 uvicorn 监听文件自动重起(见"部署"段)。验证:`curl -s http://127.0.0.1:8765/openapi.json \| python3 -c 'import sys,json;print([p for p in json.load(sys.stdin)["paths"] if "auth" in p])'` |
|
| `kill -HUP <pid>` 后 `/openapi.json` 没新接口 | uvicorn **不响应 SIGHUP**(没装 handler,落 Python 默认终止;Windows 上信号本身无效)。Ubuntu 上用 `systemctl restart zcbot`,或 unit 加 `--reload` 让 uvicorn 监听文件自动重起(见"部署"段)。验证:`curl -s http://127.0.0.1:8765/openapi.json \| python3 -c 'import sys,json;print([p for p in json.load(sys.stdin)["paths"] if "auth" in p])'` |
|
||||||
|
|
|
||||||
47
web/app.py
47
web/app.py
|
|
@ -921,6 +921,53 @@ def create_app() -> FastAPI:
|
||||||
broker.request_cancel(tid)
|
broker.request_cancel(tid)
|
||||||
return {"ok": True, "task_id": str(tid), "run_status": "cancelling"}
|
return {"ok": True, "task_id": str(tid), "run_status": "cancelling"}
|
||||||
|
|
||||||
|
# ───────────── Clear conversation ─────────────
|
||||||
|
|
||||||
|
@app.post("/v1/tasks/{task_id}/clear", tags=["messages"])
|
||||||
|
def clear_messages(task_id: str, user_id: UUID = Depends(require_user)):
|
||||||
|
"""清空当前 task 全部 messages,token 累计 / cost / run_error 归零。
|
||||||
|
|
||||||
|
同 working_dir 下的 FS 文件不动(沿用 task delete 的"FS 视图可重生"心智 —
|
||||||
|
中间产物保留,模型重起对话时可继续基于已有素材推进)。
|
||||||
|
usage_events 不动:那是用户级账户级用量记账,不该被对话清理影响。
|
||||||
|
|
||||||
|
- 活跃 run(running / cancelling)期间拒绝:409(先 cancel)
|
||||||
|
- error 状态可清:顺手 run_status='idle' + run_error=None
|
||||||
|
- 跨 user → 404
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
tid = UUID(task_id)
|
||||||
|
except ValueError:
|
||||||
|
raise HTTPException(404, f"invalid task id: {task_id!r}")
|
||||||
|
from sqlalchemy import delete as _delete
|
||||||
|
with session_scope() as s:
|
||||||
|
row = s.execute(
|
||||||
|
select(Task.run_status)
|
||||||
|
.where(Task.task_id == tid, Task.user_id == user_id)
|
||||||
|
.with_for_update()
|
||||||
|
).first()
|
||||||
|
if row is None:
|
||||||
|
raise HTTPException(404, f"task not found: {tid}")
|
||||||
|
if row.run_status in ("running", "cancelling"):
|
||||||
|
raise HTTPException(
|
||||||
|
409,
|
||||||
|
f"task has an active run (status={row.run_status}); "
|
||||||
|
f"cancel it first",
|
||||||
|
)
|
||||||
|
s.execute(_delete(Message).where(Message.task_id == tid))
|
||||||
|
s.execute(
|
||||||
|
update(Task).where(Task.task_id == tid).values(
|
||||||
|
tokens_prompt=0,
|
||||||
|
tokens_completion=0,
|
||||||
|
cost_usd=0,
|
||||||
|
run_status="idle",
|
||||||
|
run_error=None,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
task_row = s.execute(select(Task).where(Task.task_id == tid)).scalar_one()
|
||||||
|
d = _task_dict(task_row, n_messages=0)
|
||||||
|
return d
|
||||||
|
|
||||||
# ───────────── SSE events ─────────────
|
# ───────────── SSE events ─────────────
|
||||||
|
|
||||||
@app.get("/v1/tasks/{task_id}/events", tags=["tasks"])
|
@app.get("/v1/tasks/{task_id}/events", tags=["tasks"])
|
||||||
|
|
|
||||||
|
|
@ -187,9 +187,10 @@
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-bottom: 1px solid var(--border-soft);
|
border-bottom: 1px solid var(--border-soft);
|
||||||
}
|
}
|
||||||
/* 对话顶栏按钮:常态中性 + hover 上语义色 — 完成 绿/导出 蓝/废弃 橙/删除 红 */
|
/* 对话顶栏按钮:常态中性 + hover 上语义色 — 完成 绿/导出 蓝/清空 紫/废弃 橙/删除 红 */
|
||||||
#btn-done:hover:not(:disabled) { color: #27ae60; border-color: #a9dfbf; background: #e9f7ef; }
|
#btn-done:hover:not(:disabled) { color: #27ae60; border-color: #a9dfbf; background: #e9f7ef; }
|
||||||
#btn-export:hover:not(:disabled) { color: #2980b9; border-color: #aed6f1; background: #ebf5fb; }
|
#btn-export:hover:not(:disabled) { color: #2980b9; border-color: #aed6f1; background: #ebf5fb; }
|
||||||
|
#btn-clear-msgs:hover:not(:disabled) { color: #8e44ad; border-color: #d2b4de; background: #f5eef8; }
|
||||||
#btn-abandon:hover:not(:disabled) { color: #e67e22; border-color: #f5cba7; background: #fef5e7; }
|
#btn-abandon:hover:not(:disabled) { color: #e67e22; border-color: #f5cba7; background: #fef5e7; }
|
||||||
#btn-delete-task:hover:not(:disabled) { color: #c0392b; border-color: #f5b7b1; background: #fdedec; }
|
#btn-delete-task:hover:not(:disabled) { color: #c0392b; border-color: #f5b7b1; background: #fdedec; }
|
||||||
#pane-mid > .pane-head > button.small:disabled { opacity: 0.4; cursor: not-allowed; }
|
#pane-mid > .pane-head > button.small:disabled { opacity: 0.4; cursor: not-allowed; }
|
||||||
|
|
@ -623,6 +624,7 @@
|
||||||
<span class="label">对话</span>
|
<span class="label">对话</span>
|
||||||
<span class="spacer"></span>
|
<span class="spacer"></span>
|
||||||
<button id="btn-export" class="small" disabled>导出对话记录</button>
|
<button id="btn-export" class="small" disabled>导出对话记录</button>
|
||||||
|
<button id="btn-clear-msgs" class="small" disabled title="清空当前任务的对话历史(messages + token 累计归零),工作目录文件保留">清空对话</button>
|
||||||
<button id="btn-done" class="small" disabled>完成</button>
|
<button id="btn-done" class="small" disabled>完成</button>
|
||||||
<button id="btn-abandon" class="small danger" disabled>废弃</button>
|
<button id="btn-abandon" class="small danger" disabled>废弃</button>
|
||||||
<button id="btn-delete-task" class="small danger" disabled title="硬删除:清 DB 行 + messages,FS 文件不动">删除</button>
|
<button id="btn-delete-task" class="small danger" disabled title="硬删除:清 DB 行 + messages,FS 文件不动">删除</button>
|
||||||
|
|
@ -1235,6 +1237,9 @@ function renderChatMeta() {
|
||||||
$("btn-abandon").disabled = !active;
|
$("btn-abandon").disabled = !active;
|
||||||
$("btn-delete-task").disabled = false; // delete 不限 status(用户显式 confirm)
|
$("btn-delete-task").disabled = false; // delete 不限 status(用户显式 confirm)
|
||||||
$("btn-export").disabled = (t.n_messages || 0) === 0;
|
$("btn-export").disabled = (t.n_messages || 0) === 0;
|
||||||
|
// 清空对话:活跃 run 期间禁用(后端 409),无消息也无意义
|
||||||
|
const running = t.run_status === "running" || t.run_status === "cancelling";
|
||||||
|
$("btn-clear-msgs").disabled = running || (t.n_messages || 0) === 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderModelDropdown(t) {
|
function renderModelDropdown(t) {
|
||||||
|
|
@ -1314,6 +1319,11 @@ function renderMessages(msgs) {
|
||||||
let html = `<div class="role">${roleLabel}</div>`;
|
let html = `<div class="role">${roleLabel}</div>`;
|
||||||
if (typeof p.content === "string" && p.content) {
|
if (typeof p.content === "string" && p.content) {
|
||||||
html += `<div class="body">${renderMd(p.content)}</div>`;
|
html += `<div class="body">${renderMd(p.content)}</div>`;
|
||||||
|
// assistant 正文里 echo 的 <wd>/... 路径同样挂 chip 条(只对 assistant,user 输入不抽)
|
||||||
|
if (role === "assistant") {
|
||||||
|
const wd = (state.taskMeta && state.taskMeta.working_dir) || "";
|
||||||
|
html += renderArtifactBarHtml(extractArtifactRels(p.content, wd));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (Array.isArray(p.tool_calls) && p.tool_calls.length) {
|
if (Array.isArray(p.tool_calls) && p.tool_calls.length) {
|
||||||
const wd = (state.taskMeta && state.taskMeta.working_dir) || "";
|
const wd = (state.taskMeta && state.taskMeta.working_dir) || "";
|
||||||
|
|
@ -1537,6 +1547,28 @@ $("btn-delete-task").onclick = () => {
|
||||||
deleteTask(state.taskId, t.name || "(未命名)", t.n_messages || 0);
|
deleteTask(state.taskId, t.name || "(未命名)", t.n_messages || 0);
|
||||||
};
|
};
|
||||||
$("btn-export").onclick = () => state.taskId && exportTask(state.taskId);
|
$("btn-export").onclick = () => state.taskId && exportTask(state.taskId);
|
||||||
|
$("btn-clear-msgs").onclick = () => {
|
||||||
|
if (!state.taskId) return;
|
||||||
|
const t = state.taskMeta || {};
|
||||||
|
clearMessages(state.taskId, t.name || "(未命名)", t.n_messages || 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
async function clearMessages(tid, name, nMsg) {
|
||||||
|
if (!confirm(`确认清空「${name}」的对话(${nMsg} 条消息)?\n\n将删除全部对话历史并重置 token 计数;工作目录下的文件保留。`)) return;
|
||||||
|
try {
|
||||||
|
const updated = await api("POST", "/v1/tasks/" + tid + "/clear");
|
||||||
|
if (state.taskId === tid) {
|
||||||
|
state.taskMeta = updated;
|
||||||
|
renderChatMeta();
|
||||||
|
renderMessages([]);
|
||||||
|
$("chat-hint").textContent = "对话已清空";
|
||||||
|
}
|
||||||
|
loadTaskList();
|
||||||
|
} catch (e) {
|
||||||
|
if (e.status === 401) { logout(); return; }
|
||||||
|
alert("清空失败:" + e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function setTaskStatus(tid, status, name) {
|
async function setTaskStatus(tid, status, name) {
|
||||||
const labels = { completed: "已完成", abandoned: "已废弃" };
|
const labels = { completed: "已完成", abandoned: "已废弃" };
|
||||||
|
|
@ -1566,6 +1598,7 @@ async function deleteTask(tid, name, nMsg) {
|
||||||
$("btn-abandon").disabled = true;
|
$("btn-abandon").disabled = true;
|
||||||
$("btn-delete-task").disabled = true;
|
$("btn-delete-task").disabled = true;
|
||||||
$("btn-export").disabled = true;
|
$("btn-export").disabled = true;
|
||||||
|
$("btn-clear-msgs").disabled = true;
|
||||||
}
|
}
|
||||||
loadTaskList();
|
loadTaskList();
|
||||||
loadFiles();
|
loadFiles();
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue