# 实施进度 > 配合 `DESIGN.md`。本文件只记 phase 状态、决策偏差、文件量、下一步。每条 1-2 句:做了啥 + 关键判断;细节查 `git log` / `git diff` / `DESIGN §7.9`。 最后更新:2026-06-09(PPTX 前端在线预览:LibreOffice→PDF + 复用 PDF iframe,DESIGN §8.3 Stage 1) --- ## 状态 | Phase | 标题 | 状态 | 备注 | |---|---|---|---| | 1-3 | 骨架 + Skill + run_python | ✅ | 多 skill(coding/proposal/ppt/research/documents/imagegen/videogen/review/patent);CoreCoder 唯一匹配 edit;敏感 env 过滤 | | 4 | 演化性能力 | 🟡 | Model Profile + Probing ✅;版本化 prompt 未做 | | 5 | Eval Suite | ⏸ 不做 | dogfooding 替代,probe 覆盖健康检查 | | 6 | 长任务工程化 | 🟡 | task + 恢复 ✅;双层记忆 ✅;context 压缩未做 | | 7 | 打磨 | ❌ | Docker 沙盒 / 更多 skill | | §7 SaaS | DESIGN §7 路线 | 🟡 | A 事件流化 ✅;B 完工 ✅;D `/v1` JSON API ✅;D' 过渡 auth + dev SPA ✅;单活 run 锁 + cancel ✅;0004 schema 瘦身 ✅;入口归位 ✅;真 OIDC 待;**C Step 1-3 + 3d ✅(Executor + Docker 池 + DockerExecutor + fs 工具进容器)+ Step 5 部署前置对账 ✅ + 容器资源 yaml + 应用层磁盘配额(scan+gate)✅ + dogfood 网络放开 + 容器内 pip/npm 源持久化 ✅**;**Step 4 完整 egress proxy + Step 3b PGID kill 协议延后到外部用户开放前**;**外部用户开放仍需 egress proxy + xfs project quota OS 层硬化(§7.5 落地清单 #2 #4)**。 | --- ## 已完成关键能力 ### 2026-06-10 - **单轮停机判据从"步数"解耦为"是否在推进":`max_iterations` 升为纯 backstop + 新增全局「无进展」熔断 + 撞顶明确提示**:DB 诊断 task `b27466a0`(智能体介绍 PPT)所谓"中途断了"——查实=该 run 跑满 `max_iterations`(flash 旧值 50)后 `return "[reached max iterations]"` 干净停下、留一条悬空 tool 结果,用户离开 25min 回来打"继续"才续完(`run_status=idle/run_error=None`,非崩溃);"步骤太长"=少数轮 DeepSeek API 延迟 126-185s,工具本身全 <13s;顺带实测该 task DeepSeek 前缀缓存命中 92-94%,**上下文压缩对缓存几乎无害**(压缩函数确定性→旧消息压缩态稳定,只滑动边界这一处断,每轮 miss 几十~几百 token)。**洞察**:`max_iterations` 把"用户感知的轮(来回对话)"和"一轮内自主工作步数"混在一个旋钮上——自主 tool 链概念上是 1 轮,该松;真正要掐的是"空转"。落地:① yaml `max_iterations` flash 50→120 / pro 100→150,dataclass 默认 50→120,定位为安全兜底非"轮"预算;② `_RepeatGuard.record` 多返一个 `productive`(净产出=非 `[Error]` 且非一字不差重复);③ `_execute_tool_call` 三个返回点都带 `productive`(invalid-JSON/被拦=False);④ run loop 累计 `self._stall`——整步所有 tool 都无净产出则 +1、任一净产出清零,连续 `_STALL_LIMIT=8` 步空转主动停(`[stopped: no progress]`),比撞 120 早得多掐死循环,配 `_RepeatGuard` 逐指纹 HARD=4 双保险;⑤ 撞 backstop / 熔断都 emit 明确"回复『继续』可接着跑"提示,不再静默停。`tests/test_loop_repeat_guard.py` 更新 record 解包调用 + 加 `productive` 信号用例(17 例过,全量 103 过)。 - **`systemctl restart` 优雅 drain in-flight run(单实例止血,不再误标 error)**:此前 restart 硬杀 BG run 线程,下次启动 reaper 把所有 `running/cancelling` 标 `error: server restarted before run finished` —— 用户一多就不能随便重启。落地纯进程内、**零 DB 改动**:① lifespan 加 `app.state.draining`(asyncio.Event)+ `app.state.inflight`(`{asyncio.Task: task_id}`,顺手修 `create_task` 不留引用可能被 GC 的旧坑);② POST `/messages` 起 run 时登记+done 回调自摘除,draining 置位时返 503+`Retry-After`;③ lifespan `finally` 先置 draining 拒新 run,`asyncio.wait(inflight, drain_timeout)` 等收尾,超时的 `broker.request_cancel` 转协作式 cancel(下个 chunk 间隙退、标 idle 不报 error),再过 `cancel_grace` 仍没退的留给 SIGKILL(最坏退化=改前)。④ `main.py` uvicorn 加 `timeout_graceful_shutdown=5`(否则长连 SSE 无限挡在 drain 前);⑤ `config/agent.yaml` 加 `shutdown` 段(drain_timeout 30s / cancel_grace 15s,超时转 cancel = 用户按停止可重发,故偏短);⑥ dev SPA `chat.js` 发送包退避重试(503 背压 + 交接拒连 TypeError 都重试 ~26s,显"服务更新中",耗尽贴友好提示)。**部署强耦合**:unit `TimeoutStopSec` 从 10 提到 90(必须 > drain+grace+sandbox 清扫余量,否则 SIGKILL 砍掉 drain),已写进 RUN.md unit + 故障兜底。B 蓝绿(零 503 窗口)留作触发信号后再做,前置是 instance-aware reaper(§7.8)。 ### 2026-06-09 - **PPTX 前端在线预览(LibreOffice→PDF,DESIGN §8.3 Stage 1)**:此前文件区点 `.pptx` 只能下载(`preview.js._categorize` 归 fallback)。关键洞察=前端已有 PDF iframe 路径(`_showPdf`),所以只要后端把 pptx 转 PDF 就**前端几乎不动**。落地:① 新 `web/pptx_render.py`——`pptx_to_pdf()` 同步可缓存,调 `soffice --headless --convert-to pdf`、**每次独立 `-env:UserInstallation` 临时 profile** 绕单 profile 锁、超时 60s kill;soffice 路径发现复用 render_bg 思路;缓存落源同目录 `.preview/..pdf`(hash=mtime+size,源改即失效;dotdir 不污染文件列表),`_prune_stale` 清旧 hash。② 新端点 `GET /v1/files/preview_pdf`——复用 `_safe_join` 鉴权防穿越 + 仅 `.ppt(x)` + per-path `asyncio.Lock` 防并发重转 + `run_in_executor` 不堵事件循环;soffice 缺失 501 / 转换失败 500。③ `preview.js` 加 `ppt` 组,main/mini 共用 `_showPptAsPdf`(fetch PDF→iframe,带 spinner loading + 失败回退下载),`dev.html` 加 `.preview-spinner`(复用 `@keyframes spin`)。**转换跑在 web host 进程,不进沙盒**(沙盒不该有 LibreOffice;预览面向 user_root 任意 pptx,与 deck 生成解耦)。部署:host `apt install libreoffice-impress fonts-noto-cjk`(已写进 RUN.md 一次性 + 故障兜底),sandbox Dockerfile 不动。**未做**(Stage 2):常驻 soffice listener 消冷启、deck 生成后 eager 预转、缩略图导航。 - **药3 复核:`/home/ubuntu/zcbot` 幽灵路径已于 06-03 修复,新任务不复现 + 加回归测试钉死**:接续高轮数烧 token 诊断收尾。证据链——失败 task `ab063233`(06-02 03:54)/`ff1686b7`(06-03 12:02)的首条 assistant 消息(idx1)tool_calls 里就带 `glob(path=/home/ubuntu/zcbot/workspace/users/<真实uid>/数据资源展示)`,**带真 uid 只可能来自当时的 system prompt**(messages 表无 system-role 行,系统提示运行时拼接不入库);两 task 均建于 06-03「system prompt 焊死宿主路径」修复前后。fs 工具在 docker 容器里跑(容器无此宿主路径)→ `[Error] base path not found`(`glob.execute` 对不存在 base 返此错,`_display` 对 user_root 外路径回绝对)→ 重试风暴(实测 51 次)。**复核当前代码**:docker 模式即便传 `tool_base=/home/ubuntu/zcbot` + 真 uid,拼出的 prompt 只含 `/workspace/`、不含宿主路径/uid/tmp(`agent_builder.py:223-250` docker 分支注入容器路径 + 删 cwd 行);prod 走 docker backend(RUN.md)。新增 `tests/test_system_prompt_paths.py`(2 例:docker 无宿主泄漏 + host 保留本地绝对路径,过)锁住修复防回归。**三味药全部收口**;药1 重复守卫此后还兜底任何同类风暴(同一 51 次会被摁到 ~5 次)。无功能代码改动,仅加测试。 - **ppt skill 补「信息设计纪律」+ 混合背景 + pptx 预览器(治"效果还是不太行",深读 pptmaster 后的二次修正)**:用户反馈卡片式 v2 仍不够好,拆其真实产物(`大模型与智能体介绍.pptx`)定位毛病=9 页 4 页雷同卡片网格(全卡=AI 味)、发展历程做成网格(该时间轴)、智能体平铺(该闭环)、图标 0.6 寸太小、投影到处加。**深读 pptmaster 的 executor-base/executor-consultant(-top)/shared-standards 后顿悟**:它像麦肯锡的真因是**信息设计纪律(~70%)**而非 SVG 渲染(~30%),而这些**全是 editable python-pptx 能做的**——之前纠结的"可编辑 vs SVG 转换器"搞错了轴(可编辑都落 DrawingML 同一天花板,转换器零视觉增益)。落地三层:① **信息内功**——`add_takeaway`(论断标题下一句话结论框)、`add_kpi` 加 `baseline+delta`(数据语境化:数字带对比基准+升降色 `GOOD/BAD`)、`add_source`(来源)、`add_toc`(贯通整宽目录);SKILL 策略阶段加论断式标题对照表 + page_rhythm(anchor/dense/**breathing 强制打破卡片网格**)+ 内容→版式映射写进逐页大纲。② **修我搞反的投影**——pptmaster"投影是克制":`add_card` 默认 `shadow=False`(平铺对等卡描发丝边不投影)、每页 ≤2-3 投影、一容器一手段不叠;quality_check 加绿=语义状态色豁免三色制。③ **组合件 + 工具**——`add_card_grid`(均衡网格,2 行改图标左置治"图标顶置挤溢出")/`add_timeline`/`add_cycle`;`render_bg.py`(无头 Chrome 渲杂志级 mesh 渐变背景图,**混合方案**:背景图+原生可编辑白字,封面/章节);**`pptx_preview.py`(把 .pptx 渲成 PNG 肉眼验观感)——quality_check 只查结构,预览补"好不好看",当场抓到 `set_text` 多行只给第一段上色的真 bug(封面副标题第二行变暗看不见)并修复**。验证:重排「大模型与智能体」为 10 页(节奏:封面/目录/章节 anchor · 网格/时间轴 dense · 大字 breathing · 章节/闭环/网格 · 致谢),逐页渲 PNG 亲眼验收均专业,quality_check 全过。改 `skills/ppt/{SKILL.md,references/{design_principles,layouts}.md,scripts/{pptx_helpers,quality_check}.py}` + 新增 `scripts/{render_bg,pptx_preview}.py` + `SKILL_LIST.md`。**未动**:SVG→原生转换器(论证为零增益不做)、live preview server、动画;fetch_icon 的 PNG 后端(cairosvg/svglib)本机未装,暂用种子库 PNG。 ### 2026-06-08 - **loop 加病理性重复调用守卫(药1,治「不停调用同一脚本」的根因 ①②)**:接续批量化诊断——DB 实测高轮数 task 的浪费大头是「同名同参 + 无产出」的重复(`document_search` 122 次、空 `shell{}` 51 次、反复 `glob` 同一不存在路径),而 `core/loop.py` 主循环原本对此**零防护**照单全收。新增 `_RepeatGuard`(AgentLoop 实例持有、活在单次 run 内不跨 task):按 `(工具名, 精确参数 canonical-json)` 指纹跟踪「无产出重复」计数。**命门是只惩罚无产出、绝不误伤正常迭代**——同参但**结果每次不同**(改脚本后重跑 run_python、修 bug 后重跑构建)算有产出、计数清零永不拦;同参且**结果是 `[Error]` 或与之前一字不差**才累计。两档:累计 ≥`SOFT`(2)在 tool 结果尾部注入 `[重复调用警告]` 软提示(模型当轮即见);≥`HARD`(4)下一次同参调用 `should_block` 直接拦截不执行、回 `[已拦截重复调用]` 硬停消息逼其换路(一个卡死调用最多放过 ~4 次无产出重复)。**顺带堵 `_malformed_tool_calls` 的洞**:大参数畸形退化成合法空 `{}` 时 executor 每次返同一句「缺少必填参数」→ 走 dup 分支被同一机制拦下,无需单独特判空 `{}`。`_execute_tool_call` 接线:执行前 `should_block` 拦截、执行后用**截断后未加提示的原始结果**算指纹 `record`(保证同输出哈希一致)、`warn` 事件上抛拦截/首次软提示。改 `core/loop.py`;新增 `tests/test_loop_repeat_guard.py`(7 用例:同错拦截/空`{}`堵洞/同结果拦截/变化结果不拦/修好清零/SOFT 阈值/异参分别跟踪,全过)。**注**:阈值常数化(SOFT/HARD)便于后续按实跑调;药3(`/home/ubuntu/zcbot` 幽灵路径是否新任务仍复现)仍未查。 - **检索/抓取类 host 工具批量化(治高轮数烧 token,先做的「web_fetch 类」一味药)**:DB 实测诊断(`scripts/diag_*.py`)高轮数 task 的 tool_call 序列——`ff1686b7` coding 任务 `document_search` **122 次**(104 个不同 query,关键词反复微调地毯式搜不收敛)、`document_download` 28 次;`ab063233` documents 任务 **64% 的 tool 结果带错误** + `shell{}`/`run_python{}` 空参数风暴。定位「不停调脚本」是三股根因叠加(空 `{}` 风暴 / 报错重试 / 检索不收敛),其中检索/抓取的往返成本可由**工具形态改造**直接压。本轮把三个 host 工具从「一次一个」改成**接受列表、一轮并发处理一批**(按「开发期不写兼容层」直接换签名、不留单数别名):① `web_fetch` `url`→`urls`(1-10,ThreadPoolExecutor 并发 6,全批正文总预算 16000 字符按条数分摊,单条 SSRF/超时/404 不连坐);② `document_search` `query`→`queries`(1-8,**批内去重** + 批量时自动缩 `max_documents`/`content_chars_per_doc` 防爆 context,每 query 独立 try);③ `document_download` `file_name+kb_name`→`items=[{...}]`(1-10 并发,单条失败标 `[Error]` 不毁整批)。三者输出都按条标注 `=== [i/n] ... ===`、超量截断明示不静默。`tools/{web_fetch,documents}.py` 改;`tests/test_secret_host_tools.py` 同步改新形态 + 加批量并发/去重/失败隔离 3 用例(5 过);`skills/documents/SKILL.md` 签名/工作流/反模式更新(加「一个 query 一轮反复搜」「同义近重复 query 堆叠」两条反模式,呼应诊断 ③)。`DOCUMENT_SEARCH_API.md`(上游 HTTP 接口,本就单 query/次)不动。**未动**:药1(loop `(name,args)` 重复探测器 + 堵空 `{}` 洞,最高杠杆但动核心 loop)、药3(`/home/ubuntu/zcbot` 幽灵路径是否新任务仍复现)——见记忆 `project_high_turn_token_burn_root_causes`,留作后续。 - **ppt skill 视觉系统升级为「卡片式」(治"生成效果不太行")**:学 GitHub `hugohe3/ppt-master`(24.9k★)后定位根因——其好看的核心是「SVG 作画→转原生 PPTX」给足设计自由度,而 zcbot 被 python-pptx **原语**(平矩形+左色条+圆点 bullet)摁死了视觉天花板,出来就是"2010 办公模板"。岔路三选(A 自建 SVG→pptx 转换器=最高天花板但大工程且与"一脚本整建/少来回"冲突;B 升级 python-pptx 设计系统;C 混合),**选 B**(保留单脚本批量架构、原生可编辑、风险低)。落地:① `pptx_helpers.py` 加质感件——`add_card`(圆角矩形 `adjustments[0]` 调圆角 + `a:outerShdw` XML 柔和投影)/`add_gradient_rect`(`fill.gradient()` + 角度)/`add_kpi`(数字卡)/`add_icon_tile`(图标底块)/`add_pill`/`add_eyebrow`/`add_chevron`/`add_notes`(演讲者备注),`set_palette` 从主/辅/强调**派生明暗色阶** `PRIMARY_WASH/SOFT/DARK`+`ACCENT_SOFT`;`apply_brand` 封面/章节改**渐变大色块**;**所有 helper 把 `name=` 写进形状 `.name`**(原来只喂 assert_inside,导致 quality_check 拿不到语义名)。② `layouts.md` 9 版式重写成卡片式 + 扩到 **13 种**(加 L10 KPI 卡 / L11 卡片网格 / L12 流程 / L13 大数字论据)。③ **quality_check 跟新设计语言对齐**(否则每个 deck 淹在假警告里):三色制改**按色相归桶**判(主色深浅/wash tint 不算新色)、小字号/bullet 按 `.name` 豁免标签类、大号展示字(≥40pt)跳过溢出估算、bullet ≤5 改**按列**判(双栏 3+3 不误报、单列 6 仍抓)。④ SKILL.md 工作流加 opt-in 真实配图(走 imagegen,¥0.22/张,大纲标 `[img]`)+ 每页 `add_notes`;`design_principles.md` 加派生色阶/KPI 卡/图表透明底卡片化。验证:13 版式全覆盖 demo deck 建成 + quality_check 全过;单列 6-bullet 回归仍触发。改 `skills/ppt/{SKILL.md,scripts/pptx_helpers.py,scripts/quality_check.py,references/layouts.md,references/design_principles.md}` + `SKILL_LIST.md`。**未动**:SVG 路线(A)、live preview、动画——属更大工程,本轮不上。 - **system prompt 加「少来回」全局原则(广谱减轮)**:ppt 之外的长尾 task(改代码/跑数据/画图)没专属 skill 兜,加一条通用 `工作原则`:互相独立、不依赖中间结果的操作(建多页产物/批量改文件/生成整份产物)合到一个脚本或一轮并发 tool call 里做,别一步一 call(每轮重发整段上下文,轮数=token 体量线性乘数);但下一步输入要看上一步结果时(探索检索/按报错改/需用户确认)就老实分步,别硬批——精准措辞避免"过度批处理"踩掉该有的 checkpoint。定位是便宜补充(prompt 走缓存近零成本),不指望它动 100+ 轮大头(那靠结构改造)。改 `prompts/system/general_v1.md`。 - **ppt skill 工作流批量化(减高轮数 task 的来回)**:实测高成本 task 几乎全是 100+ 轮的"逐步 tool 调用循环"(rust→PPT 34 轮、文献采集 245 轮),每轮重发整段上下文,轮数是 token 体量的线性乘数。ppt 是最易压、风险最低的试点:原 §阶段二**逐页**(每页 `读spec→glob图标→一个 run_python 加页→等用户确认→下一页`,N 页 ~2N 轮)。改法:① 阶段一 spec 增「逐页大纲」表(页|版式|标题|要点|图标),作为**替代逐页确认的前置 checkpoint**——改文字大纲比建完 slide 再推翻便宜;② 阶段二改成**写一个 `build_deck.py` 一次建整 deck**(同进程 `new_presentation`→按大纲循环 `add_slide`→一次 `save`,坐标天然一致;`pptx_helpers` 模块化已消解原"逐页防漂移"理由),图标**全 deck 批量预取**(不逐页拉);③ quality_check 一次 → 改脚本重跑(不 edit 成品);④ 可选"风格探针"(先建封面+1 页看观感)兜视觉返工险。N 页从 ~2N 轮降到 ~3-4 轮。改 `skills/ppt/SKILL.md`(阶段一/二/三 + 反模式 + 文件树)、`references/layouts.md`(§通用起手换成"整 deck 单脚本"模板)、`SKILL_LIST.md`(流程描述/典型产物同步)。冒烟过:单脚本 `new_presentation`+循环 `add_slide`+`save` 建 2 页成功,API 调用与模板一致。**注**:数据采集类(host 工具中转免不了)是另一条路(拆采集/处理相位),未动。 - **修进度还原错乱 + 进度区移到对话区顶部(codex 式)**:根因(查 DB 实锤)= 上下文压缩把旧 `task_progress` tool_call 参数换成 `{"_compacted":true,"step_id":"sX"}` 这种"看着像合法调用"的标记,① 毒化模型让它后续照抄出残废 `update_step`(丢了 `step.status`)并入库,② 残废格式前端 `applyProgressAction` 读不到 `args.step` → s4/s5 永停 pending → 进度显示不对。修复:`context.py` 对 `task_progress` 参数**一律不压缩**(参数本就小,压缩省不了几个 token 却两头坏事);旧的 `_compact_task_progress_arguments` 整个删除。**进度展示重构**:删掉每条消息卡内联进度块(`renderProgressHtml`/`renderProgressInto` 移除),进度统一只在**对话区顶部**单一 `#task-progress-dock`(从 composer 上方移到 `chat-stream` 之上、`flex-shrink:0` 钉顶不滚)实时显示;**完成态折叠**——全部步骤 completed 时 dock 自动收成一行 `✓ 全部完成 · N/N 步`(`
` 点开看清单)。prompt + tool 描述改为"跑完把最后一步标 `completed`、不要 `clear`",留住全绿收尾。校验:`python -m unittest tests.test_context_compaction tests.test_task_progress_tool`(12 过,改写 `test_keeps_old_task_progress_arguments_intact` 断言参数原样保留);`node --test tests/frontend_task_progress.test.mjs`(2 过)。 - **修登录无反应(`$ is not defined`)+ 补 favicon 消 404**:`newtask.js` 用了 DOM 简写 `$`(`dom.js` 导出的 `getElementById`)却漏 import,模块加载到顶层 `$("hd-new").onclick` 即抛 `ReferenceError: $ is not defined`,中断 newtask 全部绑定及其 import 的 auth/chat 链路 → 点登录无反应。补 `import { $ } from "./dom.js"` 与其余模块对齐。另在 `dev.html` `` 加内联 SVG data-URI ``(蓝底白机器人),浏览器不再请求根 `/favicon.ico`,消掉 404;选内联 SVG 而非新增 `.ico` 文件 / 服务端路由,零新增文件零 app.py 改动。 - **新增 Codex 式 `task_progress` 进度工具 + Web 固定进度区**:`TaskProgressTool` 默认注册到 agent,支持 `set_plan/update_step/clear`,返回极短 UI-only 结果;上下文压缩对旧 `task_progress` tool_call/result 做专门折叠,避免进度历史长期占 prompt。前端新增 `progress.js` 做 task 级进度状态合并,修复 `update_step` 只带 `{id,status}` 时因缺标题不显示的问题;当前进度显示从助手消息内提升到 `#task-progress-dock`(对话流下方、输入框上方),历史消息内仍保留进度块作记录。system prompt + coding/ppt/proposal/analyze skill 加轻量使用约定,要求只在多步骤关键阶段少量更新。**部署侧补静态资源 no-cache**:`NoCacheStaticFiles` 替换默认 `StaticFiles`,让浏览器重新校验 `/static/*.js` 等资源,避免前端修复已部署但旧 `chat.js` 仍被缓存导致看不到进度区。校验:`pytest tests/test_context_compaction.py tests/test_task_progress_tool.py tests/test_executor_docker.py tests/test_static_vendor.py -v` 相关集通过;`node --test tests/frontend_task_progress.test.mjs` 2 过;`node --check web/static/js/chat.js web/static/js/progress.js` 过。 ### 2026-06-06 - **前端模块化 Step 2 收官:抽出 `chat.js`(对话视图)+ main.js 缩成 75 行入口**:最后也是最缠的一块——任务列表(浏览/筛选/滚动)+ selectTask 切换 + renderChatMeta/模型下拉 + renderMessages + live-run 助手 + sendMessage/cancel + fetchSse/handleSseEvent + 润色/粘贴文件 + 完成/废弃/删除/导出/清空(原 main.js 连续区 64–1132)→ `chat.js`(1086 行)。**决策:合一个 chat.js 而非强拆 tasks.js+stream.js**——读完依赖图确认二者共享 `state.liveRuns` + `chat-stream` DOM + run 生命周期,且 live-run 助手(renderLiveRunIfVisible/ensureRunningTaskSubscribed 等)被 selectTask 和 SSE 机器两边调用、骑墙;强拆会制造双向各 ~4-5 个 import 且边界不自然(用户已确认选合一)。导出 `loadTaskList`/`loadModels`/`selectTask`,embed/files/newtask 对这三个的 import 从 `./main.js` 改指 `./chat.js`;`formatUploadProgress` 加 export(粘贴上传进度用)。**chat 不调 enterApp → 与 main 无环**。`main.js` 仅留 `enterApp`(编排)+ `loadStorage` + Esc 关栈 + boot = **75 行入口**,import 精简到 11 行(layout/markdown/media 不再被 main 直接引用,但经 chat 仍在依赖图、副作用照常)。**校验升级**:除 node 全检 + import/export 一致性,新增**从 main BFS 的模块可达性检查**(14/14 可达,确保副作用模块不掉出图)。dev.html 4087 行单文件 → 14 个零构建 ES module + 纯 HTML;main 2719→75。**路径 1(拆文件)完成**,后续可按需进路径 2(给 chat/files 等局部引 Alpine/petite-vue 响应式)。 - **前端模块化 Step 2:抽出 `embed.js`(iframe 模式)**:父页面经 postMessage 推 token 进入应用 + 401 重签(原 main.js 1147–1209 + 顶层 `_embedInitialTaskHandled` 一次性标志)→ `embed.js`(75 行)。导出 `embedInit`(boot 调)+ `embedPostToParent`/`embedShowWaiting`(auth 的 logout 在 embed 下通知父页面/显示等待态)——后两个从 main 迁出后,`auth.js` 对它们的 import 从 `./main.js` 改指 `./embed.js`(auth 仍从 main import enterApp)。反向 import main glue `enterApp`/`loadTaskList`/`selectTask`。main↔embed、auth↔embed 均运行时调用环,安全。main.js 删至 **1154 行**(2719 行起,已搬出约 58%)。node 全检过、import/export 一致性过、静态测试 2 过。剩 main 内:`enterApp` glue + tasks(列表/选择/渲染消息)+ stream(发送/SSE)+ boot + Esc 关栈,待最后一并处理 tasks+stream。 - **前端模块化 Step 2:抽出 `newtask.js`(新建任务弹框)**:任务名 / 工作目录(新建 sentinel 或复用已有 + 二级 input 联动)/ 描述 / skill / 模型 select,提交 `POST /v1/tasks`(原 main.js 1146–1320)→ `newtask.js`(186 行)。顶层自绑 hd-new 打开 / nt-go 提交 / 各 input 联动;唯一对外导出 `loadFolderSuggestions`(供 main enterApp 初始化顶部 filter-wd、files 复制/移动后刷目录)——它从 main 迁来后,`files.js` 对它的 import 从 `./main.js` 改指 `./newtask.js`。反向 import main glue `loadModels`(加 `export`)/`loadTaskList`/`selectTask` + `logout`(auth)。main.js 删至 1220 行。node 全检过、import/export 一致性校验过、私有符号清零。 - **前端模块化 Step 2:抽出 `media.js`(工具活动标签 + artifact 抽取/渲染)+ 收敛 downloadFile 反向依赖**:对话内 `toolActivityLabel`(工具调用→中文活动名)、`extractArtifactRels`(从结果文本/working_dir 提产物路径)、`extractMediaBanner`(seedream/seedance 横幅)、`renderArtifactBarHtml`(产物 chip 条 + 图/视频内联占位)、`upgradeMediaArtifacts`(占位异步 fetch blob 填 ``/`