@bolloon/bolloon-agent 0.2.9 → 0.2.11

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 (268) hide show
  1. package/README.md +467 -467
  2. package/bin/bolloon-cli.cjs +183 -183
  3. package/bin/bolloon.cjs +0 -0
  4. package/bin/bolloon.js +157 -0
  5. package/dist/agents/agent-manifest-protocol.js +0 -52
  6. package/dist/agents/constraint-layer.js +19 -19
  7. package/dist/agents/pi-sdk.js +287 -1213
  8. package/dist/agents/shell-guard.js +2 -2
  9. package/dist/agents/workflow-pivot-loop.js +2 -81
  10. package/dist/bollharness/src/scripts/deploy-guard.js +8 -8
  11. package/dist/bollharness-integration/context-router-judgment.js +4 -4
  12. package/dist/bollharness-integration/context-router.js +292 -292
  13. package/dist/bollharness-integration/gate-state-machine.js +13 -26
  14. package/dist/bollharness-integration/integration.js +0 -71
  15. package/dist/bollharness-integration/llm-judgment-engine.js +134 -134
  16. package/dist/bollharness-integration/skill-adapter.js +127 -52
  17. package/dist/bootstrap/context-collector.js +3 -6
  18. package/dist/bootstrap/lifecycle-hooks.js +1 -15
  19. package/dist/cli-entry.js +35 -39
  20. package/dist/constraint-runtime/src/_archive_helper.js +9 -0
  21. package/dist/constraint-runtime/src/agent/coordinator.js +48 -0
  22. package/dist/constraint-runtime/src/agent/index.js +1 -0
  23. package/dist/constraint-runtime/src/assistant/index.js +12 -0
  24. package/dist/constraint-runtime/src/bootstrap/index.js +12 -0
  25. package/dist/constraint-runtime/src/bootstrap_graph.js +13 -0
  26. package/dist/constraint-runtime/src/bridge/index.js +12 -0
  27. package/dist/constraint-runtime/src/buddy/index.js +12 -0
  28. package/dist/constraint-runtime/src/cli/index.js +12 -0
  29. package/dist/constraint-runtime/src/command_graph.js +10 -0
  30. package/dist/constraint-runtime/src/commands.js +60 -0
  31. package/dist/constraint-runtime/src/components/index.js +12 -0
  32. package/dist/constraint-runtime/src/constants/index.js +12 -0
  33. package/dist/constraint-runtime/src/constraint/budget.js +22 -0
  34. package/dist/constraint-runtime/src/constraint/index.js +2 -0
  35. package/dist/constraint-runtime/src/constraint/permission.js +20 -0
  36. package/dist/constraint-runtime/src/context.js +30 -0
  37. package/dist/constraint-runtime/src/coordinator/index.js +12 -0
  38. package/dist/constraint-runtime/src/cost_hook.js +4 -0
  39. package/dist/constraint-runtime/src/cost_tracker.js +8 -0
  40. package/dist/constraint-runtime/src/deferred_init.js +10 -0
  41. package/dist/constraint-runtime/src/direct_modes.js +6 -0
  42. package/dist/constraint-runtime/src/dynamic-tool-loader.js +85 -0
  43. package/dist/constraint-runtime/src/entrypoints/index.js +12 -0
  44. package/dist/constraint-runtime/src/execution_registry.js +44 -0
  45. package/dist/constraint-runtime/src/history.js +9 -0
  46. package/dist/constraint-runtime/src/hooks/index.js +12 -0
  47. package/dist/constraint-runtime/src/index.js +26 -0
  48. package/dist/constraint-runtime/src/ink.js +4 -0
  49. package/dist/constraint-runtime/src/keybindings/index.js +12 -0
  50. package/dist/constraint-runtime/src/memdir/index.js +12 -0
  51. package/dist/constraint-runtime/src/migrations/index.js +12 -0
  52. package/dist/constraint-runtime/src/models.js +1 -0
  53. package/dist/constraint-runtime/src/moreright/index.js +12 -0
  54. package/dist/constraint-runtime/src/native_ts/index.js +12 -0
  55. package/dist/constraint-runtime/src/output_styles/index.js +12 -0
  56. package/dist/constraint-runtime/src/parity_audit.js +12 -0
  57. package/dist/constraint-runtime/src/plugins/index.js +12 -0
  58. package/dist/constraint-runtime/src/port_manifest.js +11 -0
  59. package/dist/constraint-runtime/src/prefetch.js +9 -0
  60. package/dist/constraint-runtime/src/query.js +1 -0
  61. package/dist/constraint-runtime/src/remote/index.js +12 -0
  62. package/dist/constraint-runtime/src/remote_runtime.js +9 -0
  63. package/dist/constraint-runtime/src/runtime/index.js +1 -0
  64. package/dist/constraint-runtime/src/runtime/session.js +35 -0
  65. package/dist/constraint-runtime/src/schemas/index.js +12 -0
  66. package/dist/constraint-runtime/src/screens/index.js +12 -0
  67. package/dist/constraint-runtime/src/server/index.js +12 -0
  68. package/dist/constraint-runtime/src/services/index.js +12 -0
  69. package/dist/constraint-runtime/src/session_store.js +22 -0
  70. package/dist/constraint-runtime/src/setup.js +30 -0
  71. package/dist/constraint-runtime/src/skills/index.js +1 -0
  72. package/dist/constraint-runtime/src/skills/skill-registry.js +28 -0
  73. package/dist/constraint-runtime/src/state/index.js +12 -0
  74. package/dist/constraint-runtime/src/system_init.js +20 -0
  75. package/dist/constraint-runtime/src/thinking/engine.js +42 -0
  76. package/dist/constraint-runtime/src/thinking/index.js +1 -0
  77. package/dist/constraint-runtime/src/tool_pool.js +8 -0
  78. package/dist/constraint-runtime/src/tools/OpenCLI/execAdapter.js +7 -0
  79. package/dist/constraint-runtime/src/tools/OpenCLI/listAdapters.js +7 -0
  80. package/dist/constraint-runtime/src/tools/OpenCLI/runCommand.js +7 -0
  81. package/dist/constraint-runtime/src/tools/SafeSDK/confirmTransaction.js +6 -0
  82. package/dist/constraint-runtime/src/tools/SafeSDK/createTransaction.js +8 -0
  83. package/dist/constraint-runtime/src/tools/SafeSDK/executeTransaction.js +6 -0
  84. package/dist/constraint-runtime/src/tools/SafeSDK/getBalance.js +6 -0
  85. package/dist/constraint-runtime/src/tools/SafeSDK/getPendingTransactions.js +6 -0
  86. package/dist/constraint-runtime/src/tools/SafeSDK/proposeTransaction.js +6 -0
  87. package/dist/constraint-runtime/src/tools.js +80 -0
  88. package/dist/constraint-runtime/src/transcript.js +19 -0
  89. package/dist/constraint-runtime/src/types/index.js +12 -0
  90. package/dist/constraint-runtime/src/upstream_proxy/index.js +12 -0
  91. package/dist/constraint-runtime/src/utils/index.js +12 -0
  92. package/dist/constraint-runtime/src/vim/index.js +12 -0
  93. package/dist/constraint-runtime/src/voice/index.js +12 -0
  94. package/dist/constraint-runtime/tests/agent.test.js +16 -0
  95. package/dist/constraint-runtime/tests/constraint.test.js +41 -0
  96. package/dist/constraint-runtime/tests/skill.test.js +19 -0
  97. package/dist/constraint-runtime/tests/thinking.test.js +22 -0
  98. package/dist/context-compaction/auto-compact.js +7 -7
  99. package/dist/electron-preload.js +1 -18
  100. package/dist/electron-preload.js.map +1 -1
  101. package/dist/electron.js +168 -3
  102. package/dist/electron.js.map +1 -1
  103. package/dist/index.js +116 -136
  104. package/dist/llm/config-store.js +8 -24
  105. package/dist/llm/llm-judgment-client.js +102 -102
  106. package/dist/llm/pi-ai.js +85 -126
  107. package/dist/llm/system-prompt/layers/channel/local.md +14 -14
  108. package/dist/llm/system-prompt/layers/channel/p2p-agent.md +18 -18
  109. package/dist/llm/system-prompt/layers/channel/p2p-visitor.md +19 -19
  110. package/dist/llm/system-prompt/layers/core/artifacts_storage.md +89 -89
  111. package/dist/llm/system-prompt/layers/core/evenhandedness.md +21 -21
  112. package/dist/llm/system-prompt/layers/core/hibs_reminders.md +15 -15
  113. package/dist/llm/system-prompt/layers/core/identity.md +37 -37
  114. package/dist/llm/system-prompt/layers/core/knowledge.md +17 -17
  115. package/dist/llm/system-prompt/layers/core/memory_system.md +12 -12
  116. package/dist/llm/system-prompt/layers/core/network_filesystem.md +28 -28
  117. package/dist/llm/system-prompt/layers/core/refusal.md +37 -37
  118. package/dist/llm/system-prompt/layers/core/tone.md +31 -31
  119. package/dist/llm/system-prompt/layers/core/tools.thin.md +13 -13
  120. package/dist/llm/system-prompt/layers/core/wellbeing.md +41 -41
  121. package/dist/llm/system-prompt/layers/role/architect.md +20 -20
  122. package/dist/llm/system-prompt/layers/role/expert.md +19 -19
  123. package/dist/llm/system-prompt/layers/role/implementer.md +15 -15
  124. package/dist/llm/system-prompt/layers/role/security.md +15 -15
  125. package/dist/llm/system-prompt/layers/tool/artifacts.md +72 -72
  126. package/dist/llm/system-prompt/layers/tool/bash.md +25 -25
  127. package/dist/llm/system-prompt/layers/tool/hibs_api.md +171 -171
  128. package/dist/llm/system-prompt/layers/tool/image_search.md +70 -70
  129. package/dist/llm/system-prompt/layers/tool/manifest.md +89 -89
  130. package/dist/llm/system-prompt/layers/tool/mcp_apps.md +53 -53
  131. package/dist/llm/system-prompt/layers/tool/web_search.md +83 -83
  132. package/dist/llm/tool-manifest/ask_user_input.js +1 -1
  133. package/dist/llm/tool-manifest/bash.js +3 -3
  134. package/dist/llm/tool-manifest/create_file.js +4 -4
  135. package/dist/llm/tool-manifest/fetch_sports_data.js +3 -3
  136. package/dist/llm/tool-manifest/image_search.js +2 -2
  137. package/dist/llm/tool-manifest/mcp.js +2 -2
  138. package/dist/llm/tool-manifest/message_compose.js +3 -3
  139. package/dist/llm/tool-manifest/places.js +2 -2
  140. package/dist/llm/tool-manifest/present_files.js +1 -1
  141. package/dist/llm/tool-manifest/recipe.js +2 -2
  142. package/dist/llm/tool-manifest/str_replace.js +5 -5
  143. package/dist/llm/tool-manifest/view.js +3 -3
  144. package/dist/llm/tool-manifest/weather.js +4 -4
  145. package/dist/llm/tool-manifest/web.js +4 -4
  146. package/dist/pi-ecosystem-colony/index.js +365 -0
  147. package/dist/pi-ecosystem-judgment/causal-judge.js +51 -51
  148. package/dist/pi-ecosystem-judgment/distill-prompt.js +42 -42
  149. package/dist/pi-ecosystem-judgment/distillation.js +14 -14
  150. package/dist/pi-ecosystem-judgment/monitor-gate.js +28 -28
  151. package/dist/pi-ecosystem-judgment/value-injection.js +102 -102
  152. package/dist/security/context-router-tool.js +15 -15
  153. package/dist/security/tool-gate.js +0 -11
  154. package/dist/social/ant-colony/AdaptiveHeartbeat.js +101 -0
  155. package/dist/social/ant-colony/PheromoneEngine.js +227 -0
  156. package/dist/social/ant-colony/index.js +6 -0
  157. package/dist/social/ant-colony/types.js +24 -0
  158. package/dist/test/ai-judgment-test.js +80 -0
  159. package/dist/test/bollharness-integration.test.js +318 -0
  160. package/dist/test/channel-agent-multi-dialogue.js +205 -0
  161. package/dist/test/channel-heartbeat-agent-test.js +201 -0
  162. package/dist/test/constraint-layer.test.js +164 -0
  163. package/dist/test/diap-identity-test.js +172 -0
  164. package/dist/test/diap-quick-test.js +62 -0
  165. package/dist/test/global-shared-context.test.js +315 -0
  166. package/dist/test/harness-judgment-injection.test.js +246 -0
  167. package/dist/test/harness-workflow-integrator-test.js +228 -0
  168. package/dist/test/human-value-store.test.js +243 -0
  169. package/dist/test/hybrid-integration-test.js +118 -0
  170. package/dist/test/hybrid-messenger-verify.js +55 -0
  171. package/dist/test/iroh-bistream-debug.js +38 -0
  172. package/dist/test/iroh-communication.test.js +66 -0
  173. package/dist/test/iroh-debug-test.js +57 -0
  174. package/dist/test/iroh-diap-test.js +71 -0
  175. package/dist/test/iroh-direct-connect.js +55 -0
  176. package/dist/test/iroh-e2e-fixed.js +89 -0
  177. package/dist/test/iroh-e2e-same-process.js +63 -0
  178. package/dist/test/iroh-e2e.js +66 -0
  179. package/dist/test/iroh-final-e2e.js +72 -0
  180. package/dist/test/iroh-relay-test.js +37 -0
  181. package/dist/test/iroh-simple-test.js +41 -0
  182. package/dist/test/iroh-transport-verify.js +54 -0
  183. package/dist/test/iroh-transport.test.js +37 -0
  184. package/dist/test/iroh-two-nodes.js +70 -0
  185. package/dist/test/iroh-verify.js +44 -0
  186. package/dist/test/judgment-decision.test.js +219 -0
  187. package/dist/test/llm-judgment-integration.test.js +220 -0
  188. package/dist/test/p2p-agent-complex-dialogue.js +385 -0
  189. package/dist/test/p2p-agent-dialogue.js +341 -0
  190. package/dist/test/p2p-agent-full-bidirectional.js +510 -0
  191. package/dist/test/p2p-agent-harness-flow.js +437 -0
  192. package/dist/test/p2p-agent-harness-single.js +143 -0
  193. package/dist/test/p2p-ai-dialogue-test.js +318 -0
  194. package/dist/test/p2p-cid-connect-test.js +195 -0
  195. package/dist/test/p2p-connect-receiver.js +69 -0
  196. package/dist/test/p2p-doc-transfer.js +110 -0
  197. package/dist/test/p2p-identity-page-test.js +77 -0
  198. package/dist/test/p2p-iroh-test.js +171 -0
  199. package/dist/test/p2p-minimal-test.js +241 -0
  200. package/dist/test/p2p-node-1.js +148 -0
  201. package/dist/test/p2p-node-2.js +148 -0
  202. package/dist/test/p2p-server.js +281 -0
  203. package/dist/test/p2p-two-nodes-test.js +438 -0
  204. package/dist/test/pi-sdk.test.js +44 -0
  205. package/dist/test/set-persona.js +40 -0
  206. package/dist/test/simple.test.js +9 -0
  207. package/dist/test/storage-integration.test.js +150 -0
  208. package/dist/test/subagent-manager.test.js +276 -0
  209. package/dist/test/test-gate-flow.test.js +81 -0
  210. package/dist/test/workflow-engine.test.js +87 -0
  211. package/dist/test/workflow-pivot-loop.test.js +246 -0
  212. package/dist/web/api-config.html +520 -520
  213. package/dist/web/client.js +2917 -3791
  214. package/dist/web/components/p2p/index.js +234 -276
  215. package/dist/web/components/wallet-viem.mjs +118 -118
  216. package/dist/web/index.html +374 -373
  217. package/dist/web/manifest.json +20 -20
  218. package/dist/web/server.js +23 -775
  219. package/dist/web/style.css +4737 -4737
  220. package/dist/web/ui/message-renderer.js +373 -451
  221. package/dist/web/ui/step-timeline.js +255 -351
  222. package/package.json +160 -160
  223. package/scripts/build-cli.js +215 -215
  224. package/scripts/build-web.ts +125 -125
  225. package/scripts/postinstall.js +152 -152
  226. package/bin/bolloon-daemon.sh +0 -207
  227. package/bin/ipfs +0 -0
  228. package/dist/agents/chat-segmenter.js +0 -298
  229. package/dist/agents/judgment-protocol.js +0 -479
  230. package/dist/agents/parse-tool-call.js +0 -304
  231. package/dist/agents/peer-manifest-loader.js +0 -210
  232. package/dist/agents/react-loop.js +0 -120
  233. package/dist/agents/session-store.js +0 -171
  234. package/dist/agents/tool-registry.js +0 -136
  235. package/dist/bootstrap/chat-archiver.js +0 -276
  236. package/dist/bootstrap/memory-compressor.js +0 -170
  237. package/dist/bootstrap/persona-loader.js +0 -94
  238. package/dist/electron/config.js +0 -16
  239. package/dist/electron/config.js.map +0 -1
  240. package/dist/electron/dialogs.js +0 -71
  241. package/dist/electron/dialogs.js.map +0 -1
  242. package/dist/electron/first-run.js +0 -129
  243. package/dist/electron/first-run.js.map +0 -1
  244. package/dist/electron/ipc.js +0 -16
  245. package/dist/electron/ipc.js.map +0 -1
  246. package/dist/electron/logger.js +0 -77
  247. package/dist/electron/logger.js.map +0 -1
  248. package/dist/electron/main.js +0 -60
  249. package/dist/electron/main.js.map +0 -1
  250. package/dist/electron/menu.js +0 -140
  251. package/dist/electron/menu.js.map +0 -1
  252. package/dist/electron/paths.js +0 -33
  253. package/dist/electron/paths.js.map +0 -1
  254. package/dist/electron/server.js +0 -74
  255. package/dist/electron/server.js.map +0 -1
  256. package/dist/electron/tray.js +0 -73
  257. package/dist/electron/tray.js.map +0 -1
  258. package/dist/electron/window.js +0 -69
  259. package/dist/electron/window.js.map +0 -1
  260. package/dist/network/local-inbox-bus.js +0 -73
  261. package/dist/network/p2p-outbox.js +0 -161
  262. package/dist/network/peer-fs.js +0 -420
  263. package/dist/network/peer-resource-bridge.js +0 -216
  264. package/dist/web/components/p2p/P2PModal.js +0 -188
  265. package/dist/web/components/p2p/p2p-modal.js +0 -664
  266. package/dist/web/components/p2p/p2p-tools.js +0 -248
  267. package/dist/web/input-validator.js +0 -103
  268. package/dist/web/util/safe-name.js +0 -32
@@ -1,483 +1,405 @@
1
- /**
2
- * message-renderer.ts — 对话显示 UI (2026-06-15 从 client.js 拆出, .js → .ts)
3
- *
4
- * 职责 (单一):
5
- * - 把 user / AI 内容渲染成 DOM 气泡 (含 marked.parse 渲染)
6
- * - 处理流式 token 累积 (textNode 增量, O(1), 不重排)
7
- * - finalize 流式消息为正式 AI 气泡
8
- * - 折叠 think / environment_details 块
9
- * - 复制 / 重新回答 / 蒸馏为判断 按钮
10
- * - 挂载 step-timeline (2026-06-15) 步骤状态条到每条 AI 消息内
11
- *
12
- * 状态 (本模块私有):
13
- * - streamingMessageEl / streamingTextNode / streamingText
14
- * - lastUserCommand / lastAiContent (去重)
15
- *
16
- * 依赖 (import):
17
- * - 浏览器 API (document, marked, fetch, navigator)
18
- * - ./step-timeline (4 状态步骤条模块)
19
- *
20
- * 不依赖 (零业务 import, 防循环):
21
- * - 不 import client.js
22
- * - 不 import 任何业务模块
23
- *
24
- * 输入 (从 client.js 调用):
25
- * - addMessage(content, type, save, container, usedIds, ctx)
26
- * - handleStreamTokenEvent({ streamType, content }, ctx)
27
- * - finalizeTimelineAsMessage(ctx)
28
- * - handleStepEvent({ type: 'step_start'|'step_done'|'step_error', ... }, ctx)
29
- * - escapeHtml(s)
30
- * - getMessagesContainerForCurrent(currentChannelId, messagesContainers, messagesEl)
31
- *
32
- * 输出 (DOM):
33
- * - .message / .bubble / .think-container / .env-container
34
- * - .message-streaming (流式期间, finalize 时移除)
35
- * - .message-actions (复制/重新回答/蒸馏按钮)
36
- * - .used-judgments-link (P0.5 反向引用)
37
- * - .step-timeline (气泡内步骤状态条, 由 ./step-timeline 渲染)
38
- */
39
- // ---------------------------------------------------------------------------
40
- // 类型定义
41
- // ---------------------------------------------------------------------------
42
- import { mountStepTimeline, pushStepToTimeline, migrateStepTimeline, getStepTimeline, } from './step-timeline.js';
43
- // 2026-07-01 (v0.2.6): 共享后端切 LLM 输出. 消除 <invoke>/<function_calls>/<tool_call>
44
- // 等各种 LLM 格式在前端气泡里出现的 bug.
45
- import { segmentChatReply } from '../../agents/chat-segmenter.js';
46
- // ---------------------------------------------------------------------------
47
- // 模块私有状态
48
- // ---------------------------------------------------------------------------
1
+ import {
2
+ mountStepTimeline,
3
+ pushStepToTimeline,
4
+ migrateStepTimeline,
5
+ getStepTimeline
6
+ } from "./step-timeline.js";
7
+ import { segmentChatReply } from "../../agents/chat-segmenter.js";
49
8
  let streamingMessageEl = null;
50
9
  let streamingTextNode = null;
51
- let streamingText = '';
52
- let lastUserCommand = '';
53
- let lastAiContent = '';
54
- /**
55
- * 2026-06-17: 流式状态查询 — 客户端用它在收到 server `ai` 事件时判断是否需要跳过 addMessage,
56
- * 避免和后续 `done` → finalizeTimelineAsMessage 产生双气泡.
57
- * 流式过程中 (streamingText > 0) server 推 `ai(content=fullResponse)` 时跳过,
58
- * 真正渲染走 `done` 触发的 finalizeTimelineAsMessage.
59
- */
60
- export function hasStreamingText() {
61
- return streamingText.length > 0;
10
+ let streamingText = "";
11
+ let lastUserCommand = "";
12
+ let lastAiContent = "";
13
+ function hasStreamingText() {
14
+ return streamingText.length > 0;
15
+ }
16
+ function replaceStreamingText(fullContent) {
17
+ if (!streamingTextNode || !streamingMessageEl) {
18
+ return;
19
+ }
20
+ streamingTextNode.nodeValue = String(fullContent || "");
21
+ streamingText = String(fullContent || "");
22
+ }
23
+ function injectRecoveredText(partialText, ctx = { messagesEl: null, messagesContainers: /* @__PURE__ */ new Map(), currentChannelId: null }) {
24
+ if (streamingMessageEl && streamingTextNode) {
25
+ streamingTextNode.nodeValue = String(partialText || "");
26
+ streamingText = String(partialText || "");
27
+ return;
28
+ }
29
+ handleStreamTokenEvent(
30
+ {
31
+ type: "token",
32
+ streamType: "token",
33
+ content: String(partialText || ""),
34
+ delta: String(partialText || "")
35
+ },
36
+ ctx
37
+ );
62
38
  }
63
- // 滚动限频 (60ms 16fps, 减 reflow)
64
39
  let scrollToBottomTimer = null;
65
40
  function scheduleScrollToBottom(container) {
66
- if (!container)
67
- return;
68
- if (scrollToBottomTimer)
69
- return;
70
- scrollToBottomTimer = setTimeout(() => {
71
- container.scrollTop = container.scrollHeight;
72
- scrollToBottomTimer = null;
73
- }, 60);
41
+ if (!container) return;
42
+ if (scrollToBottomTimer) return;
43
+ scrollToBottomTimer = setTimeout(() => {
44
+ container.scrollTop = container.scrollHeight;
45
+ scrollToBottomTimer = null;
46
+ }, 60);
74
47
  }
75
- // ---------------------------------------------------------------------------
76
- // 容器选择 (主入口或 per-channel 容器)
77
- // ---------------------------------------------------------------------------
78
- export function getMessagesContainerForCurrent(currentChannelId, messagesContainers, messagesEl) {
79
- if (currentChannelId && messagesContainers.get(currentChannelId)) {
80
- return messagesContainers.get(currentChannelId) || null;
81
- }
82
- return messagesEl;
48
+ function getMessagesContainerForCurrent(currentChannelId, messagesContainers, messagesEl) {
49
+ if (currentChannelId && messagesContainers.get(currentChannelId)) {
50
+ return messagesContainers.get(currentChannelId) || null;
51
+ }
52
+ return messagesEl;
83
53
  }
84
- // ---------------------------------------------------------------------------
85
- // HTML escape ( think / env 折叠块用)
86
- // ---------------------------------------------------------------------------
87
- export function escapeHtml(s) {
88
- return String(s ?? '').replace(/[&<>"']/g, (c) => ({
89
- '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;'
90
- }[c]));
54
+ function escapeHtml(s) {
55
+ return String(s ?? "").replace(/[&<>"']/g, (c) => ({
56
+ "&": "&amp;",
57
+ "<": "&lt;",
58
+ ">": "&gt;",
59
+ '"': "&quot;",
60
+ "'": "&#39;"
61
+ })[c]);
91
62
  }
92
- // ---------------------------------------------------------------------------
93
- // 主入口: 渲染一条消息气泡 (user / ai / system)
94
- // ---------------------------------------------------------------------------
95
- export function addMessage(content, type, save = true, container, usedJudgmentIds = [], ctx = { messagesEl: null, messagesContainers: new Map(), currentChannelId: null }) {
96
- const messagesEl = ctx.messagesEl || (typeof document !== 'undefined' ? document.getElementById('messages') : null);
97
- const messagesContainers = ctx.messagesContainers || new Map();
98
- const currentChannelId = ctx.currentChannelId;
99
- const msgContainer = container || getMessagesContainerForCurrent(currentChannelId, messagesContainers, messagesEl);
100
- // 内存保护: 单个 channel 容器超过 200 条, 旧会话加载时淘汰最旧
101
- if (!save && msgContainer && msgContainer.children.length > 200) {
102
- const toRemove = msgContainer.children.length - 200;
103
- for (let i = 0; i < toRemove; i++) {
104
- const first = msgContainer.firstElementChild;
105
- if (first)
106
- msgContainer.removeChild(first);
107
- }
108
- }
109
- // 去重 (save=true 时)
110
- if (save) {
111
- const lastContent = type === 'user' ? lastUserCommand : lastAiContent;
112
- if (lastContent && content === lastContent) {
113
- console.log(`[addMessage] 跳过重复的 ${type} 消息`);
114
- return;
115
- }
116
- if (type === 'user')
117
- lastUserCommand = content;
118
- else
119
- lastAiContent = content;
63
+ function addMessage(content, type, save = true, container, usedJudgmentIds = [], ctx = { messagesEl: null, messagesContainers: /* @__PURE__ */ new Map(), currentChannelId: null }) {
64
+ const messagesEl = ctx.messagesEl || (typeof document !== "undefined" ? document.getElementById("messages") : null);
65
+ const messagesContainers = ctx.messagesContainers || /* @__PURE__ */ new Map();
66
+ const currentChannelId = ctx.currentChannelId;
67
+ const msgContainer = container || getMessagesContainerForCurrent(currentChannelId, messagesContainers, messagesEl);
68
+ if (!save && msgContainer && msgContainer.children.length > 200) {
69
+ const toRemove = msgContainer.children.length - 200;
70
+ for (let i = 0; i < toRemove; i++) {
71
+ const first = msgContainer.firstElementChild;
72
+ if (first) msgContainer.removeChild(first);
120
73
  }
121
- const div = document.createElement('div');
122
- div.className = `message message-${type}`;
123
- // 2026-07-01 (v0.2.6 前后端分离): 改用 chat-segmenter 纯函数切 LLM 输出.
124
- // 之前 8 行正则漏 minimax <invoke> / Qwen function_calls / <tool_call> / {tool:..}.
125
- // 单一来源: src/agents/chat-segmenter.ts (server + client 共享).
126
- // knownToolNames: 用 ctx 传入的注册表, fallback 空集 (不识别 tool_call 就 strip 不显示).
127
- const knownToolNames = (ctx && ctx.knownToolNames) || new Set();
128
- const segments = segmentChatReply(content, { knownToolNames });
129
- // 没有可显示的 segment, 不上屏
130
- if (segments.length === 0) {
131
- return;
74
+ }
75
+ if (save) {
76
+ const lastContent = type === "user" ? lastUserCommand : lastAiContent;
77
+ if (lastContent && content === lastContent) {
78
+ console.log(`[addMessage] \u8DF3\u8FC7\u91CD\u590D\u7684 ${type} \u6D88\u606F`);
79
+ return;
132
80
  }
133
- // 渲染各 segment (按 type 走不同容器)
134
- let thinkContainer = null;
135
- let renderedAny = false;
136
- for (const seg of segments) {
137
- if (seg.type === 'think' && seg.content) {
138
- thinkContainer = buildThinkContainer(seg.content);
139
- div.appendChild(thinkContainer);
140
- renderedAny = true;
141
- }
142
- else if (seg.type === 'env_details' && seg.content) {
143
- div.appendChild(buildEnvContainer(seg.content));
144
- renderedAny = true;
145
- }
146
- else if (seg.type === 'text' && seg.content) {
147
- if (thinkContainer)
148
- div.appendChild(thinkContainer);
149
- thinkContainer = null;
150
- div.appendChild(buildBubble(seg.content, type));
151
- renderedAny = true;
152
- }
153
- else if (seg.type === 'final' && seg.content) {
154
- if (thinkContainer)
155
- div.appendChild(thinkContainer);
156
- thinkContainer = null;
157
- // final 段渲染为特殊气泡 (顶部有标记, 表示 LLM 显式终止)
158
- const finalEl = buildBubble(seg.content, type);
159
- finalEl.classList.add('bubble-final');
160
- div.appendChild(finalEl);
161
- renderedAny = true;
162
- }
163
- else if (seg.type === 'tool_call' && seg.tool) {
164
- // tool_call segment 不渲染文字 — 走 step-timeline (步骤状态条)
165
- // 这里只记录到 ctx 让外层在 addMessage 后挂到 timeline
166
- if (ctx && ctx.toolCallCallback) {
167
- ctx.toolCallCallback(seg.tool, div);
168
- }
169
- renderedAny = true; // 即使没文字, tool_call 也算"有意义"
170
- }
81
+ if (type === "user") lastUserCommand = content;
82
+ else lastAiContent = content;
83
+ }
84
+ const div = document.createElement("div");
85
+ div.className = `message message-${type}`;
86
+ let cleanContent = content;
87
+ if (type === "ai") {
88
+ cleanContent = cleanContent.replace(/<think>[\s\S]*?<\/think>/g, "");
89
+ const finalGenIdx = cleanContent.indexOf("<final gen>");
90
+ if (finalGenIdx >= 0) {
91
+ cleanContent = cleanContent.substring(0, finalGenIdx).trim();
171
92
  }
172
- if (!renderedAny) {
173
- return; // 没有渲染任何东西, 不上屏
174
- }
175
- // 纯文本用于复制按钮 — 现在从 segments 拼, 不再依赖 cleanContent 变量
176
- const rawContent = segments
177
- .filter(s => s.type === 'text' || s.type === 'final')
178
- .map(s => s.content || '')
179
- .join('\n');
180
- // 时间
181
- const time = document.createElement('div');
182
- time.className = 'time';
183
- time.textContent = new Date().toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' });
184
- // AI 消息操作按钮
185
- if (type === 'ai') {
186
- div.appendChild(buildMessageActions(div, rawContent, ctx));
187
- }
188
- // P0.5 反向引用
189
- if (type === 'ai' && Array.isArray(usedJudgmentIds) && usedJudgmentIds.length > 0) {
190
- const link = document.createElement('a');
191
- link.className = 'used-judgments-link';
192
- link.textContent = `📎 参考 ${usedJudgmentIds.length} 条原则`;
193
- link.onclick = (e) => {
194
- e.preventDefault();
195
- if (typeof ctx.openJudgmentsModalWithFilter === 'function') {
196
- ctx.openJudgmentsModalWithFilter(usedJudgmentIds);
197
- }
198
- };
199
- div.appendChild(link);
200
- }
201
- // 2026-06-15: 每条 AI 消息挂一个空 step-timeline 占位 (用户决策)
202
- // 后续 step_start/step_done 事件会通过 handleStepEvent 推入
203
- if (type === 'ai' && msgContainer) {
204
- mountStepTimeline(div, currentChannelId);
205
- }
206
- div.appendChild(time);
207
- if (msgContainer) {
208
- msgContainer.appendChild(div);
209
- scheduleScrollToBottom(msgContainer);
93
+ }
94
+ const knownToolNames = ctx && ctx.knownToolNames || /* @__PURE__ */ new Set();
95
+ const segments = segmentChatReply(cleanContent, { knownToolNames });
96
+ if (segments.length === 0) {
97
+ return;
98
+ }
99
+ let thinkContainer = null;
100
+ let renderedAny = false;
101
+ for (const seg of segments) {
102
+ if (seg.type === "think" && seg.content) {
103
+ thinkContainer = buildThinkContainer(seg.content);
104
+ div.appendChild(thinkContainer);
105
+ renderedAny = true;
106
+ } else if (seg.type === "env_details" && seg.content) {
107
+ div.appendChild(buildEnvContainer(seg.content));
108
+ renderedAny = true;
109
+ } else if (seg.type === "text" && seg.content) {
110
+ if (thinkContainer) div.appendChild(thinkContainer);
111
+ thinkContainer = null;
112
+ div.appendChild(buildBubble(seg.content, type));
113
+ renderedAny = true;
114
+ } else if (seg.type === "final" && seg.content) {
115
+ if (thinkContainer) div.appendChild(thinkContainer);
116
+ thinkContainer = null;
117
+ const finalEl = buildBubble(seg.content, type);
118
+ finalEl.classList.add("bubble-final");
119
+ div.appendChild(finalEl);
120
+ renderedAny = true;
121
+ } else if (seg.type === "tool_call" && seg.tool) {
122
+ if (ctx && ctx.toolCallCallback) {
123
+ ctx.toolCallCallback(seg.tool, div);
124
+ }
125
+ renderedAny = true;
210
126
  }
127
+ }
128
+ if (!renderedAny) {
129
+ return;
130
+ }
131
+ const rawContent = segments.filter((s) => s.type === "text" || s.type === "final").map((s) => s.content || "").join("\n");
132
+ const time = document.createElement("div");
133
+ time.className = "time";
134
+ time.textContent = (/* @__PURE__ */ new Date()).toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit" });
135
+ if (type === "ai") {
136
+ div.appendChild(buildMessageActions(div, rawContent, ctx));
137
+ }
138
+ if (type === "ai" && Array.isArray(usedJudgmentIds) && usedJudgmentIds.length > 0) {
139
+ const link = document.createElement("a");
140
+ link.className = "used-judgments-link";
141
+ link.textContent = `\u{1F4CE} \u53C2\u8003 ${usedJudgmentIds.length} \u6761\u539F\u5219`;
142
+ link.onclick = (e) => {
143
+ e.preventDefault();
144
+ if (typeof ctx.openJudgmentsModalWithFilter === "function") {
145
+ ctx.openJudgmentsModalWithFilter(usedJudgmentIds);
146
+ }
147
+ };
148
+ div.appendChild(link);
149
+ }
150
+ if (type === "ai" && msgContainer) {
151
+ mountStepTimeline(div, currentChannelId);
152
+ }
153
+ div.appendChild(time);
154
+ if (msgContainer) {
155
+ msgContainer.appendChild(div);
156
+ scheduleScrollToBottom(msgContainer);
157
+ }
211
158
  }
212
- // ---------------------------------------------------------------------------
213
- // 私有: think 折叠块
214
- // ---------------------------------------------------------------------------
215
159
  function buildThinkContainer(thinkContent) {
216
- const container = document.createElement('div');
217
- container.className = 'think-container';
218
- const toggle = document.createElement('div');
219
- toggle.className = 'think-toggle';
220
- toggle.innerHTML = '💭 思考过程 <span class="think-arrow">▸</span>';
221
- toggle.onclick = function () {
222
- const details = container.querySelector('.think-content');
223
- const arrow = toggle.querySelector('.think-arrow');
224
- if (!details || !arrow)
225
- return;
226
- if (details.style.display === 'none') {
227
- details.style.display = 'block';
228
- arrow.textContent = '▾';
229
- }
230
- else {
231
- details.style.display = 'none';
232
- arrow.textContent = '▸';
233
- }
234
- };
235
- const content = document.createElement('div');
236
- content.className = 'think-content';
237
- content.style.display = 'none';
238
- content.innerHTML = `<pre>${escapeHtml(thinkContent)}</pre>`;
239
- container.appendChild(toggle);
240
- container.appendChild(content);
241
- return container;
160
+ const container = document.createElement("div");
161
+ container.className = "think-container";
162
+ const toggle = document.createElement("div");
163
+ toggle.className = "think-toggle";
164
+ toggle.innerHTML = '\u{1F4AD} \u601D\u8003\u8FC7\u7A0B <span class="think-arrow">\u25B8</span>';
165
+ toggle.onclick = function() {
166
+ const details = container.querySelector(".think-content");
167
+ const arrow = toggle.querySelector(".think-arrow");
168
+ if (!details || !arrow) return;
169
+ if (details.style.display === "none") {
170
+ details.style.display = "block";
171
+ arrow.textContent = "\u25BE";
172
+ } else {
173
+ details.style.display = "none";
174
+ arrow.textContent = "\u25B8";
175
+ }
176
+ };
177
+ const content = document.createElement("div");
178
+ content.className = "think-content";
179
+ content.style.display = "none";
180
+ content.innerHTML = `<pre>${escapeHtml(thinkContent)}</pre>`;
181
+ container.appendChild(toggle);
182
+ container.appendChild(content);
183
+ return container;
242
184
  }
243
- // ---------------------------------------------------------------------------
244
- // 私有: environment_details 折叠块
245
- // ---------------------------------------------------------------------------
246
185
  function buildEnvContainer(envDetails) {
247
- const container = document.createElement('div');
248
- container.className = 'env-container';
249
- const toggle = document.createElement('div');
250
- toggle.className = 'env-toggle';
251
- toggle.innerHTML = '⚙️ 环境信息 <span class="env-arrow">▸</span>';
252
- toggle.onclick = function () {
253
- const details = container.querySelector('.environment-details');
254
- const arrow = toggle.querySelector('.env-arrow');
255
- if (!details || !arrow)
256
- return;
257
- if (details.style.display === 'none') {
258
- details.style.display = 'block';
259
- arrow.textContent = '▾';
260
- }
261
- else {
262
- details.style.display = 'none';
263
- arrow.textContent = '▸';
264
- }
265
- };
266
- const content = document.createElement('div');
267
- content.className = 'environment-details';
268
- content.style.display = 'none';
269
- content.innerHTML = `<pre>${escapeHtml(envDetails)}</pre>`;
270
- container.appendChild(toggle);
271
- container.appendChild(content);
272
- return container;
186
+ const container = document.createElement("div");
187
+ container.className = "env-container";
188
+ const toggle = document.createElement("div");
189
+ toggle.className = "env-toggle";
190
+ toggle.innerHTML = '\u2699\uFE0F \u73AF\u5883\u4FE1\u606F <span class="env-arrow">\u25B8</span>';
191
+ toggle.onclick = function() {
192
+ const details = container.querySelector(".environment-details");
193
+ const arrow = toggle.querySelector(".env-arrow");
194
+ if (!details || !arrow) return;
195
+ if (details.style.display === "none") {
196
+ details.style.display = "block";
197
+ arrow.textContent = "\u25BE";
198
+ } else {
199
+ details.style.display = "none";
200
+ arrow.textContent = "\u25B8";
201
+ }
202
+ };
203
+ const content = document.createElement("div");
204
+ content.className = "environment-details";
205
+ content.style.display = "none";
206
+ content.innerHTML = `<pre>${escapeHtml(envDetails)}</pre>`;
207
+ container.appendChild(toggle);
208
+ container.appendChild(content);
209
+ return container;
273
210
  }
274
- // ---------------------------------------------------------------------------
275
- // 私有: 气泡 (marked.parse 渲染)
276
- // ---------------------------------------------------------------------------
277
211
  function buildBubble(text, type) {
278
- const bubble = document.createElement('div');
279
- bubble.className = `bubble bubble-${type}`;
280
- // 安全降级: CDN 加载失败时 marked 是 escape 版本, 这里不需要二次 escape
281
- // window.marked ts 类型上不一定有, any 兜底
282
- const marked = window.marked;
283
- bubble.innerHTML = marked ? marked.parse(text) : escapeHtml(text);
284
- return bubble;
212
+ const bubble = document.createElement("div");
213
+ bubble.className = `bubble bubble-${type}`;
214
+ const marked = window.marked;
215
+ bubble.innerHTML = marked ? marked.parse(text) : escapeHtml(text);
216
+ return bubble;
285
217
  }
286
- // ---------------------------------------------------------------------------
287
- // 私有: 消息操作按钮 (复制 / 重新回答 / 蒸馏为判断)
288
- // ---------------------------------------------------------------------------
289
218
  function buildMessageActions(div, rawContent, ctx) {
290
- const actions = document.createElement('div');
291
- actions.className = 'message-actions';
292
- // 复制
293
- const copyBtn = document.createElement('button');
294
- copyBtn.className = 'action-btn copy-btn';
295
- copyBtn.innerHTML = copyIcon() + ' 复制';
296
- copyBtn.title = '复制消息';
297
- copyBtn.onclick = () => {
298
- navigator.clipboard.writeText(rawContent).then(() => {
299
- copyBtn.innerHTML = checkIcon() + ' 已复制';
300
- setTimeout(() => { copyBtn.innerHTML = copyIcon() + ' 复制'; }, 2000);
301
- });
302
- };
303
- actions.appendChild(copyBtn);
304
- // 蒸馏为判断
305
- const saveJudgmentBtn = document.createElement('button');
306
- saveJudgmentBtn.className = 'action-btn save-as-judgment';
307
- saveJudgmentBtn.title = 'AI 蒸馏为 30-80 字判断力 + 自动演化对齐';
308
- saveJudgmentBtn.setAttribute('data-decision', rawContent.substring(0, 800));
309
- if (ctx.currentChannelId)
310
- saveJudgmentBtn.setAttribute('data-channel-id', ctx.currentChannelId);
311
- saveJudgmentBtn.innerHTML = shieldIcon() + ' 蒸馏为判断';
312
- actions.appendChild(saveJudgmentBtn);
313
- // 重新回答
314
- const regenBtn = document.createElement('button');
315
- regenBtn.className = 'action-btn regenerate-btn';
316
- regenBtn.innerHTML = refreshIcon(false) + ' 重新回答';
317
- regenBtn.title = '重新生成回复';
318
- regenBtn.onclick = () => {
319
- regenBtn.innerHTML = refreshIcon(true) + ' 生成中...';
320
- regenBtn.disabled = true;
321
- const messages = div.parentElement?.querySelectorAll('.message') || [];
322
- let lastUserMsg = '';
323
- for (let i = messages.length - 1; i >= 0; i--) {
324
- const msg = messages[i];
325
- if (msg.classList.contains('message-user')) {
326
- const bubble = msg.querySelector('.bubble');
327
- if (bubble) {
328
- lastUserMsg = bubble.textContent || bubble.innerText || '';
329
- break;
330
- }
331
- }
219
+ const actions = document.createElement("div");
220
+ actions.className = "message-actions";
221
+ const copyBtn = document.createElement("button");
222
+ copyBtn.className = "action-btn copy-btn";
223
+ copyBtn.innerHTML = copyIcon() + " \u590D\u5236";
224
+ copyBtn.title = "\u590D\u5236\u6D88\u606F";
225
+ copyBtn.onclick = () => {
226
+ navigator.clipboard.writeText(rawContent).then(() => {
227
+ copyBtn.innerHTML = checkIcon() + " \u5DF2\u590D\u5236";
228
+ setTimeout(() => {
229
+ copyBtn.innerHTML = copyIcon() + " \u590D\u5236";
230
+ }, 2e3);
231
+ });
232
+ };
233
+ actions.appendChild(copyBtn);
234
+ const saveJudgmentBtn = document.createElement("button");
235
+ saveJudgmentBtn.className = "action-btn save-as-judgment";
236
+ saveJudgmentBtn.title = "AI \u84B8\u998F\u4E3A 30-80 \u5B57\u5224\u65AD\u529B + \u81EA\u52A8\u6F14\u5316\u5BF9\u9F50";
237
+ saveJudgmentBtn.setAttribute("data-decision", rawContent.substring(0, 800));
238
+ if (ctx.currentChannelId) saveJudgmentBtn.setAttribute("data-channel-id", ctx.currentChannelId);
239
+ saveJudgmentBtn.innerHTML = shieldIcon() + " \u84B8\u998F\u4E3A\u5224\u65AD";
240
+ actions.appendChild(saveJudgmentBtn);
241
+ const regenBtn = document.createElement("button");
242
+ regenBtn.className = "action-btn regenerate-btn";
243
+ regenBtn.innerHTML = refreshIcon(false) + " \u91CD\u65B0\u56DE\u7B54";
244
+ regenBtn.title = "\u91CD\u65B0\u751F\u6210\u56DE\u590D";
245
+ regenBtn.onclick = () => {
246
+ regenBtn.innerHTML = refreshIcon(true) + " \u751F\u6210\u4E2D...";
247
+ regenBtn.disabled = true;
248
+ const messages = div.parentElement?.querySelectorAll(".message") || [];
249
+ let lastUserMsg = "";
250
+ for (let i = messages.length - 1; i >= 0; i--) {
251
+ const msg = messages[i];
252
+ if (msg.classList.contains("message-user")) {
253
+ const bubble = msg.querySelector(".bubble");
254
+ if (bubble) {
255
+ lastUserMsg = bubble.textContent || bubble.innerText || "";
256
+ break;
332
257
  }
333
- fetch('/regenerate', {
334
- method: 'POST',
335
- headers: { 'Content-Type': 'application/json' },
336
- body: JSON.stringify({ channelId: ctx.currentChannelId, userMessage: lastUserMsg })
337
- }).then(res => {
338
- if (!res.ok)
339
- throw new Error('regenerate failed');
340
- }).catch(err => {
341
- console.error('重新生成失败:', err);
342
- regenBtn.innerHTML = refreshIcon(false) + ' 失败';
343
- setTimeout(() => {
344
- regenBtn.innerHTML = refreshIcon(false) + ' 重新回答';
345
- regenBtn.disabled = false;
346
- }, 2000);
347
- });
348
- };
349
- actions.appendChild(regenBtn);
350
- return actions;
258
+ }
259
+ }
260
+ fetch("/regenerate", {
261
+ method: "POST",
262
+ headers: { "Content-Type": "application/json" },
263
+ body: JSON.stringify({ channelId: ctx.currentChannelId, userMessage: lastUserMsg })
264
+ }).then((res) => {
265
+ if (!res.ok) throw new Error("regenerate failed");
266
+ }).catch((err) => {
267
+ console.error("\u91CD\u65B0\u751F\u6210\u5931\u8D25:", err);
268
+ regenBtn.innerHTML = refreshIcon(false) + " \u5931\u8D25";
269
+ setTimeout(() => {
270
+ regenBtn.innerHTML = refreshIcon(false) + " \u91CD\u65B0\u56DE\u7B54";
271
+ regenBtn.disabled = false;
272
+ }, 2e3);
273
+ });
274
+ };
275
+ actions.appendChild(regenBtn);
276
+ return actions;
351
277
  }
352
- function copyIcon() { return '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>'; }
353
- function checkIcon() { return '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"></polyline></svg>'; }
354
- function shieldIcon() { return '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2L4 6v6c0 5 3.5 9.5 8 10 4.5-.5 8-5 8-10V6l-8-4z"></path><path d="M9 12l2 2 4-4"></path></svg>'; }
355
- function refreshIcon(spin = false) { const cls = spin ? ' class="spin"' : ''; return `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"${cls}><path d="M21 2v6h-6M3 12a9 9 0 0 1 15-6.7L21 8M3 22v-6h6M21 12a9 9 0 0 1-15 6.7L3 16"></path></svg>`; }
356
- // ---------------------------------------------------------------------------
357
- // 流式 token 处理 (textNode 增量, 不重排)
358
- // ---------------------------------------------------------------------------
359
- export function handleStreamTokenEvent(data, ctx = { messagesEl: null, messagesContainers: new Map(), currentChannelId: null }) {
360
- const messagesEl = ctx.messagesEl || (typeof document !== 'undefined' ? document.getElementById('messages') : null);
361
- const messagesContainers = ctx.messagesContainers || new Map();
362
- const currentChannelId = ctx.currentChannelId;
363
- const container = getMessagesContainerForCurrent(currentChannelId, messagesContainers, messagesEl);
364
- if (!container)
365
- return;
366
- const delta = data.content || '';
367
- if (!delta)
368
- return;
369
- if (!streamingMessageEl || !streamingMessageEl.isConnected) {
370
- // 新建流式消息
371
- streamingMessageEl = document.createElement('div');
372
- streamingMessageEl.className = 'message message-ai message-streaming';
373
- streamingTextNode = document.createTextNode('');
374
- streamingMessageEl.appendChild(streamingTextNode);
375
- streamingText = '';
376
- // 2026-06-15: 流式期间也挂一个空 step-timeline 占位 — step_start/done/error 事件
377
- // 走 handleStepEvent, getStepTimeline 找到这个流式元素内的 timeline 推入
378
- mountStepTimeline(streamingMessageEl, currentChannelId);
379
- container.appendChild(streamingMessageEl);
380
- // B-3: 首个 token 来时切状态到 streaming (蓝徽), 提示用户 panel 在工作
381
- if (typeof ctx.setTimelineState === 'function') {
382
- ctx.setTimelineState('streaming');
383
- }
384
- scheduleScrollToBottom(container);
278
+ function copyIcon() {
279
+ return '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>';
280
+ }
281
+ function checkIcon() {
282
+ return '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"></polyline></svg>';
283
+ }
284
+ function shieldIcon() {
285
+ return '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2L4 6v6c0 5 3.5 9.5 8 10 4.5-.5 8-5 8-10V6l-8-4z"></path><path d="M9 12l2 2 4-4"></path></svg>';
286
+ }
287
+ function refreshIcon(spin = false) {
288
+ const cls = spin ? ' class="spin"' : "";
289
+ return `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"${cls}><path d="M21 2v6h-6M3 12a9 9 0 0 1 15-6.7L21 8M3 22v-6h6M21 12a9 9 0 0 1-15 6.7L3 16"></path></svg>`;
290
+ }
291
+ function handleStreamTokenEvent(data, ctx = { messagesEl: null, messagesContainers: /* @__PURE__ */ new Map(), currentChannelId: null }) {
292
+ const messagesEl = ctx.messagesEl || (typeof document !== "undefined" ? document.getElementById("messages") : null);
293
+ const messagesContainers = ctx.messagesContainers || /* @__PURE__ */ new Map();
294
+ const currentChannelId = ctx.currentChannelId;
295
+ const container = getMessagesContainerForCurrent(currentChannelId, messagesContainers, messagesEl);
296
+ if (!container) return;
297
+ const delta = data.content || "";
298
+ if (!delta) return;
299
+ if (!streamingMessageEl || !streamingMessageEl.isConnected) {
300
+ streamingMessageEl = document.createElement("div");
301
+ streamingMessageEl.className = "message message-ai message-streaming";
302
+ streamingTextNode = document.createTextNode("");
303
+ streamingMessageEl.appendChild(streamingTextNode);
304
+ streamingText = "";
305
+ mountStepTimeline(streamingMessageEl, currentChannelId);
306
+ container.appendChild(streamingMessageEl);
307
+ if (typeof ctx.setTimelineState === "function") {
308
+ ctx.setTimelineState("streaming");
385
309
  }
386
- if (streamingTextNode)
387
- streamingTextNode.appendData(delta);
388
- streamingText += delta;
389
310
  scheduleScrollToBottom(container);
311
+ }
312
+ if (data.streamType === "token") {
313
+ if (streamingTextNode) streamingTextNode.nodeValue = delta;
314
+ streamingText = delta;
315
+ } else {
316
+ if (streamingTextNode) streamingTextNode.appendData(delta);
317
+ streamingText += delta;
318
+ }
319
+ scheduleScrollToBottom(container);
390
320
  }
391
- // ---------------------------------------------------------------------------
392
- // 流式消息 finalize (done 事件): 移除流式元素, addMessage marked.parse
393
- // ---------------------------------------------------------------------------
394
- export function finalizeTimelineAsMessage(ctx = { messagesEl: null, messagesContainers: new Map(), currentChannelId: null }) {
395
- const messagesEl = ctx.messagesEl || (typeof document !== 'undefined' ? document.getElementById('messages') : null);
396
- const messagesContainers = ctx.messagesContainers || new Map();
397
- const currentChannelId = ctx.currentChannelId;
398
- const container = getMessagesContainerForCurrent(currentChannelId, messagesContainers, messagesEl);
399
- if (streamingText.trim().length > 0) {
400
- // 2026-06-15: finalize 时把 streaming 内的 step-timeline 整体搬到新建的正式 AI message 内
401
- // addMessage 会建一个新 timeline 占位, 先记下 streaming 的引用, addMessage 后再迁移
402
- // 避免节点从 0 重渲 (10+ 步的任务, 重渲闪烁会很厉害)
403
- const oldStreamingEl = streamingMessageEl;
404
- if (oldStreamingEl && oldStreamingEl.parentNode) {
405
- oldStreamingEl.parentNode.removeChild(oldStreamingEl);
406
- }
407
- addMessage(streamingText, 'ai', true, container, ctx.lastUsedJudgmentIds || [], ctx);
408
- if (oldStreamingEl && container) {
409
- // 找刚 addMessage 创建的最后一条 ai message
410
- const newAiMsg = container.querySelector('.message-ai:last-of-type');
411
- if (newAiMsg && newAiMsg !== oldStreamingEl) {
412
- migrateStepTimeline(oldStreamingEl, newAiMsg);
413
- }
414
- }
321
+ function finalizeTimelineAsMessage(ctx = { messagesEl: null, messagesContainers: /* @__PURE__ */ new Map(), currentChannelId: null }) {
322
+ const messagesEl = ctx.messagesEl || (typeof document !== "undefined" ? document.getElementById("messages") : null);
323
+ const messagesContainers = ctx.messagesContainers || /* @__PURE__ */ new Map();
324
+ const currentChannelId = ctx.currentChannelId;
325
+ const container = getMessagesContainerForCurrent(currentChannelId, messagesContainers, messagesEl);
326
+ if (streamingText.trim().length > 0) {
327
+ const oldStreamingEl = streamingMessageEl;
328
+ if (oldStreamingEl && oldStreamingEl.parentNode) {
329
+ oldStreamingEl.parentNode.removeChild(oldStreamingEl);
415
330
  }
416
- // 重置流式状态
417
- streamingMessageEl = null;
418
- streamingTextNode = null;
419
- streamingText = '';
420
- // B-3: finalize 时切 done 状态 (绿徽), hide 延迟由 client.js 的 hideTimelinePanel 统一管
421
- if (typeof ctx.setTimelineState === 'function') {
422
- ctx.setTimelineState('done');
331
+ addMessage(streamingText, "ai", true, container, ctx.lastUsedJudgmentIds || [], ctx);
332
+ if (oldStreamingEl && container) {
333
+ const newAiMsg = container.querySelector(".message-ai:last-of-type");
334
+ if (newAiMsg && newAiMsg !== oldStreamingEl) {
335
+ migrateStepTimeline(oldStreamingEl, newAiMsg);
336
+ }
423
337
  }
338
+ }
339
+ streamingMessageEl = null;
340
+ streamingTextNode = null;
341
+ streamingText = "";
342
+ if (typeof ctx.setTimelineState === "function") {
343
+ ctx.setTimelineState("done");
344
+ }
424
345
  }
425
- export function handleStepEvent(data, ctx = { messagesEl: null, messagesContainers: new Map(), currentChannelId: null }) {
426
- const messagesEl = ctx.messagesEl || (typeof document !== 'undefined' ? document.getElementById('messages') : null);
427
- const messagesContainers = ctx.messagesContainers || new Map();
428
- const currentChannelId = ctx.currentChannelId;
429
- const container = getMessagesContainerForCurrent(currentChannelId, messagesContainers, messagesEl);
430
- if (!container)
431
- return;
432
- if (!data || !data.type)
433
- return;
434
- // 1. 优先用正在流式的元素 (流式期间能即时看到)
435
- let target = streamingMessageEl && streamingMessageEl.isConnected
436
- ? streamingMessageEl
437
- : null;
438
- // 2. 否则用最后一条 AI message
439
- if (!target) {
440
- const aiMsgs = container.querySelectorAll('.message-ai');
441
- if (aiMsgs.length === 0)
442
- return;
443
- target = aiMsgs[aiMsgs.length - 1];
444
- }
445
- if (!target)
446
- return;
447
- const timeline = getStepTimeline(target);
448
- if (!timeline)
449
- return;
450
- pushStepToTimeline(timeline, data.type, {
451
- tool: data.tool || 'unknown',
452
- args: data.args,
453
- success: data.success,
454
- output: data.output,
455
- error: data.error,
456
- });
346
+ function handleStepEvent(data, ctx = { messagesEl: null, messagesContainers: /* @__PURE__ */ new Map(), currentChannelId: null }) {
347
+ const messagesEl = ctx.messagesEl || (typeof document !== "undefined" ? document.getElementById("messages") : null);
348
+ const messagesContainers = ctx.messagesContainers || /* @__PURE__ */ new Map();
349
+ const currentChannelId = ctx.currentChannelId;
350
+ const container = getMessagesContainerForCurrent(currentChannelId, messagesContainers, messagesEl);
351
+ if (!container) return;
352
+ if (!data || !data.type) return;
353
+ let target = streamingMessageEl && streamingMessageEl.isConnected ? streamingMessageEl : null;
354
+ if (!target) {
355
+ const aiMsgs = container.querySelectorAll(".message-ai");
356
+ if (aiMsgs.length === 0) return;
357
+ target = aiMsgs[aiMsgs.length - 1];
358
+ }
359
+ if (!target) return;
360
+ const timeline = getStepTimeline(target);
361
+ if (!timeline) return;
362
+ pushStepToTimeline(timeline, data.type, {
363
+ tool: data.tool || "unknown",
364
+ args: data.args,
365
+ success: data.success,
366
+ output: data.output,
367
+ error: data.error
368
+ });
457
369
  }
458
- // ---------------------------------------------------------------------------
459
- // 重置模块状态 (切频道时调用)
460
- // ---------------------------------------------------------------------------
461
- export function resetRendererState() {
462
- streamingMessageEl = null;
463
- streamingTextNode = null;
464
- streamingText = '';
465
- lastUserCommand = '';
466
- lastAiContent = '';
467
- if (scrollToBottomTimer) {
468
- clearTimeout(scrollToBottomTimer);
469
- scrollToBottomTimer = null;
470
- }
370
+ function resetRendererState() {
371
+ streamingMessageEl = null;
372
+ streamingTextNode = null;
373
+ streamingText = "";
374
+ lastUserCommand = "";
375
+ lastAiContent = "";
376
+ if (scrollToBottomTimer) {
377
+ clearTimeout(scrollToBottomTimer);
378
+ scrollToBottomTimer = null;
379
+ }
471
380
  }
472
- export const MessageRenderer = {
473
- addMessage,
474
- handleStreamTokenEvent,
475
- finalizeTimelineAsMessage,
476
- handleStepEvent,
477
- escapeHtml,
478
- getMessagesContainerForCurrent,
479
- resetRendererState,
381
+ const MessageRenderer = {
382
+ addMessage,
383
+ handleStreamTokenEvent,
384
+ finalizeTimelineAsMessage,
385
+ handleStepEvent,
386
+ escapeHtml,
387
+ getMessagesContainerForCurrent,
388
+ resetRendererState
480
389
  };
481
- if (typeof window !== 'undefined') {
482
- window.MR = MessageRenderer;
390
+ if (typeof window !== "undefined") {
391
+ window.MR = MessageRenderer;
483
392
  }
393
+ export {
394
+ MessageRenderer,
395
+ addMessage,
396
+ escapeHtml,
397
+ finalizeTimelineAsMessage,
398
+ getMessagesContainerForCurrent,
399
+ handleStepEvent,
400
+ handleStreamTokenEvent,
401
+ hasStreamingText,
402
+ injectRecoveredText,
403
+ replaceStreamingText,
404
+ resetRendererState
405
+ };