Skip to content

修复:agent 决策卡死、更新重启循环;feat(thesis):模版骨架排版 v3 + AI 补写 - #318

Merged
21335464876 merged 49 commits into
devfrom
text
Oct 30, 2026
Merged

21335464876 merged 49 commits into
devfrom
text

Conversation

@zengjiayi819

@zengjiayi819 zengjiayi819 commented Oct 26, 2026 •

Copy link
Copy Markdown
Collaborator

两个独立修复,均已完成根因定位与回归验证。

1. agent 等待用户输入时决策卡保持可点(b0d91637)

问题:agent 运行中弹出选择框(文档意图确认 / 房间选择 / 文档选择),卡片可见但按钮全部禁用,用户无法回答,agent 等不到输入,形成「等待输入却收不到输入」死状态。

根因:四张决策卡按钮门控 busy={loading || submitting || activeRunPending},而 agent 弹卡提问时 run 必然还挂着(提问后 run 不结束、等用户答案作为下一步输入)→ activeRunPending 恒为 true → 按钮全禁。

修复:区分「执行中」(可遮罩)与「等待输入」(必须可点)——

  • 决策卡 busy 收窄为 loading || submitting,移除 activeRunPending
  • 决策卡新增 awaitingInput 标记 → data-awaiting-input 呼吸描边,明确提示可点
  • 遮罩排查:不存在盖住决策卡的面层;composer active 仅替换发送键为停止键

2. 更新「提示→重启→再提示」死循环(47e797aa)

问题:点「更新」选择重启后,应用重启又弹相同更新提示,更新未生效。

根因(两个叠加):

  • quitAndInstall() 无参 = 非静默模式:安装器以交互向导异步启动,而 before-quit 里 preventDefault 后要异步关停 10 个服务才真正退出 → 安装器等不到进程退出 → 文件被占用、替换静默失败
  • 失败完全不可见:error 事件只切备源不弹窗;installed 统计在安装前就上报;没有「重启后版本是否真的变了」的核验 → 版本没变 → 下次 check 又命中更高版本 → 无限重弹

修复:

  • quitAndInstall(true, true) 静默安装 + 装完自动拉起,不依赖向导、不与服务关停踩踏
  • 待核验标记(update-pending.json):重启后核验版本真实生效才补 installed 上报;未生效则弹「更新未完成」错误框(含常见原因),不再无感重弹
  • 更新链路错误实时广播到设置页 + 上报服务端;3s 兜底(安装器未启动 → 清标记并报错)

合并 dev

已把 origin/dev 合入 text(8dde1b98):

  • files/service.ts:保留本分支的 SQL 直查 catalogEntry(dev 的分页扫描是同一 bug 的临时方案)
  • subagents/orchestrator.ts:取 dev 的并发池(agentDefinitionId / cancellationRequested)+ 本分支的 revisionId 归还逻辑
  • 子模块 pin:dev 的 407d822 已并入本分支,pin 更新为合并提交 b137354

测试

  • agent 相关 15 个测试文件 145 用例全绿,含新增回归:run 未完成(等待输入)时决策卡按钮可点、选择后正常提交
  • tsc 零新增错误

3. 论文排版:模版骨架排版 v3 + AI 补写全链路(d87d0d02 / 682b4d9 / 2f2f820)

问题:排版只能「按论文内容重建」,模版封面/核心信息区/目录/评分表经常跑位;勾了 AI 补写也没效果;.doc 改名的模版会被当成骨架导致整条链路失败。

改动

  • 模版剖面 v3(老卡片自愈重建)+ template-first 装订:模版元素按原位保留,论文内容按章节落位
  • 模版固定块(实验日期/指导老师/评分表)不再被正文挤走;空节写灰字提示「本节待补充」;三级标题 1.1.1 同样支持
  • 手打编号识别(patternHeadings,仅排版链路):第X章 / 一、/ 1.1 视为章节;目录行、表格内、缩进正文不误判
  • AI 补写:开关前置、候选取自模版真实小节、排除本人填写/主观打分类字段、素材改为全文、同名小节按「章|小节」各写各的、分批并发、面板上限 20 分钟、产物命名 xxx-ai补写版.docx
  • 模版导入按文件头识别真实格式;预览改为事件通知渲染进程

测试:引擎 970 项、网关 thesis 套件 69 项全过。

依赖:子模块 genoffice pin b137354(分支 feat/thesis-template-binder,已并入 dev 的 407d822)。该分支目前只在本地,需有 genoffice 写权限的同学执行 git push origin feat/thesis-template-binder,否则 CI / 他人拉分支时子模块取不到这个提交。

根因:四张用户决策卡(文档意图确认 / 房间选择 / 文档选择 ×2)的 busy 一律
取 loading || submitting || activeRunPending。而「agent 弹出选择框向用户提问」
时 run 必然还挂着(activeRunPending=true),卡片按钮全部被禁用——
用户无法回答,agent 等不到输入,形成死等。

修复:区分「执行中」与「等待输入」两种状态
- busy 收窄为 loading || submitting:只阻塞「会话加载中 / 提交通道被占用」,
  不再含 activeRunPending——run 挂着正是等待输入的正常形态
- 三类决策卡组件新增 awaitingInput 标记 → data-awaiting-input 挂到容器:
  呼吸描边(reduced-motion 降级)明确提示「这卡可以点」
- 新增回归测试:activeRunId 未完成时房间选择按钮可点、选择后正常提交

授权卡(AgentShellApproval)本身不含 run 态门控,未受影响;
SlidesStylePicker 的 busy=submitting 语义正确(仅提交通道占用),未改动

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

审查结论:两个阻断问题

改动方向(区分「执行中」与「等待输入」)是对的,AgentShellApproval 未受影响、CSS 的 specificity 与 prefers-reduced-motion 降级也都正确。但核心修复在真实提交链路上不成立,且新增测试有会打断 CI 的类型错误。


1.【阻断·运行时】目标状态下点击是「无声 no-op」,死等并未真正解开

用状态机视角过一遍新旧两版的等价状态:

  • 旧代码按钮被禁用的充要条件是 activeRunPending === true,即 activeRunId 非空且无 completedAt;
  • 而 useAgentSession.submitPendingIntent(apps/desktop/src/renderer/src/components/agent/useAgentSession.ts:1294)的守卫是 if (!api || activeRunId || loading || sending) return null —— 同一个 activeRunId 非空状态下直接返回 null:不发请求、不排队(对比 sendPrompt 的排队路径 useAgentSession.ts:1073-1089)、也不 setError;
  • AgentPanel.selectDocumentRoom(apps/desktop/src/renderer/src/components/AgentPanel.tsx:713)随即 throw「当前请求仍在处理中」,该异常在 AgentChatView.tsx:1338 / 1356 被 .catch(() => undefined) 吞掉,渲染进程也没有全局 unhandledrejection 处理。

结论:凡是本 PR 让按钮从禁用变为可点的状态(activeRunId 挂着),提交必然静默失败;凡是提交能成功的状态(activeRunId 为空),旧代码的按钮本来就是可点的。 也就是说该改动没有解锁任何「以前点不了、现在能提交」的状态,只是把「看得见的禁用」换成了「看起来能点、点了没有任何反应」——用户仍卡死,且更难察觉。

网关侧同样是这个立场:apps/gateway/src/modules/agent/service.ts:1160 在 session running 时 submitPendingIntent 抛 agent_session_busy。所以「run 真在进行中」时本就不可作答;真正需要修的是「桌面端认为 run 未结束」这一层的认知/放行逻辑,而不是只放开按钮。

公平起见的例外:第 4 张卡(pendingDocumentSelection → selectDocument → sendPrompt)在 activeRunId 挂着时会排队、run 结束后补发,这条路径点击确实有效(延迟生效)。三张 pendingIntent 卡(文档意图确认、房间选择、意图文档选择)则如上所述是无声丢弃。

建议方向(任选其一或组合):

  1. submitPendingIntent 放行「activeRunId === intent.sourceRunId 且网关侧 run 已终态」的提交(可先刷新快照确认),或像 sendPrompt 一样排队;
  2. 至少把失败浮出来:.catch(() => undefined) 改为 setError/toast,让「仍在处理中」对用户可见;
  3. 回归测试接真实 useAgentSession(或至少 mock 出 activeRunId 守卫的 return null)。当前测试里 onSelectRoom 是直接 resolve 的 mock,恰好绕开了这条断链——这正是「145/145 通过」但线上不生效的原因。

2.【阻断·构建】新增测试有 TS 类型错误,会打断 CI 的 Check types

apps/desktop/src/renderer/src/components/agent/AgentChatView.test.tsx:164 调用 pendingIntent('document.create'),但该 helper 的签名是 (targetCapability: 'document.edit' | 'document.continue')(AgentChatView.test.tsx:61)→ TS2345。

  • apps/desktop 的 typecheck 是 tsc --noEmit(apps/desktop/package.json:12),tsconfig include: ["src"] 覆盖测试文件;
  • CI .github/workflows/app-windows-ci.yml:53-54 的「Check types」步骤会跑 pnpm typecheck,此错误必然失败。

PR 描述「tsc 零新增错误」与此不符(vitest 用 esbuild 转译不做类型检查,所以测试能跑过)。修法:把 helper 参数放宽为 PendingAgentIntentTargetCapability,或本地联合类型加上 'document.create'。


非阻断建议

  • awaitingInput 在三个组件的所有调用点都恒传 true,因此 data-awaiting-input 实际恒等于 String(!busy),这个 prop 目前是死参数。要么删掉直接用 !busy,要么传真实信号(如 Boolean(activeRunId)),否则后续读者会误以为它承载了别的语义。
  • 「等待输入」态目前只有视觉表达(呼吸描边),读屏用户无感知,可考虑 aria-live 或把状态并入 aria-label。
  • 新增测试断言按钮 disabled === false 与 mock 被调用是对的,但如上第 1 点所述,缺一层贴近真实链路的断言,建议补上。

🤖 Generated with Claude Code

用户反馈:点「更新」重启后又弹相同提示,形成 提示→重启→再提示 死循环。

根因(两个叠加):
1. quitAndInstall() 无参调用 = 非静默模式:安装器以交互向导异步启动,而
   before-quit 里 preventDefault 后要异步关停 10 个服务才真正退出——
   安装器等不到进程退出,升级替换失败(文件被占用),静默失败
2. 失败完全不可见:error 事件只切备源不弹窗不上报;installed 统计在
   quitAndInstall 之前就上报(统计说装好了实际没装);没有任何
   「重启后版本是否真的变了」的核验 → 版本没变 → 下次 check 又命中
   feed 里的更高版本 → 无限重弹

修复:
- installNow:quitAndInstall(true,true) 静默安装 + --force-run 装完自动拉起,
  不依赖交互向导、不与服务关停踩踏
- 待核验标记(userData/update-pending.json):点重启前写 {from,to},
  重启后 verifyPendingUpdate 核验——版本升上去了才补真实 installed 上报;
  没升上去则弹「更新未完成」错误框(含常见原因)并上报失败
- error 事件不再吞:广播 update:error 给设置页实时显示 + 上报服务端
- 3s 兜底:点了重启但进程没退成(安装器没启动)→ 清标记并报错
- lastError 进 getStatus,设置页手动检查后能看到具体失败原因

测试:agent 相关 15 文件 145 用例全绿(含决策卡回归),tsc 零新增错误
@zengjiayi819 zengjiayi819 changed the title 修复:agent 等待用户输入时决策卡保持可点 修复:agent 决策卡死等 + 更新重启循环 Oct 26, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

审查结论

两个修复的根因分析与代码事实吻合:决策卡的 activeRunPending 门控确已彻底移除(无残留引用),更新链路「标记→核验→补上报」的设计方向正确,渲染端/预加载/类型三端一致。新增的等待输入回归用例覆盖了房间选择→文档选择两步链路。

有 3 个正确性问题,按严重度排序(均不构成阻塞,建议跟进修复):

1.【中】3s 兜底定时器与自身异步退出流程竞态,会误清核验标记并上报假错误 — apps/desktop/src/main/updater.ts:289-296

installNow 在点击「立即重启」后 3s 触发兜底,但 before-quit(apps/desktop/src/main/index.ts:4720-4821)会 preventDefault 并用 Promise.allSettled 异步关停约 10 个服务后才真正 app.quit(),静默安装器也是在退出完成时才被拉起。即从点击到安装器启动的间隔 = 服务关停耗时,并非安装失败信号:

  • 若关停耗时 > 3s(子进程带宽限期退出属常见;index.ts:1081 的注释甚至承认「before-quit 异步清理挂起」是真实场景),定时器会在正常退出过程中触发:clearPendingUpdate() 提前删掉标记 → 重启后 verifyPendingUpdate 无从核验。此时若安装随后仍失败,「更新未完成」弹窗不会出现、installed 上报丢失——本 PR 要打破的静默循环在慢退出场景下原样回归。
  • 同时会广播「安装程序未能启动」并向服务端上报 error,即使更新随后成功,统计被假失败污染。

建议:兜底不要以点击时刻起算的固定 3s 判定,而应以「退出确实未发生」为准——例如在 will-quit/quit 里取消定时器并把超时拉长到覆盖最坏关停耗时,或改为检测 quit 流程卡死(before-quit 长时间未完成)才报错。

2.【低】核验弹窗阻塞 updater 初始化,期间的更新下载不会弹安装提示 — apps/desktop/src/main/updater.ts:130

start() 里 await this.verifyPendingUpdate() 会阻塞到用户关掉「更新未完成」错误框,之后才注册 update-downloaded/error 监听并调度首次 check(updater.ts:131-157)。而 registerIpc 在 await 之前已生效(updater.ts:118):弹窗未关时用户在设置页点「检查更新」可正常触发下载,但 update-downloaded 无监听 → promptInstall 不会执行,设置页却显示「已下载完成,按弹窗提示重启生效」,提示与实际不符。建议先注册监听、再 await 核验、最后调度 check——同样满足「核验先于首次 check」的约束。

3.【低】广播循环未过滤已销毁的 webContents — apps/desktop/src/main/updater.ts:292-294(150-152 同型)

3s 兜底触发的时点恰逢窗口随退出流程销毁,对已销毁的 webContents 调用 send 会抛异常,导致循环中后续 report('error') 被跳过并产生未捕获异常日志。建议加 if (!wc.isDestroyed()) 守卫(download-progress 的既有广播是同型问题,可一并补上)。

其他核对项(无问题)

  • compareVersions 对 1.2.10/1.2.9 及 -nightly.N 后缀的比较语义满足核验需要。
  • quitAndInstall(true, true) 与 autoInstallOnAppQuit(用户选「稍后」路径无标记、也不误报)行为自洽。
  • AgentChatView 中 busy 收窄后,双击重复提交由 submitting、handledDocumentSelectionsRef、同步 dismiss 状态各自兜住。
  • update:error 仅应用内广播,report() 载荷仍只含 installId/版本/渠道,无新增隐私暴露。

zengjiayi819 added 24 commits October 26, 2026 18:23
用户反馈:导入 Word 模版后排版生成的文档未按模版排版(字体/格式/标题/页面
布局与模版不符),且模板失败静默降级无提示;要求生成文档严格按模版排版、
解析失败明确报错、模版里的图片 logo 保留。

根因(三个叠加):
1. 管线是「提取规则→重新排版」:格式卡只描述版式规则,模版文件本身不参与
   输出渲染。正文表格/图片走 original 块字节保真,但页眉部件由引擎全新生成
   ——模版页眉里的校徽 logo 天然丢失
2. 静默降级链:normalize 版式抽取失败被 catch 吞掉(不阻断)、解析代理输出
   缺字段由 validate 全部兜中文论文默认值——模版根本没解析成功时,产出的是
   一张「看起来正常」的默认样式卡,用户完全无感知
3. 解析代理无「页眉图片」的概念:facts 不抽图片,卡里没字段,无从保留

修复:
- ooxml-facts:页眉/页脚图片抽取(data:URL + 版位事实:浮动/衬底/位置/尺寸),
  text 打 [图片×N] 标记供代理印证
- 格式卡新增 header.images(validate 守门:data:URL 结构合法才收,脏数据丢弃,
  缺省空数组不编造);解析代理 SYSTEM.md 增加 images 纪律与 templateFactsGaps
  申报通道;parse 全链路显性失败(解包/归一化/事实抽取每步带阶段名抛错)
- thesis-logo-backfill:装订产出后把卡内页眉图片写回输出 docx——media part
  + 页眉 rels + inline/anchor drawing(按模版版位)+ Content-Types 默认项,
  同字节去重;不动 genoffice submodule
- UI 显性化:解析结果透传 factGaps(模版事实缺口),资料区「设为模版」成功
  但有缺口时弹琥珀色 warning toast(新增 toast warning 变体),不再静默

测试:gateway thesis-format 8 用例全绿(新增图片抽取/守门 3 例);桌面新增
thesis-logo-backfill 3 例(回填/去重/防御);tsc 双端零新增错误
用户在 AI 排版工具导入《开题与需求、系统设计报告(课程模板排版).docx》时
两次失败,报「本次处理达到模型输出上限,未能生成最终结论」。

根因:thesis-formatter 未配置 policy.model.maxTokens,继承全局 background
档缺省 8192(NXCORE_AI_BACKGROUND_MAX_TOKENS 未配置时的 config.ts:640
缺省值)。解析要输出完整论文格式卡 JSON,规范条文长的模版(该文件 7114
字符条文 vs 此前成功的模板 893 字符)在 8192 预算下顶爆 stopReason=length。
输入侧正常(合计 10.7KB),纯输出预算不足——所以「有的模版能过、有的报上限」。

修复:agent.yaml 显式 model.maxTokens: 32768(子代理 model 覆盖链已支持,
registry.ts policy.model 校验);timeoutSeconds 300→480(预算放开后长条文
实测更久,原 190s 已逼近 300s 上限)。

验证:subagents/concurrency-retry/thesis-format 三套件 22 用例全绿。
用户反馈「在 Room 产物区用 AI 排版工具,上传了模版,但它没执行排版」。
排查结论:并非没执行,而是模版解析(第一步)失败 → 格式卡未入库
(thesis_format_cards 0 行)→ 第二步 reformat 无卡可排。但 UI 只把
内部错误码与上游原始报错直接显示:

- AI 页签面板把 thesis_format_card_not_found、nginx「502 Bad Gateway」
  HTML 整页原样塞进一行小字,用户读不出发生了什么
- 资料区「设为模版」失败 toast 同样裸露上游报文

新增 shared/thesis-error-message.ts:按上游故障/额度/模版阶段/源文档阶段
分类翻译成一句可操作的话(含「模版与文档必须在同一个 Room」「解析约 1–5
分钟」「旧 .doc 请另存 .docx」等引导),未知错误保留原文截断、去 HTML 标签。
面板 hook(office-preview-registry)与资料区 toast(MaterialsPane)统一接入。

测试:新增 9 例(含 502 HTML 不泄漏、缺卡引导、未知错误不吞信息);
桌面全文套件与基线逐项一致(155 文件 / 791 用例为既存环境失败,零回归)。
用户诉求:产物区文件的「⋯」菜单(现有 在文件夹中显示 / 删除)里加一个
「排版工具」;点它弹出上传论文模版(限 Word 格式),导入后选中的文件直接
按模版排版,模版里的页眉 logo 图片要保留。

实现:
- ArtifactLibraryPane:「⋯」菜单新增「排版工具」,仅对 .docx 产物显示
  (后端只接受 docx 源,其它类型直接不出现,避免点了才报错)
- 新增 ThesisReformatDialog:两条模版来源——
  · 上传 Word 模版(pickAndSet wordOnly,选择框只收 .docx)→ 解析成功后
    **立即开排**,贴合「导入模版就排版」的预期,不用再点第二次
  · 复用本 Room 已有格式卡(单选,手动「开始排版」)
- 长任务可见性:解析/装订各 1–5 分钟,对话框全程显示当前阶段
  (正在解析模版… / 正在按模版排版…),且进行中禁止关闭,避免用户误判
  「没执行」;失败经 describeThesisError 翻成人话(不再甩 502 HTML/机器码)
- 模版事实缺口(factGaps)非空时在弹窗内显性提示
- 完成后广播 everroom:knowledge-changed,新文件(xxx-排版版.docx)即时出现
- IPC:pickAndSet 增加 options.wordOnly,主进程选择框据此收窄为 docx;
  MaterialsPane 原上传入口(支持 zip/doc/pdf)行为不变

模版 logo 保留走先前提交的 backfillThesisHeaderLogos(页眉图片按原版位写回
输出 docx),内容排版走 thesis-binder。

验证:新增 6 例接线契约测试 + tsc 零新增错误 + electron-vite build 通过
+ context-room 套件 116 例全绿。
用户反馈「产物区点排版工具、上传模版后,文档没有按模版排版」。排查发现排版
请求根本没走到装订代理:网关立刻返回 thesis_source_file_not_found。

根因:FilesService.catalogEntry 的实现是
    return this.listCatalog(200, 0).items.find((item) => item.id === fileEntryId)
只扫「最近更新的 200 条」目录。用户库已有 1460 个文件,目标文档
《开题与需求、系统设计报告.docx》按 updated_at 排名 1281——永远不在窗口内。
同一入口还服务「资料区 → 设为模版」,所以两条路一起坏。

影响面:论文排版 reformat、资料区设为模版对绝大多数历史文件直接失败;且失败
发生在派发代理之前,不产生任何子代理调用与产物文件,用户侧表现就是「点了没
反应、什么都不生成」。

修复:
- 抽出私有 catalogQuery()(同一套 join 投影)与 toCatalogFileDto()(同一套
  行→DTO 映射),listCatalog 与 catalogEntry 共用,避免两处字段漂移
- catalogEntry 改为按 id 直查(where id = ? and deleted_at is null),不再受
  分页窗口影响

验证:新增 2 例回归(205 个填充文件把目标挤出前 200 后仍能按 id 取到;不存在
的 id 与已删条目返回 null),files-store 16 例全绿;对运行中的网关实测同一文档
从 thesis_source_file_not_found 变为成功派发装订代理。
用户反馈「模版的图片 logo 都要保存下来」,实测发现真正丢失的是**封面校徽**,
不是页眉图:模版《人工智能创新创业实践课程设计报告(模板).docx》里唯一的图片
word/media/image1.png(descr="学校常用标识2",居中 inline,位于封面顶部
「通信工程学院」上方),页眉/页脚一张图都没有。

装订是「提取规则 → 重建文档」:封面由格式卡字段重新生成,原封面里的图片不会
带入,于是输出 docx 的 media 目录为空、封面没有校徽。此前只做了页眉图回填,
对这份模版完全不生效。

修复(图片字节全程不经过解析代理):
- ooxml-facts:新增 coverImages——抽取首个分节/分页之前的正文图片(校徽/校名
  标识),带尺寸与段落对齐;把 rels→media 解析与绘图事实解析抽成
  partMediaByRid / drawingFact,与页眉页脚共用
- types / validate:格式卡新增 cover.logo(单张 data:URL 守门,脏数据丢弃,
  缺省不写字段)
- service.mergeTemplateImages:网关从模版事实**确定性**写入 card.cover.logo 与
  card.header.images,不覆盖代理已给的合法值、没有事实就不编造
- SYSTEM.md:明确图片不归代理处理(禁止回显 base64——既烧输出预算又易抄坏),
  只申报「声明有图但没抽到」的缺口
- thesis-logo-backfill:新增 backfillThesisCoverLogo——把封面图按模版尺寸/对齐
  插入输出正文最前(居中段落 + inline drawing + media + document rels +
  Content-Types),与页眉回填共用 pickImages / ensureContentTypes
- office-generation:装订产出后依次回填页眉图与封面图,各自失败不阻断出稿

验证:gateway thesis-format 12 例(新增封面抽取/确定性写入/不编造/脏数据丢弃)、
桌面 thesis-logo-backfill 5 例(新增封面插入最前+居中介质+rels 注册+无图原样返回)
全绿;tsc 双端零新增错误;electron-vite build 通过。
实测发现解析出来的格式卡「外观完整但不是模版的」:模版封面明明写着
「通信工程学院 / 课程考核报告 / 2026-2027学年第一学期」,卡里却是
schoolName 空、degreeLine = 校验层兜底值「本科毕业论文(设计)」。代理拿到了
封面文本(facts.documentsText 里有)却没往封面字段填,校验层再补默认值,
于是排版出来的封面跟模版对不上——用户说的「没有按我想的模版排版」正是这个。

修复:
- ooxml-facts:新增 coverText——把首个分节/分页之前的段落文本按序抽出(≤40 行),
  让代理与兜底逻辑都有明确的封面文本块,不必从全文里找
- service.mergeTemplateImages:确定性兜底补最机械的三项(校名、文种行、学年学期、
  落款「…制」),只填空缺、绝不覆盖代理的非空值;coverText 为空就保持原样
- SYSTEM.md:明确封面字段必须取自 coverText;识别不到就写 templateFactsGaps,
  禁止留空让系统填默认值、禁止用自己熟悉的学校格式替代

验证:thesis-format 15 例全绿(新增封面兜底/不覆盖代理值/空文本不编造 3 例),
tsc 零新增错误。
实测:模版封面校徽 word/media/image1.png 有 122KB,base64 后约 4 万 token。
把它随 facts 一起发给解析代理,既挤占上下文窗口,又把输出预算挤到截断——
即使 maxTokens 已提到 32768,解析仍报「本次处理达到模型输出上限」。

图片字节本来就不需要经过模型(网关从模版事实确定性写入 card.header.images /
card.cover.logo,装机时回填)。新增 factsForAgent:派发前剥离全部 data:URL,
只保留 hasBytes/尺寸/浮动/版位等元信息;documentsText 与 coverText 原样保留。

验证:新增用例断言代理输入序列化后不含 data:image 与 base64 本体、封面图元信息
(626×89、居中)与 coverText 保留;thesis-format 16 例全绿,tsc 零新增错误。
排查论文排版「点了没反应」时发现更底层的问题:长任务每隔几分钟就被打断成
cancelled,与业务无关。

链路:runtime config 变更 → hotReloadAgentRuntimes() →
subagentRuntimeManager.invalidate() → dispose() 对所有缓存 runtime 调
runtime.dispose(),**正在跑的调用被直接中断**。原注释认为「配置变更是罕见手动
操作,可接受」,但现实中 runtime config 会被周期性重推(桌面端同步、AI 中转
会话刷新、连接器会话变更),于是一次 6 分钟的模版解析必然在中途被杀:
2026-10-27 实测 10:58、11:18 两次解析均在 4–5 分钟处变 cancelled,
slides 等其它长链路同样受害。

修复:
- runtime-manager 记录每个 revision 的在跑调用数(acquire/release);
  invalidate() 把缓存条目移出(新调用立即用新配置重建),但**仍有调用在跑的
  runtime 只标记为待回收**,等最后一个 release 再 dispose
- dispose()(网关关停)维持强制全量回收,并清理新增状态
- orchestrator:ActiveInvocation 记录 revisionId,两处终态清理路径统一 release;
  配置热重载期间在跑的调用因此不受影响

验证:新增 2 例回归(忙碌 runtime 延迟到 release 才 dispose;多调用并存时
逐个归还),subagents 12 例、thesis-format 16 例、files-store 16 例、
concurrency-retry 全绿;tsc 零新增错误。
实测模版解析耗时 364–394s 成功、偶发 >480s(模型与中转波动)。480s 上限会让
跑了八分钟的解析被杀,用户侧表现仍是「点了没反应」。代理超时提到 15 分钟,
桌面桥同步提到 20 分钟——桥必须先于网关放弃,否则客户端断开等于白跑。
封面 logo 进卡后,bind_thesis 的 formatCard 输入又带上了 122KB base64
(≈4 万 token),实测排版直接报「本次处理达到模型输出上限」。

新增 cardForAgent:派发给装订代理的卡片同样只保留图片元信息(hasBytes/尺寸/
版位),schoolName 等文字字段原样;网关→桌面桥那一跳仍传带字节的完整卡片。
与 parse 侧的 factsForAgent 共用 imageMeta。thesis-format 17 例全绿。
按用户诉求(提取并锁定模板固定元素、识别章节槽位、按标题语义映射段落、
套模板样式生成、logo 原样保留、未匹配内容标记不丢、增加删除已上传模版按钮)
给出可评审的技术方案与映射规则设计。

文档要点:
- 诊断现有管线 5 个结构性缺陷(模板骨架从未提取、索引区间映射静默丢块、
  样式只有 4 级标题、编号不提取、无未匹配容器),并用 Room 里两份真实文件
  佐证(模板 9 个章节 vs 内容 62 块/19 标题,章节名与层级都对不上)
- 四段式架构:TemplateProfile / ContentProfile → MappingPlan → RenderPlan
- 数据结构草案(TemplateProfile.slots/StyleLock/numbering、ContentProfile、
  MappingPlan 守恒字段),并说明与现有 ThesisFormatCard 的兼容投影
- 模板解析:固定元素清单(logo/页眉页脚/水印/样式锁采样/编号方案)与
  槽位识别算法(模板正文大纲即骨架)
- 映射规则:归一化规范、R0–R5 五级阶梯、similarity 打分公式、锚点词典、
  层级映射、数量不匹配四情形、编号重算、段落级落位
- 未匹配三桶 + OOXML 落地(highlight+批注)+ 块守恒断言(宁可不生成也不丢)
- 渲染自检 6 项(块守恒/logo hash/页眉/样式指纹/目录域/编号)
- 删除已上传模版按钮设计,含悬空成员引用清理(实测存在)
- M1–M5 里程碑与验收、风险取舍、5 个待拍板决策点
按 docs/thesis-slot-mapping-design.zh-CN.md 实施 M1(模板剖面,纯确定性)。

新增三个模块:
- text-normalize:剥编号(一、/1.1/(2)/第3章)、NFKC 归一、去标点空白、
  中文 2-gram token、Jaccard——模板槽位与内容章节共用同一套归一化,否则
  「一、业务场景分析」与「业务场景分析」永远对不上;同时导出编号体系归类
- thesis-anchors:结构标题锚点词典(摘要/Abstract/目录/参考文献/参考资料/
  致谢/附录/承诺书),模板侧与内容侧共用
- template-profile:TemplateProfile(fixed: page/logos/header/footer/styles/
  numbering + slots)+ 槽位识别 + 样式锁构建(卡里有就用卡,没有从 styles
  事实推导,仍没有再回退正文**并记 gap**,不静默编造)

ooxml-facts 新增两项抽取:
- outline:模板正文大纲(样式/段落的 outlineLvl、标题样式名兜底),带层级、
  styleId、numPr、封面区标记——模板自身的章节骨架就是槽位骨架(此前完全没抽)
- numbering:解析 word/numbering.xml(abstractNum/lvl 的 numFmt/lvlText/start/
  indent + num 映射 + 标题样式 numId),归类 cn-upper/cn-lower/decimal-tree/
  decimal-dot/none

实测(Room 内真实模版):16 条大纲 → 9 个槽位(7 正文章节 + 参考资料/reference
+ 附录/appendix)、编号 cn-upper「%1、」、封面 logo 626×89 入库,缺 h4/题注/公式
样式如实报 gap。

测试:thesis-format 24 例全绿(新增大纲抽取/编号方案/无编号不编造/槽位识别/
样式锁 gap/封面 logo 入列/无骨架降级 7 例),tsc 零新增错误。
新增 content-profile:把引擎的扁平块大纲变成带语义身份的内容剖面(纯确定性)。

- 标题块:剥编号(一、/1.1 等)→ normalized + originalNumber + 编号体系
  (cn/decimal/parenthesized/alpha);2-gram token;结构锚点识别
- 角色推断不直接用原始 level:按标题层级在本文档里的相对深度重分档
  (最小 level=chapter,其次 section,再深 subsection)。实测源文档章是 H2、
  节是 H3,若直接用 level 会把章当成二级
- 章节树:按层级建树并回填每棵子树的块区间 [start,end),供 R2 顺序对齐用
- 图表题注配对:紧邻图片/表格且文本匹配「图 X / 表 X / Figure X」→ isCaption
  + captionOf(供渲染套题注样式)
- 每块带 traceId + chapterTraceId,为 M3 的块守恒断言与 M4 的未匹配标记铺路
- 无标题文档记 gap(不静默)

实测(Room 内真实内容文档):62 块 → 6 章节根 + 11 小节,层级/区间/角色/
编号剥离全部正确(一、业务场景分析 → business 场景分析等)。

测试:thesis-format 29 例全绿(新增 5 例),tsc 零新增错误。
新增 mapping-plan:模板槽位 × 内容章节 → MappingPlan(分配表 + 未匹配 + 守恒核对)。

规则流水线(顺序即优先级,实测调过两轮):
  R0 锚点 → R1 精确标题 → R3 模糊(≥0.62) → 代理裁决(0.70) → R2 顺序对齐(0.85) → R4 追加(0.60)
- R3 必须在 R2 之前:先让标题语义确实对上的章节抢槽位,否则位置对齐会把它们钉死
  (实测「需求梳理」本该命中「项目需求分析」,先 R2 就被位置吃掉)
- 代理裁决早于 R2:模型给的语义归属强于位置猜测
- R4 把剩余章节追加到正文容器,永不丢弃

相似度(方案 §6.2):0.5×token Jaccard + 0.2×编号体系同类 + 0.2×顺序一致
+ 0.1×锚点命中。顺序占 0.2 是刻意的:中文论文模板章节顺序高度稳定。

守恒断言(不变量 I3):matched ∪ unmatched 必须覆盖全部内容块,否则抛
thesis_mapping_not_conservative——宁可不生成也不丢内容。另产出
pendingForSemantic(0.40–0.62 区间)供网关派发一次代理裁决。

实测(Room 内真实模版 9 槽位 × 真实内容 62 块/6 章):
6 章全部落位、守恒 62/62、0 丢失,映射表与人工判断一致
(业务场景分析→项目任务、需求梳理→项目需求分析 …)。

测试:thesis-format 39 例全绿(新增 10 例:锚点直达/顺序对齐/源多追加/源少不硬塞/
首标题前散块 R5/守恒计数/低置信钩子/代理裁决改写/裁决 null),tsc 零新增错误。
- service.parseBytes:解析成功后为承载版式的 docx 成员建 TemplateProfile
  挂到卡上(profile 只存角色/尺寸元信息,字节仍留在 header.images/cover.logo)
- service.reformat:卡带 profile 且槽位非空 → v2 路径
  (内容剖面 → 映射计划 → 装订规格),**不再派 bind_thesis 代理**;
  老卡片(无 profile)回落原代理路径,向后兼容
- mapWithProfile 产出报告:守恒核对、每条分配用的规则/置信度/块区间、
  未匹配块、needsReview(0.40–0.62 靠顺序推断的章节)、gaps
- plan-to-spec:把映射计划翻译成当前渲染器吃的 ThesisBindingSpec——
  章节树按**叶子区间**展开(DFS 前序、互不重叠、按文档序),
  参考文献/附录/致谢/摘要+关键词从对应槽位抽文本,追加章节排在正文之后

实测(真实模版 × 真实内容):19 个章节(含二级)、区间不重叠、覆盖 62/62 块
零丢失、参考文献/致谢为空(源文档确实没有);此前代理路径只产出 6 个一级章节。

测试:thesis-format 42 例全绿(新增 3 例:区间不重叠与顺序/文献致谢抽取/
未匹配进报告),subagents+files 共 70 例全绿,tsc 零错误。
- plan-to-spec:映射的 unmatched 区间转成 spec.unmatchedBlocks(带说明 label)
- gateway/desktop 类型同步该字段(bridge 原样透传到 genoffice)
- 更新 genoffice 子模块 pin:装订支持渲染「未归类内容」小节
  (红字 + 黄色高亮,内容逐块保留)

测试:thesis-format 43 例全绿(新增 1 例:未匹配块进 spec.unmatchedBlocks),
桌面 tsc 零新增错误。
新增 thesis-selfcheck(方案 §7.3),装订产出后先拆包核对:
硬伤(抛错、不出稿):
- logo 字节与模版不一致(渲染链路重编码/丢图)
- 声明启用页眉但输出没有页眉部件
- 映射报告有未匹配块,但输出没有「未归类内容」小节
软伤(warnings):
- 页脚部件缺失、目录 TOC 域缺失
- 无未匹配块却出小节
- 一级标题中文字体与模板锁不一致(抽样式失败也提示)

office-generation 接线:自检不过直接抛「论文排版自检未通过:…」,
warnings 通过 console + 返回值透出。

测试:新增 5 例(字节一致/重编码硬伤/页眉缺失/未归类小节一致性/字体软伤)全绿,
桌面 tsc 零新增错误。
映射报告(v2):
- reformat 返回 report 后留在弹窗内展示,不再直接关闭:守恒核对
  「matched/total 个内容块已按模版落位」、未匹配块数(提示在文末
  「未归类内容」小节保留并高亮)、每条分配的规则与置信度(<70% 标黄)、
  需人工复核的章节数、模板缺口列表

删除模版文件(按用户要求,不动删除卡的语义):
- 模版行右侧新增 🗑「删除模版文件」:仅当该模版来自资料区
  (sourceFileEntryId 非空)才出现——直传解析的模版没有库内文件
- 二次确认明确「格式卡会保留,已生成的文档不受影响」
- 只调 files.delete(删文件),**不调用 removeCard**(卡继续可用于排版)

bridge/shared 类型的 reformat 返回值补 report 字段。

测试:artifact-thesis-tool 9 例全绿(新增 3 例:报告留在弹窗/删除的是文件
不是卡/确认文案含格式卡会保留),桌面 tsc 零新增错误,electron-vite build 通过。
补实施清单(逐里程碑提交号)、真实数据实测结论(9 槽位 × 62 块守恒零丢失)、
v2 启用前提(需重新解析一次模版以获得 profile,旧卡片回落原路径)、
以及 5 项拍板结论。
编号重算(决策③):
- 网关把模板剖面的 numbering 挂到送渲染的卡片上(renderCard)
- plan-to-spec 的章节标题剥掉源文档原编号,避免「1.1 1.1 政策背景」重号
- 渲染器按方案生成编号(子模块 7e1e6b1)
- 实测真实模版:cn-upper → 一、/(一)/(二)/二、/(一),与模板一致

代理裁决(方案 §6.9):
- thesis-formatter 新增 adjudicate_mapping 任务:input 加 mappingSlots/
  mappingCandidates,output 加 decisions[{chapterTraceId, slotId|null, reason}]
  (slotId 允许 null = 对不上就追加,禁止硬塞)
- SYSTEM.md 补裁决纪律:每个待定章节必须且只能回一行、id 原样回填、
  不返回正文/样式、判断先看实词再看位置
- service.adjudicateMapping:只把 0.40–0.62 区间的章节(≤20 条)发出去,
  失败/为空一律回落确定性顺序对齐(裁决是加分项,不是必经路径)
- sanitizeMappingDecisions:只接受引擎问过的章节 id 与真实存在的槽位 id,
  重复取首条、reason 截断 120 字——模型编造 id 比没有裁决更危险
- 报告新增 adjudicated 计数,needsReview 仍列出裁决后仍需人工核对的章节

实测:6 条待裁决章节,模拟裁决把「一、业务场景分析」由 slot-1(R2-order)
改判 slot-2(R3-fuzzy) 并带出理由。

测试:thesis-format 48 例全绿(新增 5 例:裁决消毒/编造 id 丢弃/重复与非法输入/
reason 截断/schema 声明),子模块 thesis-binder 10 例全绿,tsc 双端零错误。
用户要求:把产物区 ⋯ 里的「排版工具」整个搬到 Word 卡片 AI 面板的排版里,
并在那里加「删除以往模版文件」,且要删干净、不留残留。

- 产物区:移除「排版工具」菜单项与其弹窗(ThesisReformatDialog 删除、
  专用 CSS 清掉),入口统一到 AI 面板
- 新 IPC thesis-format:template:delete:**先删模版文件(/v1/files/:id 硬删 +
  Room 成员引用级联),再删格式卡**;keepCard=true 则只删文件保留卡;
  文件删不掉不阻断删卡,结果如实返回 {fileDeleted, hadFile, cardKept}
- 文件删除级联修根因:deleteCatalogEntry/deleteFile 此前**不清
  room_source_memberships**,删掉的文件在 Room 里留悬空引用——这正是
  「删了还残留」的来源(实测存在 file-9bbf3f04 这类悬空行)
- 面板桥接口与 runtime hook 动作类型同步 delete-template(+keepCard),
  reformat 返回值带 report(映射报告)

测试:桌面 29 例(新增 ai-panel-thesis 7 例:导入即排版/缺口提示/映射报告/
两种删除/IPC 与类型/如实反馈/级联清引用),网关 64 例全绿,tsc 双端零错误,
genoffice 运行时已重建(build/genoffice-runtime 含新面板代码)。
子模块 560321e:删除按钮改为右上角小图标(浅灰常态、hover 转红、阻止冒泡、
确认文案「确定要删除该模板文件吗?」、确认后即时从列表移除)。

测试同步:ai-panel-thesis 10 例全绿(按钮唯一性/冒泡拦截/确认文案/即时移除/
样式灰转红),桌面 tsc 零新增错误,genoffice 运行时已重建。
子模块 16f6300:删除按钮与预览按钮同尺寸(24×24)、
行内二次确认替代 window.confirm(后者抢焦点会收起下拉面板)。

测试同步:ai-panel-thesis 11 例全绿,桌面 tsc 零新增错误,genoffice 运行时已重建。
zengjiayi819 added 17 commits October 27, 2026 19:24
按用户的产品化要求重建排版工具前端(原入口是 AI 面板里的窄条),第一期落地
可视外壳与核心交互:

视觉(参考 Notion / 飞书 / Vercel):
- 独立作用域的设计变量(--ts-*):白/浅灰底、indigo 主色 #6366f1、柔和红
  #ef4444;圆角 14px 卡片、充足留白、系统无衬线字体栈
- 一键深浅色(localStorage 持久化),全部变量级切换
- 拖拽上传区拖入时:实线 indigo 边 + 4px 柔光 + 轻微放大
- 文件卡片 hover:边框提亮 + 阴影加深 + translateY(-1px) 上浮
- 加载态用骨架屏(shimmer 动画),不用转圈

功能:
- 左栏模版库:拖拽 .docx 直传网关解析(新 IPC parseUpload,字节不落盘)、
  卡片列表(图标/文件名/时间)、缺口与失败走人话翻译、行内二次确认删除
  (新 IPC deleteTemplate:先删模版文件含 Room 成员引用级联,再删格式卡)
- 中栏待排版文档:Room 内 docx 列表、单选、开始排版、结果卡片(落位数统计)
  + 下载 Word + 重新排版
- 右栏:规则面板(严格/融合、保留 Logo/页眉页脚,后端未支持项显式标注不改
  假开关)+ 四阶段进度(解析模板结构 → 映射内容段落 → 渲染样式)
- 大纲抽屉:展示模版槽位骨架(title/kind),样式条款型模版给出降级说明

入口:Room 页右下角「智能排版」悬浮按钮打开全屏工作台。

测试:新增 thesis-studio 10 例(入口/三栏/视觉规范/拖拽/缺口提示/行内确认/
大纲抽屉/四阶段/不放假开关)全绿;桌面 tsc 零新增错误,electron-vite build 通过。
- 网关 reformat 接受 options:{ mode: strict|blend, keepLogo, keepHeaderFooter,
  headingMapping{chapter,section,subsection} };路由用 TypeBox 严格校验
- keepHeaderFooter=false → 卡片层面禁用页眉页脚(渲染器据此不生成部件)
- keepLogo=false → 桌面跳过 Logo 回填(页眉图 + 封面校徽都不写)
- mode=blend → 装订规格标记 preserveSourceFormat,渲染器正文改原文透传
  (子模块 773254e),标题/编号/页面仍按模版
- headingMapping → plan-to-spec 的 flattenLeafRanges 按角色覆盖层级
- 选项指纹进幂等键:不同选项产出不同文件,不会复用旧产物
- 工作台 UI 解开规则面板:模式单选、Logo/页眉页脚勾选、三档标题层级下拉,
  全部随「开始排版」下发;去掉「待后端接入」的占位说明

测试:网关 thesis-format 51 例(新增 3 例:融合模式标记/层级映射生效/默认不变)、
子模块 thesis-binder 13 例、桌面 thesis-studio 10 例全绿;三端 tsc 零新增错误;
genoffice 运行时与桌面 bundle 均已重建。
进度与超时(本轮重点):
- 网关新增作业注册表(jobs.ts)与两条路由:POST reformat-jobs 立即返回
  jobId(202),GET jobs/:jobId 轮询 {status, phase, progress, result, error}
- 阶段 parsing→mapping→rendering 映射 15/45/75/100,service.reformat 加 onPhase 回调
- 根治长任务超时:此前客户端挂着 5–10 分钟的单次请求,中间层 5 分钟就掐断,
  用户看到「AI 不可用/超时」而服务端其实还在跑(实测 19:00 那次客户端 5 分钟
  报错、服务端 8.5 分钟成功建卡)。改为短提交 + 轮询后不再有长请求
- 工作台改为 reformatStart + 每 900ms 轮询,进度条走真实阶段

结果对比:
- 网关在映射报告里产出 diff:左=源文档章节(层级/标题/块数),右=排版后章节
  (层级/标题/落位槽位/命中规则/块数),另列未匹配块
- 工作台「结构对比」双屏抽屉:左原内容、右排版后,未匹配块红色区单独列出
  (标注为内容结构级对比,非像素级预览)

PDF 导出:
- legacy-convert 新增 convertOfficeFileToPdf(soffice --convert-to pdf)
- 新 IPC thesis-format:export:pdf → 输出到 ~/Downloads/EverRoom 并定位
- 本机未装 LibreOffice 返回 {unavailable:true},UI 给可执行说明而不是假装成功

验证:网关 54 例、桌面 13 例全绿;三端 tsc 零错误;活体网关实测作业接口
(提交返回 jobId、坏文件 job 快速 failed 带原因、未知 job 404)。
用户澄清产品里有两处排版入口:
① Room 页右下角悬浮「智能排版」(第一期加的独立工作台,面向 md 文件)
② Word 编辑区 AI → 排版(AI 生成/手动导入的 docx 用这个)
只保留 ②,故删掉 ① 及其独立工作台组件(ThesisStudio / thesis-studio.css /
接线测试),ContextRoomPage 恢复原样,专用 CSS 清理干净。

后端能力保留(与入口无关、②可复用):作业化排版进度(reformat-jobs +
轮询)、结构对比数据(report.diff)、PDF 导出(soffice 转换,缺依赖时明确
返回 unavailable)、拖拽直传解析(parse-upload)、删模版含文件级联。

验证:桌面 tsc 零错误,electron-vite build 通过,AI 面板与产物区 14 例测试全绿,
全仓已无 ThesisStudio 引用。
子模块 ebd12c9:弹窗宽度改 fit-content + 视口内收上限、列表禁横向滚动、
删除按钮 flex-shrink:0 并把点击区放大到 28×28(图标 16px)。

测试同步:ai-panel-thesis 13 例 + 产物区 3 例全绿(新增 2 例钉住响应式约束),
genoffice 运行时已重建。
子模块 50594b4:浮层改用 width: min(336px, calc(100vw - 24px)) 自适应视口并
取消 overflow 裁剪;卡片主区 overflow: hidden + min-width: 0,长文件名不会顶飞
右侧按钮;删除按钮保持 28×28 放大点击区。

测试同步:ai-panel-thesis 14 例全绿(含按钮始终渲染与不裁剪两条约束),
genoffice 运行时已重建。
- pickThesisTemplateFromDisk在选择文件后、解析前查同名卡(trim+小写比较):
  命中即暂存路径(pendingTemplatePick,按 room 一份)并返回 duplicate=true +
  sourceName + existingCardId,不启动解析(解析要几分钟,覆盖无法撤回)
- options.overwrite=true → 直接解析暂存路径,不再弹选择框(用户已确认替换)
- AI 面板:上传入口返回 duplicate 时在面板内弹警示确认条「该论文模板已存在」,
  两颗按钮「替换原文件 / 取消上传」;不用 window.confirm(会收起下拉)
- 面板桥 pickAndSet 与 runtime hook 动作类型同步 overwrite 开关

测试:桌面 19 例全绿(新增宿主判定/overwrite 复用暂存/面板内联提示 3 类断言),
双端 tsc 零错误,genoffice 运行时已重建(bundle 含 ai-thesis-dup 与新文案)。
子模块 f8554fc:卡片行改 grid(minmax(0,1fr) auto auto),删除确认条从
「无样式的行内子元素」改为 absolute inset:0 覆盖——修掉横向滚动条与文件名
被挤没;删除按钮恒定钉在行尾。

测试同步:ai-panel-thesis 20 例全绿,genoffice 运行时已重建。
① 模板原样保留(评价表等固定表单):
- ooxml-facts 新增 fixedBlocks 抽取:正文顶层 w:tbl(教师评价表/任务书表)+
  前置标题,原样 XML;跳过含 r:embed/r:id 的块(跨文档 rels 会断)与槽位
  示例表;placement 按首个大纲槽位二分为 prelim/appendix
- TemplateProfile.fixed.fixedBlocks 持久化进格式卡;planToBindingSpec 经
  templateProfile 参数透传进 spec;service.reformat 从卡片剖面带回
- genoffice binder 用 kind:'xml' 原样注入(prelim 在首个正文槽位后一次,
  appendix 附全文末尾)——实测真实模板:源 8 表 → 输出 10 表,多出的正是
  「项目经理评价」「指导教师评分及评语」,文本逐字在、自检通过

② 目录筛选融合:mode 暴露到 AI 面板(融合=全部保留未匹配追加(缺省)/
  严格=只保留对上模板目录的内容),随排版请求透传到映射层

③ 任务化 + 10 分钟超时:面板桥新增 reformat-job / reformat-job-status 动作,
  EverRoom main 接 gateway startReformatJob/reformatJobStatus;面板提交后
  900ms 轮询,进度条走真实阶段(parsing/mapping/rendering)+ 已耗时;
  10 分钟硬上限主动中断并提示「处理超时…请稍后重试或简化文档」

验证:gateway 56 例、docx-engine 939 例、桌面 AI 面板 22 例全绿;双端 tsc
零错误;genoffice 运行时已重建。
子模块:点 🗑 → 面板内居中模态(确认删除/取消),替换原内联确认条;
测试 23 例全绿,genoffice 运行时已重建。
问题:上传论文模版慢。实测本地 OOXML 事实抽取只要 20ms,慢的 100% 是解析
代理(把 178KB 事实 + 全文喂给 LLM 等结构化卡片,30s–数分钟)。

改法:版式信息本来就全在 OOXML 里,改为确定性构建卡片,代理只在拿不到骨架时兜底。
- 新增 card-from-facts.ts:页面/分节边距、正文样式、标题层级版式、页眉页脚、
  封面字段标签、校名/文种、图片字节,全部按模板真实值填(3ms 出卡)
- ooxml-facts 补记标题段落的 run 版式(样式常留空,段落 run 才是真实观感:
  本模板 h1 实测宋体 16pt,代理猜的「黑体18」是错的)
- service.parseBytes 走快速路径;无大纲/无样式时 confident=false 自动回落代理;
  运维可用 THESIS_PARSE_ENGINE=agent 强制走代理
- mergeTemplateImages 抽成独立模块(避免 service ↔ card 循环依赖)

保真度对比(同模板 代理卡 vs 本地卡):校名/文种/正文字体一致;页面边距本地
25.4/25.4/22/22(模板真实值)优于代理的 25/25/20/20;封面字段本地识别出模板
真实标签(题目/专业/班级/学生姓名/学生学号/指导教师/日期),代理留空。

端到端等价性:用本地卡跑完整排版,守恒 56/56、20 章节、表格 8→10(含模板
评价表)、logo 字节一致、自检通过——与代理卡产物一致,但解析 3ms。

测试:gateway 58 例全绿(新增 2 例:真实模板本地取值正确性 + 不可信时回落)。
问题:预览只显示格式卡重画的示意图(一页、占位文字),看不到完整封面/信息区/
目录/正文,用户以为文档被裁切。

- 面板桥新增 preview-template 动作,宿主按卡片 sourceFileEntryId 调
  files.openOriginal → 应用自带 Office 渲染器打开模版原文件,多页滚动、
  表格/图片/排版完整还原
- 拖拽直传的模版(无资料区条目):上传时把字节留在主进程缓存(LRU 8 份),
  预览时落临时文件 → 导入资料区(按内容去重,重复预览不产生重复文件)→ 打开
  真预览
- 注册表注入 openHostFile 钩子,避免 registry 直接依赖 files 桥
- genoffice 面板删除示意渲染与 modal,按钮改调新通道;无文件时明确提示

验证:桌面 tsc 零错误;AI 面板 24 例全绿(含「示意弹窗不得回归」断言);
genoffice 运行时已重建。
用户点预览报「该模版是拖拽直传的,未留存原文件」。根因:「上传论文模版」按钮
走 parseFromDisk,把字节 multipart 上传后卡片 sourceFileEntryId 存的是 null
——上一版只给拖拽通道加了字节缓存,且缓存随应用重启清空,老卡片必然落空。

正解:让每张新卡真正持有一个资料区文件条目。
- 网关:parseFromUpload 接受可选 fileEntryId(multipart 路由加 querystring)
- 桌面两条上传路径都先入库再解析(importPathsOnce 内容哈希去重,重复导入
  不会产生第二份文件):
  · 选择框路径 importTemplateFileIntoLibrary(path) → fileId → parseFromDisk(…, fileId)
  · 拖拽路径 落临时盘 importTemplateBytesIntoLibrary(name, bytes) → parseFromBytes(…, fileId)
- 老卡片自愈:预览时资料区存在唯一同名文件 → 直接打开;缓存命中则落盘导入后打开
- 提示语改为可执行:「请删除后重新上传一次(新版本会自动保存原文件…)」

卡片关联文件后,预览(真渲染原文件)、删除级联、从资料区重新解析三条链路都成立。

验证:gateway 58 例、桌面 AI 面板 21 例全绿;双端 tsc 零错误;运行时已重建。
上一版给面板预览挂的是 openHostFile 回调钩子,但**从未赋值**,dispatch 里
 于是静默跳过:桥照样返回 opened:true,界面上却什么
都没打开——用户看到的就是「点预览没反应」。

修法:
- 把文件页「打开」的内嵌预览逻辑抽成模块级 openFileInAppPreview(bridge, fileId,
  name, options),不再依赖 IPC event(面板钩子拿不到 event)
- 面板预览与文件页打开共用这一条路径,真打开预览实例;失败直接抛错给 UI 提示
- 面板路径打开后把主窗口 restore/show/focus——预览标签在主窗口顶栏,不聚焦的话
  用户盯着 docs 窗口仍会以为没反应
- 删掉 openHostFile/ThesisPanelHostOpener 这套没人赋值的回调,接口简化为
  previewTemplate(roomId, cardId)

测试:桌面 24 例全绿,新增回归断言(必须调 openFileInAppPreview、不得再出现
openHostFile、必须 focusWindow);tsc 零错误,electron-vite 构建通过。
网关(apps/gateway)
- 模版剖面 v3:自愈重建(版本 < 3 且字节可读时重算并落库),新增
  template-first 模式与子节槽位
- 模版大纲抽取:过滤「黑体三号,段前12磅」这类排版说明行;整篇不用标题样式时
  用模版自己的目录行派生章节骨架(去页码后定级)
- 模版导入:按文件头识别真实格式(.doc 改名成 .docx 不再冒充骨架),缓存与
  回退路径都做同一校验;拿不到可用模版时给出可执行提示
- AI 补写
  · 开关在映射之前落到卡片(原来渲染阶段才写,永远不生效)
  · 候选 = 剖面空槽 + 模版正文真实小节(直接扫 OOXML,跳过表格内段落),
    排除姓名/学号/签名/评分/评语这类需要本人填写或主观打分的位置
  · 「章|小节」上下文 + 分批并发,避免同名小节被复制、避免撞面板超时
  · 素材从「每节首句」改为论文全文(原来取到的是标题本身,模型判定无素材而拒写)
  · 补写结果与失败原因都进报告 warnings,面板可见
- 排版产物文件名:勾选 AI 补写时为 xxx-ai补写版.docx,否则仍是 xxx-排版版.docx

桌面端(apps/desktop)
- 模版骨架装订接上引擎,失败回落内容重建时把原因透出到 warnings
- 主进程主动打开的预览改为事件通知渲染进程(原来没人接这条事件,点预览没反应)
- 模版 docx 旁路缓存:与对象库同盘、随卡片删除清理

Agent(agents/thesis-formatter)
- fill_sections 输入新增 sourceText(全文),纪律改为「通读全文归纳、不编造、
  本人填写类字段一律跳过」

测试
- 新增 thesis-ai-fill / thesis-template-cache / thesis-template-docx-guard /
  thesis-title-heuristic / thesis-generation / office-preview-request
- 引擎侧 970 项、网关 thesis 套件 69 项全过
- 引擎:template-first 装订、固定块顺序、空节提示、三级标题、手打编号识别
- 文档端:AI 补写面板与 20 分钟超时
@zengjiayi819 zengjiayi819 changed the title 修复:agent 决策卡死等 + 更新重启循环 修复:agent 决策卡死、更新重启循环;feat(thesis):模版骨架排版 v3 + AI 补写 Oct 30, 2026
冲突处理:
- files/service.ts:保留本分支的 SQL 直查 catalogEntry(dev 的分页扫描是同一 bug 的
  临时方案),同时保留 catalogEntriesByName 自愈入口
- subagents/orchestrator.ts:取 dev 的并发池改动(agentDefinitionId / cancellationRequested),
  再把本分支的 revisionId 归还逻辑补回去(等待式调用与分离式调用两处 finally 都归还
  runtime,热重载期间被延迟回收的实例才会真正 dispose)
- 子模块 genoffice:把 dev 的 407d822(slides 组件画廊脚本)并入本分支,
  pin 更新为合并提交 b137354
zengjiayi819 added 5 commits October 30, 2026 15:04
…ntentTargetCapability

审查指出 helper 只接受 'document.edit' | 'document.continue',用例传了
'document.create' → TS2345,会让 CI 的 Check types 失败。
引擎是 NodeNext 下的相对导入(无扩展名),被 gateway 的 tsc 拉进程序后报
TS2835,直接让 CI 的 Check types 失败。测试只需要一份块大纲,手写即可。
审查指出:把决策卡按钮从「禁用」改成「可点」并没有真正解锁,因为
submitPendingIntent 在 activeRunId 非空时直接 return null(不发请求、不排队、
不报错),调用方又 .catch(() => undefined),用户看到的是「点了没反应」。

按其建议先做「把失败浮出来」:busy 时置 error(Agent 正在处理其他请求),
让卡住的状态可见。
- 并入 thesis 模版骨架排版与 AI 补写(本分支的 feat/thesis-template-binder 已合进 main)
- 同时带上 dev 的 407d822(slides 组件画廊 PPTX 构建脚本)与 main 上其他 22 个提交
- pin 改用 main 而不是特性分支,后续升级不再依赖分支存活
本轮排查论文排版时产生的临时目录(scratch/ 效果图与预览 docx、.dsh-office.* 渲染
中间产物、.workbuddy-ai 记忆缓存)体积几兆且只在本地有用,不该入库。
@21335464876
21335464876 merged commit 38ed4e7 into dev Oct 30, 2026
2 of 3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants