@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.
- package/fe-flow.md +348 -0
- package/package.json +1 -1
- package/ui-parse.md +308 -0
- 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,529 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* chat-sdk/headless/useChatStore.js
|
|
3
|
-
*
|
|
4
|
-
* 核心状态管理,接收 SDK config,不依赖任何全局配置。
|
|
5
|
-
* 集成:多模型选择、多模式 prompt、tool-use 解析循环。
|
|
6
|
-
*/
|
|
7
|
-
import { ref, computed, shallowRef } from 'vue'
|
|
8
|
-
import { streamChat } from './api/chatApi.js'
|
|
9
|
-
import { toAPIMessages } from './api/adapters/openai.js'
|
|
10
|
-
import { buildSystemPrompt } from './buildSystemPrompt.js'
|
|
11
|
-
import { loadModes } from '../modes/index.js'
|
|
12
|
-
import { extractAllToolCalls, hasToolCall } from '../tools/parser.js'
|
|
13
|
-
import { defaultToolHandlers } from '../tools/defaults.js'
|
|
14
|
-
import { useChatTracker } from '../../tracker/useChatTracker.js'
|
|
15
|
-
|
|
16
|
-
// 第一条消息追加的引导提示
|
|
17
|
-
const FIRST_MESSAGE_HINT = '(这是本次会话的第一条消息,请先用一句话简短自我介绍,然后直接回答用户问题。)'
|
|
18
|
-
|
|
19
|
-
export function useChatStore(config = {}) {
|
|
20
|
-
// ── 模型配置 ─────────────────────────────────────────────────────
|
|
21
|
-
const models = config.models || []
|
|
22
|
-
const defaultModelId = config.defaultModel || models[0]?.id || ''
|
|
23
|
-
const selectedModelId = ref(defaultModelId)
|
|
24
|
-
|
|
25
|
-
const selectedModel = computed(() =>
|
|
26
|
-
models.find(m => m.id === selectedModelId.value) || models[0] || null
|
|
27
|
-
)
|
|
28
|
-
const availableModels = models
|
|
29
|
-
|
|
30
|
-
// ── 埋点 ─────────────────────────────────────────────────────────
|
|
31
|
-
// config.getUser 由消费方(ChatBox / createAIChat)注入,返回 { name, id }
|
|
32
|
-
const chatTracker = useChatTracker({
|
|
33
|
-
getUserName: () => config.getUser?.()?.name || '',
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
// 根据消息内容自动切换视觉模型
|
|
37
|
-
function resolveModel(uiMessages) {
|
|
38
|
-
if (!config.autoVision) return selectedModel.value
|
|
39
|
-
const hasImage = uiMessages.some(m =>
|
|
40
|
-
m.parts?.some(p => p.type === 'image')
|
|
41
|
-
)
|
|
42
|
-
if (hasImage) {
|
|
43
|
-
const visionModel = models.find(m => m.vision)
|
|
44
|
-
if (visionModel) return visionModel
|
|
45
|
-
}
|
|
46
|
-
return selectedModel.value
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// ── 模式配置 ─────────────────────────────────────────────────────
|
|
50
|
-
const modeConfigs = shallowRef({}) // { [modeId]: ModeConfig }
|
|
51
|
-
const currentMode = ref(config.defaultMode || 'code')
|
|
52
|
-
|
|
53
|
-
// 异步加载模式定义
|
|
54
|
-
async function initModes() {
|
|
55
|
-
const modeIds = config.modes || ['code']
|
|
56
|
-
modeConfigs.value = await loadModes(modeIds)
|
|
57
|
-
// 确保 defaultMode 有效
|
|
58
|
-
if (!modeConfigs.value[currentMode.value]) {
|
|
59
|
-
currentMode.value = modeIds[0] || 'code'
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
initModes()
|
|
63
|
-
|
|
64
|
-
// 当前模式的 system prompt(响应式,模式切换时自动重建)
|
|
65
|
-
const systemPrompt = computed(() => {
|
|
66
|
-
const mode = modeConfigs.value[currentMode.value]
|
|
67
|
-
if (!mode) return ''
|
|
68
|
-
return buildSystemPrompt({
|
|
69
|
-
mode,
|
|
70
|
-
allModes: Object.values(modeConfigs.value),
|
|
71
|
-
customInstructions: config.customInstructions || '',
|
|
72
|
-
})
|
|
73
|
-
})
|
|
74
|
-
|
|
75
|
-
// ── 会话状态 ─────────────────────────────────────────────────────
|
|
76
|
-
const sessions = ref([{ id: 1, title: '新对话', messages: [] }])
|
|
77
|
-
const activeId = ref(1)
|
|
78
|
-
const isLoading = ref(false)
|
|
79
|
-
const abortCtrl = ref(null)
|
|
80
|
-
|
|
81
|
-
const activeSession = computed(
|
|
82
|
-
() => sessions.value.find(s => s.id === activeId.value) || sessions.value[0]
|
|
83
|
-
)
|
|
84
|
-
|
|
85
|
-
// ── Artifact 状态(headless 层只存数据,UI 层负责渲染)─────────────
|
|
86
|
-
const activeArtifacts = ref([])
|
|
87
|
-
|
|
88
|
-
// ── tool-use 回调注册表 ───────────────────────────────────────────
|
|
89
|
-
// 格式:{ [toolName]: handler } 或 { '*': handler }
|
|
90
|
-
const toolHandlerOverrides = {}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* 注册 tool-use 回调(消费方可覆盖内置默认行为)
|
|
94
|
-
* @param {string | Function} toolNameOrHandler
|
|
95
|
-
* @param {Function} [handler]
|
|
96
|
-
*
|
|
97
|
-
* 用法1:监听特定工具
|
|
98
|
-
* onToolCall('ask_followup_question', ({ params, resolve }) => { ... })
|
|
99
|
-
*
|
|
100
|
-
* 用法2:监听所有工具
|
|
101
|
-
* onToolCall(({ name, params, resolve }) => { ... })
|
|
102
|
-
*/
|
|
103
|
-
function onToolCall(toolNameOrHandler, handler) {
|
|
104
|
-
if (typeof toolNameOrHandler === 'function') {
|
|
105
|
-
toolHandlerOverrides['*'] = toolNameOrHandler
|
|
106
|
-
} else {
|
|
107
|
-
toolHandlerOverrides[toolNameOrHandler] = handler
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// ── tool-use 循环上下文 ───────────────────────────────────────────
|
|
112
|
-
function buildToolContext(sid) {
|
|
113
|
-
return {
|
|
114
|
-
artifactEnabled: !!(config.features?.artifact),
|
|
115
|
-
|
|
116
|
-
switchMode(modeId) {
|
|
117
|
-
currentMode.value = modeId
|
|
118
|
-
},
|
|
119
|
-
|
|
120
|
-
appendSystemTip(text, type = 'system') {
|
|
121
|
-
const session = sessions.value.find(s => s.id === sid)
|
|
122
|
-
if (!session) return
|
|
123
|
-
session.messages.push({
|
|
124
|
-
id: Date.now(),
|
|
125
|
-
role: 'system-tip',
|
|
126
|
-
content: text,
|
|
127
|
-
tipType: type,
|
|
128
|
-
})
|
|
129
|
-
},
|
|
130
|
-
|
|
131
|
-
appendFollowup({ question, suggests, onSelect }) {
|
|
132
|
-
const session = sessions.value.find(s => s.id === sid)
|
|
133
|
-
if (!session) return
|
|
134
|
-
session.messages.push({
|
|
135
|
-
id: Date.now(),
|
|
136
|
-
role: 'followup',
|
|
137
|
-
question,
|
|
138
|
-
suggests,
|
|
139
|
-
onSelect,
|
|
140
|
-
})
|
|
141
|
-
},
|
|
142
|
-
|
|
143
|
-
markComplete(result) {
|
|
144
|
-
const session = sessions.value.find(s => s.id === sid)
|
|
145
|
-
if (!session) return
|
|
146
|
-
const last = session.messages[session.messages.length - 1]
|
|
147
|
-
if (last) last.completed = true
|
|
148
|
-
},
|
|
149
|
-
|
|
150
|
-
registerArtifact({ fileName, lang, code }) {
|
|
151
|
-
activeArtifacts.value.push({
|
|
152
|
-
id: Date.now(),
|
|
153
|
-
fileName,
|
|
154
|
-
lang,
|
|
155
|
-
code,
|
|
156
|
-
streaming: false,
|
|
157
|
-
})
|
|
158
|
-
},
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
/**
|
|
163
|
-
* 执行单个 tool-use
|
|
164
|
-
* 优先使用消费方注册的 handler,否则使用内置默认
|
|
165
|
-
*/
|
|
166
|
-
async function executeTool(toolCall, sid) {
|
|
167
|
-
const { name, params } = toolCall
|
|
168
|
-
const context = buildToolContext(sid)
|
|
169
|
-
|
|
170
|
-
return new Promise((resolve, reject) => {
|
|
171
|
-
const handler =
|
|
172
|
-
toolHandlerOverrides[name] ||
|
|
173
|
-
toolHandlerOverrides['*'] ||
|
|
174
|
-
defaultToolHandlers[name]
|
|
175
|
-
|
|
176
|
-
if (!handler) {
|
|
177
|
-
console.warn(`[chat-sdk] 未知工具:${name},跳过`)
|
|
178
|
-
resolve()
|
|
179
|
-
return
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
handler({ name, params, resolve, reject, context })
|
|
183
|
-
.catch(reject)
|
|
184
|
-
})
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// ── 发送消息(主函数)────────────────────────────────────────────
|
|
188
|
-
/**
|
|
189
|
-
* @param {string | object} userInput
|
|
190
|
-
* - string:纯文本
|
|
191
|
-
* - object:{ content, displayText, attachments, modelOverride }
|
|
192
|
-
* @param {number} [sessionId]
|
|
193
|
-
* @param {string} [modelOverride] 直接指定模型 id(来自 InputBar 模型选择器)
|
|
194
|
-
*/
|
|
195
|
-
async function sendMessage(userInput, sessionId, modelOverride) {
|
|
196
|
-
const sid = sessionId || activeId.value
|
|
197
|
-
const msgId = Date.now()
|
|
198
|
-
const aiId = msgId + 1
|
|
199
|
-
const session = sessions.value.find(s => s.id === sid)
|
|
200
|
-
if (!session) return
|
|
201
|
-
|
|
202
|
-
// ── 解析用户输入 ──────────────────────────────────────────────
|
|
203
|
-
let displayText, attachments, apiContent
|
|
204
|
-
|
|
205
|
-
if (typeof userInput === 'string') {
|
|
206
|
-
displayText = userInput
|
|
207
|
-
attachments = []
|
|
208
|
-
apiContent = userInput
|
|
209
|
-
} else {
|
|
210
|
-
displayText = userInput.displayText ?? (
|
|
211
|
-
typeof userInput.content === 'string'
|
|
212
|
-
? userInput.content
|
|
213
|
-
: userInput.content?.filter(b => b.type === 'text').map(b => b.text).join(' ') ?? ''
|
|
214
|
-
)
|
|
215
|
-
attachments = userInput.attachments || []
|
|
216
|
-
apiContent = userInput.content
|
|
217
|
-
modelOverride = modelOverride || userInput.modelOverride
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// ── 更新会话标题 ──────────────────────────────────────────────
|
|
221
|
-
if (session.messages.length === 0) {
|
|
222
|
-
const titleSrc = displayText || attachments.map(a => a.fileName).join(', ')
|
|
223
|
-
session.title = titleSrc.slice(0, 20) + (titleSrc.length > 20 ? '…' : '')
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
// ── 构造 UI 消息(parts 格式)─────────────────────────────────
|
|
227
|
-
const userUiMsg = {
|
|
228
|
-
id: msgId,
|
|
229
|
-
role: 'user',
|
|
230
|
-
// parts 格式:UI 层和 headless 层统一使用
|
|
231
|
-
parts: buildParts(displayText, attachments, apiContent),
|
|
232
|
-
// 兼容旧版:保留 content 字段供 MessageBubble 展示
|
|
233
|
-
content: displayText,
|
|
234
|
-
apiContent,
|
|
235
|
-
attachments,
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
const aiUiMsg = {
|
|
239
|
-
id: aiId,
|
|
240
|
-
role: 'assistant',
|
|
241
|
-
content: '',
|
|
242
|
-
parts: [],
|
|
243
|
-
streaming: true,
|
|
244
|
-
error: false,
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
session.messages.push(userUiMsg, aiUiMsg)
|
|
248
|
-
isLoading.value = true
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
const respStartTime = chatTracker.responseStart()
|
|
252
|
-
|
|
253
|
-
// ── 选择模型 ──────────────────────────────────────────────────
|
|
254
|
-
const finalModelId = modelOverride || selectedModelId.value
|
|
255
|
-
const modelCfg = models.find(m => m.id === finalModelId) || resolveModel(session.messages) || models[0]
|
|
256
|
-
|
|
257
|
-
if (!modelCfg) {
|
|
258
|
-
const m = session.messages.find(m => m.id === aiId)
|
|
259
|
-
if (m) { m.streaming = false; m.error = true; m.content = '未配置可用模型' }
|
|
260
|
-
isLoading.value = false
|
|
261
|
-
return
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
// ── 埋点:发送消息 ────────────────────────────────────────────
|
|
265
|
-
chatTracker.messageSent({
|
|
266
|
-
mode: currentMode.value,
|
|
267
|
-
modelName: modelCfg?.name || modelCfg?.id || '',
|
|
268
|
-
text: displayText,
|
|
269
|
-
attachments,
|
|
270
|
-
})
|
|
271
|
-
|
|
272
|
-
// ── 构造 API messages ─────────────────────────────────────────
|
|
273
|
-
const isFirst = session.messages.filter(m => m.role === 'user').length === 1
|
|
274
|
-
|
|
275
|
-
const historyForApi = session.messages
|
|
276
|
-
.filter(m => m.id !== aiId && (m.role === 'user' || m.role === 'assistant'))
|
|
277
|
-
.map((m, idx, arr) => {
|
|
278
|
-
// 第一条用户消息追加引导提示
|
|
279
|
-
if (m.role === 'user' && isFirst && idx === 0) {
|
|
280
|
-
const c = m.apiContent ?? m.content
|
|
281
|
-
if (typeof c === 'string') {
|
|
282
|
-
return { ...m, apiContent: c + '\n' + FIRST_MESSAGE_HINT }
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
return m
|
|
286
|
-
})
|
|
287
|
-
|
|
288
|
-
const systemMsg = { role: 'system', content: systemPrompt.value }
|
|
289
|
-
const apiMessages = toAPIMessages([systemMsg, ...historyForApi])
|
|
290
|
-
|
|
291
|
-
// ── 流式请求 ─────────────────────────────────────────────────
|
|
292
|
-
abortCtrl.value = new AbortController()
|
|
293
|
-
|
|
294
|
-
// ── 自适应速率计 ──────────────────────────────────────────────
|
|
295
|
-
// 用滑动时间窗口测量 token 到达速率,动态决定 flush 间隔:
|
|
296
|
-
// 强网(>30 token/s) → 80ms 批量合并,降低渲染频率防抖动
|
|
297
|
-
// 中速(10~30/s) → 40ms
|
|
298
|
-
// 弱网(<10 token/s) → 16ms 近实时,防止字憋着出不来
|
|
299
|
-
const RATE_WINDOW_MS = 300
|
|
300
|
-
const rateTimestamps = [] // 滑动窗口:记录最近 300ms 内每个 token 的到达时间
|
|
301
|
-
|
|
302
|
-
function getTokenRate() {
|
|
303
|
-
const now = Date.now()
|
|
304
|
-
while (rateTimestamps.length && now - rateTimestamps[0] > RATE_WINDOW_MS) {
|
|
305
|
-
rateTimestamps.shift()
|
|
306
|
-
}
|
|
307
|
-
return rateTimestamps.length / (RATE_WINDOW_MS / 1000) // token/s
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
function getFlushInterval() {
|
|
311
|
-
const rate = getTokenRate()
|
|
312
|
-
if (rate > 30) return 80
|
|
313
|
-
if (rate > 10) return 40
|
|
314
|
-
return 16
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
let tokenBuffer = '' // 字符串直接拼接,比 array.join 更快
|
|
318
|
-
let flushTimer = null
|
|
319
|
-
|
|
320
|
-
function flushTokens() {
|
|
321
|
-
flushTimer = null
|
|
322
|
-
if (!tokenBuffer) return
|
|
323
|
-
const chunk = tokenBuffer
|
|
324
|
-
tokenBuffer = ''
|
|
325
|
-
const s = sessions.value.find(s => s.id === sid)
|
|
326
|
-
const m = s?.messages.find(m => m.id === aiId)
|
|
327
|
-
if (m) m.content += chunk
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
await streamChat({
|
|
331
|
-
modelConfig: modelCfg,
|
|
332
|
-
messages: apiMessages,
|
|
333
|
-
signal: abortCtrl.value.signal,
|
|
334
|
-
|
|
335
|
-
onToken(token) {
|
|
336
|
-
rateTimestamps.push(Date.now()) // 记录到达时间,用于速率计算
|
|
337
|
-
tokenBuffer += token
|
|
338
|
-
if (!flushTimer) {
|
|
339
|
-
flushTimer = setTimeout(flushTokens, getFlushInterval())
|
|
340
|
-
}
|
|
341
|
-
},
|
|
342
|
-
|
|
343
|
-
async onDone() {
|
|
344
|
-
if (flushTimer) { clearTimeout(flushTimer) }
|
|
345
|
-
flushTokens()
|
|
346
|
-
const s = sessions.value.find(s => s.id === sid)
|
|
347
|
-
const msg = s?.messages.find(m => m.id === aiId)
|
|
348
|
-
const toolCallNames = []
|
|
349
|
-
if (msg) {
|
|
350
|
-
msg.streaming = false
|
|
351
|
-
if (hasToolCall(msg.content)) {
|
|
352
|
-
const toolCalls = extractAllToolCalls(msg.content)
|
|
353
|
-
for (const tc of toolCalls) {
|
|
354
|
-
toolCallNames.push(tc.name)
|
|
355
|
-
try { await executeTool(tc, sid) }
|
|
356
|
-
catch (e) { console.warn('[chat-sdk] tool 执行失败', tc.name, e) }
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
// ── 埋点:回复完成 ──────────────────────────────────────
|
|
361
|
-
chatTracker.responseDone({
|
|
362
|
-
startTime: respStartTime,
|
|
363
|
-
modelName: modelCfg?.name || modelCfg?.id || '',
|
|
364
|
-
toolNames: toolCallNames,
|
|
365
|
-
})
|
|
366
|
-
isLoading.value = false
|
|
367
|
-
abortCtrl.value = null
|
|
368
|
-
},
|
|
369
|
-
|
|
370
|
-
onError(err) {
|
|
371
|
-
if (flushTimer) { clearTimeout(flushTimer) }
|
|
372
|
-
flushTokens()
|
|
373
|
-
const s = sessions.value.find(s => s.id === sid)
|
|
374
|
-
const msg = s?.messages.find(m => m.id === aiId)
|
|
375
|
-
if (msg) {
|
|
376
|
-
msg.streaming = false
|
|
377
|
-
msg.error = true
|
|
378
|
-
msg.content = `请求失败:${err.message}`
|
|
379
|
-
}
|
|
380
|
-
// ── 埋点:回复报错 ──────────────────────────────────────
|
|
381
|
-
const errorType = err.message?.includes('API') ? 'api'
|
|
382
|
-
: err.message?.includes('network') || err.message?.includes('fetch') ? 'network'
|
|
383
|
-
: 'unknown'
|
|
384
|
-
chatTracker.responseDone({
|
|
385
|
-
startTime: respStartTime,
|
|
386
|
-
modelName: modelCfg?.name || modelCfg?.id || '',
|
|
387
|
-
hasError: true,
|
|
388
|
-
errorType,
|
|
389
|
-
})
|
|
390
|
-
isLoading.value = false
|
|
391
|
-
abortCtrl.value = null
|
|
392
|
-
},
|
|
393
|
-
})
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
// ── 工具方法 ─────────────────────────────────────────────────────
|
|
397
|
-
function abortStream() {
|
|
398
|
-
abortCtrl.value?.abort()
|
|
399
|
-
abortCtrl.value = null
|
|
400
|
-
isLoading.value = false
|
|
401
|
-
const session = sessions.value.find(s => s.id === activeId.value)
|
|
402
|
-
const last = session?.messages[session.messages.length - 1]
|
|
403
|
-
if (last?.streaming) {
|
|
404
|
-
last.streaming = false
|
|
405
|
-
// ── 埋点:中止 ──────────────────────────────────────────
|
|
406
|
-
chatTracker.responseAbort()
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
function switchMode(modeId) {
|
|
411
|
-
if (!modeConfigs.value[modeId]) return
|
|
412
|
-
const prev = currentMode.value
|
|
413
|
-
currentMode.value = modeId
|
|
414
|
-
// ── 埋点:模式切换 ────────────────────────────────────────
|
|
415
|
-
chatTracker.modeSwitch(prev, modeId, 'manual')
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
function selectModel(modelId) {
|
|
419
|
-
if (models.find(m => m.id === modelId)) selectedModelId.value = modelId
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
function newSession() {
|
|
423
|
-
const id = Date.now()
|
|
424
|
-
sessions.value.unshift({ id, title: '新对话', messages: [] })
|
|
425
|
-
activeId.value = id
|
|
426
|
-
activeArtifacts.value = []
|
|
427
|
-
return id
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
function clearSession(id) {
|
|
431
|
-
const session = sessions.value.find(s => s.id === id)
|
|
432
|
-
if (session) {
|
|
433
|
-
session.messages = []
|
|
434
|
-
session.title = '新对话'
|
|
435
|
-
}
|
|
436
|
-
activeArtifacts.value = []
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
function deleteSession(id) {
|
|
440
|
-
const idx = sessions.value.findIndex(s => s.id === id)
|
|
441
|
-
if (idx === -1) return
|
|
442
|
-
sessions.value.splice(idx, 1)
|
|
443
|
-
if (sessions.value.length === 0) {
|
|
444
|
-
const newId = Date.now()
|
|
445
|
-
sessions.value.push({ id: newId, title: '新对话', messages: [] })
|
|
446
|
-
activeId.value = newId
|
|
447
|
-
} else if (id === activeId.value) {
|
|
448
|
-
activeId.value = sessions.value[0].id
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
function switchSession(id) {
|
|
453
|
-
if (sessions.value.find(s => s.id === id)) {
|
|
454
|
-
activeId.value = id
|
|
455
|
-
activeArtifacts.value = []
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
return {
|
|
460
|
-
// 状态
|
|
461
|
-
sessions,
|
|
462
|
-
activeId,
|
|
463
|
-
activeSession,
|
|
464
|
-
isLoading,
|
|
465
|
-
currentMode,
|
|
466
|
-
selectedModel,
|
|
467
|
-
availableModels,
|
|
468
|
-
activeArtifacts,
|
|
469
|
-
modeConfigs,
|
|
470
|
-
|
|
471
|
-
// 方法
|
|
472
|
-
sendMessage,
|
|
473
|
-
abortStream,
|
|
474
|
-
switchMode,
|
|
475
|
-
selectModel,
|
|
476
|
-
newSession,
|
|
477
|
-
clearSession,
|
|
478
|
-
deleteSession,
|
|
479
|
-
switchSession,
|
|
480
|
-
setActiveId: (id) => { activeId.value = id },
|
|
481
|
-
|
|
482
|
-
// tool-use 回调注册
|
|
483
|
-
onToolCall,
|
|
484
|
-
|
|
485
|
-
// 埋点(供 ArtifactPanel / MessageBubble 等上层组件调用采纳行为埋点)
|
|
486
|
-
tracker: chatTracker,
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
// ── 内部工具函数 ──────────────────────────────────────────────────
|
|
491
|
-
|
|
492
|
-
/**
|
|
493
|
-
* 把 displayText + attachments + apiContent 转成统一的 parts 格式
|
|
494
|
-
*/
|
|
495
|
-
function buildParts(displayText, attachments, apiContent) {
|
|
496
|
-
const parts = []
|
|
497
|
-
|
|
498
|
-
if (displayText) {
|
|
499
|
-
parts.push({ type: 'text', text: displayText })
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
// 从 attachments 里提取图片 / 文件内容
|
|
503
|
-
for (const att of (attachments || [])) {
|
|
504
|
-
if (att.type === 'visual' && att.preview) {
|
|
505
|
-
// 图片:存 preview dataURL 供 UI 缩略图 + contentBlock 供 API
|
|
506
|
-
parts.push({
|
|
507
|
-
type: 'image',
|
|
508
|
-
url: att.contentBlock?.image_url?.url ?? att.preview,
|
|
509
|
-
preview: att.preview,
|
|
510
|
-
fileName: att.fileName,
|
|
511
|
-
})
|
|
512
|
-
} else if (att.type === 'text') {
|
|
513
|
-
// 文本/代码/Excel/Word:把 contentBlock.text 存进 part
|
|
514
|
-
const blockText = att.contentBlock?.text ?? ''
|
|
515
|
-
parts.push({ type: 'file', fileName: att.fileName, fileType: att.ext, text: blockText })
|
|
516
|
-
} else if (att.contentBlocks) {
|
|
517
|
-
// PDF:多个 blocks(text + image_url[])
|
|
518
|
-
for (const block of att.contentBlocks) {
|
|
519
|
-
if (block.type === 'text') {
|
|
520
|
-
parts.push({ type: 'file', fileName: att.fileName, fileType: 'pdf', text: block.text })
|
|
521
|
-
} else if (block.type === 'image_url') {
|
|
522
|
-
parts.push({ type: 'image', url: block.image_url.url, preview: block.image_url.url, fileName: att.fileName })
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
return parts
|
|
529
|
-
}
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* chat-sdk/index.js
|
|
3
|
-
*
|
|
4
|
-
* SDK 双入口:
|
|
5
|
-
*
|
|
6
|
-
* 1. createAIChat(config) — 完整视图模式
|
|
7
|
-
* 挂载整套对话界面(ChatBox)到指定 DOM 节点
|
|
8
|
-
*
|
|
9
|
-
* 2. useChat(config) — 仅能力模式(re-export from headless)
|
|
10
|
-
* 返回状态 + 方法,消费方完全自己写视图
|
|
11
|
-
*
|
|
12
|
-
* config 完整结构:
|
|
13
|
-
* {
|
|
14
|
-
* // 挂载节点(仅 createAIChat 需要)
|
|
15
|
-
* el: '#chat-container' | HTMLElement,
|
|
16
|
-
*
|
|
17
|
-
* // 模型配置(每个模型独立 baseURL/apiKey)
|
|
18
|
-
* models: [
|
|
19
|
-
* { id, name, baseURL, apiKey, vision: false },
|
|
20
|
-
* ],
|
|
21
|
-
* defaultModel: 'model-id',
|
|
22
|
-
* autoVision: true, // 有图片时自动切换 vision: true 的模型
|
|
23
|
-
*
|
|
24
|
-
* // 能力配置(控制 chunk 是否加载)
|
|
25
|
-
* features: {
|
|
26
|
-
* artifact: true, // ArtifactPanel + 解析能力
|
|
27
|
-
* markdown: true, // Shiki + marked + KaTeX
|
|
28
|
-
* upload: true, // 文件/图片上传
|
|
29
|
-
* codepreview: true, // CodePreview 弹框
|
|
30
|
-
* },
|
|
31
|
-
*
|
|
32
|
-
* // 用户可控开关(控制 UI 开关是否显示)
|
|
33
|
-
* // true → 显示开关,用户可关
|
|
34
|
-
* // false → 显示开关,默认关,用户可开
|
|
35
|
-
* // 不写 → 强制开,UI 不显示此开关
|
|
36
|
-
* userControls: {
|
|
37
|
-
* modelSelector: true,
|
|
38
|
-
* artifact: true,
|
|
39
|
-
* upload: true,
|
|
40
|
-
* },
|
|
41
|
-
*
|
|
42
|
-
* // Prompt 配置
|
|
43
|
-
* modes: ['code', 'ask', 'architect', 'requirements'],
|
|
44
|
-
* defaultMode: 'code',
|
|
45
|
-
* customInstructions: '',
|
|
46
|
-
* }
|
|
47
|
-
*/
|
|
48
|
-
|
|
49
|
-
import { createApp, defineAsyncComponent } from 'vue'
|
|
50
|
-
import { useChat } from './headless/index.js'
|
|
51
|
-
|
|
52
|
-
// re-export useChat,让消费方可以从同一个包导入两种用法
|
|
53
|
-
export { useChat }
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* 完整视图模式:挂载整套对话界面
|
|
57
|
-
*/
|
|
58
|
-
export async function createAIChat(config) {
|
|
59
|
-
const { el, ...chatConfig } = config
|
|
60
|
-
|
|
61
|
-
if (!el) {
|
|
62
|
-
throw new Error('[chat-sdk] createAIChat 需要传入 el 挂载节点')
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// TODO: 第二阶段实现按需加载 features 对应的 UI 组件
|
|
66
|
-
// 当前为骨架占位,后续补充完整挂载逻辑
|
|
67
|
-
|
|
68
|
-
const ChatBox = defineAsyncComponent(() =>
|
|
69
|
-
import('./core/components/ChatBox.vue')
|
|
70
|
-
)
|
|
71
|
-
|
|
72
|
-
const app = createApp(ChatBox, { config: chatConfig })
|
|
73
|
-
app.mount(typeof el === 'string' ? document.querySelector(el) : el)
|
|
74
|
-
|
|
75
|
-
return {
|
|
76
|
-
// 返回实例方法,方便外部控制
|
|
77
|
-
unmount: () => app.unmount(),
|
|
78
|
-
}
|
|
79
|
-
}
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* modes/architect.js — Architect 模式
|
|
3
|
-
* 角色:技术负责人,做规划、设计方案、拆解需求,输出技术文档
|
|
4
|
-
*/
|
|
5
|
-
export default {
|
|
6
|
-
id: 'architect',
|
|
7
|
-
name: 'Architect',
|
|
8
|
-
icon: 'Building2',
|
|
9
|
-
description: '系统设计、技术方案规划、架构拆解,输出技术文档',
|
|
10
|
-
|
|
11
|
-
rolePrompt: `你是一名经验丰富的技术负责人,擅长系统架构设计、技术方案评估、跨团队协作规划。你习惯在动手之前先把问题想清楚,输出结构化的技术文档。
|
|
12
|
-
|
|
13
|
-
你当前处于 Architect 模式,职责是:
|
|
14
|
-
- 系统架构设计与评审
|
|
15
|
-
- 技术方案的可行性分析与选型建议
|
|
16
|
-
- 将复杂需求拆解为可执行的技术任务
|
|
17
|
-
- 识别技术风险和依赖关系
|
|
18
|
-
- 输出架构图、技术方案文档(使用 render_artifact 工具)
|
|
19
|
-
|
|
20
|
-
你在 Architect 模式下的行为准则:
|
|
21
|
-
1. NEVER 直接写实现代码,若需要代码示例只写伪代码或关键片段(≤15 行)
|
|
22
|
-
2. 信息不充分时 MUST 先使用 ask_followup_question 工具收集背景信息,再给出方案,不得在信息模糊时给出确定性结论
|
|
23
|
-
3. 给出技术方案时 MUST 同时说明:方案优势、潜在风险、前置依赖、估算工作量
|
|
24
|
-
4. 涉及系统间调用关系时,MUST 明确标注:调用方向、接口协议、数据格式、异常处理策略
|
|
25
|
-
5. 方案确认后,使用 switch_mode 工具建议用户切换到 Code 模式开始实现
|
|
26
|
-
6. 输出的技术文档 MUST 使用 render_artifact 工具,格式为 Markdown`,
|
|
27
|
-
}
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* modes/ask.js — Ask 模式
|
|
3
|
-
* 角色:技术顾问,只回答问题,不主动修改代码
|
|
4
|
-
*/
|
|
5
|
-
export default {
|
|
6
|
-
id: 'ask',
|
|
7
|
-
name: 'Ask',
|
|
8
|
-
icon: 'MessageCircleQuestion',
|
|
9
|
-
description: '解释概念、回答技术问题、分析代码,不直接生成文件',
|
|
10
|
-
|
|
11
|
-
rolePrompt: `你是一名资深技术顾问,擅长用清晰易懂的方式解释复杂技术概念,能够从不同角度分析问题并给出客观建议。
|
|
12
|
-
|
|
13
|
-
你当前处于 Ask 模式,职责是:
|
|
14
|
-
- 解释技术概念、原理、最佳实践
|
|
15
|
-
- 分析用户提供的代码,指出问题和优化方向
|
|
16
|
-
- 对技术选型、架构方案给出客观评估
|
|
17
|
-
- 回答"为什么"和"怎么理解"类问题
|
|
18
|
-
|
|
19
|
-
你在 Ask 模式下的行为准则:
|
|
20
|
-
1. NEVER 主动生成完整代码文件,解释时可以使用短小的代码片段(≤20 行)作为示例
|
|
21
|
-
2. 若用户明确要求生成代码,使用 switch_mode 工具提示切换到 Code 模式
|
|
22
|
-
3. 分析用户代码时,若代码不完整或缺少上下文,MUST 使用 ask_followup_question 工具询问,不得凭空猜测
|
|
23
|
-
4. 回答要有条理:先给结论,再展开解释,最后给出延伸阅读方向
|
|
24
|
-
5. 对有争议的技术话题(如框架选型),MUST 呈现多方观点,不得给出唯一正确答案
|
|
25
|
-
6. 回复中若需要展示文档、示例文档、规范说明等较长的结构化内容(超过 30 行),MUST 使用 render_artifact 工具输出为独立文件,NEVER 将其包裹在 Markdown 代码块(如 \`\`\`markdown ... \`\`\`)中输出`,
|
|
26
|
-
}
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* modes/code.js — Code 模式
|
|
3
|
-
* 角色:全能前端/全栈工程师,负责编写、修改、重构代码
|
|
4
|
-
*/
|
|
5
|
-
export default {
|
|
6
|
-
id: 'code',
|
|
7
|
-
name: 'Code',
|
|
8
|
-
icon: 'Code2',
|
|
9
|
-
description: '编写、修改、重构代码,生成完整可运行的代码文件',
|
|
10
|
-
|
|
11
|
-
rolePrompt: `你是一名经验丰富的全栈工程师,精通 Vue3、React、TypeScript、Node.js 等主流技术栈,对前端工程化、组件设计、性能优化有深入理解。
|
|
12
|
-
|
|
13
|
-
你当前处于 Code 模式,职责是:
|
|
14
|
-
- 编写、修改、重构代码
|
|
15
|
-
- 生成完整可运行的代码文件(使用 render_artifact 工具输出)
|
|
16
|
-
- 解释代码逻辑和技术选型
|
|
17
|
-
- 发现并修复代码问题
|
|
18
|
-
|
|
19
|
-
你在 Code 模式下的行为准则:
|
|
20
|
-
1. 代码超过 20 行或构成完整可运行文件时,MUST 使用 render_artifact 工具输出,不得用普通代码块代替
|
|
21
|
-
2. 输出代码时 MUST 给出完整代码,严禁省略、截断、使用注释代替未完成部分
|
|
22
|
-
3. 若需要用户提供现有代码才能继续,MUST 使用 request_context 工具请求,不得凭空假设代码内容
|
|
23
|
-
4. 技术方案存在多个可选路径时,先简要说明各方案的取舍,再按用户场景给出推荐
|
|
24
|
-
5. NEVER 主动切换到其他模式,若用户有规划类需求请提示用户切换到 Architect 模式`,
|
|
25
|
-
}
|