@templmf/temp-solf-lmf 0.0.55 → 0.0.56
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.
- package/package.json +1 -1
- package//345/242/236/351/207/217/351/234/200/346/261/202prompt +72 -0
- package/guanwang/README.md +0 -95
- package/guanwang/docs/changelog.md +0 -145
- package/guanwang/docs/doc-maintenance.md +0 -229
- package/guanwang/docs/product.md +0 -181
- package/guanwang/docs/test-cases.md +0 -395
- package/guanwang/docs/usage.md +0 -291
- package/guanwang/env.example +0 -27
- package/guanwang/index.html +0 -13
- package/guanwang/package-lock.json +0 -3825
- package/guanwang/package.json +0 -32
- package/guanwang/public/favicon.svg +0 -4
- package/guanwang/public/react-runtime/babel.min.js +0 -4
- package/guanwang/public/react-runtime/react-dom.min.js +0 -267
- package/guanwang/public/react-runtime/react.min.js +0 -31
- package/guanwang/public/vue-repl-assets/compiler-sfc.esm-browser.js +0 -50795
- package/guanwang/public/vue-repl-assets/runtime-dom.esm-browser.js +0 -12758
- package/guanwang/public/vue-repl-assets/server-renderer.esm-browser.js +0 -8600
- package/guanwang/public/vue-repl-assets/vue.esm-browser.js +0 -18672
- package/guanwang/src/App.vue +0 -61
- package/guanwang/src/chat-sdk/core/components/ChatBox.vue +0 -305
- package/guanwang/src/chat-sdk/core/components/ChatSidebar.vue +0 -84
- package/guanwang/src/chat-sdk/core/components/InputBar.vue +0 -354
- package/guanwang/src/chat-sdk/core/components/MessageBubble.vue +0 -703
- package/guanwang/src/chat-sdk/core/useTheme.js +0 -31
- package/guanwang/src/chat-sdk/features/artifact/ArtifactCard.vue +0 -172
- package/guanwang/src/chat-sdk/features/artifact/ArtifactPanel.vue +0 -963
- package/guanwang/src/chat-sdk/features/artifact/index.js +0 -13
- package/guanwang/src/chat-sdk/features/artifact/useArtifactStore.js +0 -275
- package/guanwang/src/chat-sdk/features/codepreview/CodePreview.vue +0 -523
- package/guanwang/src/chat-sdk/features/codepreview/index.js +0 -7
- package/guanwang/src/chat-sdk/features/markdown/index.js +0 -13
- package/guanwang/src/chat-sdk/features/markdown/useMarkdown.js +0 -724
- package/guanwang/src/chat-sdk/features/mermaid/MermaidZoom.vue +0 -254
- package/guanwang/src/chat-sdk/features/upload/FileAttachment.vue +0 -142
- package/guanwang/src/chat-sdk/features/upload/index.js +0 -17
- package/guanwang/src/chat-sdk/features/upload/useFileHandler.js +0 -336
- package/guanwang/src/chat-sdk/headless/api/adapters/openai.js +0 -76
- package/guanwang/src/chat-sdk/headless/api/chatApi.js +0 -126
- package/guanwang/src/chat-sdk/headless/buildSystemPrompt.js +0 -351
- package/guanwang/src/chat-sdk/headless/index.js +0 -15
- package/guanwang/src/chat-sdk/headless/useChat.js +0 -77
- package/guanwang/src/chat-sdk/headless/useChatDB.js +0 -147
- package/guanwang/src/chat-sdk/headless/useChatStore.js +0 -529
- package/guanwang/src/chat-sdk/index.js +0 -79
- package/guanwang/src/chat-sdk/modes/architect.js +0 -27
- package/guanwang/src/chat-sdk/modes/ask.js +0 -26
- package/guanwang/src/chat-sdk/modes/code.js +0 -25
- package/guanwang/src/chat-sdk/modes/index.js +0 -36
- package/guanwang/src/chat-sdk/modes/requirements.js +0 -175
- package/guanwang/src/chat-sdk/settings/SettingsPanel.vue +0 -170
- package/guanwang/src/chat-sdk/settings/index.js +0 -9
- package/guanwang/src/chat-sdk/settings/useSettings.js +0 -122
- package/guanwang/src/chat-sdk/tools/defaults.js +0 -89
- package/guanwang/src/chat-sdk/tools/index.js +0 -16
- package/guanwang/src/chat-sdk/tools/parser.js +0 -116
- package/guanwang/src/components/CustomCursor.vue +0 -69
- package/guanwang/src/components/Footer.vue +0 -24
- package/guanwang/src/components/LoginModal.vue +0 -109
- package/guanwang/src/components/Navbar.vue +0 -193
- package/guanwang/src/components/ThemeToggle.vue +0 -25
- package/guanwang/src/composables/useArtifactStore.js +0 -253
- package/guanwang/src/composables/useAuth.js +0 -88
- package/guanwang/src/composables/useChatDB.js +0 -147
- package/guanwang/src/composables/useCountUp.js +0 -24
- package/guanwang/src/composables/useFileHandler.js +0 -345
- package/guanwang/src/composables/useTheme.js +0 -31
- package/guanwang/src/config/api.js +0 -71
- package/guanwang/src/main.js +0 -23
- package/guanwang/src/router/index.js +0 -23
- package/guanwang/src/services/authApi.js +0 -27
- package/guanwang/src/services/chatApi.js +0 -66
- package/guanwang/src/styles/global.css +0 -478
- package/guanwang/src/tracker/analyze.js +0 -73
- package/guanwang/src/tracker/config.js +0 -82
- package/guanwang/src/tracker/index.js +0 -18
- package/guanwang/src/tracker/service.js +0 -102
- package/guanwang/src/tracker/useChatTracker.js +0 -179
- package/guanwang/src/tracker/useTracker.js +0 -45
- package/guanwang/src/views/ChatView.vue +0 -65
- package/guanwang/src/views/HomeView.vue +0 -156
- package/guanwang/src/views/MarketView.vue +0 -143
- package/guanwang/src/views/PracticesView.vue +0 -190
- package/guanwang/src/views/SkillsView.vue +0 -129
- package/guanwang/temp +0 -19
- package/guanwang/vite.config.js +0 -6
|
@@ -1,345 +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
|
-
// dataURL → { mediaType, base64 }
|
|
72
|
-
function parseDataURL(dataURL) {
|
|
73
|
-
const [header, data] = dataURL.split(',')
|
|
74
|
-
const mediaType = header.match(/:(.*?);/)?.[1] || 'application/octet-stream'
|
|
75
|
-
return { mediaType, base64: data }
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// ── 各类型处理器 ──────────────────────────────────────────────────
|
|
79
|
-
|
|
80
|
-
/** 图片 → image_url content block */
|
|
81
|
-
async function processImage(file) {
|
|
82
|
-
if (file.size > MAX_IMAGE_SIZE) {
|
|
83
|
-
throw new Error(`图片过大(${formatSize(file.size)}),上限 20MB`)
|
|
84
|
-
}
|
|
85
|
-
const dataURL = await readAsDataURL(file)
|
|
86
|
-
const { mediaType, base64 } = parseDataURL(dataURL)
|
|
87
|
-
|
|
88
|
-
return {
|
|
89
|
-
type: 'visual', // 标记需要多模态模型
|
|
90
|
-
preview: dataURL, // 用于 UI 预览缩略图
|
|
91
|
-
fileName: file.name,
|
|
92
|
-
fileSize: formatSize(file.size),
|
|
93
|
-
// OpenAI 兼容格式的 image_url block
|
|
94
|
-
contentBlock: {
|
|
95
|
-
type: 'image_url',
|
|
96
|
-
image_url: { url: `data:${mediaType};base64,${base64}` },
|
|
97
|
-
},
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/** PDF → 每页转 canvas → base64 图片数组 → image_url blocks */
|
|
102
|
-
async function processPDF(file) {
|
|
103
|
-
if (file.size > MAX_PDF_SIZE) {
|
|
104
|
-
throw new Error(`PDF 过大(${formatSize(file.size)}),上限 50MB`)
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
const arrayBuffer = await readAsArrayBuffer(file)
|
|
108
|
-
|
|
109
|
-
// 动态加载 pdfjs
|
|
110
|
-
const pdfjsLib = await import('pdfjs-dist').catch(() => null)
|
|
111
|
-
if (!pdfjsLib) throw new Error('PDF 解析库加载失败,请刷新重试')
|
|
112
|
-
|
|
113
|
-
// 设置 worker(使用 CDN)
|
|
114
|
-
if (!pdfjsLib.GlobalWorkerOptions.workerSrc) {
|
|
115
|
-
pdfjsLib.GlobalWorkerOptions.workerSrc =
|
|
116
|
-
'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/4.4.168/pdf.worker.min.mjs'
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise
|
|
120
|
-
const pageCount = Math.min(pdf.numPages, MAX_PDF_PAGES)
|
|
121
|
-
const blocks = []
|
|
122
|
-
const canvas = document.createElement('canvas')
|
|
123
|
-
const ctx = canvas.getContext('2d')
|
|
124
|
-
|
|
125
|
-
// 首页作为预览缩略图
|
|
126
|
-
let previewDataURL = ''
|
|
127
|
-
|
|
128
|
-
for (let i = 1; i <= pageCount; i++) {
|
|
129
|
-
const page = await pdf.getPage(i)
|
|
130
|
-
const viewport = page.getViewport({ scale: 1.5 })
|
|
131
|
-
canvas.width = viewport.width
|
|
132
|
-
canvas.height = viewport.height
|
|
133
|
-
await page.render({ canvasContext: ctx, viewport }).promise
|
|
134
|
-
const dataURL = canvas.toDataURL('image/jpeg', 0.85)
|
|
135
|
-
if (i === 1) previewDataURL = dataURL
|
|
136
|
-
const { mediaType, base64 } = parseDataURL(dataURL)
|
|
137
|
-
blocks.push({
|
|
138
|
-
type: 'image_url',
|
|
139
|
-
image_url: { url: `data:${mediaType};base64,${base64}` },
|
|
140
|
-
})
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
const truncated = pdf.numPages > MAX_PDF_PAGES
|
|
144
|
-
? `\n(PDF 共 ${pdf.numPages} 页,已截取前 ${MAX_PDF_PAGES} 页)` : ''
|
|
145
|
-
|
|
146
|
-
return {
|
|
147
|
-
type: 'visual',
|
|
148
|
-
preview: previewDataURL,
|
|
149
|
-
fileName: file.name,
|
|
150
|
-
fileSize: formatSize(file.size),
|
|
151
|
-
pageCount: pdf.numPages,
|
|
152
|
-
processedPages: pageCount,
|
|
153
|
-
// 多个 image_url blocks + 一个说明 text block
|
|
154
|
-
contentBlocks: [
|
|
155
|
-
{ type: 'text', text: `[PDF 文件:${file.name},共 ${pageCount} 页${truncated}]` },
|
|
156
|
-
...blocks,
|
|
157
|
-
],
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
/** 文本类文件 → text content block */
|
|
162
|
-
async function processText(file) {
|
|
163
|
-
if (file.size > MAX_TEXT_SIZE) {
|
|
164
|
-
throw new Error(`文件过大(${formatSize(file.size)}),文本文件上限 2MB`)
|
|
165
|
-
}
|
|
166
|
-
const text = await readAsText(file)
|
|
167
|
-
const ext = getExt(file.name)
|
|
168
|
-
|
|
169
|
-
// 推断语言标识,用于代码块包裹
|
|
170
|
-
const LANG_MAP = {
|
|
171
|
-
js:'javascript', mjs:'javascript', cjs:'javascript',
|
|
172
|
-
ts:'typescript', jsx:'jsx', tsx:'tsx',
|
|
173
|
-
py:'python', go:'go', rs:'rust', java:'java', kt:'kotlin',
|
|
174
|
-
swift:'swift', c:'c', cpp:'cpp', cs:'csharp', php:'php', rb:'ruby',
|
|
175
|
-
sh:'bash', bash:'bash', zsh:'bash',
|
|
176
|
-
html:'html', htm:'html', css:'css', scss:'scss', sass:'sass', less:'less',
|
|
177
|
-
vue:'vue', sql:'sql', graphql:'graphql', gql:'graphql',
|
|
178
|
-
json:'json', jsonc:'json', yaml:'yaml', yml:'yaml', toml:'toml', xml:'xml',
|
|
179
|
-
md:'markdown', markdown:'markdown',
|
|
180
|
-
}
|
|
181
|
-
const lang = LANG_MAP[ext] || ''
|
|
182
|
-
|
|
183
|
-
const wrapped = lang
|
|
184
|
-
? `\`\`\`${lang}\n${text}\n\`\`\``
|
|
185
|
-
: text
|
|
186
|
-
|
|
187
|
-
return {
|
|
188
|
-
type: 'text',
|
|
189
|
-
preview: null,
|
|
190
|
-
fileName: file.name,
|
|
191
|
-
fileSize: formatSize(file.size),
|
|
192
|
-
ext,
|
|
193
|
-
contentBlock: {
|
|
194
|
-
type: 'text',
|
|
195
|
-
text: `[文件:${file.name}]\n${wrapped}`,
|
|
196
|
-
},
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
/** Excel/CSV → SheetJS → Markdown 表格 → text block */
|
|
201
|
-
async function processExcel(file) {
|
|
202
|
-
if (file.size > MAX_TEXT_SIZE) {
|
|
203
|
-
throw new Error(`文件过大(${formatSize(file.size)}),上限 2MB`)
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
const XLSX = await import('xlsx').catch(() => null)
|
|
207
|
-
if (!XLSX) throw new Error('Excel 解析库加载失败')
|
|
208
|
-
|
|
209
|
-
const ab = await readAsArrayBuffer(file)
|
|
210
|
-
const wb = XLSX.read(ab, { type: 'array' })
|
|
211
|
-
const results = []
|
|
212
|
-
|
|
213
|
-
for (const sheetName of wb.SheetNames.slice(0, 5)) {
|
|
214
|
-
const ws = wb.Sheets[sheetName]
|
|
215
|
-
const csv = XLSX.utils.sheet_to_csv(ws)
|
|
216
|
-
results.push(`### Sheet: ${sheetName}\n\`\`\`csv\n${csv}\n\`\`\``)
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
const text = results.join('\n\n')
|
|
220
|
-
return {
|
|
221
|
-
type: 'text',
|
|
222
|
-
preview: null,
|
|
223
|
-
fileName: file.name,
|
|
224
|
-
fileSize: formatSize(file.size),
|
|
225
|
-
ext: getExt(file.name),
|
|
226
|
-
contentBlock: {
|
|
227
|
-
type: 'text',
|
|
228
|
-
text: `[Excel 文件:${file.name}]\n${text}`,
|
|
229
|
-
},
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
/** Word docx → mammoth → 纯文本 → text block */
|
|
234
|
-
async function processWord(file) {
|
|
235
|
-
if (file.size > MAX_TEXT_SIZE) {
|
|
236
|
-
throw new Error(`文件过大(${formatSize(file.size)}),上限 2MB`)
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
const mammoth = await import('mammoth').catch(() => null)
|
|
240
|
-
if (!mammoth) throw new Error('Word 解析库加载失败')
|
|
241
|
-
|
|
242
|
-
const ab = await readAsArrayBuffer(file)
|
|
243
|
-
const result = await mammoth.extractRawText({ arrayBuffer: ab })
|
|
244
|
-
const text = result.value
|
|
245
|
-
|
|
246
|
-
return {
|
|
247
|
-
type: 'text',
|
|
248
|
-
preview: null,
|
|
249
|
-
fileName: file.name,
|
|
250
|
-
fileSize: formatSize(file.size),
|
|
251
|
-
ext: 'docx',
|
|
252
|
-
contentBlock: {
|
|
253
|
-
type: 'text',
|
|
254
|
-
text: `[Word 文件:${file.name}]\n${text}`,
|
|
255
|
-
},
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
// ── 主入口 ────────────────────────────────────────────────────────
|
|
260
|
-
|
|
261
|
-
/**
|
|
262
|
-
* 处理单个 File 对象,返回附件描述对象
|
|
263
|
-
*
|
|
264
|
-
* 返回结构:
|
|
265
|
-
* {
|
|
266
|
-
* id: string, // 唯一标识
|
|
267
|
-
* type: 'visual'|'text',
|
|
268
|
-
* fileName: string,
|
|
269
|
-
* fileSize: string,
|
|
270
|
-
* preview: string|null, // 图片 dataURL,用于缩略图
|
|
271
|
-
* ext: string,
|
|
272
|
-
* // 单 block 文件
|
|
273
|
-
* contentBlock: object,
|
|
274
|
-
* // 多 block 文件(PDF)
|
|
275
|
-
* contentBlocks: object[],
|
|
276
|
-
* }
|
|
277
|
-
*/
|
|
278
|
-
export async function processFile(file) {
|
|
279
|
-
const ext = getExt(file.name)
|
|
280
|
-
|
|
281
|
-
let result
|
|
282
|
-
|
|
283
|
-
if (IMAGE_TYPES.has(file.type) || file.type.startsWith('image/')) {
|
|
284
|
-
result = await processImage(file)
|
|
285
|
-
} else if (file.type === PDF_TYPE || ext === 'pdf') {
|
|
286
|
-
result = await processPDF(file)
|
|
287
|
-
} else if (EXCEL_EXTS.has(ext) || file.type.includes('spreadsheet') || file.type.includes('csv')) {
|
|
288
|
-
result = await processExcel(file)
|
|
289
|
-
} else if (WORD_EXTS.has(ext) || file.type.includes('wordprocessingml')) {
|
|
290
|
-
result = await processWord(file)
|
|
291
|
-
} else if (TEXT_EXTS.has(ext) || file.type.startsWith('text/')) {
|
|
292
|
-
result = await processText(file)
|
|
293
|
-
} else {
|
|
294
|
-
throw new Error(`暂不支持 .${ext} 类型的文件`)
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
return {
|
|
298
|
-
id: `file-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
299
|
-
...result,
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
/**
|
|
304
|
-
* 把附件列表转成 API content blocks
|
|
305
|
-
* 用于拼进 user message 的 content 数组
|
|
306
|
-
*
|
|
307
|
-
* @param {string} text - 用户输入的文字
|
|
308
|
-
* @param {Array} attachments - processFile 返回的附件数组
|
|
309
|
-
* @returns {string | Array} - 无附件时返回纯字符串;有附件时返回 content 数组
|
|
310
|
-
*/
|
|
311
|
-
export function buildMessageContent(text, attachments) {
|
|
312
|
-
if (!attachments || attachments.length === 0) return text
|
|
313
|
-
|
|
314
|
-
const blocks = []
|
|
315
|
-
|
|
316
|
-
// 先放所有附件内容
|
|
317
|
-
for (const att of attachments) {
|
|
318
|
-
if (att.contentBlocks) {
|
|
319
|
-
// PDF:多个 block
|
|
320
|
-
blocks.push(...att.contentBlocks)
|
|
321
|
-
} else if (att.contentBlock) {
|
|
322
|
-
blocks.push(att.contentBlock)
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
// 最后放用户文字(符合多模态模型的习惯:图在前,问题在后)
|
|
327
|
-
if (text) {
|
|
328
|
-
blocks.push({ type: 'text', text })
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
return blocks
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
/**
|
|
335
|
-
* 支持的文件类型说明(用于 input accept 属性和提示文案)
|
|
336
|
-
*/
|
|
337
|
-
export const ACCEPT_TYPES = [
|
|
338
|
-
'image/jpeg','image/png','image/webp','image/gif',
|
|
339
|
-
'application/pdf',
|
|
340
|
-
'.txt','.md','.js','.ts','.jsx','.tsx','.vue','.html','.css','.scss',
|
|
341
|
-
'.json','.yaml','.yml','.toml','.xml','.sql','.py','.go','.rs','.java',
|
|
342
|
-
'.sh','.bash','.env','.csv','.xlsx','.xls','.docx',
|
|
343
|
-
].join(',')
|
|
344
|
-
|
|
345
|
-
export const FILE_TYPE_HINT = '支持图片、PDF、代码、文本、Excel、Word 等格式'
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* useTheme.js
|
|
3
|
-
* 主题(深色/浅色)管理,provide/inject 模式
|
|
4
|
-
*/
|
|
5
|
-
import { ref, watch, provide, inject } from 'vue'
|
|
6
|
-
|
|
7
|
-
const THEME_KEY = Symbol('theme')
|
|
8
|
-
|
|
9
|
-
export function createTheme() {
|
|
10
|
-
const theme = ref('light')
|
|
11
|
-
|
|
12
|
-
watch(theme, val => {
|
|
13
|
-
document.documentElement.setAttribute('data-theme', val)
|
|
14
|
-
}, { immediate: true })
|
|
15
|
-
|
|
16
|
-
function toggleTheme() {
|
|
17
|
-
theme.value = theme.value === 'dark' ? 'light' : 'dark'
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
return { theme, toggleTheme }
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function provideTheme() {
|
|
24
|
-
const themeStore = createTheme()
|
|
25
|
-
provide(THEME_KEY, themeStore)
|
|
26
|
-
return themeStore
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export function useTheme() {
|
|
30
|
-
return inject(THEME_KEY)
|
|
31
|
-
}
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* API 配置
|
|
3
|
-
* 在项目根目录创建 .env.local,填入以下变量:
|
|
4
|
-
*
|
|
5
|
-
* # 文本 LLM
|
|
6
|
-
* VITE_API_BASE_URL=https://your-api-host.com
|
|
7
|
-
* VITE_API_KEY=sk-xxxxxxxxxxxxxxxxxxxx
|
|
8
|
-
* VITE_MODEL=gpt-4o
|
|
9
|
-
*
|
|
10
|
-
* # 多模型选择(逗号分隔,不填则只显示默认模型)
|
|
11
|
-
* VITE_MODELS=gpt-4o,gpt-4o-mini,claude-3-5-sonnet-20241022,deepseek-chat
|
|
12
|
-
*
|
|
13
|
-
* # 多模态视觉模型(独立 endpoint / key,不填则复用上面的配置)
|
|
14
|
-
* VITE_VL_API_BASE_URL=https://your-vl-api-host.com
|
|
15
|
-
* VITE_VL_API_KEY=sk-xxxxxxxxxxxxxxxxxxxx
|
|
16
|
-
* VITE_VL_MODEL=qwen3-vl-8b-instruct-k100
|
|
17
|
-
*
|
|
18
|
-
* # 调试:在气泡底部显示原始文本
|
|
19
|
-
* VITE_SHOW_RAW_TEXT=true
|
|
20
|
-
*/
|
|
21
|
-
export const API_CONFIG = {
|
|
22
|
-
baseURL: import.meta.env.VITE_API_BASE_URL || 'https://api.openai.com',
|
|
23
|
-
apiKey: import.meta.env.VITE_API_KEY || 'sk-your-key-here',
|
|
24
|
-
model: import.meta.env.VITE_MODEL || 'gpt-4o',
|
|
25
|
-
|
|
26
|
-
vlBaseURL: import.meta.env.VITE_VL_API_BASE_URL || null,
|
|
27
|
-
vlApiKey: import.meta.env.VITE_VL_API_KEY || null,
|
|
28
|
-
vlModel: import.meta.env.VITE_VL_MODEL || null,
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* 从 VITE_MODELS 读取可选模型列表
|
|
33
|
-
* 格式:逗号分隔的模型 id,例如:gpt-4o,gpt-4o-mini,claude-3-5-sonnet-20241022
|
|
34
|
-
* 若未配置则返回仅含默认模型的数组
|
|
35
|
-
*/
|
|
36
|
-
export function getAvailableModels() {
|
|
37
|
-
const raw = import.meta.env.VITE_MODELS || ''
|
|
38
|
-
const list = raw.split(',').map(s => s.trim()).filter(Boolean)
|
|
39
|
-
if (list.length === 0) return [API_CONFIG.model]
|
|
40
|
-
// 确保默认模型在列表中
|
|
41
|
-
if (!list.includes(API_CONFIG.model)) list.unshift(API_CONFIG.model)
|
|
42
|
-
return list
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* 根据消息内容判断是否需要视觉模型
|
|
47
|
-
*/
|
|
48
|
-
export function isVisualRequest(messages) {
|
|
49
|
-
return messages.some(m => {
|
|
50
|
-
const c = m.content
|
|
51
|
-
return Array.isArray(c) && c.some(b => b.type === 'image_url' || b.type === 'image')
|
|
52
|
-
})
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* 根据消息内容选择对应的请求配置(model / baseURL / apiKey)
|
|
57
|
-
*/
|
|
58
|
-
export function selectConfig(messages) {
|
|
59
|
-
const useVl = isVisualRequest(messages) && API_CONFIG.vlModel
|
|
60
|
-
return useVl
|
|
61
|
-
? {
|
|
62
|
-
model: API_CONFIG.vlModel,
|
|
63
|
-
baseURL: API_CONFIG.vlBaseURL || API_CONFIG.baseURL,
|
|
64
|
-
apiKey: API_CONFIG.vlApiKey || API_CONFIG.apiKey,
|
|
65
|
-
}
|
|
66
|
-
: {
|
|
67
|
-
model: API_CONFIG.model,
|
|
68
|
-
baseURL: API_CONFIG.baseURL,
|
|
69
|
-
apiKey: API_CONFIG.apiKey,
|
|
70
|
-
}
|
|
71
|
-
}
|
package/guanwang/src/main.js
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import { createApp } from 'vue'
|
|
2
|
-
import App from './App.vue'
|
|
3
|
-
import router from './router/index.js'
|
|
4
|
-
import ElementPlus from 'element-plus'
|
|
5
|
-
import 'element-plus/dist/index.css'
|
|
6
|
-
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
|
7
|
-
import 'katex/dist/katex.min.css'
|
|
8
|
-
import './styles/global.css' // 在 EP css 之后引入,覆盖主题变量
|
|
9
|
-
|
|
10
|
-
// 挂载 KaTeX 到 window,供 useMarkdown 同步调用
|
|
11
|
-
import katex from 'katex'
|
|
12
|
-
window.katex = katex
|
|
13
|
-
|
|
14
|
-
const app = createApp(App)
|
|
15
|
-
|
|
16
|
-
// 注册所有 Element Plus 图标
|
|
17
|
-
for (const [name, comp] of Object.entries(ElementPlusIconsVue)) {
|
|
18
|
-
app.component(name, comp)
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
app.use(router)
|
|
22
|
-
app.use(ElementPlus)
|
|
23
|
-
app.mount('#app')
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import { createRouter, createWebHistory } from 'vue-router'
|
|
2
|
-
import HomeView from '../views/HomeView.vue'
|
|
3
|
-
import ChatView from '../views/ChatView.vue'
|
|
4
|
-
import MarketView from '../views/MarketView.vue'
|
|
5
|
-
import SkillsView from '../views/SkillsView.vue'
|
|
6
|
-
import PracticesView from '../views/PracticesView.vue'
|
|
7
|
-
|
|
8
|
-
const routes = [
|
|
9
|
-
{ path: '/', component: HomeView },
|
|
10
|
-
{ path: '/chat', component: ChatView },
|
|
11
|
-
{ path: '/market', component: MarketView },
|
|
12
|
-
{ path: '/skills', component: SkillsView },
|
|
13
|
-
{ path: '/practices', component: PracticesView },
|
|
14
|
-
{ path: '/:pathMatch(.*)*', redirect: '/' },
|
|
15
|
-
]
|
|
16
|
-
|
|
17
|
-
const router = createRouter({
|
|
18
|
-
history: createWebHistory(),
|
|
19
|
-
routes,
|
|
20
|
-
scrollBehavior: () => ({ top: 0 }),
|
|
21
|
-
})
|
|
22
|
-
|
|
23
|
-
export default router
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* authApi.js
|
|
3
|
-
* 当前:mock 本地账号验证
|
|
4
|
-
* 后续对接后端:将 login() 替换为真实 fetch 调用,返回格式保持 { user, token }
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
const MOCK_USERS = [
|
|
8
|
-
{ id: 1, username: 'admin', password: 'admin123', name: '管理员', avatar: null, role: 'admin' },
|
|
9
|
-
{ id: 2, username: 'dev', password: 'dev123', name: '开发者', avatar: null, role: 'developer' },
|
|
10
|
-
]
|
|
11
|
-
|
|
12
|
-
function delay(ms) {
|
|
13
|
-
return new Promise(resolve => setTimeout(resolve, ms))
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export async function login(username, password) {
|
|
17
|
-
await delay(600)
|
|
18
|
-
const matched = MOCK_USERS.find(u => u.username === username && u.password === password)
|
|
19
|
-
if (!matched) throw new Error('账号或密码错误')
|
|
20
|
-
const { password: _pwd, ...user } = matched
|
|
21
|
-
const token = `mock-token-${user.id}-${Date.now()}`
|
|
22
|
-
return { user, token }
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export async function logout() {
|
|
26
|
-
await delay(200)
|
|
27
|
-
}
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* chatApi.js
|
|
3
|
-
* 封装 OpenAI 兼容格式的流式对话接口(/v1/chat/completions)
|
|
4
|
-
* 支持多模态 content 数组格式(图片 / 文本混合)
|
|
5
|
-
*/
|
|
6
|
-
import { selectConfig } from '../config/api.js'
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* 发起流式对话请求
|
|
10
|
-
* @param {object} options
|
|
11
|
-
* @param {Array} options.messages - 消息历史,content 可为字符串或数组(多模态)
|
|
12
|
-
* @param {string} [options.modelOverride] - 手动指定模型,不填则使用配置默认值
|
|
13
|
-
* @param {Function} options.onToken - (token: string) => void
|
|
14
|
-
* @param {Function} options.onDone - 流结束
|
|
15
|
-
* @param {Function} options.onError - (error: Error) => void
|
|
16
|
-
* @param {AbortSignal} options.signal
|
|
17
|
-
*/
|
|
18
|
-
export async function streamChat({ messages, modelOverride, onToken, onDone, onError, signal }) {
|
|
19
|
-
try {
|
|
20
|
-
const cfg = selectConfig(messages)
|
|
21
|
-
const model = modelOverride || cfg.model
|
|
22
|
-
const { baseURL, apiKey } = cfg
|
|
23
|
-
|
|
24
|
-
const res = await fetch(`${baseURL}/v1/chat/completions`, {
|
|
25
|
-
method: 'POST',
|
|
26
|
-
headers: {
|
|
27
|
-
'Content-Type': 'application/json',
|
|
28
|
-
'Authorization': `Bearer ${apiKey}`,
|
|
29
|
-
},
|
|
30
|
-
body: JSON.stringify({ model, stream: true, messages }),
|
|
31
|
-
signal,
|
|
32
|
-
})
|
|
33
|
-
|
|
34
|
-
if (!res.ok) {
|
|
35
|
-
const errBody = await res.text()
|
|
36
|
-
throw new Error(`API 请求失败 ${res.status}: ${errBody}`)
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const reader = res.body.getReader()
|
|
40
|
-
const decoder = new TextDecoder('utf-8')
|
|
41
|
-
let buffer = ''
|
|
42
|
-
|
|
43
|
-
while (true) {
|
|
44
|
-
const { done, value } = await reader.read()
|
|
45
|
-
if (done) break
|
|
46
|
-
buffer += decoder.decode(value, { stream: true })
|
|
47
|
-
const lines = buffer.split('\n')
|
|
48
|
-
buffer = lines.pop()
|
|
49
|
-
|
|
50
|
-
for (const line of lines) {
|
|
51
|
-
const trimmed = line.trim()
|
|
52
|
-
if (!trimmed || trimmed === 'data: [DONE]') continue
|
|
53
|
-
if (!trimmed.startsWith('data: ')) continue
|
|
54
|
-
try {
|
|
55
|
-
const json = JSON.parse(trimmed.slice(6))
|
|
56
|
-
const token = json.choices?.[0]?.delta?.content
|
|
57
|
-
if (token) onToken(token)
|
|
58
|
-
} catch { /* 非 JSON 行跳过 */ }
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
onDone?.()
|
|
62
|
-
} catch (err) {
|
|
63
|
-
if (err.name === 'AbortError') return
|
|
64
|
-
onError?.(err)
|
|
65
|
-
}
|
|
66
|
-
}
|