xiaoyuan-assistant 0.5.45

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.
@@ -0,0 +1,505 @@
1
+ import { waitForAiElement, scanAiElements, waitForRender } from './dom.js'
2
+ import { parseLocalCommands, looksLikeAnalysisLocal } from './localCommandParser.js'
3
+
4
+ function isAnalysisRequest(message = '') {
5
+ return /(分析|研判|评估|判断|原因|趋势|风险|建议|情况|长势|预测|对比|比较|统计|为什么|是否异常|异常)/i.test(String(message))
6
+ }
7
+
8
+ function isCasualChatRequest(message = '') {
9
+ const text = String(message || '').trim()
10
+ if (!text || isAnalysisRequest(text)) return false
11
+ if (/(切换|打开|关闭|进入|跳转|返回|后退|前进|刷新|选择|设置|定位|放大|缩小|显示|隐藏|查看|获取|查询|读取|分析|研判|评估)/.test(text)) return false
12
+ return /(你是谁|你叫什么|介绍一下你自己|你能做什么|讲个笑话|讲一个笑话|说个笑话|谢谢|你好|嗨|在吗|你好吗|你觉得|是什么|什么意思|怎么用|帮我解释)/.test(text)
13
+ }
14
+
15
+ function isWeatherQuestion(message = '') {
16
+ const text = String(message || '')
17
+ return /(天气|气温|温度|降雨|下雨|湿度|风速|晴|阴|多云)/.test(text) && /(今天|当前|现在|明天|这几天|怎么样|如何|多少|预报|情况)/.test(text)
18
+ }
19
+
20
+ export class XiaoyuanManager {
21
+ constructor({ registry, speech, ai, options = {} }) {
22
+ this.registry = registry
23
+ this.speech = speech
24
+ this.ai = ai
25
+ this.options = {
26
+ commandTimeout: 0,
27
+ retryDelayMs: 3000,
28
+ retryCount: 2,
29
+ stepGapMs: 3000,
30
+ afterClickWait: 120,
31
+ totalExecutionTimeout: 0,
32
+ manifestCacheMs: 500,
33
+ ...options
34
+ }
35
+ this.manifestCache = null
36
+ }
37
+
38
+ setAI(ai) { this.ai = ai }
39
+
40
+ invalidateManifest() {
41
+ this.manifestCache = null
42
+ }
43
+
44
+ getManifest({ includeDataSources = false, force = false } = {}) {
45
+ const now = Date.now()
46
+ if (!force && this.manifestCache && now - this.manifestCache.at < this.options.manifestCacheMs && this.manifestCache.includeDataSources === includeDataSources) {
47
+ return this.manifestCache.data
48
+ }
49
+
50
+ const toolManifest = this.registry.getToolManifest()
51
+ const data = {
52
+ functions: toolManifest.functions,
53
+ dataSources: includeDataSources ? toolManifest.dataSources : [],
54
+ context: toolManifest.context,
55
+ domElements: scanAiElements(),
56
+ url: window.location.href,
57
+ path: window.location.pathname
58
+ }
59
+ this.manifestCache = { at: now, includeDataSources, data }
60
+ return data
61
+ }
62
+
63
+ async getData(name, params = {}) {
64
+ const source = this.registry.data.get(name)
65
+ if (!source) throw new Error(`未注册数据源:${name}`)
66
+ return await source.get(params)
67
+ }
68
+
69
+ async waitForDataWithRetry(name, params = {}) {
70
+ const attempts = Math.max(0, Number(this.options.retryCount ?? 2)) + 1
71
+ let lastValue
72
+
73
+ for (let attempt = 0; attempt < attempts; attempt += 1) {
74
+ try {
75
+ lastValue = await this.getData(name, params)
76
+ if (this.hasUsableData(lastValue)) {
77
+ return { found: true, data: lastValue, attempts: attempt + 1 }
78
+ }
79
+ } catch (error) {
80
+ lastValue = { __error: error?.message || `数据源 ${name} 获取失败` }
81
+ }
82
+
83
+ if (attempt < attempts - 1) {
84
+ await new Promise((resolve) => setTimeout(resolve, this.options.retryDelayMs))
85
+ }
86
+ }
87
+
88
+ return { found: false, data: lastValue, attempts }
89
+ }
90
+
91
+ hasUsableData(value) {
92
+ if (value == null) return false
93
+ if (value && typeof value === 'object' && value.__error) return false
94
+ if (Array.isArray(value)) return value.length > 0
95
+ if (typeof value === 'object') return Object.keys(value).length > 0
96
+ if (typeof value === 'string') return value.trim().length > 0
97
+ return true
98
+ }
99
+
100
+ async waitBetweenSteps(index) {
101
+ if (index <= 0) return
102
+ const delay = Math.max(0, Number(this.options.stepGapMs ?? 0))
103
+ if (delay > 0) {
104
+ await new Promise((resolve) => setTimeout(resolve, delay))
105
+ }
106
+ }
107
+
108
+ async execute(command = {}) {
109
+ const name = command?.function || command?.name
110
+ const params = command?.params || {}
111
+ if (!name) return { success: false, message: '缺少 function' }
112
+
113
+ // 注册 Function:直接调用,不依赖 DOM。
114
+ const registered = this.registry.functions.get(name)
115
+ if (registered) {
116
+ try {
117
+ const result = await registered.handler({ params, context: this.registry.getContext() })
118
+ await waitForRender(1, this.options.afterClickWait)
119
+ this.invalidateManifest()
120
+ return { success: true, type: 'function', name, data: result }
121
+ } catch (error) {
122
+ return { success: false, type: 'function', name, message: error?.message || '执行失败' }
123
+ }
124
+ }
125
+
126
+ // DOM Function:先立即查找。多步骤之间由 stepGapMs 统一留出 5 秒缓冲;
127
+ // 当前步骤如果尚未渲染,则每隔 3 秒重试一次,最多重试 2 次。
128
+ let el = await waitForAiElement(name, params, { timeout: 0, interval: 50 })
129
+
130
+ if (!el) {
131
+ const retries = Math.max(0, Number(this.options.retryCount ?? 2))
132
+ for (let retry = 0; retry < retries; retry += 1) {
133
+ await new Promise((resolve) => setTimeout(resolve, this.options.retryDelayMs))
134
+ el = await waitForAiElement(name, params, { timeout: 0, interval: 50 })
135
+ if (el) break
136
+ }
137
+ }
138
+
139
+ if (!el) {
140
+ return { success: false, type: 'dom', name, message: '等待页面加载后仍未找到对应操作' }
141
+ }
142
+
143
+ return await this.clickAiElement(el, name)
144
+ }
145
+
146
+ async clickAiElement(el, name) {
147
+ try {
148
+ el.click()
149
+ } catch (error) {
150
+ return { success: false, type: 'dom', name, message: error?.message || '点击失败' }
151
+ }
152
+
153
+ await waitForRender(1, this.options.afterClickWait)
154
+ this.invalidateManifest()
155
+ return { success: true, type: 'dom', name }
156
+ }
157
+
158
+ normalizeStep(step) {
159
+ if (!step) return null
160
+ if (step.type === 'analysis') {
161
+ if (!Array.isArray(step.dataRequests) || !step.dataRequests.length) return null
162
+ return { type: 'analysis', dataRequests: step.dataRequests, instruction: step.instruction || '' }
163
+ }
164
+ if (step.function || step.name) return { type: 'action', function: step.function || step.name, params: step.params || {} }
165
+ return null
166
+ }
167
+
168
+ async plan(message, hooks = {}) {
169
+ if (typeof this.ai !== 'function') {
170
+ return { type: 'chat', reply: 'AI 模型接口尚未配置。', steps: [] }
171
+ }
172
+ const manifest = this.getManifest({ includeDataSources: isAnalysisRequest(message) })
173
+
174
+ const pushed = []
175
+ const onPlanStep = async (step) => {
176
+ const normalized = this.normalizeStep(step)
177
+ if (!normalized) return
178
+ pushed.push(normalized)
179
+ await hooks.onPlanStep?.({ step: normalized, index: pushed.length - 1 })
180
+ }
181
+
182
+ const result = await this.ai({
183
+ mode: 'plan',
184
+ message,
185
+ manifest,
186
+ getData: (name, params) => this.getData(name, params),
187
+ execute: (command) => this.execute(command),
188
+ onPlanStep
189
+ })
190
+
191
+ if (pushed.length) {
192
+ return { ...(result || {}), type: 'workflow', steps: pushed }
193
+ }
194
+ return result || { type: 'workflow', steps: [] }
195
+ }
196
+
197
+ async analyze(message, plan, dataResults, dataSourceManifest = []) {
198
+ if (typeof this.ai !== 'function') {
199
+ return { type: 'analysis', reply: 'AI 模型接口尚未配置。', analysis: '', steps: [] }
200
+ }
201
+ return this.ai({
202
+ mode: 'analysis',
203
+ message,
204
+ manifest: {
205
+ context: this.registry.getContext(),
206
+ dataSources: dataSourceManifest
207
+ },
208
+ plan,
209
+ dataResults,
210
+ getData: (name, params) => this.getData(name, params),
211
+ execute: (command) => this.execute(command)
212
+ })
213
+ }
214
+
215
+ async ask(message) { return this.run(message) }
216
+
217
+ describeCommand(command = {}) {
218
+ const name = command?.function || command?.name || ''
219
+ const params = command?.params || {}
220
+ const value = params?.value ?? params?.param ?? params?.target ?? params?.id ?? params?.name
221
+ const registered = this.registry.functions.get(name)
222
+ let description = registered?.description || ''
223
+
224
+ if (!description) {
225
+ const elements = this.getManifest({ includeDataSources: false }).domElements
226
+ const match = elements.find((item) => item.name === name && (value == null || item.params.some((v) => String(v) === String(value))))
227
+ description = match?.description || ''
228
+ }
229
+
230
+ description = String(description || '').trim()
231
+ const valueText = value !== undefined && value !== null && value !== '' ? String(value) : ''
232
+ if (description && valueText) return `${description}(${valueText})`
233
+ if (description) return description
234
+ if (valueText) return `${name}(${valueText})`
235
+ return name || '下一步操作'
236
+ }
237
+
238
+ async executeAnalysisStep(step, message, plan, hooks) {
239
+ const requests = Array.isArray(step.dataRequests) ? step.dataRequests.filter((item) => item?.name) : []
240
+ if (!requests.length) return { success: false, type: 'analysis', message: '没有找到可分析的数据源。' }
241
+ await hooks.onAnalysisStart?.({ step, requests })
242
+
243
+ // 多数据源并行获取,避免土壤/气象/作物等多个数据源串行等待,提升分析前置速度。
244
+ const fetchedResults = await Promise.all(
245
+ requests.map(async (request) => ({
246
+ name: request.name,
247
+ fetched: await this.waitForDataWithRetry(request.name, request.params || {})
248
+ }))
249
+ )
250
+ const dataResults = Object.fromEntries(
251
+ fetchedResults.map(({ name, fetched }) => [name, fetched.data])
252
+ )
253
+
254
+ const relevantSources = this.registry.getToolManifest().dataSources.filter((source) => requests.some((item) => item.name === source.name))
255
+ let analysisResult
256
+ try {
257
+ analysisResult = await this.analyze(message, plan, dataResults, relevantSources)
258
+ } catch (error) {
259
+ const messageText = error?.message || 'AI 分析失败'
260
+ try { await hooks.onAnalysisError?.({ step, requests, error, message: messageText, dataResults }) } catch (_) {}
261
+ return {
262
+ success: false,
263
+ type: 'analysis',
264
+ name: 'analysis',
265
+ message: messageText,
266
+ analysis: ''
267
+ }
268
+ }
269
+
270
+ try {
271
+ await hooks.onAnalysisDone?.({ step, requests, result: analysisResult, dataResults })
272
+ } catch (error) {
273
+ console.warn('[小园] 分析完成播报 hook 异常,继续后续流程:', error)
274
+ }
275
+
276
+ return {
277
+ success: true,
278
+ type: 'analysis',
279
+ name: 'analysis',
280
+ data: analysisResult,
281
+ analysis: analysisResult?.analysis || analysisResult?.reply || ''
282
+ }
283
+ }
284
+
285
+ async executeLocalSteps(steps, message, hooks, started) {
286
+ const execution = []
287
+ const queue = []
288
+ let index = 0
289
+
290
+ for (const step of steps) {
291
+ if (index > 0 && queue[index - 1]?.type !== 'analysis') {
292
+ await this.waitBetweenSteps(index)
293
+ }
294
+ queue.push(step)
295
+ const description = step.type === 'analysis' ? '分析当前数据' : this.describeCommand(step)
296
+ try { await hooks.onBeforeStep?.({ index, total: steps.length, command: step, description }) } catch (error) { console.warn('[小园] 步骤前置播报失败,继续执行当前步骤:', error) }
297
+
298
+ const item = step.type === 'analysis'
299
+ ? await this.executeAnalysisStep(step, message, { type: 'workflow', steps }, hooks)
300
+ : await this.execute(step)
301
+
302
+ const result = { ...item, index }
303
+ execution.push(result)
304
+
305
+ if (step.type !== 'analysis') {
306
+ try { await hooks.onStep?.({ index, total: steps.length, command: step, description, result }) } catch (error) { console.warn('[小园] 步骤完成 hook 失败:', error) }
307
+ }
308
+
309
+ if (result.success === false) {
310
+ return {
311
+ type: 'workflow',
312
+ reply: '',
313
+ steps: queue,
314
+ commands: queue.filter((item) => item.type !== 'analysis'),
315
+ dataRequests: queue.flatMap((item) => item.type === 'analysis' ? (item.dataRequests || []) : []),
316
+ execution,
317
+ summary: { total: execution.length, success: execution.filter((item) => item.success !== false).length, failed: 1, plannerDone: true },
318
+ local: true,
319
+ duration: Date.now() - started
320
+ }
321
+ }
322
+
323
+ index += 1
324
+ }
325
+
326
+ return {
327
+ type: 'workflow',
328
+ reply: '',
329
+ steps: queue,
330
+ commands: queue.filter((item) => item.type !== 'analysis'),
331
+ dataRequests: queue.flatMap((item) => item.type === 'analysis' ? (item.dataRequests || []) : []),
332
+ execution,
333
+ summary: { total: execution.length, success: execution.filter((item) => item.success !== false).length, failed: 0, plannerDone: true },
334
+ local: true,
335
+ duration: Date.now() - started
336
+ }
337
+ }
338
+
339
+ async runPlannerForUnresolved(unresolvedText, localManifest, message, queue, existingExecution, hooks, started) {
340
+ const execution = [...existingExecution]
341
+ let idx = execution.length
342
+ let planResult
343
+ try {
344
+ planResult = await this.ai({
345
+ mode: 'plan',
346
+ message: unresolvedText,
347
+ manifest: localManifest,
348
+ getData: (name, params) => this.getData(name, params),
349
+ execute: (command) => this.execute(command)
350
+ })
351
+ } catch (error) {
352
+ return {
353
+ type: 'workflow',
354
+ reply: '',
355
+ steps: queue,
356
+ execution,
357
+ local: true,
358
+ plannerError: error?.message || String(error),
359
+ duration: Date.now() - started
360
+ }
361
+ }
362
+
363
+ const steps = Array.isArray(planResult?.steps) ? planResult.steps : []
364
+ for (const rawStep of steps) {
365
+ const step = this.normalizeStep(rawStep)
366
+ if (!step) continue
367
+ if (idx > 0 && queue[idx - 1]?.type !== 'analysis') {
368
+ await this.waitBetweenSteps(idx)
369
+ }
370
+ queue.push(step)
371
+ const description = step.type === 'analysis' ? '分析当前数据' : this.describeCommand(step)
372
+ try { await hooks.onBeforeStep?.({ index: idx, total: null, command: step, description }) } catch (error) { console.warn('[小园] Planner 步骤前置播报失败,继续执行:', error) }
373
+
374
+ const item = step.type === 'analysis'
375
+ ? await this.executeAnalysisStep(step, message, { type: 'workflow', steps: queue }, hooks)
376
+ : await this.execute(step)
377
+
378
+ const result = { ...item, index: idx }
379
+ execution.push(result)
380
+ if (step.type !== 'analysis') {
381
+ await hooks.onStep?.({ index: idx, total: null, command: step, description, result })
382
+ }
383
+ idx += 1
384
+ if (result.success === false) break
385
+ }
386
+
387
+ return {
388
+ type: 'workflow',
389
+ reply: planResult?.reply || '',
390
+ steps: queue,
391
+ commands: queue.filter((step) => step.type !== 'analysis'),
392
+ dataRequests: queue.flatMap((step) => step.type === 'analysis' ? (step.dataRequests || []) : []),
393
+ execution,
394
+ summary: { total: execution.length, success: execution.filter((item) => item.success !== false).length, failed: execution.filter((item) => item.success === false).length, plannerDone: true },
395
+ local: false,
396
+ duration: Date.now() - started
397
+ }
398
+ }
399
+
400
+ async chatDirect(message, started = Date.now()) {
401
+ if (typeof this.ai !== 'function') {
402
+ return { type: 'chat', reply: 'AI 模型接口尚未配置。', steps: [], local: true, duration: Date.now() - started }
403
+ }
404
+
405
+ const chatResult = await this.ai({
406
+ mode: 'chat',
407
+ message,
408
+ manifest: { context: this.registry.getContext() }
409
+ })
410
+
411
+ return {
412
+ ...(chatResult || {}),
413
+ type: 'chat',
414
+ steps: [],
415
+ local: false,
416
+ duration: Date.now() - started
417
+ }
418
+ }
419
+
420
+ async run(message, hooks = {}) {
421
+ const started = Date.now()
422
+ const localManifest = this.getManifest({ includeDataSources: true, force: true })
423
+ const localPlan = parseLocalCommands(message, localManifest)
424
+
425
+ // 核心路由:先用本地 JS 判断是不是“大屏指令/数据分析”。
426
+ // 如果完全没有可执行指令,也没有可用的数据分析步骤,就直接交给 AI 聊天,
427
+ // 不再调用 Planner AI。这样“Vue3 生命周期是什么?”、“讲个笑话”等问题只会发起一次 Chat 请求。
428
+ if (!localPlan.steps.length && !localPlan.unresolved.length) {
429
+ return this.chatDirect(message, started)
430
+ }
431
+
432
+ // 如果本地已经识别出完整工作流,直接执行,完全不调用 Planner AI。
433
+ if (localPlan.ok && localPlan.steps.length) {
434
+ return await this.executeLocalSteps(localPlan.steps, message, hooks, started)
435
+ }
436
+
437
+ // 部分已识别:先执行本地明确的步骤。剩余无法识别的自然语言,
438
+ // 不再交给 Planner,而是在本地步骤完成后直接交给 Chat AI 回答。
439
+ if (localPlan.partial && localPlan.steps.length) {
440
+ const execution = []
441
+ const steps = [...localPlan.steps]
442
+ let index = 0
443
+
444
+ for (const step of steps) {
445
+ if (index > 0 && steps[index - 1]?.type !== 'analysis') {
446
+ await this.waitBetweenSteps(index)
447
+ }
448
+ const description = step.type === 'analysis' ? '分析当前数据' : this.describeCommand(step)
449
+ try { await hooks.onBeforeStep?.({ index, total: null, command: step, description }) } catch (error) { console.warn('[小园] 部分解析步骤前置播报失败,继续执行:', error) }
450
+
451
+ const item = step.type === 'analysis'
452
+ ? await this.executeAnalysisStep(step, message, { type: 'workflow', steps }, hooks)
453
+ : await this.execute(step)
454
+
455
+ const result = { ...item, index }
456
+ execution.push(result)
457
+
458
+ if (step.type !== 'analysis') {
459
+ hooks.onStep?.({ index, total: null, command: step, description, result })
460
+ }
461
+
462
+ if (result.success === false) {
463
+ return {
464
+ type: 'workflow',
465
+ reply: '',
466
+ steps,
467
+ execution,
468
+ local: true,
469
+ duration: Date.now() - started
470
+ }
471
+ }
472
+
473
+ index += 1
474
+ }
475
+
476
+ const unresolvedText = localPlan.unresolved.join(';').trim()
477
+ if (unresolvedText) {
478
+ // 未识别部分直接作为普通问题交给 AI,避免再次走 Planner。
479
+ const chatResult = await this.chatDirect(unresolvedText, started)
480
+ return {
481
+ ...chatResult,
482
+ type: 'workflow',
483
+ steps,
484
+ execution,
485
+ unresolvedReply: chatResult.reply || '',
486
+ reply: chatResult.reply || '',
487
+ local: false,
488
+ duration: Date.now() - started
489
+ }
490
+ }
491
+
492
+ return {
493
+ type: 'workflow',
494
+ reply: '',
495
+ steps,
496
+ execution,
497
+ local: true,
498
+ duration: Date.now() - started
499
+ }
500
+ }
501
+
502
+ // 纯自然语言问题:即使本地拆分不出 action,也不要尝试 Planner,直接 Chat AI。
503
+ return this.chatDirect(message, started)
504
+ }
505
+ }
@@ -0,0 +1,49 @@
1
+ export class Registry {
2
+ constructor() {
3
+ this.functions = new Map()
4
+ this.data = new Map()
5
+ this.context = {}
6
+ }
7
+
8
+ registerFunction(definition) {
9
+ if (!definition?.name || typeof definition.handler !== 'function') {
10
+ throw new Error('registerFunction 需要 name 与 handler')
11
+ }
12
+ this.functions.set(definition.name, {
13
+ name: definition.name,
14
+ description: definition.description || '',
15
+ params: definition.params || {},
16
+ handler: definition.handler
17
+ })
18
+ return () => this.functions.delete(definition.name)
19
+ }
20
+
21
+ registerData(definition) {
22
+ if (!definition?.name || typeof definition.get !== 'function') {
23
+ throw new Error('registerData 需要 name 与 get')
24
+ }
25
+ this.data.set(definition.name, {
26
+ name: definition.name,
27
+ description: definition.description || '',
28
+ schema: definition.schema || {},
29
+ get: definition.get
30
+ })
31
+ return () => this.data.delete(definition.name)
32
+ }
33
+
34
+ setContext(context = {}) {
35
+ this.context = { ...this.context, ...context }
36
+ }
37
+
38
+ getContext() {
39
+ return { ...this.context }
40
+ }
41
+
42
+ getToolManifest() {
43
+ return {
44
+ functions: [...this.functions.values()].map(({ name, description, params }) => ({ name, description, params })),
45
+ dataSources: [...this.data.values()].map(({ name, description, schema }) => ({ name, description, schema })),
46
+ context: this.getContext()
47
+ }
48
+ }
49
+ }
package/src/index.js ADDED
@@ -0,0 +1,100 @@
1
+ import { inject } from 'vue'
2
+ import XiaoyuanAssistant from './components/XiaoyuanAssistant.vue'
3
+ import { Registry } from './core/registry.js'
4
+ import { XiaoyuanManager } from './core/manager.js'
5
+ import { SpeechService } from './voice/speech.js'
6
+ import './styles/index.css'
7
+ import { createSiliconFlowProvider } from './providers/siliconflow.js'
8
+ import { registerBuiltinFunctions } from './core/builtins.js'
9
+ import { HewoyiTTSProvider } from './voice/hewoyi.js'
10
+
11
+ const KEY = Symbol('xiaoyuan')
12
+
13
+ let singleton = null
14
+
15
+ export function useXiaoyuan() {
16
+ if (!singleton) throw new Error('请先在 main.js 中 app.use(Xiaoyuan)')
17
+ return singleton
18
+ }
19
+
20
+ const Xiaoyuan = {
21
+ install(app, options = {}) {
22
+ if (singleton) return
23
+
24
+ const registry = new Registry()
25
+ registerBuiltinFunctions(registry)
26
+ const normalizedOptions = {
27
+ ...options,
28
+ enableTTS: options.enableTTS !== false,
29
+ enableWakeWord: options.enableWakeWord !== false,
30
+ wakeWord: options.wakeWord || '你好小园'
31
+ }
32
+
33
+ const ttsOptions = normalizedOptions.tts || {}
34
+ const ttsProvider = new HewoyiTTSProvider({
35
+ apiUrl: ttsOptions.apiUrl,
36
+ apiKey: ttsOptions.apiKey,
37
+ voice: ttsOptions.voice || 'zh-CN-XiaoyiNeural',
38
+ format: ttsOptions.format || 'mp3',
39
+ speed: ttsOptions.speed || '',
40
+ model: ttsOptions.model || '',
41
+ type: ttsOptions.type || 'speech',
42
+ requestTimeoutMs: ttsOptions.requestTimeoutMs ?? 7000
43
+ })
44
+
45
+ const speech = new SpeechService({
46
+ lang: normalizedOptions.lang || 'zh-CN',
47
+ rate: normalizedOptions.ttsRate ?? 1,
48
+ pitch: normalizedOptions.ttsPitch ?? 1,
49
+ volume: normalizedOptions.ttsVolume ?? 1,
50
+ ttsProvider: ttsOptions.provider === 'hewoyi' ? ttsProvider : null
51
+ })
52
+
53
+ // 默认直接使用内置 SiliconFlow Provider。
54
+ // 仍允许通过 aiProvider / ai 完全替换为自己的模型。
55
+ // 对外只暴露两个 AI 配置:模型与请求路径。
56
+ // temperature / maxTokens / thinking 等执行策略由 SDK 内部自动决定。
57
+ const siliconflowOptions = {
58
+ model: normalizedOptions.model,
59
+ apiUrl: normalizedOptions.aiUrl,
60
+ apiKey: normalizedOptions.apiKey
61
+ }
62
+
63
+ const defaultAIProvider = createSiliconFlowProvider(siliconflowOptions)
64
+
65
+ const manager = new XiaoyuanManager({
66
+ registry,
67
+ speech,
68
+ ai: normalizedOptions.aiProvider || normalizedOptions.ai || defaultAIProvider
69
+ })
70
+
71
+ const api = {
72
+ tts: ttsProvider,
73
+ registerFunction: (definition) => registry.registerFunction(definition),
74
+ registerData: (definition) => registry.registerData(definition),
75
+ setContext: (ctx) => registry.setContext(ctx),
76
+ getContext: () => registry.getContext(),
77
+ getManifest: () => manager.getManifest(),
78
+ getData: (name, params) => manager.getData(name, params),
79
+ execute: (command) => manager.execute(command),
80
+ ask: (message) => manager.run(message),
81
+ speech,
82
+ manager,
83
+ createSiliconFlowProvider
84
+ }
85
+
86
+ singleton = { api, registry, manager, speech, options: normalizedOptions }
87
+
88
+ app.config.globalProperties.$xiaoyuan = api
89
+ app.provide(KEY, singleton)
90
+ app.provide('xiaoyuan', singleton)
91
+ app.component('XiaoyuanAssistant', XiaoyuanAssistant)
92
+
93
+ if (typeof window !== 'undefined') {
94
+ window.$xiaoyuan = api
95
+ }
96
+ }
97
+ }
98
+
99
+ export { XiaoyuanAssistant, KEY, createSiliconFlowProvider, HewoyiTTSProvider }
100
+ export default Xiaoyuan
package/src/prompts.js ADDED
@@ -0,0 +1,24 @@
1
+ export const DEFAULT_SYSTEM_PROMPT = `你是“大屏助手小园”的快速意图路由器。
2
+ 你的唯一任务:先听懂用户人话,再判断属于“页面操作(action)”、“数据分析(analysis)”还是“普通聊天(chat)”。
3
+ 规则:
4
+ 1. 只能使用能力清单里真实存在的 Function 和 DataSource。
5
+ 2. 页面操作:把用户人话拆成按原顺序执行的独立 action step,并提取 params.value;不要分析。
6
+ 3. 多个动作必须拆开,不能漏掉后续动作。
7
+ 4. data-ai-description 用来理解 Function 语义;data-ai-param 用来定位具体目标。
8
+ 5. 数据分析:输出 analysis step,并选择真正需要的数据源。
9
+ 6. 只有分析任务才需要 DataSource;纯页面操作不需要读取数据。
10
+ 7. 不要输出内部解释,不要输出 Markdown,只输出 JSON/逐行 JSON step。
11
+ 示例:
12
+ 用户:“切换到集成监管,并切换到2023年”
13
+ 输出:
14
+ {"type":"action","function":"handleMenuClick","params":{"value":"集成监管"}}
15
+ {"type":"action","function":"changeYear","params":{"value":"2023"}}
16
+ 用户:“分析一下当前土壤数据”
17
+ 输出:
18
+ {"type":"analysis","dataRequests":[{"name":"soilData","params":{}}],"instruction":"分析当前土壤数据"}`
19
+
20
+ export function buildSystemPrompt(customPrompt = '') {
21
+ return customPrompt?.trim()
22
+ ? `${DEFAULT_SYSTEM_PROMPT}\n\n项目自定义规则:\n${customPrompt.trim()}`
23
+ : DEFAULT_SYSTEM_PROMPT;
24
+ }