diff --git a/DESIGN.md b/DESIGN.md index 4611ac7..d97b455 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -417,7 +417,7 @@ create index on usage_events (model_profile, created_at); | Running task 被 rename / delete | 后端校验 + UI 禁按钮(详 §7.4) | | 误删 folder | 二确认 + 输入 folder 名;真要再加 trash bin | | DB-then-FS 中断留孤儿目录 | rename 顺序 DB UPDATE → FS rename(FS 失败回滚 DB);delete 后台 GC 周期扫"FS 有但 DB 无引用" | -| 同 folder 多 task 并发写同名 | 文件级悲观锁,冲突早失败 | +| 同 folder 多 task 并发写同名 | known limitation,实践频率近 0(同 wd 多 task 是"项目对话历史轨迹",非并发);dev SPA chat 区顶 banner 软警告(`GET /v1/tasks?working_dir=&run_status=running,cancelling` 拉同 wd 活跃邻居),不挡发送;宪法文件已由 `-` 命名隔离(§7.9 2026-05-20);真高频出现再加 gate | | 同 task 并发 POST messages 撞 `messages.idx` | `POST /messages` 单活 run gate:`SELECT … FOR UPDATE` 锁 task + `run_status in ('running','cancelling')` → 409;启动 lifespan reaper 把孤儿 `running`/`cancelling` 全标 error。未来 multi-worker 换 heartbeat / lease | | Run 跑太久 / 用户想中断 | `POST /v1/tasks/{id}/cancel` 协作式;LLM 走 streaming,chunk 间 poll cancel → 延迟 ~ 单 chunk 间隔(100ms 级)| | Sandbox 出站越权 | egress allowlist 起步只放 LLM + PyPI | @@ -449,6 +449,8 @@ create index on usage_events (model_profile, created_at); **Tasks/Messages 在 PG 但 skill 产物在 FS**:tasks / messages 需要查询、过滤、全文搜、跨 task 统计 — DB 强项;skill 产物(`*.pptx` / `*.docx` / `sections/*.md`)终用户拿走,期望文件管理器看到、Office 打开、邮件发出 — 进 DB 要做"导出"多余操作。**FS 是产物天然存储,DB 是元数据 / 状态 / 索引天然存储**。同理 §7.5 bind mount = user root,容器里 ≡ 用户在 Web UI 看到的目录,无中间层翻译。 +**同 wd 多 task 并发不做 gate / clone / 物理隔离,只做软警告**(2026-05-21):候选方案过 γ(同 wd 单活 run gate)/ short_id 全产物隔离 / clone task 三种 — 最终都判定过度工程。dogfood 经验:同 wd 多 task 主要是"项目对话历史轨迹",并发频率近 0(用户开新 task 多数是想换思路重启,但不与旧 task 同时跑)。**走 Claude Code 同款"信任 + 软警告 + 承认 limitation"**(它官方文档把"多 session 同 cwd plan 文件互覆"也定为 known limitation,推荐 git worktree 但不强制),不在主路径加复杂度。dev SPA 在 selectTask + SSE 收尾两个触发点拉 `GET /v1/tasks?working_dir=&run_status=running,cancelling`,有命中挂 banner;真高频再升级 γ。**为什么不选 γ**:同 wd 单活硬挡破坏"扁平共享中间产物"对应的对话切换流畅性,且 cancelling 状态可能阻塞用户 retry 时一个错觉的"我没在跑啊";**为什么不选 short_id 全产物**:破坏 §7.1 同 wd 共享中间产物语义(扁平 figures/sections/ 跨 task 复用)+ SKILL.md 改造成本;**为什么不选 clone task**:解决的是"真要并行"罕见场景,工程量(cp -r + 新 task 流程 + UI 入口)对零频场景过重。 + **task 级「宪法」文件靠文件名隔离,不 cascade / 不入 DB / 不开物理子目录**(2026-05-20):同 working_dir 多 task **共享中间产物**(`source/` / `sections/` / `figures/`)是真实价值(素材跨多本子复用),但 spec 这种 task 1:1 宪法文件必须隔离(两本子 spec 直接撞)。文件名 `--..md`:`task_short_id`(`task_id.hex[:8]`,永不变)主锚,glob `*--*..md` 字典序最大 = current 版本;`` 让"重定调"写新文件而非 edit 覆盖,旧版自然成历史快照;`` 仅作"建时元数据 / 人类可读说明",改 name 不 cascade(由 short_id 兜底定位)。**反方案不选**:① cascade rename — in-flight run 期间文件丢 + 复杂度上升;② DB 化(spec 入 PG)— 架构最干净但工作量 5-10×,且失"用户直接编辑 markdown"能力,且 spec 字段还在演化没必要这么早 schema 化;③ 物理 task 子目录(`//`)— 破坏 §7.4 中间产物扁平共享设计。**升级到 DB 化的信号**:dev SPA 想做结构化编辑视图 / 想跨 task 查询 spec 字段(基金类型 / 经费 / 考核指标)/ markdown 版本文件堆积乱。约定由 `core/agent_builder.py::_build_system_prompt` 单点注入(`task_id` / `today` 实际值嵌入),所有 skill SKILL.md 引用同一份(目前 proposal / ppt 的 `spec`,未来 `outline` 等同款)。 --- diff --git a/PROGRESS.md b/PROGRESS.md index 018a19f..f9f29f3 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -2,7 +2,7 @@ > 配合 `DESIGN.md`。本文件只记 phase 状态、决策偏差、文件量、下一步。每条 2-4 句:做了啥 + 关键判断 + 没动什么;细节查 `git log` / `git diff`。 -最后更新:2026-05-21(paper_server → zcbot research skill:新增 skills/research/{SKILL.md,paper.py};run_python 注入 PYTHONPATH=base_dir 让 skill 内 helper 可 import;paper_server 侧 PaperDetailSerializer 加 abstract 由用户重新部署) +最后更新:2026-05-21(同 wd 多 task 并发软警告 banner + task header `📁 wd` 仅在 name≠wdName 时挂 + `/v1/tasks` 加 `run_status` 筛选) --- @@ -23,6 +23,8 @@ ### 2026-05-21 +- **同 wd 并发软警告 banner + task header `📁 wd` 仅在 name≠wdName 时显示 + `/v1/tasks` 加 `run_status` 筛选**:用户问"task + working_dir 设计如何 / 同 wd 多 task 并发咋处理",评估了 γ(同 wd 单活 gate)/ short_id 全产物隔离 / clone task 三个方案均判定过度工程 —— dogfood 经验同 wd 基本不并发,定了 Claude Code 同款"信任 + 软警告 + 承认 limitation"。**后端**:`GET /v1/tasks` 加 `run_status` query 参数(逗号分隔,allowlist `idle/running/cancelling/error`,非法静默忽略),拼 `Task.run_status.in_(set)` 条件;复用现有 `(user_id, working_dir)` 索引,同 wd 活跃 task 常态 0/1 行近零开销。**前端**:① `refreshConcurrentWarnings()` 在 `selectTask` + SSE 收尾两点 fire-and-forget 拉 `working_dir=<末段名>&run_status=running,cancelling&page_size=10`,过滤 task_id != current 后存 `state.concurrentWarnings`;② `renderConcurrentWarning()` 在 `#chat-meta` 后 / `#chat-stream` 前插 `#wd-concurrent-warn` 黄底 banner(⚠ + 项目名 + 邻居 task name + run_status + "等 N 个"),非阻塞不挡发送;③ `renderChatMeta` 把 `📁 wdName` 改为"仅 wdName !== taskName 时显示"(留空 fallback 多数场景 name == wd,显示是噪音;不同时显示提示项目归属)。**对比方案**:γ 硬挡破坏对话切换流畅性,short_id 全产物破坏 §7.1 扁平共享语义 + SKILL.md 改造成本,clone task 工程量对零频场景过重;软警告 ~80 行(后端 10 + 前端 70)实现意图,真高频再升级。**没动**:不轮询(接受"邻居 task 在我浏览时收尾,banner 短暂 stale 几秒"边界 — 同 wd 并发本就近 0)、警告无"不再提示"开关、不点击跳目标 task、宪法文件 short_id 命名约定保留(§7.9 2026-05-20 不动)、不加 clone / gate / 物理隔离。**文档**:DESIGN §7.8 风险表"文件级悲观锁"(本就未实现)行替换为"软警告 + known limitation";§7.9 新增 2026-05-21 取舍条说明 γ/short_id/clone 三方案为何都不选 + 引 Claude Code 同款设计。 + - **paper_server → zcbot research skill(查文献 / get abstract / 拉 PDF)**:用户要 zcbot 能查内部部署的 paper_server(`http://paper.xxhhcty.xyz:8080/`,OpenAlex 元数据 + Sci-Hub PDF 抓取)。**范式判断**:不做 tool(频次低 + zcbot 没 ToolSearch 基建,3 个函数 schema 永驻 chat context 不划算)、不做 MCP(部署/分发成本)、不裸 `run_python` 调 httpx(每次重复写 base_url / 字段名,且易漂移)、不做 helper-lib(LLM 不知道该 import 啥) → **做成 skill**(同 proposal/ppt 范式,SKILL.md + paper.py helper 同目录,LLM `load_skill("research")` 后用 `run_python` 调 helper)。**新增**:① `skills/research/SKILL.md`:何时用 / 何时不用 / 三函数签名 + 示例 / 工作流(search → 筛选 → get_paper 看 abstract → 必要时 fetch_pdf → read PDF)/ 错误处理 / 反模式。② `skills/research/paper.py`(~110 行):`search(keyword, year, doi, has_pdf, limit)` → paper_server `/api/resm/paper/` list 端点,精简 9 字段返(避 abstract 在 list 时 dump 给 LLM 太大);`get_paper(id_or_doi)` → retrieve 端点,**依赖 paper_server 侧 PaperDetailSerializer 加 abstract 字段**(由用户改 serializer + redeploy);`fetch_pdf(id_or_doi, working_dir)` → `/resm/paper//pdf/` 流式下载到 `/papers/.pdf`,已存在跳过,`has_fulltext_pdf=False` 抛 RuntimeError;`_resolve_to_id` DOI → id(`10.` 前缀启发式);base_url 默认 `http://paper.xxhhcty.xyz:8080` 可 `PAPER_SERVER_URL` env 覆盖。③ **`tools/run_python.py` 注入 PYTHONPATH=base_dir**(关键 enabler):子进程 cwd 是 zcbot 仓库根,但默认 PYTHONPATH 不含项目根 → 不能 `from skills.research.paper import ...`;`env["PYTHONIOENCODING"]` 那行后加 `env["PYTHONPATH"] = str(self.base_dir) + os.pathsep + env.get("PYTHONPATH", "")`,LLM 能直接 import 不必折腾 sys.path。**没动**:tool 系统 / `agent_builder.py` / config / `ModelCapabilities` / ToolSearch 基建(独立决策,触发条件:tool 数 >20 或 schema 总 token >3k)/ paper_server filterset / search_fields / urls / models / paper_pdf_view / DESIGN(skill 是已有抽象)/ RUN(`PAPER_SERVER_URL` 是可选 env,有默认值)。**Tradeoffs**:① skill 内 helper 范式让 paper_server API 漂移时改一处(`paper.py`)而不是 prompt + tool schema;② DOI 启发式 `_is_doi` 容易误判像 `arxiv/2401.xxxxx` 这种非标准串(prefix 不是 `10.`),paper_server 内部用真 DOI(`10.xxx/...`)所以本库内场景稳;③ `search(limit>50)` 自动夹紧到 50 防 LLM 误用一次性拉全表。**遗留**:paper_server 侧 `PaperDetailSerializer` 加 abstract 由用户负责(handoff §A 描述);redeploy 后跑 `scripts/smoke_paper_skill.py`(三步:search list shape / get_paper abstract / fetch_pdf 落盘 + 复用)。 - **dev SPA chip 维度二次校准:工具 I/O 走产物白名单 + 助手正文无条件挂 chip 绕开 seenRels**:截图反馈"助手回复里 echo 的产物图路径(`rust介绍/figures/...png`)没挂 chip"。复盘上一条改动 + `febe04a`:① 上一条把工具 I/O 的 chip gate 也解了 —— 实际意图是"glob/grep 列出的引用不该挂(否则把命中的老 figures/foo.png 当新产物展示)"故 gate 该留;② `febe04a` 的 `seenRels` 全局去重把"防同图被 inline 两次"做过头了,把助手正文 echo 的同路径 chip 也吃掉。**最终模型(三条规则)**:① 工具 I/O(args/result):chip 抽取只对产物工具(seedream/seedance);② 产物工具的产物图/视频:inline 大图;③ 助手正文 echo 的路径:**永远**挂 chip(绕开 seenRels)+ 强制 `allowInlineMedia=false`(只小按钮,绝不重复 inline 大图 —— 因为产物工具上面已经 inline 过了)。**改动**:`renderMessages` 3 处(tool 卡 / assistant 正文 / assistant tool_calls args)+ SSE 2 处(tool_call / tool_result)按上面规则改写;`pickFresh`(seenRels 读写)只在产物工具的两处保留(防同图 inline 二次),assistant 正文改成 `renderArtifactBarHtml(extractArtifactRels(...), false)` —— 不读不写 seenRels,直接 chip。SSE 处 `upgradeMediaArtifacts` 同步 gate 到 `if (isProducer)` 下,非产物工具不发 blob fetch。**为什么 chip 重复出现无害**:chip 是 monospace 小字 + 5px 圆角小按钮,占 1 行;同路径在 tool 结果 + assistant 正文都出现,体感是"工具产出了它 + 助手又提到它",是合理叙事节点,跟"两张同样的大 PNG 占整屏"完全不同视觉量级。**对比方案**:① 助手正文也走 seenRels 但区分 chip/inline 类型(seen=path 同时也存 cat),只去重 inline、放过 chip — 复杂度涨,逻辑分支多;② 后端 tool_result 元信息显式标 `produced_files`(前端不再启发式抽路径)— 干净但 SSE/历史回放/seedream 全要改,成本最大,不上。当前方案 4 行实现意图。**没动**:`extractArtifactRels` regex / `_categorize` / 媒体 blob 缓存 / chip 点击委托 / 后端 / DESIGN(纯前端 UX 反复)/ RUN。**遗留**:用户提"绝对路径有些没挂 chip",等具体例子再排(可能是 wd_name 与历史路径段不齐 / 跨 task 路径)。 diff --git a/web/app.py b/web/app.py index eaf731b..4c16c99 100644 --- a/web/app.py +++ b/web/app.py @@ -648,6 +648,7 @@ def create_app() -> FastAPI: working_dir: Optional[str] = None, q: Optional[str] = None, ordering: Optional[str] = None, + run_status: Optional[str] = None, user_id: UUID = Depends(require_user), ): """列出当前 user 的 task,分页 + 多维筛选 + 排序。 @@ -657,6 +658,8 @@ def create_app() -> FastAPI: - `skill` 精确匹配(空忽略) - `working_dir` 末段目录名(如 `水泥申报`);后端自动拼 `workspace/users//` 比对 - `q` 模糊搜索 name + description(ILIKE,大小写不敏感) + - `run_status` 逗号分隔,allowlist `idle/running/cancelling/error`;非法值静默忽略 + (dev SPA 拉同 wd 活跃 task 用,通常 `running,cancelling`) - `ordering` DRF 风格,逗号分隔,`-field` 倒序;allowlist `created_at/updated_at/name/status`; 非法字段静默忽略;**默认 `-created_at`**(创建时间倒序) 返回标准分页壳 `{page, page_size, count, results}` —— count 供前端算总页数。 @@ -668,6 +671,10 @@ def create_app() -> FastAPI: skill = (skill or "").strip() or None wd_name = (working_dir or "").strip() or None q_text = (q or "").strip() or None + rs_allowed = ("idle", "running", "cancelling", "error") + run_status_set = { + s.strip() for s in (run_status or "").split(",") if s.strip() in rs_allowed + } or None # 组装 WHERE conditions = [Task.user_id == user_id] @@ -679,6 +686,8 @@ def create_app() -> FastAPI: # 末段 → 完整 db form。同 working_dir 多 task 共享时,这是命中入口。 wd_db = f"workspace/users/{user_id}/{wd_name}" conditions.append(Task.working_dir == wd_db) + if run_status_set: + conditions.append(Task.run_status.in_(run_status_set)) if q_text: pat = f"%{q_text}%" conditions.append(Task.name.ilike(pat) | Task.description.ilike(pat)) diff --git a/web/static/dev.html b/web/static/dev.html index 2359210..c7acb29 100644 --- a/web/static/dev.html +++ b/web/static/dev.html @@ -259,6 +259,11 @@ font-size: 12px; color: var(--muted); display: flex; gap: 12px; align-items: center; flex-wrap: wrap; } #chat-meta .tid { font-family: monospace; color: var(--text); } #chat-meta .spacer { flex: 1; } + /* 同 wd 并发软警告 banner — 非阻塞,只提示中间产物互覆风险 */ + #wd-concurrent-warn { padding: 6px 12px; border-bottom: 1px solid #f0c36d; + background: #fff8e1; color: #6a4500; font-size: 12px; } + #wd-concurrent-warn .tname { font-weight: 600; } + #wd-concurrent-warn .rs { font-family: monospace; opacity: 0.7; } #chat-stream { flex: 1; overflow-y: auto; overflow-x: hidden; padding: 12px; display: flex; flex-direction: column; gap: 8px; @@ -662,6 +667,7 @@
(未选中任务)
+
请在左侧选一个任务