tagword-codeagent 1.1.6__py3-none-any.whl

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 (173) hide show
  1. codeagent/__init__.py +10 -0
  2. codeagent/__main__.py +13 -0
  3. codeagent/cli/__init__.py +13 -0
  4. codeagent/cli/entrypoint.py +161 -0
  5. codeagent/cli/main.py +979 -0
  6. codeagent/core/__init__.py +2 -0
  7. codeagent/core/attachments.py +342 -0
  8. codeagent/core/audio_models.py +50 -0
  9. codeagent/core/bootstrap.py +178 -0
  10. codeagent/core/default_presets.py +159 -0
  11. codeagent/core/env.py +106 -0
  12. codeagent/core/folder_picker.py +151 -0
  13. codeagent/core/image_understanding.py +90 -0
  14. codeagent/core/models.py +6 -0
  15. codeagent/core/paths.py +238 -0
  16. codeagent/core/pricing.py +107 -0
  17. codeagent/core/process_ports.py +258 -0
  18. codeagent/core/seed_bridge.py +21 -0
  19. codeagent/core/settings.py +98 -0
  20. codeagent/core/speech_synth.py +330 -0
  21. codeagent/core/team_config.py +247 -0
  22. codeagent/core/team_manager.py +245 -0
  23. codeagent/core/token_counter.py +41 -0
  24. codeagent/core/tts_voices.py +103 -0
  25. codeagent/core/usage_billing.py +72 -0
  26. codeagent/core/vision_models.py +93 -0
  27. codeagent/memory/diary.py +144 -0
  28. codeagent/persona_defaults/README.md +12 -0
  29. codeagent/persona_defaults/agent.md +352 -0
  30. codeagent/persona_defaults/identity.md +29 -0
  31. codeagent/persona_defaults/memory.md +4 -0
  32. codeagent/persona_defaults/skills.md +325 -0
  33. codeagent/persona_defaults/soul.md +38 -0
  34. codeagent/persona_defaults/tools.md +41 -0
  35. codeagent/persona_defaults/user.md +18 -0
  36. codeagent/runtime/__init__.py +11 -0
  37. codeagent/runtime/agent_context.py +6 -0
  38. codeagent/runtime/agent_runtime.py +21 -0
  39. codeagent/runtime/compact_prompt.py +9 -0
  40. codeagent/runtime/compact_state.py +40 -0
  41. codeagent/runtime/llm_worker.py +132 -0
  42. codeagent/runtime/orchestrator.py +161 -0
  43. codeagent/runtime/prompt_enrichment.py +324 -0
  44. codeagent/runtime/session_manager.py +30 -0
  45. codeagent/runtime/task_split.py +38 -0
  46. codeagent/runtime/turn_loop.py +13 -0
  47. codeagent/runtime/worker.py +48 -0
  48. codeagent/server/__init__.py +505 -0
  49. codeagent/server/app_factory.py +1292 -0
  50. codeagent/server/attachment_api.py +261 -0
  51. codeagent/server/hub.py +175 -0
  52. codeagent/server/icon.png +0 -0
  53. codeagent/server/self_healing.py +143 -0
  54. codeagent/server/team_engine.py +243 -0
  55. codeagent/server/tts_api.py +51 -0
  56. codeagent/server/webui_api_app.py +3180 -0
  57. codeagent/skills/__init__.py +2 -0
  58. codeagent/skills/select.py +144 -0
  59. codeagent/tools/__init__.py +6 -0
  60. codeagent/tools/agent_tools.py +10 -0
  61. codeagent/web/__init__.py +1 -0
  62. codeagent/web/__pycache__/__init__.cpython-313.pyc +0 -0
  63. codeagent/web/__pycache__/app.cpython-313.pyc +0 -0
  64. codeagent/web/__pycache__/auth_impl.cpython-313.pyc +0 -0
  65. codeagent/web/auth_impl.py +326 -0
  66. codeagent/web/static/00-marked.min.js +6 -0
  67. codeagent/web/static/00-mermaid.min.js +2314 -0
  68. codeagent/web/static/00-purify.min.js +3 -0
  69. codeagent/web/static/00-storage.js +83 -0
  70. codeagent/web/static/00-utils.js +51 -0
  71. codeagent/web/static/01-theme.css +96 -0
  72. codeagent/web/static/01a-scroll.js +86 -0
  73. codeagent/web/static/01b-think.js +62 -0
  74. codeagent/web/static/01c-session-identity.js +81 -0
  75. codeagent/web/static/01c-session-thinking.js +61 -0
  76. codeagent/web/static/01c-session-tree-menus.js +349 -0
  77. codeagent/web/static/01c-session-tree.js +531 -0
  78. codeagent/web/static/01c-session-utilities.js +83 -0
  79. codeagent/web/static/01c-vision-model.js +132 -0
  80. codeagent/web/static/01d-image-gen-model.js +84 -0
  81. codeagent/web/static/01e-audio-model.js +97 -0
  82. codeagent/web/static/01f-compose-ui.js +88 -0
  83. codeagent/web/static/01g-tts-settings.js +128 -0
  84. codeagent/web/static/01h-music-model.js +85 -0
  85. codeagent/web/static/01i-model-stack.js +291 -0
  86. codeagent/web/static/01j-video-gen-model.js +85 -0
  87. codeagent/web/static/01k-model-stack-bootstrap.js +66 -0
  88. codeagent/web/static/01k-model-stack-state.js +209 -0
  89. codeagent/web/static/01q-inflight.js +195 -0
  90. codeagent/web/static/02-reset.css +22 -0
  91. codeagent/web/static/02-session-ui.js +201 -0
  92. codeagent/web/static/03-layout-p1.css +2 -0
  93. codeagent/web/static/03-layout-p2.css +2 -0
  94. codeagent/web/static/03a-links-stream.js +88 -0
  95. codeagent/web/static/03b-stream-text.js +119 -0
  96. codeagent/web/static/03c-render-bubble.js +520 -0
  97. codeagent/web/static/03d-agent-bubble.js +126 -0
  98. codeagent/web/static/03e-tool-trace.js +182 -0
  99. codeagent/web/static/03f-timeline.js +54 -0
  100. codeagent/web/static/03g-bubble-tts.js +292 -0
  101. codeagent/web/static/04-components-p1.css +1 -0
  102. codeagent/web/static/04-components-p2b.css +1 -0
  103. codeagent/web/static/04-components-p3.css +2 -0
  104. codeagent/web/static/04-components-p4.css +1 -0
  105. codeagent/web/static/04-components-p5.css +1 -0
  106. codeagent/web/static/04-components-p6.css +58 -0
  107. codeagent/web/static/04-components-p7.css +4 -0
  108. codeagent/web/static/04-components-p8.css +1 -0
  109. codeagent/web/static/04-components-p9.css +1 -0
  110. codeagent/web/static/04-ws-connect.js +351 -0
  111. codeagent/web/static/04-ws-helpers.js +116 -0
  112. codeagent/web/static/05-chat-p1.css +1058 -0
  113. codeagent/web/static/05-chat-p2.css +1 -0
  114. codeagent/web/static/05-chat-p3.css +295 -0
  115. codeagent/web/static/05a-session-history.js +176 -0
  116. codeagent/web/static/05b-session-list.js +114 -0
  117. codeagent/web/static/05c-dialog.js +123 -0
  118. codeagent/web/static/05d-project-create.js +172 -0
  119. codeagent/web/static/05e-project-remote.js +143 -0
  120. codeagent/web/static/06-chat.js +552 -0
  121. codeagent/web/static/06-utilities.css +181 -0
  122. codeagent/web/static/06b-attachments.js +304 -0
  123. codeagent/web/static/06c-camera.js +488 -0
  124. codeagent/web/static/07-activity-bar.css +127 -0
  125. codeagent/web/static/09-git-p1.css +1 -0
  126. codeagent/web/static/09-git-p2.css +1 -0
  127. codeagent/web/static/09a-init-bind.js +110 -0
  128. codeagent/web/static/09b-panel-toggle.js +80 -0
  129. codeagent/web/static/09c-api-refresh.js +100 -0
  130. codeagent/web/static/09d-status-log.js +80 -0
  131. codeagent/web/static/09e-commit.js +80 -0
  132. codeagent/web/static/09f-remote.js +93 -0
  133. codeagent/web/static/11a-cron-parse.js +117 -0
  134. codeagent/web/static/11b-cron-panel.js +100 -0
  135. codeagent/web/static/11c-cron-card.js +133 -0
  136. codeagent/web/static/11d-cron-form.js +105 -0
  137. codeagent/web/static/11e-env-config.js +168 -0
  138. codeagent/web/static/11f-env-llm-a-state.js +136 -0
  139. codeagent/web/static/11f-env-llm-b-form.js +409 -0
  140. codeagent/web/static/11f-env-llm-c-board.js +322 -0
  141. codeagent/web/static/11g-env-paths-git.js +973 -0
  142. codeagent/web/static/11h-env-mcp-a-state.js +161 -0
  143. codeagent/web/static/11h-env-mcp-b-form.js +229 -0
  144. codeagent/web/static/11h-env-mcp-c-board.js +565 -0
  145. codeagent/web/static/12-plugins.js +127 -0
  146. codeagent/web/static/13a-skills.js +156 -0
  147. codeagent/web/static/13a-todo-utils.js +140 -0
  148. codeagent/web/static/13b-todo-fetch.js +106 -0
  149. codeagent/web/static/13b-tools.js +127 -0
  150. codeagent/web/static/13c-todo-crud.js +92 -0
  151. codeagent/web/static/14-activity-bar.js +177 -0
  152. codeagent/web/static/15-plans.css +194 -0
  153. codeagent/web/static/15-plans.js +219 -0
  154. codeagent/web/static/16-mobile.css +612 -0
  155. codeagent/web/static/16-mobile.js +178 -0
  156. codeagent/web/static/17-preset-ux.css +287 -0
  157. codeagent/web/static/18-agent-mgr.css +616 -0
  158. codeagent/web/static/18-agent-mgr.js +204 -0
  159. codeagent/web/static/24-health-dashboard.js +68 -0
  160. codeagent/web/static/99-brand-polish.css +152 -0
  161. codeagent/web/static/body.html +824 -0
  162. codeagent/web/static/setup/setup_form.html +171 -0
  163. codeagent/web/static/setup/setup_head.html +100 -0
  164. codeagent/web/static/setup/setup_tail.html +264 -0
  165. codeagent/web/static/setup/setup_test.html +38 -0
  166. codeagent/web/web_login.html +265 -0
  167. codeagent/web/webui.html +35 -0
  168. tagword_codeagent-1.1.6.dist-info/METADATA +273 -0
  169. tagword_codeagent-1.1.6.dist-info/RECORD +173 -0
  170. tagword_codeagent-1.1.6.dist-info/WHEEL +5 -0
  171. tagword_codeagent-1.1.6.dist-info/entry_points.txt +3 -0
  172. tagword_codeagent-1.1.6.dist-info/licenses/LICENSE +21 -0
  173. tagword_codeagent-1.1.6.dist-info/top_level.txt +1 -0
@@ -0,0 +1,520 @@
1
+ function attachmentApiUrl(attachmentId) {
2
+ const id = String(attachmentId || '').trim();
3
+ if (!id) return '';
4
+ const sid = typeof sessionId !== 'undefined' ? sessionId : 'web-chat';
5
+ const aid = typeof agentId !== 'undefined' ? agentId : 'default';
6
+ return '/api/attachments/' + encodeURIComponent(id)
7
+ + '?session_id=' + encodeURIComponent(sid)
8
+ + '&agent_id=' + encodeURIComponent(aid);
9
+ }
10
+
11
+ /** 将 Markdown / 正文中的 attachment: 引用改为可加载的附件 URL */
12
+ function rewriteAttachmentRefsInMarkdown(raw) {
13
+ let t = String(raw || '');
14
+ const toUrl = function(id) { return attachmentApiUrl(id); };
15
+
16
+ // ![alt](attachment:id) 或 ![alt](attachment:id filename)
17
+ t = t.replace(
18
+ /!\[([^\]]*)\]\(attachment:([^)\s]+)(?:\s+[^)]*)?\)/gi,
19
+ function(_, alt, id) { return '![' + alt + '](' + toUrl(id) + ')'; }
20
+ );
21
+ // [text](attachment:id)
22
+ t = t.replace(
23
+ /\[([^\]]+)\]\(attachment:([^)\s]+)(?:\s+[^)]*)?\)/gi,
24
+ function(_, text, id) { return '[' + text + '](' + toUrl(id) + ')'; }
25
+ );
26
+ // 裸 [attachment:id filename] → 内联图片(与 vision 提示格式一致)
27
+ t = t.replace(
28
+ /\[attachment:([^\s\]]+)(?:\s+([^\]]+))?\]/gi,
29
+ function(_, id, fn) {
30
+ const alt = (fn || id || 'image').trim();
31
+ return '\n\n![' + alt + '](' + toUrl(id) + ')\n\n';
32
+ }
33
+ );
34
+ // 模型误写的绝对 URL(如 https://i0.hitpt.com/api/attachments/...)→ 同源附件路径
35
+ t = t.replace(
36
+ /https?:\/\/[^/]+\/api\/attachments\/([^?\s)"']+)(\?[^)\s"']*)?/gi,
37
+ function(_, id, _q) { return toUrl(id); }
38
+ );
39
+ // 本地文件系统绝对路径(如 /home/u2/xxx/tmp/file.png)→ 通过 /api/file-serve 代理加载
40
+ // 匹配 ![alt](/absolute/path/file.ext) 形式的图片引用
41
+ t = t.replace(
42
+ /!\[([^\]]*)\]\(\/([^)]+\.\w+)\)/gi,
43
+ function(_, alt, rawPath) {
44
+ var path = rawPath.split('?')[0];
45
+ if (!path.startsWith('home/u2/')) return _;
46
+ return '![' + alt + '](/api/file-serve?path=' + encodeURIComponent('/' + path) + ')';
47
+ }
48
+ );
49
+ return t;
50
+ }
51
+
52
+ function markChatInlineImage(img) {
53
+ if (!img || !img.classList) return;
54
+ if (!img.classList.contains('chat-inline-img')) img.classList.add('chat-inline-img');
55
+ }
56
+
57
+ function chatImageDownloadName(img) {
58
+ const alt = String((img && img.alt) || '').trim();
59
+ if (alt && alt !== 'generated' && alt !== 'image') {
60
+ return alt.replace(/[^\w.\-\u4e00-\u9fff]+/g, '_').slice(0, 120) || 'image.png';
61
+ }
62
+ const src = String((img && img.src) || '');
63
+ const m = src.match(/\/api\/attachments\/([^/?#]+)/);
64
+ const base = m ? m[1] : 'image';
65
+ return /\.[a-z0-9]{2,5}$/i.test(base) ? base : base + '.png';
66
+ }
67
+
68
+ var _chatImageLightbox = null;
69
+
70
+ function ensureChatImageLightbox() {
71
+ if (_chatImageLightbox) return _chatImageLightbox;
72
+ const root = document.createElement('div');
73
+ root.id = 'chatImageLightbox';
74
+ root.className = 'chat-img-lightbox';
75
+ root.setAttribute('aria-hidden', 'true');
76
+ root.innerHTML =
77
+ '<div class="chat-img-lightbox__backdrop" data-close="1"></div>' +
78
+ '<div class="chat-img-lightbox__panel" role="dialog" aria-modal="true" aria-label="图片预览">' +
79
+ ' <div class="chat-img-lightbox__stage">' +
80
+ ' <img class="chat-img-lightbox__img" alt=""/>' +
81
+ ' <div class="chat-img-lightbox__actions">' +
82
+ ' <button type="button" class="chat-img-btn chat-img-lightbox__close" title="关闭" aria-label="关闭">&times;</button>' +
83
+ ' <a class="chat-img-btn chat-img-lightbox__dl" href="#" download title="下载" aria-label="下载">↓</a>' +
84
+ ' </div>' +
85
+ ' </div>' +
86
+ '</div>';
87
+ document.body.appendChild(root);
88
+ function closeLb() {
89
+ root.classList.remove('is-open');
90
+ root.setAttribute('aria-hidden', 'true');
91
+ document.body.classList.remove('chat-img-lightbox-open');
92
+ }
93
+ root.querySelector('.chat-img-lightbox__backdrop').addEventListener('click', closeLb);
94
+ root.querySelector('.chat-img-lightbox__close').addEventListener('click', closeLb);
95
+ document.addEventListener('keydown', function(ev) {
96
+ if (ev.key === 'Escape' && root.classList.contains('is-open')) closeLb();
97
+ });
98
+ _chatImageLightbox = { root: root, close: closeLb };
99
+ return _chatImageLightbox;
100
+ }
101
+
102
+ function openChatImageLightbox(src, downloadName) {
103
+ const url = String(src || '').trim();
104
+ if (!url) return;
105
+ const lb = ensureChatImageLightbox();
106
+ const img = lb.root.querySelector('.chat-img-lightbox__img');
107
+ const dl = lb.root.querySelector('.chat-img-lightbox__dl');
108
+ img.src = url;
109
+ img.alt = downloadName || 'image';
110
+ if (dl) {
111
+ dl.href = url;
112
+ dl.download = downloadName || 'image.png';
113
+ }
114
+ lb.root.classList.add('is-open');
115
+ lb.root.setAttribute('aria-hidden', 'false');
116
+ document.body.classList.add('chat-img-lightbox-open');
117
+ }
118
+
119
+ function wrapChatImage(img) {
120
+ if (!img || !img.parentNode || img.closest('.chat-img-wrap')) return;
121
+ markChatInlineImage(img);
122
+ const wrap = document.createElement('div');
123
+ wrap.className = 'chat-img-wrap';
124
+ const parent = img.parentNode;
125
+ parent.insertBefore(wrap, img);
126
+ wrap.appendChild(img);
127
+
128
+ const actions = document.createElement('div');
129
+ actions.className = 'chat-img-wrap__actions';
130
+
131
+ const zoomBtn = document.createElement('button');
132
+ zoomBtn.type = 'button';
133
+ zoomBtn.className = 'chat-img-btn chat-img-btn--zoom';
134
+ zoomBtn.title = '放大';
135
+ zoomBtn.setAttribute('aria-label', '放大');
136
+ zoomBtn.textContent = '⤢';
137
+
138
+ const dlName = chatImageDownloadName(img);
139
+ const dlBtn = document.createElement('a');
140
+ dlBtn.className = 'chat-img-btn chat-img-btn--dl';
141
+ dlBtn.href = img.src || '#';
142
+ dlBtn.download = dlName;
143
+ dlBtn.title = '下载';
144
+ dlBtn.setAttribute('aria-label', '下载');
145
+ dlBtn.textContent = '↓';
146
+
147
+ function openPreview(ev) {
148
+ if (ev) { ev.preventDefault(); ev.stopPropagation(); }
149
+ const src = img.currentSrc || img.src;
150
+ if (!src) return;
151
+ openChatImageLightbox(src, dlName);
152
+ }
153
+
154
+ zoomBtn.addEventListener('click', openPreview);
155
+ img.addEventListener('click', openPreview);
156
+ img.setAttribute('role', 'button');
157
+ img.tabIndex = 0;
158
+ img.addEventListener('keydown', function(ev) {
159
+ if (ev.key === 'Enter' || ev.key === ' ') openPreview(ev);
160
+ });
161
+
162
+ actions.appendChild(zoomBtn);
163
+ actions.appendChild(dlBtn);
164
+ wrap.appendChild(actions);
165
+ }
166
+
167
+ /** 为会话内图片增加放大 / 下载控件 */
168
+ function enhanceChatImagesInBubble(root) {
169
+ if (!root || !root.querySelectorAll) return;
170
+ const sel = 'img.chat-inline-img, .bubble .md-content img, .oa-tool-gen-images img, .bubble-user__img';
171
+ root.querySelectorAll(sel).forEach(function(img) {
172
+ if (!img.src || img.closest('.chat-img-wrap')) return;
173
+ wrapChatImage(img);
174
+ });
175
+ }
176
+
177
+ /** 修复已渲染 DOM 中残留的 attachment: / 缺 query 的附件链接 */
178
+ function hydrateAttachmentImagesInBubble(root) {
179
+ if (!root || !root.querySelectorAll) return;
180
+ root.querySelectorAll('img[src]').forEach(function(img) {
181
+ const src = String(img.getAttribute('src') || '');
182
+ const m = src.match(/^attachment:([^?\s#]+)/i);
183
+ if (m) {
184
+ markChatInlineImage(img);
185
+ img.src = attachmentApiUrl(m[1]);
186
+ return;
187
+ }
188
+ if (src.indexOf('/api/attachments/') >= 0) {
189
+ markChatInlineImage(img);
190
+ if (src.indexOf('session_id=') < 0) {
191
+ const path = src.split('?')[0];
192
+ img.src = attachmentApiUrl(path.replace(/^\/api\/attachments\//, ''));
193
+ }
194
+ return;
195
+ }
196
+ const ext = src.match(/https?:\/\/[^/]+\/api\/attachments\/([^?\s#]+)/i);
197
+ if (ext) {
198
+ markChatInlineImage(img);
199
+ img.src = attachmentApiUrl(ext[1]);
200
+ }
201
+ });
202
+ root.querySelectorAll('a[href^="attachment:"]').forEach(function(a) {
203
+ const m = String(a.getAttribute('href') || '').match(/^attachment:([^?\s#]+)/i);
204
+ if (m) a.setAttribute('href', attachmentApiUrl(m[1]));
205
+ });
206
+ enhanceChatImagesInBubble(root);
207
+ renderMermaidInBubble(root); // 异步渲染气泡中的 mermaid 图(fire-and-forget)
208
+ }
209
+
210
+ function renderMarkdown(raw) {
211
+ const t = rewriteAttachmentRefsInMarkdown(String(raw || ''));
212
+
213
+ // 先把 ```mermaid 块抽出来,替换成 <pre class="mermaid"> 占位符,
214
+ // 避免 marked/DOMPurify/code-block-wrap 增强链路误处理它
215
+ const mermaidBlocks = [];
216
+ const tWithPlaceholders = t.replace(/```mermaid\s*([\s\S]*?)```/g, function(_, code) {
217
+ const idx = mermaidBlocks.length;
218
+ mermaidBlocks.push(String(code || '').trim());
219
+ return '<p data-mermaid-idx="' + idx + '"></p>';
220
+ });
221
+
222
+ const plain = '<div class="md-content md-content--plain">' + escapeHtml(t) + '</div>';
223
+ if (typeof marked === 'undefined' || typeof DOMPurify === 'undefined') return plain;
224
+ try {
225
+ marked.setOptions({ breaks: true, gfm: true });
226
+ const html = marked.parse(tWithPlaceholders);
227
+ const clean = DOMPurify.sanitize(html);
228
+ // 给代码块添加复制按钮(浮动右上角)
229
+ var enhanced = clean;
230
+ enhanced = enhanced.replace(
231
+ /<pre><code(?:\s+class="language-(\w+)")?>/g,
232
+ function(match, lang) {
233
+ return '<div class="code-block-wrap"><button class="code-copy-btn" type="button" title="复制代码" aria-label="复制代码"><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg></button><pre><code' + (lang ? ' class="language-' + lang + '"' : '') + '>';
234
+ }
235
+ );
236
+ enhanced = enhanced.replace(/<\/code><\/pre>/g, '</code></pre></div>');
237
+ // 把占位 <p data-mermaid-idx="N"></p> 替换成真正的 <pre class="mermaid"> 节点
238
+ // (放在 code-block-wrap 增强之后,避开其正则)
239
+ enhanced = enhanced.replace(/<p data-mermaid-idx="(\d+)"><\/p>/g, function(_, idx) {
240
+ const i = Number(idx);
241
+ const code = mermaidBlocks[i] != null ? mermaidBlocks[i] : '';
242
+ return '<pre class="mermaid" data-mermaid-idx="' + i + '">' + escapeHtml(code) + '</pre>';
243
+ });
244
+ return '<div class="md-content">' + enhanced + '</div>';
245
+ } catch (_) { return plain; }
246
+ }
247
+
248
+ /**
249
+ * 把 mermaid 输出的 SVG 字符串转成 PNG data URL。
250
+ * - 用 canvas 绘制,便于在手机端/聊天软件/邮件里正常显示。
251
+ * - scale=2 输出 2 倍像素,高 DPI 屏更清晰。
252
+ * - 失败时返回 null(调用方应 fallback 到 SVG)。
253
+ */
254
+ async function svgToPngDataUrl(svgStr, scale) {
255
+ try {
256
+ const pixelRatio = (scale && scale > 0) ? scale : 2;
257
+ // 解析 viewBox 取宽高(mermaid 输出通常不带 width/height 属性)
258
+ const tmp = document.createElement('div');
259
+ tmp.innerHTML = svgStr;
260
+ const svgEl = tmp.querySelector('svg');
261
+ if (!svgEl) return null;
262
+ // 确保有 viewBox;无则用 getBBox 取(此时 svg 必须已挂载到 DOM,但我们后面会处理)
263
+ let vb = svgEl.getAttribute('viewBox');
264
+ let w = parseFloat(svgEl.getAttribute('width'));
265
+ let h = parseFloat(svgEl.getAttribute('height'));
266
+ if (!vb && svgEl.getBBox) {
267
+ try {
268
+ // getBBox 需要在 DOM 中,先挂到隐藏容器
269
+ tmp.style.position = 'absolute';
270
+ tmp.style.visibility = 'hidden';
271
+ document.body.appendChild(tmp);
272
+ const bb = svgEl.getBBox();
273
+ document.body.removeChild(tmp);
274
+ if (bb && bb.width > 0 && bb.height > 0) {
275
+ w = bb.width;
276
+ h = bb.height;
277
+ vb = '0 0 ' + w + ' ' + h;
278
+ svgEl.setAttribute('viewBox', vb);
279
+ } else {
280
+ document.body.removeChild(tmp);
281
+ return null;
282
+ }
283
+ } catch (_) {
284
+ if (tmp.parentNode) tmp.parentNode.removeChild(tmp);
285
+ return null;
286
+ }
287
+ }
288
+ if (!vb) {
289
+ // 都没有就放弃 PNG 转换
290
+ return null;
291
+ }
292
+ // 从 viewBox 解析尺寸
293
+ const vbParts = vb.split(/[\s,]+/).map(Number);
294
+ let vbW = vbParts[2], vbH = vbParts[3];
295
+ if (!vbW || !vbH) {
296
+ // 退回到 width/height 属性
297
+ if (!w || !h) return null;
298
+ vbW = w; vbH = h;
299
+ }
300
+ // 给 svg 加上显式 width/height(必须有,canvas 才能绘制)
301
+ svgEl.setAttribute('width', String(vbW));
302
+ svgEl.setAttribute('height', String(vbH));
303
+ // 套一层白色背景,避免 PNG 透明
304
+ const bgId = 'mermaid-bg-' + Date.now() + '-' + Math.random().toString(36).slice(2, 6);
305
+ const bgRect = '<rect x="0" y="0" width="100%" height="100%" fill="white"/>';
306
+ if (svgEl.querySelector('defs')) {
307
+ // 把背景 rect 注入到第一个非 defs 子元素之前
308
+ const firstNonDefs = Array.from(svgEl.children).find(function(c) {
309
+ return c.tagName && c.tagName.toLowerCase() !== 'defs' && c.tagName.toLowerCase() !== 'style';
310
+ });
311
+ if (firstNonDefs) {
312
+ firstNonDefs.insertAdjacentHTML('beforebegin', bgRect);
313
+ } else {
314
+ svgEl.insertAdjacentHTML('beforeend', bgRect);
315
+ }
316
+ } else {
317
+ svgEl.insertAdjacentHTML('afterbegin', '<defs></defs>' + bgRect);
318
+ }
319
+ // 序列化
320
+ const finalSvg = new XMLSerializer().serializeToString(svgEl);
321
+ const blob = new Blob([finalSvg], { type: 'image/svg+xml;charset=utf-8' });
322
+ const blobUrl = URL.createObjectURL(blob);
323
+
324
+ // 用 Image 加载 SVG(必须是 Blob URL 或 data URL,不能用 inline svg 串)
325
+ const img = new Image();
326
+ img.crossOrigin = 'anonymous';
327
+ const loaded = new Promise(function(resolve, reject) {
328
+ img.onload = function() { resolve(); };
329
+ img.onerror = function(ev) { reject(new Error('SVG image load failed')); };
330
+ });
331
+ img.src = blobUrl;
332
+ await loaded;
333
+
334
+ const canvas = document.createElement('canvas');
335
+ canvas.width = Math.ceil(vbW * pixelRatio);
336
+ canvas.height = Math.ceil(vbH * pixelRatio);
337
+ const ctx = canvas.getContext('2d');
338
+ if (!ctx) {
339
+ URL.revokeObjectURL(blobUrl);
340
+ return null;
341
+ }
342
+ // 先铺白底(部分浏览器 SVG 透明区域会留空)
343
+ ctx.fillStyle = '#ffffff';
344
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
345
+ ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
346
+ URL.revokeObjectURL(blobUrl);
347
+ return canvas.toDataURL('image/png');
348
+ } catch (_) {
349
+ return null;
350
+ }
351
+ }
352
+
353
+ /** 触发浏览器下载(兼容 data URL 或 Blob URL),用完自动 revoke */
354
+ function triggerDownload(href, fileName) {
355
+ try {
356
+ const a = document.createElement('a');
357
+ a.href = href;
358
+ a.download = fileName;
359
+ document.body.appendChild(a);
360
+ a.click();
361
+ document.body.removeChild(a);
362
+ // data URL 不需要 revoke;Blob URL 需 revoke(调用方负责)
363
+ } catch (_) {}
364
+ }
365
+
366
+ /**
367
+ * 扫描 root 内所有 <pre class="mermaid">,调用 mermaid.render() 转成 SVG。
368
+ * 必须等 DOM 渲染完成后调用(通常在 enhanceChatImagesInBubble 之后)。
369
+ * 失败时把节点替换为红框错误提示,不抛出。
370
+ */
371
+ async function renderMermaidInBubble(root) {
372
+ if (typeof window === 'undefined' || typeof window.mermaid === 'undefined') return;
373
+ if (!root || !root.querySelectorAll) return;
374
+ const nodes = root.querySelectorAll('pre.mermaid:not([data-mermaid-rendered])');
375
+ if (!nodes.length) return;
376
+
377
+ try {
378
+ window.mermaid.initialize({
379
+ startOnLoad: false,
380
+ securityLevel: 'loose',
381
+ theme: 'default',
382
+ fontFamily: 'inherit',
383
+ });
384
+ } catch (_) { /* already initialized */ }
385
+
386
+ for (const node of Array.from(nodes)) {
387
+ const code = node.textContent || '';
388
+ if (!code.trim()) continue;
389
+ node.setAttribute('data-mermaid-rendered', '1');
390
+ const id = 'mermaid-bubble-' + Date.now() + '-' + Math.random().toString(36).slice(2, 8);
391
+ try {
392
+ const result = await window.mermaid.render(id, code);
393
+ const svg = (result && result.svg) ? result.svg : '';
394
+ const wrap = document.createElement('div');
395
+ wrap.className = 'mermaid-host';
396
+ wrap.innerHTML = svg;
397
+ // 提取渲染产物的 svg 字符串(来自 wrap 内的 <svg>),便于下载/放大预览
398
+ const svgEl = wrap.querySelector('svg');
399
+ const svgStr = svgEl ? svgEl.outerHTML : svg;
400
+ // 文件名:使用 mermaid 第一个单词作为前缀,附时间戳
401
+ const firstWord = (code.match(/^\s*([A-Za-z][\w-]*)/) || [, 'diagram'])[1].toLowerCase();
402
+ const baseName = 'mermaid-' + firstWord + '-' + Date.now();
403
+ const svgFileName = baseName + '.svg';
404
+ const pngFileName = baseName + '.png';
405
+
406
+ // hover 时显示的右上角操作按钮(复用 .chat-img-btn-* 视觉风格)
407
+ const actions = document.createElement('div');
408
+ actions.className = 'mermaid-host__actions';
409
+
410
+ // 防止并发:用户连续点击时复用同一个转换 Promise
411
+ let pngPromise = null;
412
+ const getPng = function() {
413
+ if (!pngPromise) pngPromise = svgToPngDataUrl(svgStr, 2);
414
+ return pngPromise;
415
+ };
416
+
417
+ const zoomBtn = document.createElement('button');
418
+ zoomBtn.type = 'button';
419
+ zoomBtn.className = 'chat-img-btn chat-img-btn--zoom';
420
+ zoomBtn.title = '放大预览';
421
+ zoomBtn.setAttribute('aria-label', '放大预览');
422
+ zoomBtn.textContent = '⤢';
423
+ zoomBtn.addEventListener('click', async function(ev) {
424
+ ev.preventDefault();
425
+ ev.stopPropagation();
426
+ try {
427
+ const pngDataUrl = await getPng();
428
+ if (pngDataUrl) {
429
+ openChatImageLightbox(pngDataUrl, pngFileName);
430
+ } else {
431
+ // fallback:转换失败时用 SVG data URL
432
+ const svgDataUrl = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svgStr);
433
+ openChatImageLightbox(svgDataUrl, svgFileName);
434
+ }
435
+ } catch (_) {}
436
+ });
437
+
438
+ const dlBtn = document.createElement('a');
439
+ dlBtn.className = 'chat-img-btn chat-img-btn--dl';
440
+ dlBtn.href = '#';
441
+ dlBtn.download = pngFileName;
442
+ dlBtn.title = '下载 PNG';
443
+ dlBtn.setAttribute('aria-label', '下载 PNG');
444
+ dlBtn.textContent = '↓';
445
+ dlBtn.addEventListener('click', async function(ev) {
446
+ ev.preventDefault();
447
+ try {
448
+ const pngDataUrl = await getPng();
449
+ if (pngDataUrl) {
450
+ triggerDownload(pngDataUrl, pngFileName);
451
+ } else {
452
+ // fallback:PNG 转换失败 → 下载原始 SVG
453
+ const blob = new Blob([svgStr], { type: 'image/svg+xml;charset=utf-8' });
454
+ const url = URL.createObjectURL(blob);
455
+ triggerDownload(url, svgFileName);
456
+ setTimeout(function() { URL.revokeObjectURL(url); }, 1000);
457
+ }
458
+ } catch (_) {}
459
+ });
460
+
461
+ // svg 元素自身点击也触发放大
462
+ if (svgEl) {
463
+ svgEl.style.cursor = 'zoom-in';
464
+ svgEl.setAttribute('role', 'button');
465
+ svgEl.tabIndex = 0;
466
+ svgEl.addEventListener('click', function() { zoomBtn.click(); });
467
+ svgEl.addEventListener('keydown', function(ev) {
468
+ if (ev.key === 'Enter' || ev.key === ' ') { ev.preventDefault(); zoomBtn.click(); }
469
+ });
470
+ }
471
+
472
+ actions.appendChild(zoomBtn);
473
+ actions.appendChild(dlBtn);
474
+ wrap.appendChild(actions);
475
+
476
+ node.parentNode.replaceChild(wrap, node);
477
+ } catch (err) {
478
+ const msg = (err && err.message) ? err.message : String(err);
479
+ const errBox = document.createElement('div');
480
+ errBox.className = 'mermaid-error';
481
+ errBox.textContent = '⚠️ Mermaid 渲染失败:' + msg;
482
+ node.parentNode.replaceChild(errBox, node);
483
+ }
484
+ }
485
+ }
486
+
487
+ function stripOaToolOrphans(s) {
488
+ let t = String(s || '');
489
+ t = t.replace(/<\/\s*parameter\s*>/gi, '');
490
+ t = t.replace(/<\/\s*function\s*>/gi, '');
491
+ t = t.replace(/<\/\s*tool_call\s*>/gi, '');
492
+ t = t.replace(/<\s*tool_call\s*>/gi, '');
493
+ t = t.replace(/<\s*function\s*=\s*\w+\s*>/gi, '');
494
+ t = t.replace(/<\s*parameter\s*=\s*\w+\s*>/gi, '');
495
+ return t.replace(/\n{3,}/g, '\n\n').trim();
496
+ }
497
+
498
+ function extractOaToolCallBlocks(s) {
499
+ const blocks = [];
500
+ const re = /<tool_call>\s*[\s\S]*?<\/tool_call>/gi;
501
+ const text = String(s || '').replace(re, (m) => {
502
+ blocks.push(m);
503
+ return '\n\n';
504
+ });
505
+ return { text: text.replace(/\n{3,}/g, '\n\n').trim(), blocks: blocks };
506
+ }
507
+
508
+ function oaToolDetailsHtml(rawXml, summaryLabel) {
509
+ const lab = escapeHtml(summaryLabel || '内联工具调用 (XML)');
510
+ return '<details class="oa-tool-trace"><summary>' + lab + '</summary><pre class="oa-tool-trace-pre">' + escapeHtml(rawXml) + '</pre></details>';
511
+ }
512
+
513
+ function oaToolTraceRowHtml(row, index) {
514
+ const name = (row && (row.tool || row.name)) || 'tool';
515
+ const args = row && row.arguments != null ? String(row.arguments) : '';
516
+ const res = row && row.result != null ? String(row.result) : '';
517
+ const body = '参数:\n' + args + '\n\n输出:\n' + res;
518
+ const summary = '工具:' + name + (index > 0 ? ' (' + (index + 1) + ')' : '');
519
+ return '<details class="oa-tool-trace"><summary>' + escapeHtml(summary) + '</summary><pre class="oa-tool-trace-pre">' + escapeHtml(body) + '</pre></details>';
520
+ }
@@ -0,0 +1,126 @@
1
+ function buildAgentBubbleInnerHtml(text, toolTrace, reasoningContent) {
2
+ // 注意:toolTrace 参数不再内联渲染。工具链统一由 bubbleAgentWithSplitToolTrace
3
+ // 或 appendAgentToolTraceRowToLog 渲染为独立块(系统消息样式),不与文本气泡混合。
4
+ // 这样可以避免工具链在文本气泡内和独立块中重复展示两只。
5
+ // reasoningContent 不展现给用户,仅用于 LLM 上下文恢复。
6
+ let inner = '';
7
+ const ex = extractOaToolCallBlocks(stripOaToolOrphans(normReply(text)));
8
+ inner += renderMarkdown(ex.text);
9
+ ex.blocks.forEach((blk, i) => {
10
+ inner += oaToolDetailsHtml(blk, '内联工具调用' + (ex.blocks.length > 1 ? ' (' + (i + 1) + ')' : ''));
11
+ });
12
+ return inner;
13
+ }
14
+
15
+ function bubble(role, text, opts) {
16
+ opts = opts || {};
17
+ const at = opts.at;
18
+ // Skip empty/whitespace-only agent bubbles (tool-only assistant messages should only render their tool blocks)
19
+ if (role === 'agent' && !(text || '').trim()) {
20
+ // Still render tool blocks if present
21
+ const tt = Array.isArray(opts.toolTrace) ? opts.toolTrace : [];
22
+ if (tt.length) {
23
+ tt.forEach(function(row, i) {
24
+ appendAgentToolTraceRowToLog(row, i, tt.length, { prepend: !!opts.prepend, skipScroll: true });
25
+ });
26
+ }
27
+ if (!opts.skipScroll) scrollLog();
28
+ return;
29
+ }
30
+ const wrap = document.createElement('div');
31
+ wrap.className = 'msg-row msg-' + role;
32
+ const col = document.createElement('div');
33
+ col.className = 'msg-col';
34
+ const meta = document.createElement('div');
35
+ meta.className = 'msg-meta';
36
+ const label = role === 'user' ? '你' : '助手';
37
+ const ts = formatBubbleTime(at);
38
+ meta.textContent = ts ? label + ' · ' + ts : label;
39
+
40
+ // ── 存储原始消息索引(来自 session/history API 的 idx 字段)用于回滚 ──
41
+ var msgIdx = (opts && opts.idx != null) ? opts.idx : null;
42
+ if (msgIdx != null) wrap.setAttribute('data-msg-idx', String(msgIdx));
43
+
44
+ if (role === 'user') {
45
+ (function addRollbackBtn(mEl, wrapEl) {
46
+ var rb = document.createElement('span');
47
+ rb.className = 'rollback-btn';
48
+ rb.textContent = '↩ 回滚';
49
+ rb.title = '从这条消息之后重新开始对话';
50
+ rb.addEventListener('click', async function (e) {
51
+ e.stopPropagation();
52
+ var idx = wrapEl.getAttribute('data-msg-idx');
53
+ if (!idx) { alert('无法确定消息索引,请刷新页面后重试。'); return; }
54
+ if (confirm('确定从第 ' + idx + ' 条消息之后回滚?此后所有消息将被忽略,但不会删除。')) {
55
+ try {
56
+ var r = await fetch('/api/chat/rollback', {
57
+ method: 'POST',
58
+ headers: { 'Content-Type': 'application/json' },
59
+ credentials: 'same-origin',
60
+ body: JSON.stringify({
61
+ session_id: sessionId,
62
+ agent_id: agentId,
63
+ message_idx: parseInt(idx, 10)
64
+ })
65
+ });
66
+ var j = await r.json();
67
+ if (!r.ok) { alert('回滚失败:' + (j.detail || r.statusText)); return; }
68
+ // 清除现有 log,重新加载会话历史
69
+ if (typeof clearLog === 'function') clearLog();
70
+ if (typeof loadSessionHistoryIntoLog === 'function') {
71
+ await loadSessionHistoryIntoLog();
72
+ }
73
+ } catch (err) { alert('回滚请求失败:' + err.message); }
74
+ }
75
+ });
76
+ mEl.appendChild(document.createTextNode(' '));
77
+ mEl.appendChild(rb);
78
+ })(meta, wrap);
79
+ }
80
+ const b = document.createElement('div');
81
+ b.className = 'bubble bubble-' + role;
82
+ if (role === 'agent') b.innerHTML = buildAgentBubbleInnerHtml(text, [], null);
83
+ else {
84
+ b.innerHTML = renderMarkdown(normReply(text));
85
+ if (role === 'user') {
86
+ const atts = opts.attachments;
87
+ if (Array.isArray(atts) && atts.length && typeof renderUserAttachmentsInBubble === 'function') {
88
+ renderUserAttachmentsInBubble(b, atts);
89
+ } else if (Array.isArray(opts.attachmentIds) && opts.attachmentIds.length) {
90
+ renderUserAttachmentsInBubble(b, opts.attachmentIds.map(function(id) {
91
+ return { id: id, kind: 'image', filename: id };
92
+ }));
93
+ }
94
+ }
95
+ }
96
+ ensureLinksOpenNewTab(b);
97
+ col.appendChild(meta);
98
+ col.appendChild(b);
99
+ if (typeof appendBubbleTtsBar === 'function') {
100
+ appendBubbleTtsBar(col, text, { role: role });
101
+ }
102
+ wrap.appendChild(col);
103
+ if (opts.prepend && log.firstChild) {
104
+ log.insertBefore(wrap, log.firstChild);
105
+ } else {
106
+ log.appendChild(wrap);
107
+ }
108
+ if (!opts.prepend && !opts.skipScroll) scrollLog();
109
+ // 工具链渲染为独立块(不与文本气泡混合),避免重复展示
110
+ if (role === 'agent') {
111
+ const tt = Array.isArray(opts.toolTrace) ? opts.toolTrace : [];
112
+ if (opts.prepend) {
113
+ // 预置(加载旧历史)时:工具块应插入到文本气泡之后、原 log 最前内容之前
114
+ // 逐个 prepend 会反转顺序,故从后向前遍历以保持正序
115
+ for (var i = tt.length - 1; i >= 0; i--) {
116
+ appendAgentToolTraceRowToLog(tt[i], i, tt.length, { prepend: true, skipScroll: true });
117
+ }
118
+ } else {
119
+ tt.forEach(function(row, i) {
120
+ appendAgentToolTraceRowToLog(row, i, tt.length, { skipScroll: true });
121
+ });
122
+ }
123
+ }
124
+ }
125
+
126
+