@templmf/temp-solf-lmf 0.0.55 → 0.0.57

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 (89) hide show
  1. package/fe-flow.md +348 -0
  2. package/package.json +1 -1
  3. package/ui-parse.md +308 -0
  4. package//345/242/236/351/207/217/351/234/200/346/261/202prompt +72 -0
  5. package/guanwang/README.md +0 -95
  6. package/guanwang/docs/changelog.md +0 -145
  7. package/guanwang/docs/doc-maintenance.md +0 -229
  8. package/guanwang/docs/product.md +0 -181
  9. package/guanwang/docs/test-cases.md +0 -395
  10. package/guanwang/docs/usage.md +0 -291
  11. package/guanwang/env.example +0 -27
  12. package/guanwang/index.html +0 -13
  13. package/guanwang/package-lock.json +0 -3825
  14. package/guanwang/package.json +0 -32
  15. package/guanwang/public/favicon.svg +0 -4
  16. package/guanwang/public/react-runtime/babel.min.js +0 -4
  17. package/guanwang/public/react-runtime/react-dom.min.js +0 -267
  18. package/guanwang/public/react-runtime/react.min.js +0 -31
  19. package/guanwang/public/vue-repl-assets/compiler-sfc.esm-browser.js +0 -50795
  20. package/guanwang/public/vue-repl-assets/runtime-dom.esm-browser.js +0 -12758
  21. package/guanwang/public/vue-repl-assets/server-renderer.esm-browser.js +0 -8600
  22. package/guanwang/public/vue-repl-assets/vue.esm-browser.js +0 -18672
  23. package/guanwang/src/App.vue +0 -61
  24. package/guanwang/src/chat-sdk/core/components/ChatBox.vue +0 -305
  25. package/guanwang/src/chat-sdk/core/components/ChatSidebar.vue +0 -84
  26. package/guanwang/src/chat-sdk/core/components/InputBar.vue +0 -354
  27. package/guanwang/src/chat-sdk/core/components/MessageBubble.vue +0 -703
  28. package/guanwang/src/chat-sdk/core/useTheme.js +0 -31
  29. package/guanwang/src/chat-sdk/features/artifact/ArtifactCard.vue +0 -172
  30. package/guanwang/src/chat-sdk/features/artifact/ArtifactPanel.vue +0 -963
  31. package/guanwang/src/chat-sdk/features/artifact/index.js +0 -13
  32. package/guanwang/src/chat-sdk/features/artifact/useArtifactStore.js +0 -275
  33. package/guanwang/src/chat-sdk/features/codepreview/CodePreview.vue +0 -523
  34. package/guanwang/src/chat-sdk/features/codepreview/index.js +0 -7
  35. package/guanwang/src/chat-sdk/features/markdown/index.js +0 -13
  36. package/guanwang/src/chat-sdk/features/markdown/useMarkdown.js +0 -724
  37. package/guanwang/src/chat-sdk/features/mermaid/MermaidZoom.vue +0 -254
  38. package/guanwang/src/chat-sdk/features/upload/FileAttachment.vue +0 -142
  39. package/guanwang/src/chat-sdk/features/upload/index.js +0 -17
  40. package/guanwang/src/chat-sdk/features/upload/useFileHandler.js +0 -336
  41. package/guanwang/src/chat-sdk/headless/api/adapters/openai.js +0 -76
  42. package/guanwang/src/chat-sdk/headless/api/chatApi.js +0 -126
  43. package/guanwang/src/chat-sdk/headless/buildSystemPrompt.js +0 -351
  44. package/guanwang/src/chat-sdk/headless/index.js +0 -15
  45. package/guanwang/src/chat-sdk/headless/useChat.js +0 -77
  46. package/guanwang/src/chat-sdk/headless/useChatDB.js +0 -147
  47. package/guanwang/src/chat-sdk/headless/useChatStore.js +0 -529
  48. package/guanwang/src/chat-sdk/index.js +0 -79
  49. package/guanwang/src/chat-sdk/modes/architect.js +0 -27
  50. package/guanwang/src/chat-sdk/modes/ask.js +0 -26
  51. package/guanwang/src/chat-sdk/modes/code.js +0 -25
  52. package/guanwang/src/chat-sdk/modes/index.js +0 -36
  53. package/guanwang/src/chat-sdk/modes/requirements.js +0 -175
  54. package/guanwang/src/chat-sdk/settings/SettingsPanel.vue +0 -170
  55. package/guanwang/src/chat-sdk/settings/index.js +0 -9
  56. package/guanwang/src/chat-sdk/settings/useSettings.js +0 -122
  57. package/guanwang/src/chat-sdk/tools/defaults.js +0 -89
  58. package/guanwang/src/chat-sdk/tools/index.js +0 -16
  59. package/guanwang/src/chat-sdk/tools/parser.js +0 -116
  60. package/guanwang/src/components/CustomCursor.vue +0 -69
  61. package/guanwang/src/components/Footer.vue +0 -24
  62. package/guanwang/src/components/LoginModal.vue +0 -109
  63. package/guanwang/src/components/Navbar.vue +0 -193
  64. package/guanwang/src/components/ThemeToggle.vue +0 -25
  65. package/guanwang/src/composables/useArtifactStore.js +0 -253
  66. package/guanwang/src/composables/useAuth.js +0 -88
  67. package/guanwang/src/composables/useChatDB.js +0 -147
  68. package/guanwang/src/composables/useCountUp.js +0 -24
  69. package/guanwang/src/composables/useFileHandler.js +0 -345
  70. package/guanwang/src/composables/useTheme.js +0 -31
  71. package/guanwang/src/config/api.js +0 -71
  72. package/guanwang/src/main.js +0 -23
  73. package/guanwang/src/router/index.js +0 -23
  74. package/guanwang/src/services/authApi.js +0 -27
  75. package/guanwang/src/services/chatApi.js +0 -66
  76. package/guanwang/src/styles/global.css +0 -478
  77. package/guanwang/src/tracker/analyze.js +0 -73
  78. package/guanwang/src/tracker/config.js +0 -82
  79. package/guanwang/src/tracker/index.js +0 -18
  80. package/guanwang/src/tracker/service.js +0 -102
  81. package/guanwang/src/tracker/useChatTracker.js +0 -179
  82. package/guanwang/src/tracker/useTracker.js +0 -45
  83. package/guanwang/src/views/ChatView.vue +0 -65
  84. package/guanwang/src/views/HomeView.vue +0 -156
  85. package/guanwang/src/views/MarketView.vue +0 -143
  86. package/guanwang/src/views/PracticesView.vue +0 -190
  87. package/guanwang/src/views/SkillsView.vue +0 -129
  88. package/guanwang/temp +0 -19
  89. package/guanwang/vite.config.js +0 -6
@@ -1,336 +0,0 @@
1
- /**
2
- * useFileHandler.js
3
- * 附件处理核心
4
- *
5
- * 文件类型路由:
6
- * 图片(jpg/png/webp/gif/svg)→ base64 → image_url content block → 多模态模型
7
- * PDF → 每页转图片 base64 → 多模态模型
8
- * 代码/文本/json/csv/md → 读文本 → 拼进 text block → LLM
9
- * Excel(xlsx/xls) → SheetJS 提取 → text block → LLM
10
- * Word(docx) → mammoth 提取 → text block → LLM
11
- * 其他 → 拒绝,给出提示
12
- */
13
-
14
- // ── 类型分组 ──────────────────────────────────────────────────────
15
- const IMAGE_TYPES = new Set(['image/jpeg','image/png','image/webp','image/gif','image/svg+xml'])
16
- const PDF_TYPE = 'application/pdf'
17
- const TEXT_EXTS = new Set([
18
- 'txt','md','markdown','js','mjs','cjs','ts','jsx','tsx',
19
- 'vue','html','htm','css','scss','sass','less',
20
- 'json','jsonc','yaml','yml','toml','xml','svg',
21
- 'sh','bash','zsh','fish','py','go','rs','java','kt','swift','c','cpp','h','cs','php','rb',
22
- 'sql','graphql','gql','env','gitignore','dockerfile',
23
- ])
24
- const EXCEL_EXTS = new Set(['xlsx','xls','csv'])
25
- const WORD_EXTS = new Set(['docx'])
26
-
27
- // 单文件大小上限
28
- const MAX_IMAGE_SIZE = 20 * 1024 * 1024 // 20MB
29
- const MAX_TEXT_SIZE = 2 * 1024 * 1024 // 2MB
30
- const MAX_PDF_SIZE = 50 * 1024 * 1024 // 50MB
31
- const MAX_PDF_PAGES = 20 // PDF 最多处理前 N 页(防止 token 超限)
32
-
33
- // ── 工具函数 ──────────────────────────────────────────────────────
34
- function getExt(name) {
35
- return name.split('.').pop()?.toLowerCase() || ''
36
- }
37
-
38
- function formatSize(bytes) {
39
- if (bytes < 1024) return `${bytes} B`
40
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
41
- return `${(bytes / 1024 / 1024).toFixed(1)} MB`
42
- }
43
-
44
- function readAsDataURL(file) {
45
- return new Promise((resolve, reject) => {
46
- const reader = new FileReader()
47
- reader.onload = e => resolve(e.target.result)
48
- reader.onerror = () => reject(new Error('文件读取失败'))
49
- reader.readAsDataURL(file)
50
- })
51
- }
52
-
53
- function readAsText(file) {
54
- return new Promise((resolve, reject) => {
55
- const reader = new FileReader()
56
- reader.onload = e => resolve(e.target.result)
57
- reader.onerror = () => reject(new Error('文件读取失败'))
58
- reader.readAsText(file, 'utf-8')
59
- })
60
- }
61
-
62
- function readAsArrayBuffer(file) {
63
- return new Promise((resolve, reject) => {
64
- const reader = new FileReader()
65
- reader.onload = e => resolve(e.target.result)
66
- reader.onerror = () => reject(new Error('文件读取失败'))
67
- reader.readAsArrayBuffer(file)
68
- })
69
- }
70
-
71
- // ── 各类型处理器 ──────────────────────────────────────────────────
72
-
73
- /** 图片 → image_url content block */
74
- async function processImage(file) {
75
- if (file.size > MAX_IMAGE_SIZE) {
76
- throw new Error(`图片过大(${formatSize(file.size)}),上限 20MB`)
77
- }
78
- const dataURL = await readAsDataURL(file)
79
-
80
- return {
81
- type: 'visual', // 标记需要多模态模型
82
- preview: dataURL, // 用于 UI 预览缩略图
83
- fileName: file.name,
84
- fileSize: formatSize(file.size),
85
- // OpenAI 兼容格式的 image_url block,直接使用完整 dataURL 避免拆分重组出错
86
- contentBlock: {
87
- type: 'image_url',
88
- image_url: { url: dataURL },
89
- },
90
- }
91
- }
92
-
93
- /** PDF → 每页转 canvas → base64 图片数组 → image_url blocks */
94
- async function processPDF(file) {
95
- if (file.size > MAX_PDF_SIZE) {
96
- throw new Error(`PDF 过大(${formatSize(file.size)}),上限 50MB`)
97
- }
98
-
99
- const arrayBuffer = await readAsArrayBuffer(file)
100
-
101
- // 动态加载 pdfjs
102
- const pdfjsLib = await import('pdfjs-dist').catch(() => null)
103
- if (!pdfjsLib) throw new Error('PDF 解析库加载失败,请刷新重试')
104
-
105
- // 设置 worker(使用 CDN)
106
- if (!pdfjsLib.GlobalWorkerOptions.workerSrc) {
107
- pdfjsLib.GlobalWorkerOptions.workerSrc =
108
- 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/4.4.168/pdf.worker.min.mjs'
109
- }
110
-
111
- const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise
112
- const pageCount = Math.min(pdf.numPages, MAX_PDF_PAGES)
113
- const blocks = []
114
- const canvas = document.createElement('canvas')
115
- const ctx = canvas.getContext('2d')
116
-
117
- // 首页作为预览缩略图
118
- let previewDataURL = ''
119
-
120
- for (let i = 1; i <= pageCount; i++) {
121
- const page = await pdf.getPage(i)
122
- const viewport = page.getViewport({ scale: 1.5 })
123
- canvas.width = viewport.width
124
- canvas.height = viewport.height
125
- await page.render({ canvasContext: ctx, viewport }).promise
126
- const dataURL = canvas.toDataURL('image/jpeg', 0.85)
127
- if (i === 1) previewDataURL = dataURL
128
- blocks.push({
129
- type: 'image_url',
130
- image_url: { url: dataURL },
131
- })
132
- }
133
-
134
- const truncated = pdf.numPages > MAX_PDF_PAGES
135
- ? `\n(PDF 共 ${pdf.numPages} 页,已截取前 ${MAX_PDF_PAGES} 页)` : ''
136
-
137
- return {
138
- type: 'visual',
139
- preview: previewDataURL,
140
- fileName: file.name,
141
- fileSize: formatSize(file.size),
142
- pageCount: pdf.numPages,
143
- processedPages: pageCount,
144
- // 多个 image_url blocks + 一个说明 text block
145
- contentBlocks: [
146
- { type: 'text', text: `[PDF 文件:${file.name},共 ${pageCount} 页${truncated}]` },
147
- ...blocks,
148
- ],
149
- }
150
- }
151
-
152
- /** 文本类文件 → text content block */
153
- async function processText(file) {
154
- if (file.size > MAX_TEXT_SIZE) {
155
- throw new Error(`文件过大(${formatSize(file.size)}),文本文件上限 2MB`)
156
- }
157
- const text = await readAsText(file)
158
- const ext = getExt(file.name)
159
-
160
- // 推断语言标识,用于代码块包裹
161
- const LANG_MAP = {
162
- js:'javascript', mjs:'javascript', cjs:'javascript',
163
- ts:'typescript', jsx:'jsx', tsx:'tsx',
164
- py:'python', go:'go', rs:'rust', java:'java', kt:'kotlin',
165
- swift:'swift', c:'c', cpp:'cpp', cs:'csharp', php:'php', rb:'ruby',
166
- sh:'bash', bash:'bash', zsh:'bash',
167
- html:'html', htm:'html', css:'css', scss:'scss', sass:'sass', less:'less',
168
- vue:'vue', sql:'sql', graphql:'graphql', gql:'graphql',
169
- json:'json', jsonc:'json', yaml:'yaml', yml:'yaml', toml:'toml', xml:'xml',
170
- md:'markdown', markdown:'markdown',
171
- }
172
- const lang = LANG_MAP[ext] || ''
173
-
174
- const wrapped = lang
175
- ? `\`\`\`${lang}\n${text}\n\`\`\``
176
- : text
177
-
178
- return {
179
- type: 'text',
180
- preview: null,
181
- fileName: file.name,
182
- fileSize: formatSize(file.size),
183
- ext,
184
- contentBlock: {
185
- type: 'text',
186
- text: `[文件:${file.name}]\n${wrapped}`,
187
- },
188
- }
189
- }
190
-
191
- /** Excel/CSV → SheetJS → Markdown 表格 → text block */
192
- async function processExcel(file) {
193
- if (file.size > MAX_TEXT_SIZE) {
194
- throw new Error(`文件过大(${formatSize(file.size)}),上限 2MB`)
195
- }
196
-
197
- const XLSX = await import('xlsx').catch(() => null)
198
- if (!XLSX) throw new Error('Excel 解析库加载失败')
199
-
200
- const ab = await readAsArrayBuffer(file)
201
- const wb = XLSX.read(ab, { type: 'array' })
202
- const results = []
203
-
204
- for (const sheetName of wb.SheetNames.slice(0, 5)) {
205
- const ws = wb.Sheets[sheetName]
206
- const csv = XLSX.utils.sheet_to_csv(ws)
207
- results.push(`### Sheet: ${sheetName}\n\`\`\`csv\n${csv}\n\`\`\``)
208
- }
209
-
210
- const text = results.join('\n\n')
211
- return {
212
- type: 'text',
213
- preview: null,
214
- fileName: file.name,
215
- fileSize: formatSize(file.size),
216
- ext: getExt(file.name),
217
- contentBlock: {
218
- type: 'text',
219
- text: `[Excel 文件:${file.name}]\n${text}`,
220
- },
221
- }
222
- }
223
-
224
- /** Word docx → mammoth → 纯文本 → text block */
225
- async function processWord(file) {
226
- if (file.size > MAX_TEXT_SIZE) {
227
- throw new Error(`文件过大(${formatSize(file.size)}),上限 2MB`)
228
- }
229
-
230
- const mammoth = await import('mammoth').catch(() => null)
231
- if (!mammoth) throw new Error('Word 解析库加载失败')
232
-
233
- const ab = await readAsArrayBuffer(file)
234
- const result = await mammoth.extractRawText({ arrayBuffer: ab })
235
- const text = result.value
236
-
237
- return {
238
- type: 'text',
239
- preview: null,
240
- fileName: file.name,
241
- fileSize: formatSize(file.size),
242
- ext: 'docx',
243
- contentBlock: {
244
- type: 'text',
245
- text: `[Word 文件:${file.name}]\n${text}`,
246
- },
247
- }
248
- }
249
-
250
- // ── 主入口 ────────────────────────────────────────────────────────
251
-
252
- /**
253
- * 处理单个 File 对象,返回附件描述对象
254
- *
255
- * 返回结构:
256
- * {
257
- * id: string, // 唯一标识
258
- * type: 'visual'|'text',
259
- * fileName: string,
260
- * fileSize: string,
261
- * preview: string|null, // 图片 dataURL,用于缩略图
262
- * ext: string,
263
- * // 单 block 文件
264
- * contentBlock: object,
265
- * // 多 block 文件(PDF)
266
- * contentBlocks: object[],
267
- * }
268
- */
269
- export async function processFile(file) {
270
- const ext = getExt(file.name)
271
-
272
- let result
273
-
274
- if (IMAGE_TYPES.has(file.type) || file.type.startsWith('image/')) {
275
- result = await processImage(file)
276
- } else if (file.type === PDF_TYPE || ext === 'pdf') {
277
- result = await processPDF(file)
278
- } else if (EXCEL_EXTS.has(ext) || file.type.includes('spreadsheet') || file.type.includes('csv')) {
279
- result = await processExcel(file)
280
- } else if (WORD_EXTS.has(ext) || file.type.includes('wordprocessingml')) {
281
- result = await processWord(file)
282
- } else if (TEXT_EXTS.has(ext) || file.type.startsWith('text/')) {
283
- result = await processText(file)
284
- } else {
285
- throw new Error(`暂不支持 .${ext} 类型的文件`)
286
- }
287
-
288
- return {
289
- id: `file-${Date.now()}-${Math.random().toString(36).slice(2)}`,
290
- ...result,
291
- }
292
- }
293
-
294
- /**
295
- * 把附件列表转成 API content blocks
296
- * 用于拼进 user message 的 content 数组
297
- *
298
- * @param {string} text - 用户输入的文字
299
- * @param {Array} attachments - processFile 返回的附件数组
300
- * @returns {string | Array} - 无附件时返回纯字符串;有附件时返回 content 数组
301
- */
302
- export function buildMessageContent(text, attachments) {
303
- if (!attachments || attachments.length === 0) return text
304
-
305
- const blocks = []
306
-
307
- // 先放所有附件内容
308
- for (const att of attachments) {
309
- if (att.contentBlocks) {
310
- // PDF:多个 block
311
- blocks.push(...att.contentBlocks)
312
- } else if (att.contentBlock) {
313
- blocks.push(att.contentBlock)
314
- }
315
- }
316
-
317
- // 最后放用户文字(符合多模态模型的习惯:图在前,问题在后)
318
- if (text) {
319
- blocks.push({ type: 'text', text })
320
- }
321
-
322
- return blocks
323
- }
324
-
325
- /**
326
- * 支持的文件类型说明(用于 input accept 属性和提示文案)
327
- */
328
- export const ACCEPT_TYPES = [
329
- 'image/jpeg','image/png','image/webp','image/gif',
330
- 'application/pdf',
331
- '.txt','.md','.js','.ts','.jsx','.tsx','.vue','.html','.css','.scss',
332
- '.json','.yaml','.yml','.toml','.xml','.sql','.py','.go','.rs','.java',
333
- '.sh','.bash','.env','.csv','.xlsx','.xls','.docx',
334
- ].join(',')
335
-
336
- export const FILE_TYPE_HINT = '支持图片、PDF、代码、文本、Excel、Word 等格式'
@@ -1,76 +0,0 @@
1
- /**
2
- * headless/api/adapters/openai.js
3
- *
4
- * UI 消息格式 → OpenAI / 兼容格式 转换器
5
- *
6
- * UI 消息格式(headless 内部):
7
- * {
8
- * id, role, parts: [
9
- * { type: 'text', text: '...' },
10
- * { type: 'image', url: 'blob:...', fileName: '...' },
11
- * { type: 'file', text: '...', fileName: '...' },
12
- * ],
13
- * streaming, error, artifacts
14
- * }
15
- *
16
- * OpenAI API 格式:
17
- * { role, content: string | array }
18
- */
19
-
20
- /**
21
- * 把 UI 消息列表转成 OpenAI API 需要的 messages 数组
22
- * @param {UIMessage[]} uiMessages
23
- * @returns {APIMessage[]}
24
- */
25
- export function toAPIMessages(uiMessages) {
26
- return uiMessages
27
- .filter(m => !m.error)
28
- .map(m => ({
29
- role: m.role,
30
- // 优先用 apiContent(InputBar 已由 buildMessageContent 组装好的 blocks)
31
- // 回退到 parts(历史消息或纯文本消息)
32
- content: m.apiContent != null
33
- ? buildContent(m.apiContent)
34
- : buildContent(m.parts),
35
- }))
36
- }
37
-
38
- /**
39
- * @param {string | Array} input - 纯字符串 / parts 数组 / apiContent blocks 数组
40
- */
41
- function buildContent(input) {
42
- // 纯字符串直接返回
43
- if (typeof input === 'string') return input
44
- if (!input || input.length === 0) return ''
45
-
46
- // 纯文本单条:返回字符串避免不必要的数组格式
47
- if (input.length === 1 && input[0].type === 'text') {
48
- return input[0].text
49
- }
50
-
51
- // 数组(parts 或 apiContent blocks)
52
- return input.map(part => {
53
- if (part.type === 'text') {
54
- return { type: 'text', text: part.text ?? '' }
55
- }
56
- // parts 格式的图片
57
- if (part.type === 'image') {
58
- return {
59
- type: 'image_url',
60
- image_url: { url: part.url },
61
- }
62
- }
63
- // parts 格式的文件(文本类)
64
- if (part.type === 'file') {
65
- return {
66
- type: 'text',
67
- text: part.text ?? `[文件: ${part.fileName}]`,
68
- }
69
- }
70
- // apiContent 格式的 image_url block(来自 useFileHandler)
71
- if (part.type === 'image_url') {
72
- return part
73
- }
74
- return null
75
- }).filter(Boolean)
76
- }
@@ -1,126 +0,0 @@
1
- /**
2
- * chat-sdk/headless/api/chatApi.js
3
- *
4
- * 流式对话请求,接收 modelConfig 对象,不依赖任何全局配置。
5
- * 上层(useChatStore)负责根据当前选中模型和消息内容选出 modelConfig 传入。
6
- *
7
- * 内置 <think>...</think> 过滤:Qwen3 等模型流式输出时会携带思考块,
8
- * 通过状态机在 token 层面过滤,思考内容不传给 onToken。
9
- */
10
-
11
- /**
12
- * 发起流式对话请求
13
- * @param {object} opts
14
- * @param {object} opts.modelConfig - { id, baseURL, apiKey }
15
- * @param {Array} opts.messages - API 格式消息数组(已由 adapter 转换)
16
- * @param {Function} opts.onToken - (token: string) => void
17
- * @param {Function} opts.onDone - () => void
18
- * @param {Function} opts.onError - (err: Error) => void
19
- * @param {AbortSignal} opts.signal
20
- */
21
- export async function streamChat({ modelConfig, messages, onToken, onDone, onError, signal }) {
22
- try {
23
- const { id: model, baseURL, apiKey } = modelConfig
24
-
25
- const res = await fetch(`${baseURL}/v1/chat/completions`, {
26
- method: 'POST',
27
- headers: {
28
- 'Content-Type': 'application/json',
29
- 'Authorization': `Bearer ${apiKey}`,
30
- },
31
- body: JSON.stringify({
32
- model,
33
- stream: true,
34
- messages,
35
- // 关闭 Qwen3 思考模式;其他模型忽略此参数
36
- enable_thinking: false,
37
- }),
38
- signal,
39
- })
40
-
41
- if (!res.ok) {
42
- const errBody = await res.text()
43
- throw new Error(`API 请求失败 ${res.status}: ${errBody}`)
44
- }
45
-
46
- const reader = res.body.getReader()
47
- const decoder = new TextDecoder('utf-8')
48
- let buffer = ''
49
-
50
- // ── <think> 过滤状态机 ────────────────────────────────────────
51
- // 部分模型即使 enable_thinking:false 仍可能输出 <think> 块,做双重兜底
52
- let inThink = false // 当前是否在 <think>...</think> 内
53
- let thinkBuf = '' // 未完成的标签碎片缓冲
54
-
55
- const OPEN_TAG = '<think>'
56
- const CLOSE_TAG = '</think>'
57
-
58
- function filterThink(token) {
59
- // 把 token 追加到缓冲,逐字符处理
60
- thinkBuf += token
61
- let output = ''
62
-
63
- while (thinkBuf.length > 0) {
64
- if (!inThink) {
65
- const openIdx = thinkBuf.indexOf(OPEN_TAG)
66
- if (openIdx === -1) {
67
- // 没有开标签,但末尾可能是开标签的前缀,保留等待后续 token
68
- const maxSafe = thinkBuf.length - (OPEN_TAG.length - 1)
69
- if (maxSafe > 0) {
70
- output += thinkBuf.slice(0, maxSafe)
71
- thinkBuf = thinkBuf.slice(maxSafe)
72
- }
73
- break
74
- }
75
- // 开标签前的内容直接输出
76
- output += thinkBuf.slice(0, openIdx)
77
- thinkBuf = thinkBuf.slice(openIdx + OPEN_TAG.length)
78
- inThink = true
79
- } else {
80
- const closeIdx = thinkBuf.indexOf(CLOSE_TAG)
81
- if (closeIdx === -1) {
82
- // 还没找到关闭标签,继续等待(保留末尾可能是关闭标签前缀的部分)
83
- const maxSafe = thinkBuf.length - (CLOSE_TAG.length - 1)
84
- if (maxSafe > 0) thinkBuf = thinkBuf.slice(maxSafe)
85
- else thinkBuf = thinkBuf // 全部保留
86
- break
87
- }
88
- // 跳过 think 内容,从关闭标签后继续
89
- thinkBuf = thinkBuf.slice(closeIdx + CLOSE_TAG.length)
90
- inThink = false
91
- }
92
- }
93
-
94
- return output
95
- }
96
- // ─────────────────────────────────────────────────────────────
97
-
98
- while (true) {
99
- const { done, value } = await reader.read()
100
- if (done) break
101
- buffer += decoder.decode(value, { stream: true })
102
- const lines = buffer.split('\n')
103
- buffer = lines.pop()
104
-
105
- for (const line of lines) {
106
- const trimmed = line.trim()
107
- if (!trimmed || trimmed === 'data: [DONE]') continue
108
- if (!trimmed.startsWith('data: ')) continue
109
- try {
110
- const json = JSON.parse(trimmed.slice(6))
111
- const token = json.choices?.[0]?.delta?.content
112
-
113
- if (token) {
114
- // const filtered = filterThink(token)
115
- // if (filtered) onToken(filtered)
116
- onToken(token)
117
- }
118
- } catch { /* 非 JSON 行跳过 */ }
119
- }
120
- }
121
- onDone?.()
122
- } catch (err) {
123
- if (err.name === 'AbortError') return
124
- onError?.(err)
125
- }
126
- }