magic-agent 1.0.2 → 1.0.46

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. package/CLIENT-CONTRACT.md +254 -8
  2. package/README.md +48 -5
  3. package/client-ui/index.html +32 -0
  4. package/client-ui/pc/app.css +89 -1
  5. package/client-ui/pc/index.html +18 -8
  6. package/client-ui/pc/js/00-base.js +8 -30
  7. package/client-ui/pc/js/05-task-list-render.js +4 -1
  8. package/client-ui/pc/js/07-chat-stream-render.js +2 -85
  9. package/client-ui/pc/js/10-markdown.js +3 -171
  10. package/client-ui/pc/js/18-askbox.js +2 -176
  11. package/client-ui/pc/js/90-glue.js +1 -1
  12. package/client-ui/pc/js/91-workbench-stubs.js +4 -10
  13. package/client-ui/pc/js/93-rail.js +10 -3
  14. package/client-ui/shared/conv/01-dom.js +52 -0
  15. package/client-ui/shared/conv/02-media-kinds.js +34 -0
  16. package/client-ui/shared/conv/03-chat-message.js +1561 -0
  17. package/client-ui/shared/conv/06-ask-answer.js +377 -0
  18. package/client-ui/shared/conv/07-stream-delegation.js +158 -0
  19. package/client-ui/{pc/js → shared/conv}/09-stream-apply.js +121 -6
  20. package/client-ui/shared/conv/10-markdown-render.js +252 -0
  21. package/client-ui/shared/conv/18-askbox-markup.js +185 -0
  22. package/client-ui/{pc/js → shared/conv}/19-attachments.js +365 -21
  23. package/client-ui/shared/conv/27-conv-ledger.js +381 -0
  24. package/client-ui/{pc/js → shared/conv}/29-msg-variants.js +112 -3
  25. package/client-ui/shared/conv/README.md +257 -0
  26. package/client-ui/shared/conv/conv.css +1638 -0
  27. package/client-ui/shared/conv/dom-map.md +170 -0
  28. package/client-ui/shared/conv/manifest.json +44 -0
  29. package/client-ui/shared/conversation.css +368 -190
  30. package/client-ui/shared/conversation.js +1722 -788
  31. package/client-ui/shared/host-glue.js +143 -0
  32. package/client-ui/shared/transport-http.js +130 -6
  33. package/client-ui-mobile/index.html +2417 -103
  34. package/cmd/magic-client/clientctl.go +273 -0
  35. package/cmd/magic-client/main.go +71 -5
  36. package/go.mod +6 -3
  37. package/go.sum +4 -1
  38. package/internal/agent/arkclaw.go +3 -2
  39. package/internal/agent/arkclaw_stream.go +126 -8
  40. package/internal/agent/arkclaw_stream_test.go +97 -15
  41. package/internal/agent/arkclaw_test.go +3 -3
  42. package/internal/agent/attachments.go +18 -5
  43. package/internal/agent/attachments_test.go +7 -1
  44. package/internal/agent/capability_test.go +11 -1
  45. package/internal/agent/claude.go +96 -158
  46. package/internal/agent/codebuddy.go +108 -133
  47. package/internal/agent/codebuddy_gateway.go +8 -4
  48. package/internal/agent/codebuddy_gateway_stream.go +82 -2
  49. package/internal/agent/codebuddy_gateway_test.go +236 -1
  50. package/internal/agent/codebuddy_stream_test.go +262 -0
  51. package/internal/agent/codex.go +50 -9
  52. package/internal/agent/codex_stream_test.go +30 -0
  53. package/internal/agent/codex_test.go +24 -0
  54. package/internal/agent/custom_models.go +260 -8
  55. package/internal/agent/custom_models_test.go +204 -7
  56. package/internal/agent/dsh.go +464 -130
  57. package/internal/agent/dsh_resume.go +258 -0
  58. package/internal/agent/dsh_sdk.go +68 -17
  59. package/internal/agent/dsh_sdk_test.go +102 -2
  60. package/internal/agent/dsh_test.go +409 -36
  61. package/internal/agent/engine.go +26 -6
  62. package/internal/agent/engine_base.go +46 -2
  63. package/internal/agent/engine_base_test.go +29 -0
  64. package/internal/agent/engine_test.go +101 -0
  65. package/internal/agent/env.go +78 -2
  66. package/internal/agent/env_test.go +37 -0
  67. package/internal/agent/itemtracker.go +271 -0
  68. package/internal/agent/launch.go +192 -0
  69. package/internal/agent/launch_test.go +206 -0
  70. package/internal/agent/launch_unix.go +15 -0
  71. package/internal/agent/launch_windows.go +24 -0
  72. package/internal/agent/limitnotice.go +103 -0
  73. package/internal/agent/limitnotice_test.go +211 -0
  74. package/internal/agent/llmengine.go +2 -2
  75. package/internal/agent/models.go +3 -2
  76. package/internal/agent/openclaw.go +4 -3
  77. package/internal/agent/openclaw_acp.go +7 -0
  78. package/internal/agent/openclaw_test.go +2 -2
  79. package/internal/agent/permission.go +100 -11
  80. package/internal/agent/permission_test.go +100 -36
  81. package/internal/agent/repair_models_test.go +194 -0
  82. package/internal/agent/runcmd.go +13 -3
  83. package/internal/agent/runner.go +7 -1
  84. package/internal/agent/runner_test.go +1 -1
  85. package/internal/agent/stream.go +777 -43
  86. package/internal/agent/stream_test.go +3 -2
  87. package/internal/agent/stream_timeline_test.go +905 -0
  88. package/internal/agent/streamjson.go +36 -2
  89. package/internal/agent/toolkind.go +20 -1
  90. package/internal/agent/toolkind_test.go +7 -1
  91. package/internal/agent/tools_suffix_test.go +10 -3
  92. package/internal/agent/trae.go +5 -3
  93. package/internal/agent/workspace_test.go +52 -0
  94. package/internal/cli/ask.go +282 -51
  95. package/internal/cli/ask_test.go +15 -11
  96. package/internal/cli/attach_test.go +32 -20
  97. package/internal/cli/clientcmd.go +156 -0
  98. package/internal/cli/events.go +53 -8
  99. package/internal/cli/events_test.go +230 -0
  100. package/internal/cli/keepalive_test.go +101 -6
  101. package/internal/cli/login.go +61 -4
  102. package/internal/cli/login_test.go +99 -0
  103. package/internal/cli/permission_test.go +5 -1
  104. package/internal/cli/repair.go +60 -0
  105. package/internal/cli/repair_test.go +51 -0
  106. package/internal/cli/root.go +195 -32
  107. package/internal/cli/session.go +183 -29
  108. package/internal/cli/session_test.go +144 -5
  109. package/internal/cli/sessionlog.go +385 -38
  110. package/internal/cli/sessionlog_test.go +708 -0
  111. package/internal/cli/sessionlog_user_test.go +142 -1
  112. package/internal/cli/sessiontitle.go +141 -0
  113. package/internal/client/browse.go +456 -0
  114. package/internal/client/browse_test.go +199 -0
  115. package/internal/client/cors_test.go +35 -0
  116. package/internal/client/daemon.go +525 -0
  117. package/internal/client/daemon_test.go +212 -0
  118. package/internal/client/detach_unix.go +19 -0
  119. package/internal/client/detach_windows.go +27 -0
  120. package/internal/client/pair.go +3 -3
  121. package/internal/client/plugins.go +376 -0
  122. package/internal/client/plugins_test.go +223 -0
  123. package/internal/client/proxy.go +374 -0
  124. package/internal/client/proxy_test.go +212 -0
  125. package/internal/client/service.go +1464 -48
  126. package/internal/client/service_test.go +649 -206
  127. package/internal/client/session_events_test.go +130 -0
  128. package/internal/client/wsproxy.go +113 -0
  129. package/internal/client/wsproxy_test.go +102 -0
  130. package/internal/session/delete.go +151 -0
  131. package/internal/session/delete_test.go +103 -0
  132. package/internal/session/session.go +155 -20
  133. package/internal/session/session_append.go +79 -21
  134. package/internal/session/session_test.go +124 -10
  135. package/internal/session/sessionlog.go +572 -6
  136. package/internal/session/sessionlog_read_test.go +410 -0
  137. package/internal/session/title.go +486 -0
  138. package/internal/session/title_test.go +298 -0
  139. package/npm/bin/magic-agent.js +78 -10
  140. package/npm/bin/magic-client.js +100 -0
  141. package/npm/dist/darwin-arm64/magic-agent +0 -0
  142. package/npm/dist/darwin-arm64/magic-client +0 -0
  143. package/npm/dist/darwin-x64/magic-agent +0 -0
  144. package/npm/dist/darwin-x64/magic-client +0 -0
  145. package/npm/dist/linux-arm64/magic-agent +0 -0
  146. package/npm/dist/linux-arm64/magic-client +0 -0
  147. package/npm/dist/linux-x64/magic-agent +0 -0
  148. package/npm/dist/linux-x64/magic-client +0 -0
  149. package/npm/dist/win32-x64/magic-agent.exe +0 -0
  150. package/npm/dist/win32-x64/magic-client.exe +0 -0
  151. package/npm/install.js +36 -13
  152. package/npm/lib/platform.js +27 -5
  153. package/package.json +8 -3
  154. package/client-ui/pc/js/03-chat-message.js +0 -880
  155. package/client-ui/pc/js/06-ask-answer.js +0 -204
  156. /package/client-ui/{pc/js → shared/conv}/08-ask-event-map.js +0 -0
  157. /package/client-ui/{pc → shared}/vendor/highlight.min.js +0 -0
  158. /package/client-ui/{pc → shared}/vendor/marked.min.js +0 -0
  159. /package/client-ui/{pc → shared}/vendor/purify.min.js +0 -0
@@ -1,120 +1,5 @@
1
- /* ===== 基础组件:融合输入框(askBox)=====
2
- 用户 2026-09-17 定稿:「令对话框变成 1 个基础组件,与工作台首页的要一样共用」+
3
- 「是要做成前端组件 改了大家都跟着变」。全应用**只此一份**标记与行为,
4
- 两处实例共用(此前是手抄三份,改一处漏两处):
5
- home2 → 工作台首页入口
6
- home → 任务对话视图
7
- + 菜单 = 5 个类型选项(需求 / 模拟测试 / 录屏 / 产品文档 / 测试用例),选中后以**胶囊**挂进对话框:
8
- 挂「需求」胶囊 → 发送 = 新建需求(正文存入待处理,不发起对话);
9
- 挂「模拟测试」胶囊 → 发送 = 开一轮模拟测试(见 24-agent-test.js);
10
- 挂「录屏」胶囊 → 发送 = 录屏模式(见 25-record.js);
11
- 其余 / 没挂 = 普通对话。
12
- 需求管理页的那个输入框已下线 —— 新建需求的唯一入口在对话框的 + 菜单里。
13
- 实例差异只走配置:id 前缀 p、占位文案。
14
- 挂载:askBoxMountAll() —— 按 ASK_INSTANCES 逐个替换宿主 innerHTML(box + 三只菜单)。
15
- ⚠️ 改这里 = 改所有实例,不必再去各页面抄一遍;新增输入框请复用本组件而非复制标记。
16
-
17
- ⚠️ 2026-09-21 第一版(用户:「对话窗口不选择模型了 直接使用配置界面的模型列表自动处理」):
18
- 右下角只留一枚「引擎」选择器,模型按钮与它的菜单整体下线 —— 用哪个模型改由**配置界面**
19
- (设置窗口「引擎配置」)里该引擎的**有序降级链**决定(链首 = 主选,其后是兜底候选,
20
- 执行侧按顺序自动试,见 src/js/04-task-list.js + desktop/chat-model.cjs)。
21
- → 第二版(「不能选择 但是要显示用的是哪个」):补一枚**只读模型标签**如实显示。
22
- → 第三版(「工作台的对话框中可以选模型 在任务ui中只是显示」):首页可选、任务对话只读。
23
- → **第四版(2026-09-21 定稿,用户:「还是都可以选择模型 但是自动切换模型的时候 对话框下拉的
24
- 选中模型显示也要变」):两个实例都恢复成可选的模型下拉**(按钮 + 菜单),
25
- 并且「自动切换模型」时下拉的选中项/文案**必须跟着变成实际在跑的那个**。
26
- ⚠️ 别再按「只读标签」那两版改回去(只读标签 `-model-ro` / `.ag-mdl-ro` 已删干净):
27
- 现在两处控件**同源同形态**,实例差异回到只有占位文案一条。
28
- ⚠️ 与「两处真相」的关系:模型选择**是**允许的(用户要),但「这次到底下发哪个」仍只有一份
29
- 算法(`qaModelFor()`,见 04-task-list.js)—— 界面只负责把选择写回去、把实际值画出来。 */
30
- const ASK_ICON = {
31
- plus: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round"><path d="M12 5v14M5 12h14"/></svg>',
32
- access: '<svg class="i" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9"><circle cx="12" cy="12" r="9"/><path d="M12 8h.01M11 11.5h1v5h1" stroke-linecap="round"/></svg>',
33
- engine: '<svg class="logo" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"><rect x="7" y="7" width="10" height="10" rx="2"/><path d="M12 2.5v3M12 18.5v3M2.5 12h3M18.5 12h3M5.3 5.3l2.1 2.1M16.6 16.6l2.1 2.1M18.7 5.3l-2.1 2.1M7.4 16.6l-2.1 2.1"/></svg>',
34
- /* 模型选择器上那枚「地球」小标(用图标而不是文字前缀,免得按钮变成「模型:xxx」那种表单感)。 */
35
- model: '<svg class="logo" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" aria-hidden="true"><circle cx="12" cy="12" r="9.2"/><path d="M12 2.8c3.2 4.4 3.2 14 0 18.4M12 2.8c-3.2 4.4-3.2 14 0 18.4M3.4 9h17.2M3.4 15h17.2"/></svg>',
36
- chev: '<svg class="chev" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 9l6 6 6-6"/></svg>',
37
- arrow: '<svg class="ico-arrow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 19V5M5 12l7-7 7 7"/></svg>',
38
- stop: '<svg class="ico-stop" viewBox="0 0 24 24" fill="currentColor"><rect x="6.5" y="6.5" width="11" height="11" rx="2.4"/></svg>',
39
- };
40
-
41
- /** + 菜单的**七种**「胶囊」类型(用户 2026-09-17 定稿 3 种;2026-09-21 依次加「模拟测试」「录屏」「截屏」;
42
- * 2026-09-23 加「短视频」:在工作台对话中增加新模式,通过集成调用 magic-shorts CLI 完成短视频制作)——
43
- * + 点开 = 7 个选项,选中哪类,哪类就以**胶囊**挂进对话框;
44
- * 发送语义由胶囊决定:**挂了「需求」胶囊 → 发送 = 新建需求**(正文存入待处理,不发起对话),
45
- * 挂「模拟测试」= 开测、挂「录屏」= 录屏模式、挂「截屏」= 截屏模式、挂「短视频」= 短视频流水线,
46
- * 否则 = 普通对话(胶囊随发送清掉)。
47
- * ⚠️ 前五种(req / sim / rec / shot / shorts)是**改发送语义的模式**,后两种(doc / case)只是普通对话里挂个标记 ——
48
- * 排序不是随便排的,改发送语义的那几种必须靠前(见 19-attachments.js::askAddMenuBuild)。
49
- * ⚠️ 键盘契约:**Enter = 发送 / 存入,Shift+Enter = 换行**。输入域是多行 textarea,
50
- * 所有「Enter 发送」处理器都必须判 `!e.shiftKey`(漏判会吃掉换行,多行输入形同虚设)。
51
- * ⚠️ 且必须先过 `qaImeComposing`:**输入法合成中(候选词还没上屏)的那一下 Enter 归输入法,
52
- * 不是发送**(用户 2026-09-28:「输入时的 enter 不要发出去」)。
53
- * 合成态一律放行 —— 不 preventDefault、不提交,让候选词正常上屏;其余 Enter 一下即发,
54
- * 不做二次确认(用户 2026-09-29:「不需要两次 enter」)。 */
55
- const ASK_CHIP_LABEL = { req: "需求", sim: "模拟测试", rec: "录屏", shot: "截屏", shorts: "短视频", doc: "产品文档", case: "测试用例" };
56
- const ASK_CHIP_SIDE = { req: "发送即新建", sim: "发送即开测", rec: "发送即录屏", shot: "发送即截屏", shorts: "发送即短视频", doc: "普通对话", case: "普通对话" };
57
-
58
- /** 组件标记:返回 box + **四只菜单**(+ / 引擎 / 模型 / 权限)的 HTML;菜单挂 box 之后,交由 qaMenuPlace 定位。
59
- * ⚠️ 站内实例**必须渲染成同一个样子**(用户 2026-09-17 复查截图后强调):
60
- * 同一个 box 类、同样的 +/权限胶囊、同样的引擎 + 模型下拉、同样位置的提示行、同样只有「一个发送钮」。
61
- * 实例之间只允许不同:① 占位文案。(2026-09-21 中途曾按实例分过模型控件形态 ——
62
- * 首页可选 / 任务对话只读 —— 用户随后定稿「还是都可以选择模型」,已还原成同源同形态。)
63
- * 曾经踩过:给某个实例省掉 +/权限、另加「取消」与一段说明文字 →
64
- * 用户一眼看出「两个对话框明显不一样,怎么会是共用」。别再按「上下文是否需要」去裁剪。
65
- * 输入域统一 textarea(rows=1 + field-sizing 自增高)—— 多行能力归组件本身。 */
66
- function askBoxMarkup(cfg){
67
- const p = cfg.p;
68
- const boxCls = cfg.boxCls || "home-box";
69
- const ph = escHtml((cfg.ph && cfg.ph.ask) || "今天帮你做些什么?");
70
- /* 待发队列行(用户 2026-09-18:「新需求排列在会话框上面」):**会话框上方**的一行,
71
- 处理中提交的下一条排在这里等自动提交(见 qaMsgQueue 那段)。内容由 qaMsgQueueRender 现填。
72
- ⚠️ 只给**会话框**(home 实例)画 —— home2 是新建对话入口,它提交的永远是「新会话」,
73
- 不排队,也不该挂一行永远空的 DOM。 */
74
- const queueRow = cfg.p === QA_QUEUE_INSTANCE
75
- ? `<div class="ag-queue" id="${p}-queue" data-testid="${p}-queue" hidden></div>` : "";
76
- return `
77
- <!-- 右上角吉祥物「大喵」= 应用图标抠图(用户 2026-09-17);纯装饰,
78
- 绝对定位在 .home-entry 里、框顶之上,不占布局也不拦点击(见 .ag-mascot 注释) -->
79
- <img class="ag-mascot" id="${p}-mascot" src="assets/damiao-mascot.png"
80
- alt="" aria-hidden="true" draggable="false">
81
- ${queueRow}
82
- <div class="ag-box ${boxCls}" id="${p}-box" data-testid="${p}-box">
83
- <div class="ag-atts" id="${p}-atts" data-testid="${p}-atts" hidden></div>
84
- <div class="ag-top">
85
- <!-- 胶囊在输入域**之前**、同一个 flex 行里 → 与文字同行(装不下才由 flex-wrap 折行) -->
86
- <div class="ag-refs" id="${p}-refs" data-testid="${p}-refs" hidden></div>
87
- <textarea id="${p}-input" data-testid="${p}-input" rows="1" placeholder="${ph}" autocomplete="off"></textarea>
88
- <div class="ag-hints" id="${p}-hints"><span class="ag-hint" id="${p}-hint-at"><b>@</b>引用对话文件</span><span class="ag-hint-sep">,</span><span class="ag-hint" id="${p}-hint-slash"><b>/</b>调用技能与指令</span></div>
89
- </div>
90
- <div class="ag-bot">
91
- <button class="ag-plus" id="${p}-plus" data-testid="${p}-plus" title="选择需求 / 模拟测试 / 录屏 / 产品 / 用例" aria-haspopup="listbox" aria-expanded="false" type="button">${ASK_ICON.plus}</button>
92
- <!-- 访问权限 = **四档选择器**(2026-09-18 用户:「这个地方要选择」)。
93
- 改造前是一枚二值胶囊(点一下在「允许完全访问 / 受限访问」间切换)——
94
- 现在点开四档菜单,按钮文案 = 当前档位,选中态由 qaPermSync() 统一刷。
95
- ⚠️ 两个实例必须渲染成同一个样子:文案与选中态都来自**同一份** qaPerm。 -->
96
- <button class="ag-access" id="${p}-access" data-testid="${p}-access" title="选择访问权限" aria-haspopup="listbox" aria-expanded="false" type="button">${ASK_ICON.access}<span class="txt">自动审批</span>${ASK_ICON.chev}</button>
97
- <div class="ag-right">
98
- <span class="ag-busy" aria-hidden="true"></span>
99
- <button class="ag-model ag-engine" id="${p}-engine" data-testid="${p}-engine" type="button" title="选择引擎">${ASK_ICON.engine}<span class="txt">claude</span>${ASK_ICON.chev}</button>
100
- <!-- 模型 = **可选**(两个实例同款;用户 2026-09-21 定稿「还是都可以选择模型
101
- 但是自动切换模型的时候 对话框下拉的选中模型显示也要变」)。
102
- 文案与选中态由 qaModelPickSync() 现填;首帧给一个不撒谎的占位。 -->
103
- <button class="ag-model ag-mdl-pick" id="${p}-model" data-testid="${p}-model" type="button" title="选择模型">${ASK_ICON.model}<span class="txt">引擎默认</span>${ASK_ICON.chev}</button>
104
- <button class="ag-send" id="${p}-send" data-testid="${p}-send" title="发送" type="button">${ASK_ICON.arrow}${ASK_ICON.stop}</button>
105
- </div>
106
- </div>
107
- </div>
108
- <div class="ag-menu scroll-y" id="${p}-engine-menu" hidden><span class="ag-menu-cap">引擎</span><button class="ag-model-opt on" data-engine="claude" type="button">claude</button></div>
109
- <div class="ag-menu scroll-y" id="${p}-model-menu" data-testid="${p}-model-menu" hidden></div>
110
- <!-- 访问权限菜单:4 档,内容由 qaPermMenuBuild() 现填(见该函数注释) -->
111
- <div class="ag-menu scroll-y ag-perm" id="${p}-perm-menu" data-testid="${p}-perm-menu" role="listbox" hidden></div>
112
- <!-- + 菜单:5 个类型选项(需求 / 模拟测试 / 录屏 / 产品文档 / 测试用例)—— 内容由 askAddMenuBuild 现填 -->
113
- <div class="ag-menu scroll-y ag-add" id="${p}-add-menu" data-testid="${p}-add-menu" hidden></div>`;
114
- }
115
-
116
- /** 站内实例清单(唯一真相):挂载、事件绑定、选择器数组都以它为准。
117
- * ⚠️ 2026-09-17:需求管理页的「新建需求」输入框已下线(新建需求的唯一入口 = 对话框 + 菜单)。 */
1
+ /* 宿主半:输入框实例清单(ASK_INSTANCES / ASK_CFG)与授权档位菜单(qaPerm*)。
2
+ 输入框标记与胶囊在内核:conv/18-askbox-markup.js。 */
118
3
  const ASK_INSTANCES = ["home2", "home"];
119
4
  /** 各实例配置 —— 允许不同的只有占位文案;尺寸/控件/结构一律相同
120
5
  * ⚠️ 2026-09-21 中途曾在这里加过 `modelCtl`(首页可选 / 任务对话只读),用户随后定稿
@@ -201,62 +86,3 @@ function qaPermSet(id){
201
86
  }
202
87
 
203
88
  /** 实例运行态:已选的胶囊类型(按实例分开 —— 两个输入框互不干扰) */
204
- const askChips = {}; // p → ["req" | "sim" | "rec" | "doc" | "case", ...]
205
- const askInputOf = (p)=> $("#" + p + "-input");
206
- const askHasReq = (p)=> (askChips[p] || []).includes("req");
207
- /** 挂了「模拟测试」胶囊吗(2026-09-21 新增的模式,发送即开测 —— 见 24-agent-test.js)。
208
- * 与「需求」胶囊同一套机制:语义由胶囊决定,拦截点在 qaAddBind 的捕获阶段。 */
209
- const askHasSim = (p)=> (askChips[p] || []).includes("sim");
210
- /** 挂了「录屏」胶囊吗(2026-09-21 新增的模式,发送即录屏 —— 见 25-record.js)。
211
- * 与前三枚同一套机制;⚠️ 拦截顺序是 rec → shot → sim → req(改发送语义的四枚里,越"重"的越先判,
212
- * 见 09-ask-event.js 的注释)。 */
213
- const askHasRec = (p)=> (askChips[p] || []).includes("rec");
214
- /** 挂了「截屏」胶囊吗(2026-09-21 新增的模式,发送即截屏 —— 见 25-record.js)。
215
- * 与「录屏」是**兄弟**(同一套前置体检 / MCP 工具 / 兜底),只差「要一帧还是要一段」。 */
216
- const askHasShot = (p)=> (askChips[p] || []).includes("shot");
217
- /** 挂了「短视频」胶囊吗(2026-09-23 新增的模式,发送即跑短视频流水线 —— 见 28-shorts.js)。
218
- * 与「录屏 / 截屏」同一套拦截机制(捕获阶段截发送 → desk.ask{ mode }),但它**不碰浏览器**:
219
- * 引擎只产任务单(```shorts-task 围栏块),壳逐阶段执行 magic-shorts CLI(选题 → 脚本 → 配音 → 成片)。 */
220
- const askHasShorts = (p)=> (askChips[p] || []).includes("shorts");
221
-
222
- /** 胶囊行渲染(+ 菜单选中后出现在对话框里的胶囊,可单个摘掉) */
223
- function askChipsSync(p){
224
- const el = $("#" + p + "-refs"); if(!el) return;
225
- const list = askChips[p] || [];
226
- el.hidden = !list.length;
227
- el.innerHTML = list.map(kind=>
228
- `<span class="ag-ref" title="${escHtml(ASK_CHIP_LABEL[kind] || kind)}">` +
229
- `<span class="rk">${escHtml(ASK_CHIP_LABEL[kind] || kind)}</span>` +
230
- `<button class="rx" data-kind="${kind}" type="button" aria-label="移除${escHtml(ASK_CHIP_LABEL[kind] || kind)}胶囊">×</button>` +
231
- `</span>`).join("");
232
- }
233
- function askChipToggle(p, kind){
234
- if(!ASK_CHIP_LABEL[kind]) return;
235
- const list = askChips[p] || (askChips[p] = []);
236
- const i = list.indexOf(kind);
237
- if(i >= 0){ list.splice(i, 1); toast(`已移除${ASK_CHIP_LABEL[kind]}胶囊`); }
238
- else {
239
- list.push(kind);
240
- toast(kind === "req" ? "已挂需求胶囊 · 发送即新建需求"
241
- : kind === "sim" ? "已挂模拟测试胶囊 · 发送即开测(数字分身 ↔ 默认引擎,按判据打分)"
242
- : kind === "rec" ? "已挂录屏胶囊 · 发送即录屏(引擎驱动内嵌浏览器录一段,产物落「浏览器工具 → 产物」)"
243
- : kind === "shorts" ? "已挂短视频胶囊 · 发送即跑流水线(选题 → 脚本 → 配音 → 成片,几分钟)"
244
- : `已挂${ASK_CHIP_LABEL[kind]}胶囊`);
245
- }
246
- askChipsSync(p);
247
- }
248
- function askChipDrop(p, kind){
249
- const list = askChips[p] || [];
250
- const i = list.indexOf(kind);
251
- if(i < 0) return;
252
- list.splice(i, 1);
253
- askChipsSync(p);
254
- toast(`已移除${ASK_CHIP_LABEL[kind] || kind}胶囊`);
255
- }
256
- /** 清空胶囊(发送后调用) */
257
- function askChipsClear(p){
258
- if(!(askChips[p] || []).length) return;
259
- askChips[p] = [];
260
- askChipsSync(p);
261
- }
262
-
@@ -35,7 +35,7 @@ const HOST_DESK = (window.desk && typeof window.desk.ask === "function") ? windo
35
35
  const MAGIC_TOKEN = window.MAGIC_TOKEN
36
36
  || new URLSearchParams(location.search).get("token") || "";
37
37
  const MAGIC_BASE = (window.MAGIC_BASE || "").replace(/\/+$/, "");
38
- const transport = HOST_DESK ? null : window.MagicTransportHttp({ base: MAGIC_BASE, token: MAGIC_TOKEN });
38
+ const transport = HOST_DESK ? null : window.MagicTransportHttp({ base: MAGIC_BASE, token: MAGIC_TOKEN, client: "pc" });
39
39
 
40
40
  /* ---------- 引擎行归一 ----------
41
41
  上游 magic-test 的 engines.cjs 会把 CLI 输出规范化;组件这一版直接吃 CLI 的原始行,
@@ -88,15 +88,8 @@ function qaEngHintSync(){
88
88
  });
89
89
  }
90
90
 
91
- /* 27-conv-log.js:找不到历史时那句说明(如实)。原样保留。 */
92
- function qaConvNoticeHtml(text, kind){
93
- return `<div class="qa-hist-note${kind ? " " + kind : ""}">${escHtml(text)}</div>`;
94
- }
95
- function qaConvLostText(t){
96
- const hasSid = !!(t && t.sessionId);
97
- return "这条对话的历史没有留存下来(这一版还没有本地消息库)。"
98
- + (hasSid ? "会话本身还在 —— 直接在这里追问,引擎会接着原来的上下文继续。" : "这一轮没有拿到会话 id,追问会新起一段上下文。");
99
- }
91
+ /* 27-conv-ledger.js 的「找不到历史」两句文案已在**内核**(那是纯文案,与宿主无关)——
92
+ 这里不再各留一份:重复顶层声明会让整个脚本 SyntaxError。 */
100
93
 
101
94
  /* ================= ② 诚实桩(这条能力这一版没有) ================= */
102
95
 
@@ -120,7 +113,8 @@ function qaTaskHistoryRecover(){ return Promise.resolve(""); }
120
113
  ⚠️ 这意味着**刷新页面历史就没了**:左栏条目来自 localStorage 的 qaTasks(04-task-list.js),
121
114
  点开时靠 t.snapshot 还原当前会话画面;其余(逐轮消息表)没有。持久化 = 增强点之一。 */
122
115
  function qaMsgLogLoad(){ return Promise.resolve([]); }
123
- function qaConvTurnSync(){ /* 无消息库可写 */ }
116
+ /* qaConvTurnSync(台账写入)在内核 27-conv-ledger.js —— 本版无消息库,内核那份已经会
117
+ 安全地「写进内存台账」,这里不必再挡一道。 */
124
118
 
125
119
  /* 截图相册 / 文档阅读器(右栏)—— 本版无右栏 */
126
120
  function reqShotUrl(){ return ""; }
@@ -81,13 +81,20 @@ async function qaRailLoad(quiet){
81
81
  if(!r || !r.ok){ qaRailErr = (r && r.error) || "会话清单读不到"; }
82
82
  else {
83
83
  const all = Array.isArray(r.sessions) ? r.sessions : [];
84
- /* ⚠️ 判据只有一条:**有没有 `session_id`**(有 = 一条真会话,能续接、能回放)。
85
- · 没有 `session_id` 的是一次性调用(归纳 / 判定 / 派发里的一轮),不是会话,不列;
84
+ /* ⚠️ 判据(2026-10-18 起分两档):**有 `session_id` 的一律列**;还没有 `session_id` 的
85
+ (引擎还没报上来)只在「在跑 或 有日志」时列。
86
+ 原先第一档就是「有没有 `session_id`」,于是这类会话被整条丢掉 —— **正在跑的那条会从
87
+ 列表里消失**(用户 2026-10-18:「掌天瓶左栏显示的不对」,同一条会话移动端有、左栏没有)。
88
+ core 的落盘本来就是 `<run_id>.jsonl`(拿到 session_id 才改名),`--session-log` 认 run_id,
89
+ 所以这类行点开是有历史的,不是「点了没东西」的死行。真死行(跑完了、没 id、没日志)不列。
86
90
  · 有 `session_id` 但 `hasLog=false` 的**照样列** —— 引擎当时是非流式跑的,没留下事件
87
91
  日志,可它仍然是一条 bot 会话(**审查轮就是这一类**:用户 2026-09-29「是审查也是
88
92
  bot,所以应该显示在对话 ui 的左边」)。早先我按 hasLog 把它们筛掉了,结果是审查
89
93
  在列表里整个消失。这类行会带「无历史」标记,点开也如实说清为什么没有。 */
90
- qaRailAll = all.filter((s) => String(s.sessionId || "").trim());
94
+ qaRailAll = all.filter((s) => {
95
+ if (String(s.sessionId || "").trim()) return true; // 有会话 id:照旧一律列(无历史那类带标记)
96
+ return !!(s.alive || s.hasLog); // 还没有会话 id:在跑 / 有日志才列
97
+ });
91
98
  qaRailSkipped = all.length - qaRailAll.length;
92
99
  qaRailNoLog = qaRailAll.filter((s) => !s.alive && !s.hasLog).length;
93
100
  qaRailErr = "";
@@ -0,0 +1,52 @@
1
+ /* DOM 选择器与幂等写 —— 对话内核的公共基座(唯一实现)。
2
+ ⚠️ 宿主不要再各留一份同名实现:重复声明会让整个脚本 SyntaxError(见 tests/check-conv-shards.mjs)。
3
+ 宿主半(按需包加载器 / 首屏字体 / 工作台 state)留在 magic-test 的 01-core.js 与 magic-agent pc 的 00-base.js。 */
4
+ const $ = (s,el=document)=>el.querySelector(s);
5
+ const $$ = (s,el=document)=>[...el.querySelectorAll(s)];
6
+
7
+ /** 幂等写 innerHTML:**渲染结果字符串没变就不碰 DOM**,返回是否真的写了。
8
+ *
9
+ * 为什么需要(2026-09-19 用户报障:「处理中的任务显示一直在变 一会消失一会又显示在第一条」):
10
+ * 左栏任务列表与需求看板都是靠「整份 innerHTML 重建」来刷新的,而**运行中每 700ms**
11
+ * 就有一次 output 增量落库(reqboard.cjs 的 LIVE_FLUSH_MS)→ 广播 update → 两侧各重建一次。
12
+ * 需求看板的卡片带入场动画(reqCardPop 的 0% 是 opacity:0),于是「处理中」那张卡每 0.7s
13
+ * 闪一下;左栏虽无动画,但整份重建会打散 hover 态与行尾菜单锚点,并让条目在「最新 5 条」
14
+ * 折叠线上反复进出 —— 两条链路的观感都是「列表一直在变」。
15
+ * 而这些重建绝大多数是**白做**:output / 会话快照这些高频变化的字段根本不上卡片和列表行。
16
+ *
17
+ * 判据用**最终 HTML 字符串**而不是逐个字段比对:字符串相同 ⇒ DOM 必然相同;
18
+ * 以后往卡片/行里加字段也不用回来补白名单 —— 漏一个字段就又会闪。
19
+ * ⚠️ 别把「随渲染时刻变化的量」(如用 Date.now() 算出的已运行秒数)拼进 HTML:
20
+ * 那样每次字符串都不同,幂等直接失效(已运行秒数由 reqTickStart 就地改文本)。 */
21
+ function htmlWrite(el, html){
22
+ if(!el) return false;
23
+ if(el.__html === html) return false; // 与上次写进去的完全一致 → 跳过
24
+ el.__html = html;
25
+ el.innerHTML = html;
26
+ return true;
27
+ }
28
+
29
+ /* ---------- 输入法合成守卫(2026-09-28)----------
30
+ 用户:「在工作台的对话框中输入文字时的 enter 不要马上一按就发出去」。
31
+ 中文输入法里,**选词那一下按的 Enter 不是提交** —— 它归输入法,用来把候选词上屏。
32
+ 对话框里的回车提交(home2 首页新建 / home 任务对话)原先只判 `e.key === "Enter"`,
33
+ 于是拼音打到一半敲回车选词,消息先发走了。
34
+ 判据两条:
35
+ ① `e.isComposing` —— 合成期间的 keydown 自带这个标记(选词那一下也是 true);
36
+ ② `keyCode === 229` 兜底 —— 合成中按下的键在所有浏览器里都报 229。
37
+ 合成态一律**放行**:不 preventDefault、不提交、不吞事件,输入框照常收字。
38
+ ⚠️ 本工程只跑 Chromium(Electron 壳 + <webview>),`isComposing` 在选词那一下是可靠的;
39
+ 真要兼容旧 WebKit 才需要再挂一份 compositionstart/end 的元素级标记。 */
40
+ function qaImeComposing(e){
41
+ return !!e && (e.isComposing === true || e.keyCode === 229);
42
+ }
43
+
44
+ /** 转义动态插值:域名、文件名这些来自外部(Chrome cookie / 磁盘 / 引擎输出),
45
+ * 拼进 HTML 前必须转义,否则一个带 < 的名字就能注入标记。
46
+ * ⚠️ 它以前长在**宿主**里(magic-test 的 15-layout / pc 的 00-base),四个宿主各留一份,
47
+ * 组件宿主还得再抄 —— 而内核多个分片都要用它。阶段三收进内核:**全站只有这一份**。 */
48
+ function escHtml(s){
49
+ return String(s == null ? "" : s)
50
+ .replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;")
51
+ .replace(/"/g, "&quot;").replace(/'/g, "&#39;");
52
+ }
@@ -0,0 +1,34 @@
1
+ /* 媒体类型与「产物路径」的**常量表**(纯声明,零宿主依赖)—— `QA_IMG_EXT_SRC` /
2
+ `QA_ART_EXT_SRC` / `QA_IMG_EXT` / `QA_ART_EXT` / `QA_SHOT_MARK` / `QA_SHOT_ITEM` /
3
+ `QA_IMG_LINE` / `QA_SHOT_PATH_RE` 都长在这儿。
4
+ *
5
+ * ── 为什么单独一片(2026-10-16 阶段三)──
6
+ * `19-attachments.js` 还带着**宿主耦合**的渲染与探测(qaShotsMount / qaArtMount /
7
+ * desk:path-probe / toast),而**组件宿主**(移动端 / PC 对话页)只要这张表 ——
8
+ * 引整片 19 会被要求提供 toast / wsDirOf 那批它根本用不到的宿主符号。
9
+ * 拆开之后:桌面/工作台引 19(连带这张表),组件宿主只引这一片。
10
+ *
11
+ * ⚠️ **后缀清单只此一份**:谁要加后缀就改这里 —— PC 宿主、移动宿主、magic-test 三处
12
+ * 的行内缩略图与产物台账都跟着变(清单漂移的症状是「同一张图这边有、那边没有」)。
13
+ */
14
+ const QA_SHOT_MARK = /^\[粘贴的截图\]\s*$/; // 对话侧:标记独占一行
15
+ const QA_SHOT_ITEM = /^[-*]\s*(\S.*?)\s*$/; // 紧随其后的 `- <路径>`
16
+ const QA_IMG_LINE = /^\[图片\]\s*(\S.*?)\s*$/; // 飞书侧:一行一条
17
+ /* 图片后缀(2026-10-12 加 svg / heic / avif):这一档有缩略图,是既有行为。
18
+ ⚠️ 与 QA_ART_EXT 是**包含关系**:`qaArtKind` 先认图,其余才轮到视频 / 音频 / 文件。
19
+ ⚠️ 2026-10-16(阶段三):后缀清单**只此一份** —— `QA_IMG_EXT` 与行内识别的
20
+ `QA_SHOT_PATH_RE` 都由 `QA_IMG_EXT_SRC` 派生(组件宿主也引这两个,别再各写一套;
21
+ 清单漂移的后果是「同一条路径在 PC 有缩略图、在手机没有」,最难查)。 */
22
+ const QA_IMG_EXT_SRC = "png|jpe?g|gif|webp|bmp|svg|heic|avif";
23
+ const QA_IMG_EXT = new RegExp("\\.(?:" + QA_IMG_EXT_SRC + ")$", "i");
24
+ const QA_ART_EXT = /\.(?:png|jpe?g|gif|webp|bmp|svg|heic|avif|mp4|mov|m4v|webm|mkv|avi|mp3|wav|m4a|aac|ogg|flac|md|markdown|txt|pdf|docx?|xlsx?|pptx?|csv|zip|glb|gltf|psd|fig)$/i;
25
+ const QA_ART_EXT_SRC = "png|jpe?g|gif|webp|bmp|svg|heic|avif|mp4|mov|m4v|webm|mkv|avi|mp3|wav|m4a|aac|ogg|flac|md|markdown|txt|pdf|docx?|xlsx?|pptx?|csv|zip|glb|gltf|psd|fig";
26
+ /** 整行就是一个产物路径(可带 `- ` 列表前缀)→ 摘成卡。
27
+ * 相对路径只在这一档认(必须带目录分隔符,光一个 `report.md` 太像正文了)。 */
28
+ const QA_ART_PATH_SRC = "(?:file://|/|~/|[A-Za-z0-9_.@-]+/)"; // 绝对 / `~/` / `file://` / 相对(带目录)
29
+ const QA_ART_LINE = new RegExp("^(?:[-*]\\s*)?(" + QA_ART_PATH_SRC + "[^\\s\"'`<>()\\[\\],。;:、)】]*?\\.(?:" + QA_ART_EXT_SRC + "))$", "i");
30
+ /* ⚠️ 后缀清单由 `QA_IMG_EXT_SRC` 派生(只此一份,见上):以前这里硬写着
31
+ `png|jpe?g|gif|webp|bmp` —— 少 svg/heic/avif,于是「正文里那张 svg 在 PC 有缩略图、
32
+ 在手机只剩一串路径」。 */
33
+ const QA_SHOT_PATH_RE = new RegExp(
34
+ "(?:file://|/|~/)[^\\s\"'`<>()\\[\\],。;:、)】]+?\\.(?:" + QA_IMG_EXT_SRC + ")(?![A-Za-z0-9])", "gi");