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
@@ -2181,7 +2181,9 @@ body::after{content:"";position:fixed;inset:0;z-index:60;pointer-events:none;opa
2181
2181
  /* 运行态:**输入域不再锁**(2026-09-18 用户:「改成支持输入」)—— 照常可写、可回车提交,
2182
2182
  提交即排队(见 qaMsgQueue)。故这里既没有 opacity 降调、也没有 not-allowed 光标;
2183
2183
  占位文案由 qaBusyEx 换成「排队」说明,颜色走常态(锁已不存在,朱砂占位会误读成「不许写」)。
2184
- ⚠️ +(附件)仍锁:处理中贴图没有意义(要等下一轮才发得出去),见 askAttBind 的 busyGate。 */
2184
+ ⚠️ + 菜单(模式胶囊)仍锁:处理中挂胶囊 = 换这一条的发送语义,与「排队」那条路冲突。
2185
+ 贴图**不再受限**(2026-10-19 用户:「加截图没啥冲突 不应该限制」)—— 附件条照常可加,
2186
+ 与排队的那条一起发出,见 askAttBind。 */
2185
2187
  .ag-box.is-busy .ag-plus:disabled{opacity:.4;cursor:not-allowed}
2186
2188
  .ag-box.is-busy .ag-plus:disabled:hover{background:none}
2187
2189
  /* ---- 待发队列行(会话框上方)----
@@ -4850,9 +4852,44 @@ body::after{content:"";position:fixed;inset:0;z-index:60;pointer-events:none;opa
4850
4852
  animation:qaHistPulse 1.2s ease-in-out infinite}
4851
4853
  .qa-hist-note.lost{border-left-color:var(--cinnabar-hi)}
4852
4854
  @keyframes qaHistPulse{0%,100%{opacity:1}50%{opacity:.55}}
4855
+ /* ----- 读历史时的骨架屏(2026-10-20):点(还活着)/ 扫光条(在推进)/ 占位行(内容要来了)。
4856
+ 只动 transform·opacity,配合分片回放让帧,主线程忙时也不会整块冻住。 */
4857
+ .qa-hist-note.qa-hist-loading{animation:none;padding:10px 12px 11px;overflow:hidden}
4858
+ .qa-hist-head{display:flex;align-items:center;gap:8px}
4859
+ .qa-hist-dots{display:inline-flex;align-items:center;gap:3px;flex:none}
4860
+ .qa-hist-dots i{width:5px;height:5px;border-radius:50%;background:var(--indigo);opacity:.3;
4861
+ animation:qaHistDot 1s ease-in-out infinite}
4862
+ .qa-hist-dots i:nth-child(2){animation-delay:.15s}
4863
+ .qa-hist-dots i:nth-child(3){animation-delay:.3s}
4864
+ .qa-hist-bar{position:relative;height:1px;margin:9px 0;background:var(--line);overflow:hidden}
4865
+ .qa-hist-bar::after{content:"";position:absolute;inset:0;transform:translateX(-100%);
4866
+ background:linear-gradient(90deg,transparent,var(--indigo),transparent);
4867
+ animation:qaHistSweep 1.6s ease-in-out infinite}
4868
+ .qa-hist-lines{display:flex;flex-direction:column;gap:6px}
4869
+ .qa-hist-lines span{position:relative;display:block;height:8px;border-radius:4px;overflow:hidden;
4870
+ background:rgba(28,25,23,.06)}
4871
+ .qa-hist-lines span:nth-child(1){width:72%}
4872
+ .qa-hist-lines span:nth-child(2){width:90%}
4873
+ .qa-hist-lines span:nth-child(3){width:54%}
4874
+ .qa-hist-lines span::after{content:"";position:absolute;inset:0;transform:translateX(-100%);
4875
+ background:linear-gradient(90deg,transparent,rgba(255,255,255,.75),transparent);
4876
+ animation:qaHistShimmer 1.8s ease-in-out infinite}
4877
+ .qa-hist-lines span:nth-child(2)::after{animation-delay:.12s}
4878
+ .qa-hist-lines span:nth-child(3)::after{animation-delay:.24s}
4879
+ @keyframes qaHistDot{0%,100%{transform:translateY(0);opacity:.28}40%{transform:translateY(-3px);opacity:1}}
4880
+ @keyframes qaHistSweep{0%{transform:translateX(-100%)}60%,100%{transform:translateX(100%)}}
4881
+ @keyframes qaHistShimmer{0%{transform:translateX(-100%)}55%,100%{transform:translateX(100%)}}
4853
4882
  [data-theme="dark"] .qa-hist-note{background:rgba(255,255,255,.04)}
4854
4883
  [data-theme="dark"] .qa-hist-note.wait{color:#525252}
4855
4884
  [data-theme="dark"] .qa-hist-note.lost{border-left-color:#F87171}
4885
+ [data-theme="dark"] .qa-hist-lines span{background:rgba(255,255,255,.09)}
4886
+ [data-theme="dark"] .qa-hist-lines span::after{
4887
+ background:linear-gradient(90deg,transparent,rgba(255,255,255,.16),transparent)}
4888
+ @media (prefers-reduced-motion:reduce){
4889
+ .qa-hist-dots i,.qa-hist-bar::after,.qa-hist-lines span::after{animation:none}
4890
+ .qa-hist-dots i{opacity:.7}
4891
+ .qa-hist-bar::after{transform:none;opacity:.55}
4892
+ }
4856
4893
 
4857
4894
  /* ===== M6b. 历史消息里的**思考块 / 工具卡**(.qa-hist-step · 2026-09-23)=====
4858
4895
  27-conv-log.js 的 qaConvMsgsHtml 按 kind 分流:thinking / tool 两类消息不再是纯文本
@@ -5052,6 +5089,52 @@ body::after{content:"";position:fixed;inset:0;z-index:60;pointer-events:none;opa
5052
5089
  .qa-fc-p{flex:1;min-width:0;font-family:var(--mono);font-size:10px;color:var(--paper-dim);
5053
5090
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
5054
5091
 
5092
+ /* ---- TodoWrite 待办清单(qaTodoNode;从 TodoWrite/update_plan 参数推导)----
5093
+ 与 .qa-fc 同一帧语言:同高的汇总头 + 内容区;状态只落 class。 */
5094
+ .qa-todo{min-width:0}
5095
+ .qa-todo-hd{display:flex;align-items:center;height:26px;padding:0 8px;
5096
+ border-bottom:1px solid var(--line);background:rgba(28,25,23,.028)}
5097
+ .qa-todo-sum{font-family:var(--mono);font-size:10px;color:var(--paper-mute);
5098
+ white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
5099
+ .qa-todo-list{padding:5px 8px 7px}
5100
+ .qa-todo-row{display:flex;align-items:baseline;gap:7px;padding:2.5px 0;
5101
+ font-size:11.5px;line-height:1.5;color:var(--paper-dim)}
5102
+ .qa-todo-ico{width:12px;height:12px;flex:none;align-self:center;border-radius:50%;
5103
+ border:1.3px solid var(--line);display:grid;place-items:center;color:#FFF}
5104
+ .qa-todo-ico svg{width:8px;height:8px;display:block;stroke:currentColor;fill:none;
5105
+ stroke-width:3;stroke-linecap:round;stroke-linejoin:round}
5106
+ .qa-todo-row.is-in_progress{color:var(--paper);font-weight:600}
5107
+ .qa-todo-row.is-in_progress .qa-todo-ico{border-color:var(--amber);color:var(--amber)}
5108
+ .qa-todo-row.is-completed{color:var(--paper-mute)}
5109
+ .qa-todo-row.is-completed .qa-todo-ico{border-color:var(--jade);background:var(--jade)}
5110
+ .qa-todo-row.is-completed .qa-todo-t{text-decoration:line-through}
5111
+
5112
+ /* ---- 「任务列表」面板形态(.qa-todo-panel;一条应答只有一块,见 03-chat-message 的 todoList)----
5113
+ 与工具卡明细里那一段(无标题)共用上面那套帧语言,只多三样:左侧标题、右端计数与开合箭头、
5114
+ 收起时藏列表(标题栏留着 —— 收起的是一份清单,不是一个入口)。
5115
+ 开合态只落 `open` 类:快照恢复走 `innerHTML = snapshot`,类跟着还原,不会「恢复后又弹开」。 */
5116
+ .qa-todo-panel{min-width:0;margin:0 0 8px;border:1px solid var(--line);
5117
+ border-radius:var(--r-md);overflow:hidden;background:#FFF}
5118
+ .qa-todo-panel .qa-todo-hd{gap:7px}
5119
+ .qa-todo-tt{flex:none;font-size:11.5px;font-weight:600;color:var(--paper)}
5120
+ /* 计数推到右端(只对面板生效:工具卡里那一段是唯一子节点,不该被推走任何东西) */
5121
+ .qa-todo-panel .qa-todo-sum{margin-left:auto}
5122
+ .qa-todo-toggle{flex:none;display:grid;place-items:center;width:16px;height:16px;
5123
+ border-radius:4px;color:var(--paper-mute);cursor:pointer}
5124
+ .qa-todo-toggle:hover{background:rgba(28,25,23,.05)}
5125
+ .qa-todo-toggle svg{width:11px;height:11px;fill:none;stroke:currentColor;stroke-width:2.4;
5126
+ stroke-linecap:round;stroke-linejoin:round;transition:transform .15s}
5127
+ .qa-todo-panel.open .qa-todo-toggle svg{transform:rotate(180deg)}
5128
+ .qa-todo-panel:not(.open) .qa-todo-list{display:none}
5129
+
5130
+ /* ---- 助手图片块(KindImage / P0-1)----
5131
+ 正文区(.qa-t)与委派卡嵌套区(.qa-sub)共用同一张图卡:细边 + 圆角、宽度自适应
5132
+ 不撑爆气泡;data URI 直接进 src,不走 markdown 管线。 */
5133
+ .qa-img{display:block;margin:6px 0;max-width:min(360px,100%);border:1px solid var(--line);
5134
+ border-radius:var(--r-md);overflow:hidden}
5135
+ .qa-img img{display:block;width:100%;height:auto}
5136
+ .qa-sub .qa-img{margin:4px 0;max-width:240px}
5137
+
5055
5138
  /* ---- V5 工具明细改成「段栈面板」(anywhere ToolDetailPanel)----
5056
5139
  原写法:虚线顶边 + 裸段落 + `.qa-tool-sec+.qa-tool-sec{margin-top:7px}`。
5057
5140
  新写法:外框 1px + 段间 border-top —— 段与段的分界由**结构**给出,不再靠各自的内外边距,
@@ -5088,6 +5171,11 @@ body::after{content:"";position:fixed;inset:0;z-index:60;pointer-events:none;opa
5088
5171
  [data-theme="dark"] .qa-cf-bd pre,[data-theme="dark"] .qa-tool-sec pre{
5089
5172
  color:#D6D3D1;background:transparent}
5090
5173
  [data-theme="dark"] .qa-tool-bd > * + *,[data-theme="dark"] .qa-fc-hd{border-color:#3B3835}
5174
+ [data-theme="dark"] .qa-todo-hd{background:rgba(245,244,243,.045);border-color:#3B3835}
5175
+ [data-theme="dark"] .qa-todo-ico{border-color:#57534E}
5176
+ [data-theme="dark"] .qa-todo-row.is-in_progress{color:#F5F4F3}
5177
+ [data-theme="dark"] .qa-todo-row.is-completed{color:#A8A29E}
5178
+ [data-theme="dark"] .qa-img{border-color:#3B3835}
5091
5179
  [data-theme="dark"] .qa-cf-copy:hover{background:rgba(245,244,243,.09);color:#F5F4F3}
5092
5180
  [data-theme="dark"] .qa-mk-row[data-tog="1"]:hover{background:rgba(245,244,243,.05)}
5093
5181
  [data-theme="dark"] .qa-st{background:rgba(245,244,243,.05);border-color:#3B3835}
@@ -131,21 +131,31 @@ body { overflow: hidden; background: var(--bg-page); }
131
131
 
132
132
  <div id="toast" role="status"><span id="toast-msg"></span></div>
133
133
 
134
- <script src="vendor/marked.min.js"></script>
135
- <script src="vendor/purify.min.js"></script>
134
+ <!-- 第三方库已归口 `shared/vendor/`(内核与各宿主共用一份,别再往 pc/ 里存副本):
135
+ marked(markdown)/ purify(消毒)/ highlight(代码高亮,由内核按需加载 → 地址在下面给) -->
136
+ <script src="../shared/vendor/marked.min.js"></script>
137
+ <script src="../shared/vendor/purify.min.js"></script>
138
+ <script>window.QA_HLJS_URL = "../shared/vendor/highlight.min.js";</script>
139
+ <!-- 对话内核(唯一实现,见 client-ui/shared/conv/README.md):宿主只按 manifest 的顺序引它,别在这里改顺序 -->
140
+ <script src="../shared/conv/01-dom.js"></script>
141
+ <script src="../shared/conv/02-media-kinds.js"></script>
136
142
  <script src="js/00-base.js"></script>
137
143
  <script src="js/04-task-list.js"></script>
138
144
  <script src="js/91-workbench-stubs.js"></script>
145
+ <script src="../shared/conv/10-markdown-render.js"></script>
139
146
  <script src="js/10-markdown.js"></script>
140
- <script src="js/03-chat-message.js"></script>
141
- <script src="js/29-msg-variants.js"></script>
142
- <script src="js/08-ask-event-map.js"></script>
143
- <script src="js/09-stream-apply.js"></script>
147
+ <script src="../shared/conv/03-chat-message.js"></script>
148
+ <script src="../shared/conv/29-msg-variants.js"></script>
149
+ <script src="../shared/conv/08-ask-event-map.js"></script>
150
+ <script src="../shared/conv/09-stream-apply.js"></script>
144
151
  <script src="js/07-chat-stream-render.js"></script>
145
- <script src="js/06-ask-answer.js"></script>
152
+ <script src="../shared/conv/07-stream-delegation.js"></script>
153
+ <script src="../shared/conv/06-ask-answer.js"></script>
146
154
  <script src="js/05-task-list-render.js"></script>
147
- <script src="js/19-attachments.js"></script>
155
+ <script src="../shared/conv/19-attachments.js"></script>
156
+ <script src="../shared/conv/18-askbox-markup.js"></script>
148
157
  <script src="js/18-askbox.js"></script>
158
+ <script src="../shared/conv/27-conv-ledger.js"></script>
149
159
  <script src="../shared/transport-http.js"></script>
150
160
  <script src="js/92-composer.js"></script>
151
161
  <script src="js/93-rail.js"></script>
@@ -1,36 +1,14 @@
1
- /* 基础助手(从 magic-test 的 01-core.js / 15-layout.js 原样搬来)。
1
+ /* 基础助手:`escHtml` / `toast` / 工作台 `state`(**宿主专属**的那几样)。
2
2
  *
3
- * 这一份是为了让**逐字复制过来的对话框代码**能跑起来 —— 它们按 magic-test 的全局函数风格写的
4
- * (`$("#home-input")` 这类),所以这里保持同名同义,不去改那些文件里的调用点。
5
- * 改动请优先改上游(magic-test 的 src/js/),再同步到这里,避免两份漂移。
6
- */
3
+ * ⚠️ `$` / `$$` / `htmlWrite` / `qaImeComposing` 已随阶段二收敛进**对话内核**
4
+ * (`client-ui/shared/conv/01-dom.js`)—— 这里不再各留一份(重复顶层声明 = 整脚本 SyntaxError)。
5
+ * 这份文件是为了让**逐字复制过来的对话框代码**能跑起来 —— 它们按 magic-test 的全局函数风格写的
6
+ * (`$("#home-input")` 这类),所以保持同名同义,不去改那些文件里的调用点。
7
+ * toast 要宿主的 #toast 节点(见 index.html),所以它只能留在宿主这一侧。 */
7
8
 
8
- /* −− 选择器 −−
9
- ⚠️ 注意是 **CSS 选择器**版,不是 id 版:调用点写的是 `$("#home-input")`。 */
10
- const $ = (s, el = document) => el.querySelector(s);
11
- const $$ = (s, el = document) => [...el.querySelectorAll(s)];
12
9
 
13
- /** 幂等写 innerHTML:渲染结果字符串没变就不碰 DOM。
14
- * 上游为「运行中列表每 700ms 重建一次导致闪动」而写;对话框的流式重绘也吃这条。 */
15
- function htmlWrite(el, html) {
16
- if (!el) return false;
17
- if (el.__html === html) return false;
18
- el.__html = html;
19
- el.innerHTML = html;
20
- return true;
21
- }
22
-
23
- function escHtml(s) {
24
- return String(s == null ? "" : s)
25
- .replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;")
26
- .replace(/"/g, "&quot;").replace(/'/g, "&#39;");
27
- }
28
-
29
- /** 输入法合成守卫:中文输入法里「选词那一下按的 Enter」归输入法,不是提交。
30
- * 对话框的回车提交必须先过它,否则拼音打到一半敲回车会把半截消息发出去。 */
31
- function qaImeComposing(e) {
32
- return !!e && (e.isComposing === true || e.keyCode === 229);
33
- }
10
+ /* ⚠️ `escHtml` 也进了内核(`client-ui/shared/conv/01-dom.js`)—— 它是纯函数、零宿主依赖,
11
+ 而内核多个分片都要用它。这里不再定义(重复顶层声明 = 整脚本 SyntaxError)。 */
34
12
 
35
13
  let toastTimer = null;
36
14
  function toast(msg, asHtml) {
@@ -405,7 +405,10 @@ function qaSessionBindFor(taskId, sessionId){
405
405
  qaTaskPersist(); qaDbUpdate(t, { sessionId });
406
406
  }
407
407
  /* 本轮结束(成功/失败/被停)后仍没有会话 id → 记一笔 sidLost。
408
- 为什么要记:`session_id` 只在 CLI 的 result 行里给,中途被停/超时的那轮永远拿不到它;
408
+ ⚠️ 「session_id 只在 result 行里给」这句写于 `--events` 之前:现在 core 一拿到 id 就**每行都带**
409
+ (internal/cli/events.go 的 eventAttrs),PC 也在每个事件上当场绑锚点(09-stream-apply.js 开头的
410
+ sidAny,2026-10-15 起)—— 所以中途被停 / 超时的那轮通常已经拿到 id,不会再走到这里。
411
+ 这条兜底留给「整轮真的一条 id 都没带」的情形(老 CLI、非 --events 形态、引擎自己没回 id)。
409
412
  没有这个标记,下一轮追问就只能从零开一个新会话(引擎失忆 = 用户看到的「没了 session」)。
410
413
  有这个标记 → 追问时下发 `-c`,续接该目录最近一次会话(引擎自己那条),至少接得回去。 */
411
414
  function qaTaskMarkSidLost(){ qaTaskMarkSidLostFor(qaActiveId); }
@@ -1,88 +1,5 @@
1
- /* ---------- #qa-stream 事件委托:工具卡 / 思考条的开合 / 提问卡点选 ----------
2
- 为什么必须委托:快照恢复(qaTaskSwitch / qaDbLoad / 启动恢复)走的是
3
- `s.innerHTML = t.snapshot` —— DOM 节点被整份重建,绑在卡片上的监听随旧节点消失。
4
- 表现就是「点侧栏历史任务后,WebSearch 这类工具卡点了没反应」(2026-09-16 用户报障)。
5
- 委托挂在容器 #qa-stream 上(它从不被替换,只换 innerHTML),历史卡片照样可开合。
6
- ⚠️ 提问卡(⑤ .qa-ask)同理:恢复出来的历史提问卡也要能点选 —— 它比工具卡更怕丢监听。
7
- 幂等:重复调用只绑一次。 */
8
- let qaStreamDelegated = false;
9
- function bindQaStreamDelegation(){
10
- const s = $("#qa-stream");
11
- if(!s || qaStreamDelegated) return;
12
- qaStreamDelegated = true;
13
- s.addEventListener("click", e=>{
14
- // ⑤ 提问卡:选项 / 提交 / 「其他」确认(先于工具卡判定 —— 卡里没有 .qa-tool-head,互不干扰)
15
- const askOpt = e.target.closest && e.target.closest(".qa-ask-opt");
16
- if(askOpt){ const c = askOpt.closest(".qa-ask"); if(c) qaAskPick(c, askOpt); return; }
17
- const askSub = e.target.closest && e.target.closest(".qa-ask-sub");
18
- if(askSub){
19
- const c = askSub.closest(".qa-ask");
20
- if(c && !qaAskAnswered(c) && qaAskAllDone(c)) qaAskMark(c, qaAskText(c));
21
- return;
22
- }
23
- const askGo = e.target.closest && e.target.closest(".qa-ask-go");
24
- if(askGo){
25
- const c = askGo.closest(".qa-ask");
26
- const inp = c && c.querySelector(".qa-ask-other input");
27
- if(c) qaAskFree(c, inp && inp.value);
28
- return;
29
- }
30
- const toolHead = e.target.closest && e.target.closest(".qa-tool-head");
31
- if(toolHead){
32
- const card = toolHead.closest(".qa-tool"); if(!card) return;
33
- card.dataset.touched = "1"; // 记「用户手动开合过」,自动收起让位
34
- card.classList.toggle("open");
35
- return;
36
- }
37
- /* ②③ 复制按钮(代码面板帧 / 工具明细段,2026-09-28):
38
- 要复制的原文落位是**固定的** —— 面板里是 `.qa-cf-bd pre`,明细段里是 `.qa-tool-sec pre`
39
- (见 29-msg-variants 的 qaCodeFrameNode / QA_COPY_BTN)。按这个约定取文本,
40
- 所以按钮本身不带任何 data-*,快照重建后照样能复制。
41
- 点完切成对勾 1.2s:原来没有反馈,用户会怀疑没复制上、连点好几次。 */
42
- const cpBtn = e.target.closest && e.target.closest(".qa-cf-copy");
43
- if(cpBtn){
44
- const host = cpBtn.closest(".qa-cf, .qa-tool-sec");
45
- const pre = host && host.querySelector("pre");
46
- if(typeof qaCopyText === "function") qaCopyText(pre ? pre.textContent : "");
47
- cpBtn.classList.add("copied");
48
- setTimeout(()=>cpBtn.classList.remove("copied"), 1200);
49
- return;
50
- }
51
- /* ② 单行标记行开合(图标 + 人话标题 + 状态徽章 + 可展开明细)。
52
- ⚠️ 没有明细(data-tog="0")时不可点 —— 否则点了一下什么都没有,像坏了。 */
53
- const mkRow = e.target.closest && e.target.closest(".qa-mk-row");
54
- if(mkRow){
55
- if(mkRow.dataset.tog !== "1") return;
56
- const mk = mkRow.closest(".qa-mk"); if(!mk) return;
57
- mk.dataset.touched = "1";
58
- mk.classList.toggle("open");
59
- return;
60
- }
61
- const thinkTg = e.target.closest && e.target.closest(".qa-think-toggle");
62
- if(thinkTg){
63
- const th = thinkTg.closest(".qa-think"); if(!th) return;
64
- th.dataset.touched = "1";
65
- th.classList.toggle("open");
66
- return;
67
- }
68
- const toolsTg = e.target.closest && e.target.closest(".qa-tools-toggle");
69
- if(toolsTg){
70
- const ts = toolsTg.closest(".qa-tools"); if(!ts) return;
71
- ts.dataset.touched = "1";
72
- ts.classList.toggle("open");
73
- }
74
- });
75
- // 「其他」行的回车确认(input 的 Enter 不会走 click;同样挂在容器上才不怕快照重建)
76
- // ⚠️ 合成中(输入法选词那一敲)不算确认,见 qaImeComposing —— 这行输入框同样要吃中文。
77
- s.addEventListener("keydown", e=>{
78
- if(qaImeComposing(e)) return;
79
- if(e.key !== "Enter") return;
80
- const inp = e.target.closest && e.target.closest(".qa-ask-other input");
81
- if(!inp) return;
82
- e.preventDefault();
83
- const c = inp.closest(".qa-ask"); if(c) qaAskFree(c, inp.value);
84
- });
85
- }
1
+ /* 宿主半:左栏任务绑定 + 发送 / 停止 / 忙碌锁。
2
+ 「#qa-stream 事件委托」在内核:conv/07-stream-delegation.js。 */
86
3
  function bindNavTasks(){
87
4
  const box = $("#nav-tasks"); if(!box) return;
88
5
  box.addEventListener("click", e=>{
@@ -1,176 +1,8 @@
1
- /* ---------- 完整 Markdown 渲染器(专业栈:marked + DOMPurify + highlight.js)----------
2
- 解析:marked(GFM)——表格对齐 / 任务列表 / 删除线 / 自动链接 / 围栏语言标签,
3
- 远超手写正则的边界正确性(嵌套列表、松散列表、行内混排等)。
4
- 净化:DOMPurify——文档正文多来自网页采集(不可信源),渲染后必须消毒,
5
- 防脚本 / 事件属性注入;放行 title / target / align / class 等阅读必需属性。
6
- 高亮:highlight.js——代码块按 ```lang 自动着色(hljs.highlightElement)。
7
- ⚠️ 它是**按需加载**的(2026-09-19):129KB,只有真渲染出代码块时才注入,
8
- 见下方 loadHljs(),不要在 index.html 里同步引它。
9
- 保留契约:仍返回 { html, anchors }。anchors 为标题索引(level/text/id),
10
- 供右侧目录(.md-toc)点击定位;标题锚点 id 稳定(h-1 / h-2 …),
11
- 悬停显示「#」。与既有的 .md-body 版式 / .doc-reader 结构完全对接。 */
12
-
13
- const MD_STACK_READY = (typeof marked !== "undefined") && (typeof DOMPurify !== "undefined");
14
- let _mdAnchors = [], _mdHSeq = 0;
15
-
16
- /** 取纯文本(去行内标记),用于目录标题 */
17
- function mdPlainText(s){
18
- return String(s||"")
19
- .replace(/!\[([^\]]*)\]\([^)]*\)/g,"$1")
20
- .replace(/\[([^\]]*)\]\([^)]*\)/g,"$1")
21
- .replace(/`([^`]*)`/g,"$1")
22
- .replace(/\*\*([^*]+)\*\*/g,"$1").replace(/__([^_]+)__/g,"$1")
23
- .replace(/\*([^*]+)\*/g,"$1").replace(/_([^_]+)_/g,"$1")
24
- .replace(/~~([^~]+)~~/g,"$1")
25
- .replace(/\s+/g," ").trim();
26
- }
27
-
28
- /* 一次性配置 marked 渲染器:
29
- - heading → 稳定锚点 id + 收集目录 + 「#」锚点(.h-anc,::before 出 # 号)
30
- - code → 保留 code-lang 角标 + hljs 类,随后由 highlightElement 上色
31
- - listitem → GFM 任务项改写成既有 .task-li 结构;普通项回退默认渲染
32
- - link → 外链加 target=_blank rel=noopener,防钓鱼落页 */
33
- if(MD_STACK_READY){
34
- marked.use({
35
- gfm:true,
36
- breaks:false,
37
- renderer:{
38
- heading(tok){
39
- const inner = this.parser.parseInline(tok.tokens||[]);
40
- const id = "h-" + (++_mdHSeq);
41
- _mdAnchors.push({ level:tok.depth, text:mdPlainText(tok.text), id });
42
- return `<h${tok.depth} id="${id}">${inner}<a class="h-anc" href="#${id}" title="定位此标题" aria-hidden="true"></a></h${tok.depth}>\n`;
43
- },
44
- code(tok){
45
- const lang = String(tok.lang||"").split(/\s+/)[0];
46
- const cls = "hljs" + (lang ? " language-" + lang : "");
47
- // 转义代码正文:既保证 <tag> 原样显示,也避免消毒阶段误删
48
- const body = String(tok.text||"")
49
- .replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;");
50
- return `<pre>${lang?`<span class="code-lang">${lang}</span>`:""}<code class="${cls}">${body}</code></pre>\n`;
51
- },
52
- listitem(tok){
53
- if(tok.task){
54
- const body = this.parser.parse(tok.tokens||[], !!tok.loose);
55
- return `<li class="task-li${tok.checked?" done":""}"><span class="bx">✓</span><span class="tx">${body}</span></li>\n`;
56
- }
57
- return false; // 回退 marked 默认(保留嵌套 <ul>/<ol>)
58
- },
59
- link(tok){
60
- const text = this.parser.parseInline(tok.tokens||[]);
61
- const href = String(tok.href||"");
62
- const ti = tok.title ? ` title="${tok.title}"` : "";
63
- const ext = /^https?:/i.test(href);
64
- return `<a href="${href}"${ext?' target="_blank" rel="noopener"':""}${ti}>${text}</a>`;
65
- },
66
- },
67
- });
68
- }
69
-
70
- /** 手写兜底:vendor 未加载时仍能阅读(仅线性块级,极端降级) */
71
- function mdFallback(src){
72
- const esc = (s)=>String(s??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;");
73
- let html = "", inCode = false, buf = [];
74
- const flushPara = ()=>{ if(buf.length){ html += "<p>" + esc(buf.join(" ")) + "</p>"; buf = []; } };
75
- String(src||"").split(/\r?\n/).forEach((ln)=>{
76
- if(/^\s*```/.test(ln)){ flushPara(); if(inCode){ html += `<pre><code>${esc(buf.join("\n"))}</code></pre>`; buf=[]; } inCode=!inCode; return; }
77
- if(inCode){ buf.push(ln); return; }
78
- const hm = /^(#{1,6})\s+(.*)$/.exec(ln);
79
- if(hm){ flushPara(); const lv=hm[1].length, id="h-"+(++_mdHSeq);
80
- _mdAnchors.push({level:lv,text:mdPlainText(hm[2]),id});
81
- html += `<h${lv} id="${id}">${esc(hm[2])}</h${lv}>`; return; }
82
- if(/^\s*$/.test(ln)){ flushPara(); return; }
83
- buf.push(ln.trim());
84
- });
85
- flushPara();
86
- return html;
87
- }
88
-
89
- /** 把「独占一行的图片路径」变成 markdown 图片,让对话里**直接看到图**(2026-09-19)。
90
- *
91
- * 为什么需要:引擎产出/引用的截图通常写成**一行绝对路径**(或本项目入库时的 `[图片] <路径>`),
92
- * 不是 markdown —— 不转的话对话里只有一行字,看不到图;而飞书那边的完成回执也会把同一批图
93
- * 上传成内联图(见 feishu-bridge.cjs::extractImageRefs),两边口径对齐。
94
- *
95
- * ⚠️ 只认**独占一行**的绝对路径 + 常见图片扩展名:
96
- * · 不碰行内路径(正文里提到 `src/js/xxx.png` 不该变成图);
97
- * · 路径里不含空格 / 引号 / 括号(含了就不转,免得把 markdown 语法拼坏);
98
- * · alt 也填路径 —— 图加载不出来时还能看见它指向哪。 */
99
- function mdLocalizeImagePaths(src){
100
- const EX = "png|jpe?g|gif|webp|bmp";
101
- return String(src||"")
102
- // 本项目入库时给飞书图片的写法:`[图片] /abs/x.png`
103
- .replace(new RegExp("^[ \\t]*\\[图片\\][ \\t]*(/\\S+\\.(?:" + EX + "))[ \\t]*$", "gim"),
104
- (_a, p) => `![${p}](${p})`)
105
- // 裸路径独占一行
106
- .replace(new RegExp("^[ \\t]*(/[^\\s`'\"()<>]+\\.(?:" + EX + "))[ \\t]*$", "gim"),
107
- (_a, p) => `![${p}](${p})`);
108
- }
109
-
110
- /** 渲染 Markdown → { html, anchors } */
111
- function mdToHtml(src){
112
- _mdAnchors = []; _mdHSeq = 0;
113
- src = mdLocalizeImagePaths(src);
114
- if(!MD_STACK_READY){ return { html: mdFallback(src), anchors: _mdAnchors }; }
115
- let raw;
116
- try{
117
- raw = marked.parse(String(src||""));
118
- }catch(e){
119
- console.warn("[md] marked 解析失败,回退简易渲染:", e && e.message);
120
- return { html: mdFallback(src), anchors: _mdAnchors };
121
- }
122
- // 消毒:放行阅读必需的属性 / 标签;禁 script、事件属性、javascript: 协议
123
- const html = DOMPurify.sanitize(raw, {
124
- ADD_ATTR: ["target", "rel", "align", "class", "id", "title", "disabled", "checked", "type"],
125
- FORBID_TAGS: ["script", "style", "iframe", "form", "input"],
126
- FORBID_ATTR: ["onerror", "onload", "onclick", "style"],
127
- });
128
- return { html, anchors: _mdAnchors };
129
- }
130
-
131
- /** 只取 HTML(兼容旧调用) */
132
- function mdToHtmlOnly(src){ return mdToHtml(src).html; }
133
-
134
- /* ---------- highlight.js 按需加载(2026-09-19)----------
135
- 129KB,比 marked + purify + floating-ui 加起来还大 —— 但只有真渲染出代码块时才用得到。
136
- 原来它在 index.html 里同步加载,每次启动都白解析一遍。现在首次需要时才注入;
137
- 注入完成前代码块以纯文本呈现(下面的 typeof hljs 分支本来就兜住了,不影响功能)。
138
- 流式渲染每闭合一段都会调一次 highlightIn → 加载期间可能登记多个根节点,
139
- 用 Set 去重,加载完统一补扫一遍。 */
140
- let _hljsLoading = false;
141
- const _hljsPending = new Set();
142
- function loadHljs(){
143
- if(typeof hljs !== "undefined" || _hljsLoading) return;
144
- _hljsLoading = true;
145
- const s = document.createElement("script");
146
- s.src = "vendor/highlight.min.js";
147
- s.onload = () => {
148
- _hljsLoading = false;
149
- _hljsPending.forEach((r)=>{ try{ highlightIn(r); }catch(e){ /* 节点可能已卸载,忽略 */ } });
150
- _hljsPending.clear();
151
- };
152
- s.onerror = () => { _hljsLoading = false; }; // 加载失败就一直纯文本,不影响功能
153
- document.head.appendChild(s);
154
- }
155
-
156
- /** 对已插入 DOM 的代码块执行语法高亮;hljs 还没就绪就先登记,加载完补扫。
157
- * ⚠️ 先查有没有代码块再决定要不要惊动 129KB —— 渲染普通文档(无 ``` 块)时
158
- * highlightIn 照样会被调一次,不能让它把 highlight.js 白拉下来。 */
159
- function highlightIn(root){
160
- if(!root) return;
161
- const blocks = root.querySelectorAll("pre code");
162
- if(!blocks.length) return;
163
- if(typeof hljs === "undefined"){ _hljsPending.add(root); loadHljs(); return; }
164
- blocks.forEach((el)=>{
165
- if(el.dataset.hl) return;
166
- try{ hljs.highlightElement(el); el.dataset.hl = "1"; }catch(e){ /* 未知语言静默跳过 */ }
167
- });
168
- }
169
-
170
1
  /* ---------- 右栏多标签系统 ----------
171
- tab 形态:{ id, kind:'browser'|'doc', docId?, el, paneEl }
2
+ tab 形态:{ id, kind:'browser'|'doc'|'features'|'shots'|'artifact', docId?/path?, el, paneEl }
172
3
  - 浏览器工具标签常驻、不可关闭
173
- - 每份产品文档直接就是一个标签页(不经任何索引层、无弹窗),内容即完整 Markdown 阅读器 */
4
+ - 每份产品文档直接就是一个标签页(不经任何索引层、无弹窗),内容即完整 Markdown 阅读器
5
+ - 对话产物(图 / 视频 / 音频 / 文档 …)点开也是一枚标签(openArtifactTab),可就地预览 */
174
6
  let dockTabs = []; // 全部标签(含常驻浏览器)
175
7
  let activeDockTab = null; // 当前激活 tab id
176
8
  let docTabSeq = 0;