@templmf/temp-solf-lmf 0.0.53 → 0.0.55

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 (87) hide show
  1. package/guanwang/README.md +95 -0
  2. package/guanwang/docs/changelog.md +145 -0
  3. package/guanwang/docs/doc-maintenance.md +229 -0
  4. package/guanwang/docs/product.md +181 -0
  5. package/guanwang/docs/test-cases.md +395 -0
  6. package/guanwang/docs/usage.md +291 -0
  7. package/guanwang/env.example +27 -0
  8. package/guanwang/index.html +13 -0
  9. package/guanwang/package-lock.json +3825 -0
  10. package/guanwang/package.json +32 -0
  11. package/guanwang/public/favicon.svg +4 -0
  12. package/guanwang/public/react-runtime/babel.min.js +4 -0
  13. package/guanwang/public/react-runtime/react-dom.min.js +267 -0
  14. package/guanwang/public/react-runtime/react.min.js +31 -0
  15. package/guanwang/public/vue-repl-assets/compiler-sfc.esm-browser.js +50795 -0
  16. package/guanwang/public/vue-repl-assets/runtime-dom.esm-browser.js +12758 -0
  17. package/guanwang/public/vue-repl-assets/server-renderer.esm-browser.js +8600 -0
  18. package/guanwang/public/vue-repl-assets/vue.esm-browser.js +18672 -0
  19. package/guanwang/src/App.vue +61 -0
  20. package/guanwang/src/chat-sdk/core/components/ChatBox.vue +305 -0
  21. package/guanwang/src/chat-sdk/core/components/ChatSidebar.vue +84 -0
  22. package/guanwang/src/chat-sdk/core/components/InputBar.vue +354 -0
  23. package/guanwang/src/chat-sdk/core/components/MessageBubble.vue +703 -0
  24. package/guanwang/src/chat-sdk/core/useTheme.js +31 -0
  25. package/guanwang/src/chat-sdk/features/artifact/ArtifactCard.vue +172 -0
  26. package/guanwang/src/chat-sdk/features/artifact/ArtifactPanel.vue +963 -0
  27. package/guanwang/src/chat-sdk/features/artifact/index.js +13 -0
  28. package/guanwang/src/chat-sdk/features/artifact/useArtifactStore.js +275 -0
  29. package/guanwang/src/chat-sdk/features/codepreview/CodePreview.vue +523 -0
  30. package/guanwang/src/chat-sdk/features/codepreview/index.js +7 -0
  31. package/guanwang/src/chat-sdk/features/markdown/index.js +13 -0
  32. package/guanwang/src/chat-sdk/features/markdown/useMarkdown.js +724 -0
  33. package/guanwang/src/chat-sdk/features/mermaid/MermaidZoom.vue +254 -0
  34. package/guanwang/src/chat-sdk/features/upload/FileAttachment.vue +142 -0
  35. package/guanwang/src/chat-sdk/features/upload/index.js +17 -0
  36. package/guanwang/src/chat-sdk/features/upload/useFileHandler.js +336 -0
  37. package/guanwang/src/chat-sdk/headless/api/adapters/openai.js +76 -0
  38. package/guanwang/src/chat-sdk/headless/api/chatApi.js +126 -0
  39. package/guanwang/src/chat-sdk/headless/buildSystemPrompt.js +351 -0
  40. package/guanwang/src/chat-sdk/headless/index.js +15 -0
  41. package/guanwang/src/chat-sdk/headless/useChat.js +77 -0
  42. package/guanwang/src/chat-sdk/headless/useChatDB.js +147 -0
  43. package/guanwang/src/chat-sdk/headless/useChatStore.js +529 -0
  44. package/guanwang/src/chat-sdk/index.js +79 -0
  45. package/guanwang/src/chat-sdk/modes/architect.js +27 -0
  46. package/guanwang/src/chat-sdk/modes/ask.js +26 -0
  47. package/guanwang/src/chat-sdk/modes/code.js +25 -0
  48. package/guanwang/src/chat-sdk/modes/index.js +36 -0
  49. package/guanwang/src/chat-sdk/modes/requirements.js +175 -0
  50. package/guanwang/src/chat-sdk/settings/SettingsPanel.vue +170 -0
  51. package/guanwang/src/chat-sdk/settings/index.js +9 -0
  52. package/guanwang/src/chat-sdk/settings/useSettings.js +122 -0
  53. package/guanwang/src/chat-sdk/tools/defaults.js +89 -0
  54. package/guanwang/src/chat-sdk/tools/index.js +16 -0
  55. package/guanwang/src/chat-sdk/tools/parser.js +116 -0
  56. package/guanwang/src/components/CustomCursor.vue +69 -0
  57. package/guanwang/src/components/Footer.vue +24 -0
  58. package/guanwang/src/components/LoginModal.vue +109 -0
  59. package/guanwang/src/components/Navbar.vue +193 -0
  60. package/guanwang/src/components/ThemeToggle.vue +25 -0
  61. package/guanwang/src/composables/useArtifactStore.js +253 -0
  62. package/guanwang/src/composables/useAuth.js +88 -0
  63. package/guanwang/src/composables/useChatDB.js +147 -0
  64. package/guanwang/src/composables/useCountUp.js +24 -0
  65. package/guanwang/src/composables/useFileHandler.js +345 -0
  66. package/guanwang/src/composables/useTheme.js +31 -0
  67. package/guanwang/src/config/api.js +71 -0
  68. package/guanwang/src/main.js +23 -0
  69. package/guanwang/src/router/index.js +23 -0
  70. package/guanwang/src/services/authApi.js +27 -0
  71. package/guanwang/src/services/chatApi.js +66 -0
  72. package/guanwang/src/styles/global.css +478 -0
  73. package/guanwang/src/tracker/analyze.js +73 -0
  74. package/guanwang/src/tracker/config.js +82 -0
  75. package/guanwang/src/tracker/index.js +18 -0
  76. package/guanwang/src/tracker/service.js +102 -0
  77. package/guanwang/src/tracker/useChatTracker.js +179 -0
  78. package/guanwang/src/tracker/useTracker.js +45 -0
  79. package/guanwang/src/views/ChatView.vue +65 -0
  80. package/guanwang/src/views/HomeView.vue +156 -0
  81. package/guanwang/src/views/MarketView.vue +143 -0
  82. package/guanwang/src/views/PracticesView.vue +190 -0
  83. package/guanwang/src/views/SkillsView.vue +129 -0
  84. package/guanwang/temp +19 -0
  85. package/guanwang/vite.config.js +6 -0
  86. package/package.json +1 -1
  87. package/guanwang.zip +0 -0
@@ -0,0 +1,724 @@
1
+ /**
2
+ * useMarkdown.js v3
3
+ *
4
+ * 核心改动:废弃全局 _artifactStore 注入模式
5
+ * renderMarkdown / createStreamingParser 直接接收 renderContext 参数:
6
+ * { messageId, artifactStore }
7
+ * buildCodeBlock 通过函数参数拿到上下文,多实例完全隔离,无竞争。
8
+ */
9
+ import { ref } from 'vue'
10
+ import { marked } from 'marked'
11
+ import DOMPurify from 'dompurify'
12
+
13
+ // ─────────────────────────────────────────────────────────────────
14
+ // Shiki 单例(全局共享,只初始化一次)
15
+ // ─────────────────────────────────────────────────────────────────
16
+ let _hl = null
17
+ let _hlLoading = false
18
+ export const shikiReady = ref(false)
19
+
20
+ const SUPPORTED_LANGS = [
21
+ 'javascript','typescript','vue','html','css','scss',
22
+ 'json','bash','shell','python','java','go','rust',
23
+ 'markdown','yaml','toml','sql','graphql','xml','jsx','tsx',
24
+ ]
25
+
26
+ // Shiki 初始化 Promise(单例,并发调用共享同一个 Promise,避免 setInterval 泄漏)
27
+ let _hlPromise = null
28
+
29
+ async function getShiki() {
30
+ if (_hl) return _hl
31
+ if (_hlPromise) return _hlPromise // 并发调用等同一个 Promise
32
+ _hlPromise = (async () => {
33
+ try {
34
+ const { createHighlighter } = await import('shiki')
35
+ _hl = await createHighlighter({ themes: ['one-dark-pro','github-light'], langs: SUPPORTED_LANGS })
36
+ shikiReady.value = true
37
+ } catch (e) {
38
+ console.warn('[shiki] init failed', e)
39
+ } finally {
40
+ _hlPromise = null // 无论成功失败都重置,下次可重试
41
+ }
42
+ return _hl
43
+ })()
44
+ return _hlPromise
45
+ }
46
+ getShiki() // 预热
47
+
48
+ function currentTheme() {
49
+ return document.documentElement.getAttribute('data-theme') === 'light'
50
+ ? 'github-light' : 'one-dark-pro'
51
+ }
52
+
53
+ function highlightCode(code, lang) {
54
+ const plainHtml = `<pre class="shiki-pre"><code>${escapeHtml(code)}</code></pre>`
55
+ if (!_hl) return plainHtml
56
+ // 语言不在支持列表时直接走 plain fallback,避免 Shiki 内部用 'text' 语言输出空内容
57
+ if (!_hl.getLoadedLanguages().includes(lang)) return plainHtml
58
+ try { return _hl.codeToHtml(code, { lang, theme: currentTheme() }) }
59
+ catch { return plainHtml }
60
+ }
61
+
62
+ // ─────────────────────────────────────────────────────────────────
63
+ // KaTeX
64
+ // ─────────────────────────────────────────────────────────────────
65
+ function renderKatex(tex, display) {
66
+ try { return window.katex?.renderToString(tex, { displayMode: display, throwOnError: false }) ?? tex }
67
+ catch { return tex }
68
+ }
69
+
70
+ // ─────────────────────────────────────────────────────────────────
71
+ // 工具
72
+ // ─────────────────────────────────────────────────────────────────
73
+ function escapeHtml(s) {
74
+ return (s || '').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')
75
+ .replace(/"/g,'&quot;').replace(/'/g,'&#39;')
76
+ }
77
+
78
+ let _uid = 0
79
+ function uid() { return `mb${++_uid}` }
80
+
81
+ // 全局代码块内容注册表(供 handleCodeAction 复制/预览用)
82
+ // 用 messageId 分组,组件卸载时调用 clearBlocksByMessage 清理,防止无限堆积
83
+ export const pendingBlocks = new Map()
84
+
85
+ /**
86
+ * 清理指定 messageId 下的所有代码块缓存
87
+ * 由 MessageBubble onUnmounted 调用
88
+ */
89
+ export function clearBlocksByMessage(messageId) {
90
+ if (!messageId) return
91
+ for (const [key, val] of pendingBlocks) {
92
+ if (val.messageId === messageId) pendingBlocks.delete(key)
93
+ }
94
+ }
95
+
96
+ // 流式 artifact Loading 卡片里显示的语言标签(简短版)
97
+ const LANG_LABEL_INLINE = {
98
+ vue:'Vue SFC', html:'HTML', jsx:'React JSX', tsx:'React TSX',
99
+ javascript:'JavaScript', js:'JavaScript', typescript:'TypeScript', ts:'TypeScript',
100
+ python:'Python', go:'Go', rust:'Rust', java:'Java', kotlin:'Kotlin', swift:'Swift',
101
+ css:'CSS', scss:'SCSS', bash:'Shell', sh:'Shell',
102
+ json:'JSON', yaml:'YAML', toml:'TOML', sql:'SQL',
103
+ mermaid:'Mermaid', svg:'SVG', math:'Math', latex:'LaTeX', markdown:'Markdown',
104
+ }
105
+
106
+ // ─────────────────────────────────────────────────────────────────
107
+ // SVG 图标常量
108
+ // ─────────────────────────────────────────────────────────────────
109
+ const SVG_COPY = `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="14" height="14" x="8" y="8" rx="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>`
110
+ const SVG_EYE = `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z"/><circle cx="12" cy="12" r="3"/></svg>`
111
+ const SVG_ZOOM = `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 3 21 3 21 9"/><polyline points="9 21 3 21 3 15"/><line x1="21" y1="3" x2="14" y2="10"/><line x1="3" y1="21" x2="10" y2="14"/></svg>`
112
+ const PREVIEWABLE = new Set(['html','vue','jsx','tsx','javascript','js','css','svg','markdown','md'])
113
+
114
+ // ─────────────────────────────────────────────────────────────────
115
+ // buildCodeBlock:接收渲染上下文,完全隔离
116
+ // ctx = { messageId, artifactStore, blockIndex }
117
+ // ─────────────────────────────────────────────────────────────────
118
+ function buildCodeBlock(lang, code, id, isStreaming = false, ctx = null) {
119
+ const displayLang = lang || 'text'
120
+
121
+ // 改动4:code 为空时不渲染任何代码块
122
+ if (!code || !code.trim()) return ''
123
+
124
+ pendingBlocks.set(id, { lang: displayLang, code, messageId: ctx?.messageId })
125
+
126
+ // ── Artifact 路由 ──────────────────────────────────────────────
127
+ const store = ctx?.artifactStore
128
+ const msgId = ctx?.messageId
129
+ if (store && msgId) {
130
+ const forceArtifact = ctx?.isArtifact // AI 显式标记了 [artifact]
131
+ const autoArtifact = store.shouldBeArtifact?.(lang, code) // 兜底规则
132
+ if (forceArtifact || autoArtifact) {
133
+ const artId = store.register({ messageId: msgId, lang: displayLang, code, index: ctx.blockIndex || 0 })
134
+ pendingBlocks.set(id, { lang: displayLang, code, artId, messageId: ctx?.messageId })
135
+ return `<div class="artifact-placeholder" data-art-id="${artId}" data-block-id="${id}"></div>`
136
+ }
137
+ }
138
+
139
+ // ── 特殊块类型 ─────────────────────────────────────────────────
140
+ if (lang === 'mermaid') {
141
+ return `<div class="code-wrapper mermaid-wrapper" id="${id}">
142
+ <div class="cb-actions"><span class="cb-lang">mermaid</span>
143
+ <div style="display:flex;gap:4px"><button class="cb-btn" data-action="zoom-mermaid" data-id="${id}">${SVG_ZOOM} 放大</button><button class="cb-btn" data-action="copy" data-id="${id}">${SVG_COPY} 复制</button></div>
144
+ </div>
145
+ <div class="mermaid-container">
146
+ <div class="mermaid-chart mermaid-pending" data-mermaid="${encodeURIComponent(code)}">
147
+ <div class="mermaid-placeholder"><span class="mermaid-placeholder__dot"/><span class="mermaid-placeholder__dot"/><span class="mermaid-placeholder__dot"/></div>
148
+ </div>
149
+ </div>
150
+ </div>`
151
+ }
152
+
153
+ if (lang === 'math' || lang === 'latex') {
154
+ return `<div class="code-wrapper math-wrapper" id="${id}">
155
+ <div class="cb-actions"><span class="cb-lang">math</span></div>
156
+ <div class="math-display">${renderKatex(code, true)}</div>
157
+ </div>`
158
+ }
159
+
160
+ // ── 普通代码块 ─────────────────────────────────────────────────
161
+ const previewBtn = PREVIEWABLE.has(lang) && !ctx?.noPreview
162
+ ? `<button class="cb-btn" data-action="preview" data-id="${id}">${SVG_EYE} 预览</button>` : ''
163
+ const encodedCode = btoa(unescape(encodeURIComponent(code)))
164
+ const shikiHtml = `<div class="shiki-live" data-id="${id}" data-lang="${lang}" data-code="${encodedCode}">${highlightCode(code, lang)}</div>`
165
+
166
+ return `<div class="code-wrapper${isStreaming ? ' code-streaming' : ''}" id="${id}">
167
+ <div class="cb-actions">
168
+ <span class="cb-lang">${displayLang}</span>
169
+ <div style="display:flex;gap:4px">${previewBtn}
170
+ <button class="cb-btn" data-action="copy" data-id="${id}">${SVG_COPY} 复制</button>
171
+ </div>
172
+ </div>
173
+ ${shikiHtml}
174
+ </div>`
175
+ }
176
+
177
+ // ─────────────────────────────────────────────────────────────────
178
+ // makeRenderer:每次渲染时创建,携带当次的上下文
179
+ // ─────────────────────────────────────────────────────────────────
180
+ function makeRenderer(ctx = null) {
181
+ const r = new marked.Renderer()
182
+ let blockIndex = 0
183
+ r.code = ({ text, lang }) => {
184
+ blockIndex++
185
+ return buildCodeBlock(lang || 'text', text || '', uid(), false, ctx ? { ...ctx, blockIndex } : null)
186
+ }
187
+ r.codespan = ({ text }) => `<code class="inline-code">${escapeHtml(text)}</code>`
188
+ r.image = (href, title, text) =>
189
+ `<img src="${href||''}" alt="${escapeHtml(text||'')}" title="${escapeHtml(title||'')}" loading="lazy" style="max-width:100%;border-radius:8px;margin:8px 0"/>`
190
+ r.link = (href, title, text) =>
191
+ `<a href="${href||''}" target="_blank" rel="noopener noreferrer">${text||href||''}</a>`
192
+ return r
193
+ }
194
+
195
+ // ─────────────────────────────────────────────────────────────────
196
+ // LaTeX 预处理
197
+ // ─────────────────────────────────────────────────────────────────
198
+ function preprocessLatex(md) {
199
+ md = md.replace(/\$\$([^$]+?)\$\$/gs, (_, t) => `\n\`\`\`math\n${t.trim()}\n\`\`\`\n`)
200
+ md = md.replace(/(?<!\$)\$(?!\$)([^$\n]+?)\$(?!\$)/g, (_, t) => {
201
+ try { return window.katex?.renderToString(t, { displayMode: false, throwOnError: false }) ?? `$${t}$` }
202
+ catch { return `$${t}$` }
203
+ })
204
+ return md
205
+ }
206
+
207
+ // ─────────────────────────────────────────────────────────────────
208
+ // DOMPurify 配置
209
+ // ─────────────────────────────────────────────────────────────────
210
+ const PURIFY_OPTS = {
211
+ ADD_TAGS: ['math','semantics','mrow','mi','mo','mn','msup','msub','mfrac','annotation'],
212
+ ADD_ATTR: ['data-action','data-id','data-lang','data-mermaid','data-code','data-art-id','data-block-id','xmlns','encoding'],
213
+ FORCE_BODY: true,
214
+ }
215
+
216
+ // ─────────────────────────────────────────────────────────────────
217
+ // StreamingMarkdownParser
218
+ // ─────────────────────────────────────────────────────────────────
219
+ class StreamingMarkdownParser {
220
+ constructor(ctx = null) {
221
+ this.ctx = ctx // { messageId, artifactStore }
222
+ this.reset()
223
+ }
224
+
225
+ reset() {
226
+ this.inCode = false
227
+ this.codeLang = ''
228
+ this.isArtifact = false
229
+ this.codeLines = []
230
+ this.codeBlockId = null
231
+ this.fenceChar = '`'
232
+ this.fenceLen = 3
233
+ this.innerDepth = 0 // 内层未关闭围栏计数
234
+ this._streamArtIdx = 0
235
+ }
236
+
237
+ parse(content) {
238
+ const lines = content.split('\n')
239
+ let i = 0
240
+ const closedMdChunks = []
241
+ let streamingCodeLang = null
242
+ let streamingCodeLines = null
243
+ let tailText = ''
244
+
245
+ while (i < lines.length) {
246
+ const line = lines[i]
247
+ if (!this.inCode) {
248
+ const fence = line.match(/^(`{3,}|~{3,})(.*)/)
249
+ if (fence) {
250
+ if (tailText) { closedMdChunks.push(tailText); tailText = '' }
251
+ const infoLine = fence[2].trim()
252
+ const parts = infoLine.split(/\s+/)
253
+ this.inCode = true
254
+ this.fenceChar = fence[1][0]
255
+ this.fenceLen = fence[1].length
256
+ this.codeLang = parts[0] || 'text'
257
+ this.isArtifact = parts.includes('[artifact]')
258
+ this.codeLines = []
259
+ this.innerDepth = 0
260
+ i++; continue
261
+ }
262
+ tailText += (tailText ? '\n' : '') + line
263
+ } else {
264
+ // 精确匹配外层关闭围栏 + innerDepth 追踪内层代码块
265
+ // 防止 markdown 等语言内部的嵌套围栏行被误判为外层关闭
266
+ const closeRe = new RegExp(`^${this.fenceChar}{${this.fenceLen}}\\s*$`)
267
+ const innerOpenM = line.match(new RegExp(`^${this.fenceChar}{3,}(\\S+)`))
268
+ if (closeRe.test(line)) {
269
+ if (this.innerDepth > 0) {
270
+ // 内层代码块的关闭行,不是外层关闭,继续收集
271
+ this.innerDepth--
272
+ this.codeLines.push(line)
273
+ } else {
274
+ // 真正的外层关闭
275
+ const info = this.isArtifact ? `${this.codeLang} [artifact]` : this.codeLang
276
+ closedMdChunks.push(`\`\`\`${info}\n${this.codeLines.join('\n')}\n\`\`\``)
277
+ this.inCode = false; this.codeLang = ''; this.isArtifact = false
278
+ this.codeLines = []; this.fenceLen = 3; this.fenceChar = '`'
279
+ this.innerDepth = 0
280
+ }
281
+ i++; continue
282
+ } else if (innerOpenM) {
283
+ // 内层代码块开启(带语言标识符),计数加一
284
+ this.innerDepth++
285
+ }
286
+ this.codeLines.push(line)
287
+ }
288
+ i++
289
+ }
290
+
291
+ if (this.inCode) {
292
+ streamingCodeLang = this.codeLang
293
+ streamingCodeLines = [...this.codeLines]
294
+ }
295
+
296
+ // 渲染已闭合部分:使用 safeMarkdownParse,彻底避免 marked 被嵌套围栏截断
297
+ let closedHtml = ''
298
+ if (closedMdChunks.length > 0) {
299
+ closedHtml = safeMarkdownParse(closedMdChunks.join('\n\n'), this.ctx)
300
+ }
301
+
302
+ // 流式尾部
303
+ let tailHtml = ''
304
+ if (streamingCodeLang !== null) {
305
+ const code = streamingCodeLines.join('\n')
306
+ if (!this.codeBlockId) this.codeBlockId = uid()
307
+ const id = this.codeBlockId
308
+ pendingBlocks.set(id, { lang: streamingCodeLang, code, messageId: this.ctx?.messageId })
309
+
310
+ const store = this.ctx?.artifactStore
311
+ const msgId = this.ctx?.messageId
312
+ // isArtifact:AI 在 infoLine 里显式声明了 [artifact],或由 shouldBeArtifact 兜底判断
313
+ const isArtifact = this.isArtifact || store?.shouldBeArtifact?.(streamingCodeLang, code)
314
+
315
+ if (store && msgId && isArtifact) {
316
+ if (!this._streamArtIdx) this._streamArtIdx = 1
317
+ const artId = store.preRegister?.({
318
+ messageId: msgId,
319
+ lang: streamingCodeLang,
320
+ index: this._streamArtIdx,
321
+ }) || store.register({
322
+ messageId: msgId,
323
+ lang: streamingCodeLang,
324
+ code,
325
+ index: this._streamArtIdx,
326
+ })
327
+
328
+ if (code) {
329
+ store.register({ messageId: msgId, lang: streamingCodeLang, code, index: this._streamArtIdx })
330
+ store.updateCode?.(artId, code)
331
+ }
332
+
333
+ const langLabel = LANG_LABEL_INLINE[streamingCodeLang] || streamingCodeLang
334
+ const lineCount = code ? code.split('\n').length : 0
335
+ const lineTip = lineCount > 0 ? `· ${lineCount} 行` : ''
336
+ tailHtml = `<div class="artifact-gen-hint">
337
+ <span class="artifact-gen-hint__dot"></span>
338
+ <span class="artifact-gen-hint__text">正在生成 ${langLabel} 文件 ${lineTip}</span>
339
+ </div>`
340
+ } else if (!code.trim()) {
341
+ tailHtml = ''
342
+ } else {
343
+ tailHtml = `<div class="code-wrapper code-streaming" id="${id}">
344
+ <div class="cb-actions">
345
+ <span class="cb-lang">${streamingCodeLang}</span>
346
+ <div class="cb-streaming-badge"><span class="cb-dot"></span>输出中</div>
347
+ </div>
348
+ <div class="shiki-live" data-id="${id}" data-lang="${streamingCodeLang}">${highlightCode(code, streamingCodeLang)}</div>
349
+ </div>`
350
+ }
351
+ } else if (tailText.trim()) {
352
+ tailHtml = renderInlineSafe(tailText)
353
+ }
354
+
355
+ return { closedHtml, tailHtml }
356
+ }
357
+ }
358
+
359
+ function renderInlineSafe(text) {
360
+ // 先提取行内反引号内容,再 escape,最后还原,避免路径字符串被误处理
361
+ const inlines = []
362
+ const protected_ = text.replace(/`([^`\n]+?)`/g, (_, t) => {
363
+ inlines.push(t)
364
+ return `\x00IL${inlines.length - 1}\x00`
365
+ })
366
+ let html = escapeHtml(protected_)
367
+ html = html.replace(/\x00IL(\d+)\x00/g, (_, i) =>
368
+ `<code class="inline-code">${escapeHtml(inlines[+i])}</code>`
369
+ )
370
+ html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
371
+ html = html.replace(/\*(.+?)\*/g, '<em>$1</em>')
372
+ html = html.replace(/\n/g, '<br>')
373
+ return `<p class="streaming-tail">${html}</p>`
374
+ }
375
+
376
+ // ─────────────────────────────────────────────────────────────────
377
+ // safeMarkdownParse
378
+ // 在交给 marked 之前,先把所有代码围栏块提取为 placeholder,
379
+ // 渲染后再把 placeholder 替换为 buildCodeBlock 的输出。
380
+ // 这样 marked 永远看不到嵌套围栏,彻底解决内容截断问题。
381
+ // ─────────────────────────────────────────────────────────────────
382
+ function safeMarkdownParse(content, ctx) {
383
+ const blocks = []
384
+
385
+ // ── 用逐行状态机提取代码围栏块 ──
386
+ // CommonMark:inCode 状态下所有行都是代码内容,只认精确匹配的外层关闭行,不做嵌套计数
387
+ const lines = content.split('\n')
388
+ let inCode = false
389
+ let fenceChar = '`'
390
+ let fenceLen = 3
391
+ let codeLang = ''
392
+ let isArtifact = false
393
+ let codeLines = []
394
+ let innerDepth = 0 // 内层未关闭围栏计数
395
+ const outLines = []
396
+
397
+ for (let i = 0; i < lines.length; i++) {
398
+ const line = lines[i]
399
+ if (!inCode) {
400
+ const m = line.match(/^(`{3,}|~{3,})(.*)/)
401
+ if (m) {
402
+ const infoLine = m[2].trim()
403
+ const parts = infoLine.split(/\s+/)
404
+ inCode = true
405
+ fenceChar = m[1][0]
406
+ fenceLen = m[1].length
407
+ codeLang = parts[0] || 'text'
408
+ isArtifact = parts.includes('[artifact]')
409
+ codeLines = []
410
+ innerDepth = 0
411
+ } else {
412
+ outLines.push(line)
413
+ }
414
+ } else {
415
+ // 精确匹配外层关闭围栏 + innerDepth 追踪内层代码块
416
+ // 防止 markdown 等语言内部的嵌套围栏行被误判为外层关闭
417
+ const closeRe = new RegExp(`^${fenceChar}{${fenceLen}}\\s*$`)
418
+ const innerOpenM = line.match(new RegExp(`^${fenceChar}{3,}(\\S+)`))
419
+ if (closeRe.test(line)) {
420
+ if (innerDepth > 0) {
421
+ // 内层代码块的关闭行,depth-- 后继续收集
422
+ innerDepth--
423
+ codeLines.push(line)
424
+ } else {
425
+ // 真正的外层关闭
426
+ const ph = `\x00CODEBLOCK_${blocks.length}\x00`
427
+ blocks.push({ lang: codeLang, code: codeLines.join('\n'), isArtifact })
428
+ outLines.push(ph)
429
+ inCode = false; codeLang = ''; isArtifact = false; codeLines = []; innerDepth = 0
430
+ }
431
+ } else {
432
+ if (innerOpenM) innerDepth++ // 内层代码块开启(带语言标识符),计数加一
433
+ codeLines.push(line)
434
+ }
435
+ }
436
+ }
437
+ // 未闭合的围栏(如流式截断):同样作为代码块处理,而不是丢成普通文本
438
+ // 这样 svg/vue/html 等语言仍能正确路由到面板,截断的代码也能显示代码块 UI
439
+ if (inCode && codeLines.length > 0) {
440
+ const ph = `\x00CODEBLOCK_${blocks.length}\x00`
441
+ blocks.push({ lang: codeLang, code: codeLines.join('\n'), isArtifact })
442
+ outLines.push(ph)
443
+ }
444
+
445
+ const extracted = outLines.join('\n')
446
+
447
+ // ── 提前保护行内反引号,防止 gfm 模式下路径字符串被误解析 ──
448
+ const inlineCodes = []
449
+ const protectedContent = extracted.replace(/`([^`\n]+?)`/g, (_, text) => {
450
+ const ph = `\x00INLINE_${inlineCodes.length}\x00`
451
+ inlineCodes.push(text)
452
+ return ph
453
+ })
454
+
455
+ // 对非代码内容做 LaTeX 预处理,然后交给 marked
456
+ marked.use({ renderer: makeRenderer(null), gfm: true, breaks: true })
457
+ let html = marked.parse(preprocessLatex(protectedContent))
458
+
459
+ // 还原行内代码占位符
460
+ if (inlineCodes.length > 0) {
461
+ html = html.replace(/\x00INLINE_(\d+)\x00/g, (_, idx) =>
462
+ `<code class="inline-code">${escapeHtml(inlineCodes[+idx])}</code>`
463
+ )
464
+ }
465
+
466
+ // 把 placeholder 替换为真实代码块 HTML(携带 ctx)
467
+ if (blocks.length > 0) {
468
+ let blockIndex = 0
469
+ // placeholder 可能被 marked 包在 <p> 里,先拆出来再替换
470
+ html = html.replace(/<p>(\x00CODEBLOCK_\d+\x00)<\/p>/g, '$1')
471
+ html = html.replace(/\x00CODEBLOCK_(\d+)\x00/g, (_, idx) => {
472
+ const { lang, code, isArtifact } = blocks[+idx]
473
+ blockIndex++
474
+ return buildCodeBlock(lang, code, uid(), false, ctx ? { ...ctx, blockIndex, isArtifact } : null)
475
+ })
476
+ }
477
+
478
+ return DOMPurify.sanitize(html, PURIFY_OPTS)
479
+ }
480
+
481
+ // ─────────────────────────────────────────────────────────────────
482
+ // 对外 API
483
+ // ─────────────────────────────────────────────────────────────────
484
+
485
+ /**
486
+ * 全量渲染
487
+ * @param {string} content
488
+ * @param {object} ctx - { messageId, artifactStore } 可选
489
+ */
490
+ export function renderMarkdown(content, ctx = null) {
491
+ if (!content) return ''
492
+ return safeMarkdownParse(content, ctx)
493
+ }
494
+
495
+ /**
496
+ * 创建流式解析器(每条消息独享,ctx 在创建时绑定)
497
+ * @param {object} ctx - { messageId, artifactStore }
498
+ */
499
+ export function createStreamingParser(ctx = null) {
500
+ const parser = new StreamingMarkdownParser(ctx)
501
+ return {
502
+ parse(content) { parser.reset(); return parser.parse(content) },
503
+ reset() { parser.reset() },
504
+ updateCtx(newCtx){ parser.ctx = newCtx },
505
+ }
506
+ }
507
+
508
+ // ─────────────────────────────────────────────────────────────────
509
+ // sanitizeMermaidCode:预处理 mermaid 代码
510
+ // 把节点标签文本([] {} 内)里的圆括号转为 HTML 实体,
511
+ // 避免 AI 生成的 "(备注文字)" 被 mermaid 误解析为节点语法导致 Parse error
512
+ // ─────────────────────────────────────────────────────────────────
513
+ export function sanitizeMermaidCode(code) {
514
+ const lines = code.split('\n')
515
+ return lines.map(line => {
516
+ // 跳过注释行和图表类型声明行,不做处理
517
+ if (/^\s*%%/.test(line) || /^\s*(flowchart|graph|sequenceDiagram|classDiagram|stateDiagram|gantt|pie|gitGraph|erDiagram|journey|mindmap|timeline|quadrantChart|xychart|block|architecture|requirementDiagram)\b/.test(line)) {
518
+ return line
519
+ }
520
+ // [] 内文本的 () 转为 HTML 实体
521
+ return line
522
+ .replace(/\[([^\]]*)\]/g, (_, inner) =>
523
+ '[' + inner.replace(/\(/g, '&#40;').replace(/\)/g, '&#41;') + ']'
524
+ )
525
+ .replace(/\{([^}]*)\}/g, (_, inner) =>
526
+ '{' + inner.replace(/\(/g, '&#40;').replace(/\)/g, '&#41;') + '}'
527
+ )
528
+ }).join('\n')
529
+ }
530
+
531
+ // ─────────────────────────────────────────────────────────────────
532
+ // postRender:Shiki + Mermaid
533
+ // ─────────────────────────────────────────────────────────────────
534
+ export async function postRender(el, { skipMermaid = false } = {}) {
535
+ if (!el) return
536
+ const tasks = []
537
+
538
+ const shikiEls = el.querySelectorAll('.shiki-live[data-id], .shiki-pre[data-id]')
539
+ if (shikiEls.length > 0) {
540
+ tasks.push(getShiki().then(hl => {
541
+ if (!hl) return
542
+ shikiEls.forEach(node => {
543
+ const id = node.dataset.id
544
+ const lang = node.dataset.lang || 'text'
545
+ const stored = pendingBlocks.get(id)
546
+ let code
547
+ if (stored?.code !== undefined) {
548
+ code = stored.code
549
+ } else if (node.dataset.code) {
550
+ try { code = decodeURIComponent(escape(atob(node.dataset.code))) } catch { code = node.textContent }
551
+ } else {
552
+ code = node.querySelector('code')?.textContent ?? node.textContent
553
+ }
554
+ // 语言不支持时直接降级,不传 'text' 给 Shiki(避免部分版本输出空内容)
555
+ if (!hl.getLoadedLanguages().includes(lang)) {
556
+ const fallback = document.createElement('pre')
557
+ fallback.className = 'shiki-pre'
558
+ fallback.style.cssText = 'margin:0;border-radius:0 0 8px 8px;font-size:13px;line-height:1.6;overflow-x:auto;padding:14px 16px'
559
+ fallback.innerHTML = `<code>${escapeHtml(code)}</code>`
560
+ node.replaceWith(fallback)
561
+ return
562
+ }
563
+ const safeLang = lang
564
+ // 始终尝试高亮,失败或结果为空时降级为带样式的 <pre>,绝不留白
565
+ const fallbackHtml = `<pre class="shiki-pre" style="margin:0;border-radius:0 0 8px 8px;font-size:13px;line-height:1.6;overflow-x:auto;padding:14px 16px"><code>${escapeHtml(code)}</code></pre>`
566
+ try {
567
+ const div = document.createElement('div')
568
+ div.innerHTML = hl.codeToHtml(code, { lang: safeLang, theme: currentTheme() })
569
+ const shikiNode = div.firstElementChild
570
+ if (shikiNode) {
571
+ shikiNode.style.cssText = 'margin:0;border-radius:0 0 8px 8px;font-size:13px;line-height:1.6;overflow-x:auto'
572
+ node.replaceWith(shikiNode)
573
+ } else {
574
+ // Shiki 返回空节点,用 fallback
575
+ const fallback = document.createElement('div')
576
+ fallback.innerHTML = fallbackHtml
577
+ node.replaceWith(fallback.firstElementChild)
578
+ }
579
+ } catch {
580
+ // Shiki 抛异常,用 fallback
581
+ const fallback = document.createElement('div')
582
+ fallback.innerHTML = fallbackHtml
583
+ node.replaceWith(fallback.firstElementChild)
584
+ }
585
+ })
586
+ }))
587
+ }
588
+
589
+ const mermaidEls = el.querySelectorAll('.mermaid-chart[data-mermaid]:not([data-rendered])')
590
+ if (mermaidEls.length > 0 && !skipMermaid) {
591
+ tasks.push(import('mermaid').then(({ default: mermaid }) => {
592
+ const isDark = document.documentElement.getAttribute('data-theme') !== 'light'
593
+ mermaid.initialize({ startOnLoad: false, suppressErrorRendering: true, theme: isDark ? 'dark' : 'default', fontFamily: "Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, 微软雅黑, Arial, sans-serif" })
594
+ mermaidEls.forEach(async el => {
595
+ const rawCode = decodeURIComponent(el.dataset.mermaid || '')
596
+ // 预处理:把节点标签文本([] {} 内)里的圆括号转为 HTML 实体
597
+ // 避免 AI 生成的 "(备注)" 被 mermaid 误解析为圆形节点语法导致 Parse error
598
+ const code = sanitizeMermaidCode(rawCode)
599
+ const id = 'mer-' + Math.random().toString(36).slice(2)
600
+ try {
601
+ const { svg } = await mermaid.render(id, code)
602
+ // 先在屏幕外解析 SVG,取真实高度,再平滑过渡,避免从占位高度突变到 SVG 高度
603
+ const tmp = document.createElement('div')
604
+ tmp.style.cssText = 'position:absolute;visibility:hidden;pointer-events:none;left:-9999px'
605
+ tmp.innerHTML = svg
606
+ document.body.appendChild(tmp)
607
+ const svgEl = tmp.querySelector('svg')
608
+ // 读取 SVG 自带 height 属性或 viewBox 推算,取不到则用 200px 兜底
609
+ let targetH = 200
610
+ if (svgEl) {
611
+ const h = svgEl.getAttribute('height')
612
+ if (h && parseFloat(h) > 0) {
613
+ targetH = Math.min(parseFloat(h), 800) // 超大图表最高限制 800px
614
+ } else {
615
+ const vb = svgEl.getAttribute('viewBox')
616
+ if (vb) {
617
+ const parts = vb.split(/[\s,]+/)
618
+ if (parts.length >= 4) targetH = Math.min(parseFloat(parts[3]) || 200, 800)
619
+ }
620
+ }
621
+ }
622
+ // 检查 mermaid 是否返回了错误 SVG(aria-roledescription="error")
623
+ // 11.x 有时不抛异常而是静默渲染一个错误占位 SVG
624
+ const isErrorSvg = svgEl?.getAttribute('aria-roledescription') === 'error'
625
+ document.body.removeChild(tmp)
626
+
627
+ if (isErrorSvg) {
628
+ el.classList.remove('mermaid-pending')
629
+ el.closest('.mermaid-wrapper')?.classList.add('mermaid-fallback')
630
+ el.innerHTML = `<pre class="mermaid-raw"><code>${escapeHtml(code)}</code></pre>`
631
+ console.warn('[mermaid] error svg detected, fallback to raw code')
632
+ return
633
+ }
634
+
635
+ // 设置容器高度(平滑过渡到目标高度),再注入 SVG
636
+ el.style.minHeight = `${targetH}px`
637
+ el.style.transition = 'min-height 0.25s ease'
638
+ el.innerHTML = svg
639
+ el.classList.remove('mermaid-pending')
640
+ el.dataset.rendered = '1' // 标记已渲染,防止流式期间重复触发
641
+ // SVG 注入后 fade in
642
+ const svgNode = el.querySelector('svg')
643
+ if (svgNode) {
644
+ svgNode.style.opacity = '0'
645
+ svgNode.style.transition = 'opacity 0.2s ease'
646
+ requestAnimationFrame(() => { svgNode.style.opacity = '1' })
647
+ }
648
+ // 渲染完成后移除 min-height 约束,让 SVG 自然撑开
649
+ setTimeout(() => { el.style.minHeight = ''; el.style.transition = '' }, 300)
650
+ } catch (e) {
651
+ // 静默降级:不向用户暴露 mermaid 内部报错(如 "Syntax error in text")
652
+ // 改为折叠显示原始代码,用户可复制修正,不影响整体阅读体验
653
+ el.classList.remove('mermaid-pending')
654
+ const escaped = escapeHtml(code)
655
+ el.closest('.mermaid-wrapper')?.classList.add('mermaid-fallback')
656
+ el.innerHTML = `<pre class="mermaid-raw"><code>${escaped}</code></pre>`
657
+ console.warn('[mermaid] render failed:', e.message)
658
+ }
659
+ })
660
+ }))
661
+ }
662
+
663
+ await Promise.allSettled(tasks)
664
+ }
665
+
666
+ // ─────────────────────────────────────────────────────────────────
667
+ // 模块级主题变更订阅器(单例 MutationObserver,避免每条消息都创建一个)
668
+ // MessageBubble 通过 onThemeChange / offThemeChange 订阅/取消订阅
669
+ // ─────────────────────────────────────────────────────────────────
670
+ const _themeSubscribers = new Set()
671
+ let _themeObserver = null
672
+
673
+ function ensureThemeObserver() {
674
+ if (_themeObserver) return
675
+ _themeObserver = new MutationObserver(() => {
676
+ _themeSubscribers.forEach(fn => fn())
677
+ })
678
+ _themeObserver.observe(document.documentElement, {
679
+ attributes: true, attributeFilter: ['data-theme'],
680
+ })
681
+ }
682
+
683
+ export function onThemeChange(fn) {
684
+ ensureThemeObserver()
685
+ _themeSubscribers.add(fn)
686
+ }
687
+
688
+ export function offThemeChange(fn) {
689
+ _themeSubscribers.delete(fn)
690
+ // 所有订阅者都退订时断开 Observer,释放资源
691
+ if (_themeSubscribers.size === 0 && _themeObserver) {
692
+ _themeObserver.disconnect()
693
+ _themeObserver = null
694
+ }
695
+ }
696
+
697
+ // ─────────────────────────────────────────────────────────────────
698
+ // 代码块点击委托
699
+ // ─────────────────────────────────────────────────────────────────
700
+ export function handleCodeAction(e, openPreview) {
701
+ const btn = e.target.closest('[data-action]')
702
+ if (!btn) return
703
+ const { action, id } = btn.dataset
704
+ const block = pendingBlocks.get(id)
705
+ if (!block) return
706
+
707
+ if (action === 'copy') {
708
+ navigator.clipboard?.writeText(block.code).catch(() => {})
709
+ const orig = btn.innerHTML
710
+ btn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="#4ade80" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg> 已复制`
711
+ btn.style.color = '#4ade80'
712
+ setTimeout(() => { btn.innerHTML = orig; btn.style.color = '' }, 2000)
713
+ }
714
+ if (action === 'preview' && openPreview) {
715
+ openPreview({ lang: block.lang, code: block.code })
716
+ }
717
+ if (action === 'zoom-mermaid' && openPreview) {
718
+ // 从 DOM 里取已渲染好的 SVG,传给 openPreview 展示全屏预览
719
+ const wrapper = btn.closest('.mermaid-wrapper')
720
+ const svgEl = wrapper?.querySelector('.mermaid-chart svg')
721
+ const svgHtml = svgEl ? svgEl.outerHTML : ''
722
+ if (svgHtml) openPreview({ lang: 'mermaid-svg', code: block.code, svgHtml })
723
+ }
724
+ }