magic-agent 1.0.3 → 1.0.49

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 (153) 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 +109 -3
  55. package/internal/agent/custom_models_test.go +74 -0
  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 +69 -1
  66. package/internal/agent/itemtracker.go +271 -0
  67. package/internal/agent/launch.go +192 -0
  68. package/internal/agent/launch_test.go +206 -0
  69. package/internal/agent/launch_unix.go +15 -0
  70. package/internal/agent/launch_windows.go +24 -0
  71. package/internal/agent/limitnotice.go +103 -0
  72. package/internal/agent/limitnotice_test.go +211 -0
  73. package/internal/agent/llmengine.go +2 -2
  74. package/internal/agent/models.go +3 -2
  75. package/internal/agent/openclaw.go +4 -3
  76. package/internal/agent/openclaw_acp.go +7 -0
  77. package/internal/agent/openclaw_test.go +2 -2
  78. package/internal/agent/permission.go +100 -11
  79. package/internal/agent/permission_test.go +100 -36
  80. package/internal/agent/runcmd.go +13 -3
  81. package/internal/agent/runner.go +7 -1
  82. package/internal/agent/runner_test.go +1 -1
  83. package/internal/agent/stream.go +777 -43
  84. package/internal/agent/stream_test.go +3 -2
  85. package/internal/agent/stream_timeline_test.go +905 -0
  86. package/internal/agent/streamjson.go +36 -2
  87. package/internal/agent/toolkind.go +20 -1
  88. package/internal/agent/toolkind_test.go +7 -1
  89. package/internal/agent/tools_suffix_test.go +10 -3
  90. package/internal/agent/trae.go +5 -3
  91. package/internal/agent/workspace_test.go +52 -0
  92. package/internal/cli/ask.go +260 -33
  93. package/internal/cli/ask_test.go +15 -11
  94. package/internal/cli/attach_test.go +32 -20
  95. package/internal/cli/clientcmd.go +156 -0
  96. package/internal/cli/events.go +53 -8
  97. package/internal/cli/events_test.go +230 -0
  98. package/internal/cli/keepalive_test.go +101 -6
  99. package/internal/cli/permission_test.go +5 -1
  100. package/internal/cli/root.go +174 -32
  101. package/internal/cli/session.go +175 -28
  102. package/internal/cli/session_test.go +144 -5
  103. package/internal/cli/sessionlog.go +385 -38
  104. package/internal/cli/sessionlog_test.go +708 -0
  105. package/internal/cli/sessionlog_user_test.go +142 -1
  106. package/internal/cli/sessiontitle.go +141 -0
  107. package/internal/client/browse.go +456 -0
  108. package/internal/client/browse_test.go +199 -0
  109. package/internal/client/cors_test.go +35 -0
  110. package/internal/client/daemon.go +525 -0
  111. package/internal/client/daemon_test.go +212 -0
  112. package/internal/client/detach_unix.go +19 -0
  113. package/internal/client/detach_windows.go +27 -0
  114. package/internal/client/pair.go +3 -3
  115. package/internal/client/plugins.go +376 -0
  116. package/internal/client/plugins_test.go +223 -0
  117. package/internal/client/proxy.go +374 -0
  118. package/internal/client/proxy_test.go +212 -0
  119. package/internal/client/service.go +1464 -48
  120. package/internal/client/service_test.go +649 -206
  121. package/internal/client/session_events_test.go +130 -0
  122. package/internal/client/wsproxy.go +113 -0
  123. package/internal/client/wsproxy_test.go +102 -0
  124. package/internal/session/delete.go +151 -0
  125. package/internal/session/delete_test.go +103 -0
  126. package/internal/session/session.go +155 -20
  127. package/internal/session/session_append.go +79 -21
  128. package/internal/session/session_test.go +124 -10
  129. package/internal/session/sessionlog.go +572 -6
  130. package/internal/session/sessionlog_read_test.go +410 -0
  131. package/internal/session/title.go +486 -0
  132. package/internal/session/title_test.go +298 -0
  133. package/npm/bin/magic-agent.js +101 -10
  134. package/npm/bin/magic-client.js +118 -0
  135. package/npm/dist/darwin-arm64/magic-agent +0 -0
  136. package/npm/dist/darwin-arm64/magic-client +0 -0
  137. package/npm/dist/darwin-x64/magic-agent +0 -0
  138. package/npm/dist/darwin-x64/magic-client +0 -0
  139. package/npm/dist/linux-arm64/magic-agent +0 -0
  140. package/npm/dist/linux-arm64/magic-client +0 -0
  141. package/npm/dist/linux-x64/magic-agent +0 -0
  142. package/npm/dist/linux-x64/magic-client +0 -0
  143. package/npm/dist/win32-x64/magic-agent.exe +0 -0
  144. package/npm/dist/win32-x64/magic-client.exe +0 -0
  145. package/npm/install.js +36 -13
  146. package/npm/lib/platform.js +27 -5
  147. package/package.json +8 -3
  148. package/client-ui/pc/js/03-chat-message.js +0 -880
  149. package/client-ui/pc/js/06-ask-answer.js +0 -204
  150. /package/client-ui/{pc/js → shared/conv}/08-ask-event-map.js +0 -0
  151. /package/client-ui/{pc → shared}/vendor/highlight.min.js +0 -0
  152. /package/client-ui/{pc → shared}/vendor/marked.min.js +0 -0
  153. /package/client-ui/{pc → shared}/vendor/purify.min.js +0 -0
@@ -0,0 +1,185 @@
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
+ /* 提问确认卡的**停靠位**(2026-09-30 用户:「确认框不应该显示在消息中 需要显示在对话框上」)。
77
+ 它和上面那条待发队列是同一套做法:**只给会话框(home 实例)** 画 —— 首页(home2)是新建
78
+ 对话的入口,它那边根本不会有引擎提问的卡(卡只出现在 #qa-stream 那一侧)。
79
+ ⚠️ 位置刻意压在 .ag-box **之上**(比队列更贴输入框):确认框是「现在就等我回答」的东西,
80
+ 应该紧贴输入框;而 .home-entry 之内还避开了浮层「大喵」(它压在整个 .home-entry 之上
81
+ 那 56px 带里,dock 若放在外面就会被猫遮住右下角 —— 实测过,见 app.css .qa-askdock 注释)。
82
+ 卡片本体由 qaAskDockSync 按会话现搬(见 06-ask-answer.js「提问卡停靠位」),这里只给位置。 */
83
+ const askDockRow = cfg.p === QA_QUEUE_INSTANCE
84
+ ? `<div class="qa-askdock" id="${p}-askdock" data-testid="qa-askdock" hidden></div>` : "";
85
+ return `
86
+ <!-- 右上角吉祥物「大喵」= 应用图标抠图(用户 2026-09-17);纯装饰,
87
+ 绝对定位在 .home-entry 里、框顶之上,不占布局也不拦点击(见 .ag-mascot 注释) -->
88
+ <img class="ag-mascot" id="${p}-mascot" src="assets/damiao-mascot.png"
89
+ alt="" aria-hidden="true" draggable="false">
90
+ ${queueRow}
91
+ ${askDockRow}
92
+ <div class="ag-box ${boxCls}" id="${p}-box" data-testid="${p}-box">
93
+ <div class="ag-atts" id="${p}-atts" data-testid="${p}-atts" hidden></div>
94
+ <div class="ag-top">
95
+ <!-- 胶囊在输入域**之前**、同一个 flex 行里 → 与文字同行(装不下才由 flex-wrap 折行) -->
96
+ <div class="ag-refs" id="${p}-refs" data-testid="${p}-refs" hidden></div>
97
+ <textarea id="${p}-input" data-testid="${p}-input" rows="1" placeholder="${ph}" autocomplete="off"></textarea>
98
+ <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>
99
+ </div>
100
+ <div class="ag-bot">
101
+ <button class="ag-plus" id="${p}-plus" data-testid="${p}-plus" title="选择需求 / 模拟测试 / 录屏 / 产品 / 用例" aria-haspopup="listbox" aria-expanded="false" type="button">${ASK_ICON.plus}</button>
102
+ <!-- 访问权限 = **四档选择器**(2026-09-18 用户:「这个地方要选择」)。
103
+ 改造前是一枚二值胶囊(点一下在「允许完全访问 / 受限访问」间切换)——
104
+ 现在点开四档菜单,按钮文案 = 当前档位,选中态由 qaPermSync() 统一刷。
105
+ ⚠️ 两个实例必须渲染成同一个样子:文案与选中态都来自**同一份** qaPerm。 -->
106
+ <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>
107
+ <div class="ag-right">
108
+ <span class="ag-busy" aria-hidden="true"></span>
109
+ <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>
110
+ <!-- 模型 = **可选**(两个实例同款;用户 2026-09-21 定稿「还是都可以选择模型
111
+ 但是自动切换模型的时候 对话框下拉的选中模型显示也要变」)。
112
+ 文案与选中态由 qaModelPickSync() 现填;首帧给一个不撒谎的占位。 -->
113
+ <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>
114
+ <button class="ag-send" id="${p}-send" data-testid="${p}-send" title="发送" type="button">${ASK_ICON.arrow}${ASK_ICON.stop}</button>
115
+ </div>
116
+ </div>
117
+ </div>
118
+ <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>
119
+ <div class="ag-menu scroll-y" id="${p}-model-menu" data-testid="${p}-model-menu" hidden></div>
120
+ <!-- 访问权限菜单:4 档,内容由 qaPermMenuBuild() 现填(见该函数注释) -->
121
+ <div class="ag-menu scroll-y ag-perm" id="${p}-perm-menu" data-testid="${p}-perm-menu" role="listbox" hidden></div>
122
+ <!-- + 菜单:5 个类型选项(需求 / 模拟测试 / 录屏 / 产品文档 / 测试用例)—— 内容由 askAddMenuBuild 现填 -->
123
+ <div class="ag-menu scroll-y ag-add" id="${p}-add-menu" data-testid="${p}-add-menu" hidden></div>`;
124
+ }
125
+
126
+ /** 站内实例清单(唯一真相):挂载、事件绑定、选择器数组都以它为准。
127
+ * ⚠️ 2026-09-17:需求管理页的「新建需求」输入框已下线(新建需求的唯一入口 = 对话框 + 菜单)。 */
128
+ const askChips = {}; // p → ["req" | "sim" | "rec" | "doc" | "case", ...]
129
+ const askInputOf = (p)=> $("#" + p + "-input");
130
+ const askHasReq = (p)=> (askChips[p] || []).includes("req");
131
+ /** 挂了「模拟测试」胶囊吗(2026-09-21 新增的模式,发送即开测 —— 见 24-agent-test.js)。
132
+ * 与「需求」胶囊同一套机制:语义由胶囊决定,拦截点在 qaAddBind 的捕获阶段。 */
133
+ const askHasSim = (p)=> (askChips[p] || []).includes("sim");
134
+ /** 挂了「录屏」胶囊吗(2026-09-21 新增的模式,发送即录屏 —— 见 25-record.js)。
135
+ * 与前三枚同一套机制;⚠️ 拦截顺序是 rec → shot → sim → req(改发送语义的四枚里,越"重"的越先判,
136
+ * 见 09-ask-event.js 的注释)。 */
137
+ const askHasRec = (p)=> (askChips[p] || []).includes("rec");
138
+ /** 挂了「截屏」胶囊吗(2026-09-21 新增的模式,发送即截屏 —— 见 25-record.js)。
139
+ * 与「录屏」是**兄弟**(同一套前置体检 / MCP 工具 / 兜底),只差「要一帧还是要一段」。 */
140
+ const askHasShot = (p)=> (askChips[p] || []).includes("shot");
141
+ /** 挂了「短视频」胶囊吗(2026-09-23 新增的模式,发送即跑短视频流水线 —— 见 28-shorts.js)。
142
+ * 与「录屏 / 截屏」同一套拦截机制(捕获阶段截发送 → desk.ask{ mode }),但它**不碰浏览器**:
143
+ * 引擎只产任务单(```shorts-task 围栏块),壳逐阶段执行 magic-shorts CLI(选题 → 脚本 → 配音 → 成片)。 */
144
+ const askHasShorts = (p)=> (askChips[p] || []).includes("shorts");
145
+
146
+ /** 胶囊行渲染(+ 菜单选中后出现在对话框里的胶囊,可单个摘掉) */
147
+ function askChipsSync(p){
148
+ const el = $("#" + p + "-refs"); if(!el) return;
149
+ const list = askChips[p] || [];
150
+ el.hidden = !list.length;
151
+ el.innerHTML = list.map(kind=>
152
+ `<span class="ag-ref" title="${escHtml(ASK_CHIP_LABEL[kind] || kind)}">` +
153
+ `<span class="rk">${escHtml(ASK_CHIP_LABEL[kind] || kind)}</span>` +
154
+ `<button class="rx" data-kind="${kind}" type="button" aria-label="移除${escHtml(ASK_CHIP_LABEL[kind] || kind)}胶囊">×</button>` +
155
+ `</span>`).join("");
156
+ }
157
+ function askChipToggle(p, kind){
158
+ if(!ASK_CHIP_LABEL[kind]) return;
159
+ const list = askChips[p] || (askChips[p] = []);
160
+ const i = list.indexOf(kind);
161
+ if(i >= 0){ list.splice(i, 1); toast(`已移除${ASK_CHIP_LABEL[kind]}胶囊`); }
162
+ else {
163
+ list.push(kind);
164
+ toast(kind === "req" ? "已挂需求胶囊 · 发送即新建需求"
165
+ : kind === "sim" ? "已挂模拟测试胶囊 · 发送即开测(数字分身 ↔ 默认引擎,按判据打分)"
166
+ : kind === "rec" ? "已挂录屏胶囊 · 发送即录屏(引擎驱动内嵌浏览器录一段,产物落「浏览器工具 → 产物」)"
167
+ : kind === "shorts" ? "已挂短视频胶囊 · 发送即跑流水线(选题 → 脚本 → 配音 → 成片,几分钟)"
168
+ : `已挂${ASK_CHIP_LABEL[kind]}胶囊`);
169
+ }
170
+ askChipsSync(p);
171
+ }
172
+ function askChipDrop(p, kind){
173
+ const list = askChips[p] || [];
174
+ const i = list.indexOf(kind);
175
+ if(i < 0) return;
176
+ list.splice(i, 1);
177
+ askChipsSync(p);
178
+ toast(`已移除${ASK_CHIP_LABEL[kind] || kind}胶囊`);
179
+ }
180
+ /** 清空胶囊(发送后调用) */
181
+ function askChipsClear(p){
182
+ if(!(askChips[p] || []).length) return;
183
+ askChips[p] = [];
184
+ askChipsSync(p);
185
+ }