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.
- package/CHANGELOG.md +388 -0
- package/README.md +229 -0
- package/XIAOYUAN-BUSINESS-FLOW.md +1214 -0
- package/example/App.vue +43 -0
- package/examples/main-siliconflow.js +24 -0
- package/examples/server/package.json +7 -0
- package/examples/server/siliconflow.cjs +117 -0
- package/package.json +18 -0
- package/scripts/cache-received-tts.mjs +69 -0
- package/src/assets/received-command.local.js +2 -0
- package/src/assets/received-command.mp3 +0 -0
- package/src/assets/received-command.zh-CN-XiaoxiaoNeural.mp3 +0 -0
- package/src/assets/received-command.zh-CN-XiaoyiNeural.mp3 +0 -0
- package/src/components/XiaoyuanAssistant.vue +519 -0
- package/src/core/builtins.js +188 -0
- package/src/core/dom.js +156 -0
- package/src/core/localCommandParser.js +434 -0
- package/src/core/manager.js +505 -0
- package/src/core/registry.js +49 -0
- package/src/index.js +100 -0
- package/src/prompts.js +24 -0
- package/src/providers/siliconflow.js +296 -0
- package/src/styles/index.css +166 -0
- package/src/vite-plugin.js +66 -0
- package/src/voice/freetts.js +238 -0
- package/src/voice/hewoyi.js +774 -0
- package/src/voice/localReceivedTTS.js +106 -0
- package/src/voice/speech.js +329 -0
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import { DEFAULT_SYSTEM_PROMPT } from '../prompts.js'
|
|
2
|
+
|
|
3
|
+
function extractJson(text) {
|
|
4
|
+
if (!text) return null
|
|
5
|
+
const cleaned = String(text)
|
|
6
|
+
.replace(/^```json\s*/i, '')
|
|
7
|
+
.replace(/^```\s*/i, '')
|
|
8
|
+
.replace(/```$/i, '')
|
|
9
|
+
.trim()
|
|
10
|
+
|
|
11
|
+
try { return JSON.parse(cleaned) } catch (_) {}
|
|
12
|
+
const start = cleaned.indexOf('{')
|
|
13
|
+
const end = cleaned.lastIndexOf('}')
|
|
14
|
+
if (start >= 0 && end > start) {
|
|
15
|
+
try { return JSON.parse(cleaned.slice(start, end + 1)) } catch (_) {}
|
|
16
|
+
}
|
|
17
|
+
return null
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function compactManifest(manifest, { includeDataSources = false, includeSchemas = false } = {}) {
|
|
21
|
+
const domElements = (manifest?.domElements || []).map(({ name, description, params }) => ({
|
|
22
|
+
name,
|
|
23
|
+
description,
|
|
24
|
+
params: Array.isArray(params) ? params.slice(0, 40) : []
|
|
25
|
+
}))
|
|
26
|
+
|
|
27
|
+
const functions = (manifest?.functions || []).map(({ name, description, params }) => ({
|
|
28
|
+
name,
|
|
29
|
+
description,
|
|
30
|
+
params
|
|
31
|
+
}))
|
|
32
|
+
|
|
33
|
+
const dataSources = (manifest?.dataSources || []).map((item) => ({
|
|
34
|
+
name: item.name,
|
|
35
|
+
description: item.description,
|
|
36
|
+
...(includeSchemas ? { schema: item.schema || {} } : {})
|
|
37
|
+
}))
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
functions,
|
|
41
|
+
domElements,
|
|
42
|
+
...(includeDataSources ? { dataSources } : {})
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function looksLikeAnalysis(message = '') {
|
|
47
|
+
return /(分析|研判|评估|判断|原因|趋势|风险|建议|情况|长势|预测|对比|比较|统计|为什么|是否异常|异常)/i.test(String(message))
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 规划阶段:只做“人话 -> Function/参数/分析步骤”,不做推理。
|
|
51
|
+
// 使用 NDJSON(每行一个 step)方便流式拿到第一步后立即执行,后续步骤继续排队。
|
|
52
|
+
const PLANNER_PROMPT = `你是大屏助手“小园”的快速意图路由器。
|
|
53
|
+
把用户人话转换成按原顺序执行的 step。
|
|
54
|
+
页面操作输出 action step;数据分析输出 analysis step;普通聊天输出 chat。
|
|
55
|
+
只能使用清单中的真实 Function/DataSource。
|
|
56
|
+
data-ai-description 用于理解功能;data-ai-param 用于提取目标参数。
|
|
57
|
+
多步指令必须逐项拆分,不得遗漏。
|
|
58
|
+
只输出逐行 JSON。`;
|
|
59
|
+
function parseNdjsonLine(line) {
|
|
60
|
+
const raw = String(line || '').trim()
|
|
61
|
+
if (!raw || raw === '[DONE]') return null
|
|
62
|
+
const clean = raw.replace(/^data:\s*/i, '').trim()
|
|
63
|
+
if (!clean) return null
|
|
64
|
+
const parsed = extractJson(clean)
|
|
65
|
+
if (!parsed) return null
|
|
66
|
+
if (parsed.type === 'action' && (parsed.function || parsed.name)) return parsed
|
|
67
|
+
if (parsed.type === 'analysis' && Array.isArray(parsed.dataRequests)) return parsed
|
|
68
|
+
return null
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function requestChat({ fetchImpl, url, apiKey, model, systemPrompt, userPrompt, thinking, maxTokens, temperature, thinkingBudget, timeoutMs = 0, responseFormat = null, stream = false, onChunk = null, onStep = null }) {
|
|
72
|
+
const headers = {
|
|
73
|
+
'Content-Type': 'application/json',
|
|
74
|
+
Authorization: `Bearer ${apiKey}`
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const body = {
|
|
78
|
+
model,
|
|
79
|
+
temperature,
|
|
80
|
+
max_tokens: maxTokens,
|
|
81
|
+
enable_thinking: thinking,
|
|
82
|
+
...(thinking ? { thinking_budget: thinkingBudget ?? 8192 } : {}),
|
|
83
|
+
...(responseFormat ? { response_format: responseFormat } : {}),
|
|
84
|
+
stream,
|
|
85
|
+
messages: [
|
|
86
|
+
{ role: 'system', content: systemPrompt },
|
|
87
|
+
{ role: 'user', content: userPrompt }
|
|
88
|
+
]
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
let controller
|
|
92
|
+
let timer
|
|
93
|
+
if (typeof AbortController !== 'undefined' && timeoutMs > 0) {
|
|
94
|
+
controller = new AbortController()
|
|
95
|
+
timer = setTimeout(() => controller.abort(), timeoutMs)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
let response
|
|
99
|
+
try {
|
|
100
|
+
response = await fetchImpl(url, {
|
|
101
|
+
method: 'POST',
|
|
102
|
+
headers,
|
|
103
|
+
body: JSON.stringify(body),
|
|
104
|
+
signal: controller?.signal
|
|
105
|
+
})
|
|
106
|
+
} catch (error) {
|
|
107
|
+
if (error?.name === 'AbortError') throw new Error('AI 请求超时,请稍后重试')
|
|
108
|
+
throw error
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
if (!response.ok) {
|
|
113
|
+
const bodyText = await response.text().catch(() => '')
|
|
114
|
+
throw new Error(`SiliconFlow 请求失败 ${response.status}${bodyText ? `:${bodyText.slice(0, 500)}` : ''}`)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (!stream) {
|
|
118
|
+
const data = await response.json()
|
|
119
|
+
const message = data?.choices?.[0]?.message || {}
|
|
120
|
+
const content = message.content
|
|
121
|
+
if (typeof content === 'string' && content.trim()) return content
|
|
122
|
+
const reasoning = message.reasoning_content
|
|
123
|
+
if (typeof reasoning === 'string' && reasoning.trim()) return reasoning
|
|
124
|
+
throw new Error('SiliconFlow 未返回有效内容')
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (!response.body?.getReader) {
|
|
128
|
+
throw new Error('当前浏览器不支持流式 AI 响应')
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const reader = response.body.getReader()
|
|
132
|
+
const decoder = new TextDecoder('utf-8')
|
|
133
|
+
let buffer = ''
|
|
134
|
+
let contentAll = ''
|
|
135
|
+
const emitted = []
|
|
136
|
+
|
|
137
|
+
while (true) {
|
|
138
|
+
const { value, done } = await reader.read()
|
|
139
|
+
if (done) break
|
|
140
|
+
const text = decoder.decode(value, { stream: true })
|
|
141
|
+
buffer += text
|
|
142
|
+
|
|
143
|
+
const lines = buffer.split(/\r?\n/)
|
|
144
|
+
buffer = lines.pop() || ''
|
|
145
|
+
|
|
146
|
+
for (const line of lines) {
|
|
147
|
+
const trimmed = line.trim()
|
|
148
|
+
if (!trimmed || trimmed.startsWith(':')) continue
|
|
149
|
+
const payload = trimmed.startsWith('data:') ? trimmed.slice(5).trim() : trimmed
|
|
150
|
+
if (payload === '[DONE]') continue
|
|
151
|
+
let chunk
|
|
152
|
+
try { chunk = JSON.parse(payload) } catch (_) { continue }
|
|
153
|
+
|
|
154
|
+
const delta = chunk?.choices?.[0]?.delta || {}
|
|
155
|
+
const part = typeof delta.content === 'string' ? delta.content : ''
|
|
156
|
+
if (!part) continue
|
|
157
|
+
contentAll += part
|
|
158
|
+
onChunk?.(part)
|
|
159
|
+
|
|
160
|
+
// 优先识别完整的一行 JSON step;拿到就立即回调给 Manager 排队执行。
|
|
161
|
+
const linesNow = contentAll.split(/\r?\n/)
|
|
162
|
+
for (let i = emitted.length; i < linesNow.length - 1; i += 1) {
|
|
163
|
+
const step = parseNdjsonLine(linesNow[i])
|
|
164
|
+
emitted.push(linesNow[i])
|
|
165
|
+
if (step) onStep?.(step)
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// 处理最后一行没有换行的情况
|
|
171
|
+
const tail = parseNdjsonLine(buffer) || parseNdjsonLine(contentAll.split(/\r?\n/).filter(Boolean).at(-1))
|
|
172
|
+
if (tail) onStep?.(tail)
|
|
173
|
+
|
|
174
|
+
return contentAll
|
|
175
|
+
} finally {
|
|
176
|
+
if (timer) clearTimeout(timer)
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function createSiliconFlowProvider(options = {}) {
|
|
181
|
+
const fetchImpl = options.fetch || globalThis.fetch
|
|
182
|
+
const url = options.apiUrl
|
|
183
|
+
const apiKey = options.apiKey
|
|
184
|
+
const model = options.model
|
|
185
|
+
const customSystemPrompt = options.systemPrompt || ''
|
|
186
|
+
|
|
187
|
+
const systemPrompt = customSystemPrompt
|
|
188
|
+
? `${DEFAULT_SYSTEM_PROMPT}\n\n项目自定义规则:\n${customSystemPrompt}`
|
|
189
|
+
: DEFAULT_SYSTEM_PROMPT
|
|
190
|
+
|
|
191
|
+
if (typeof fetchImpl !== 'function') throw new Error('当前环境没有可用的 fetch')
|
|
192
|
+
if (!url) throw new Error('请传入 apiUrl')
|
|
193
|
+
if (!apiKey) throw new Error('请传入 apiKey')
|
|
194
|
+
if (!model) throw new Error('请传入 model')
|
|
195
|
+
|
|
196
|
+
return async ({ mode = 'plan', message, manifest, plan, dataResults, onPlanStep }) => {
|
|
197
|
+
if (mode === 'chat') {
|
|
198
|
+
const context = JSON.stringify(manifest?.context || {}, null, 0)
|
|
199
|
+
const chatSystemPrompt = `你是“大屏助手小园”。自然、简洁地回答用户的日常聊天。不要输出内部 Function、DOM、DataSource 或执行细节。对需要实时外部数据的问题,如果上下文没有提供真实数据,不要编造。` + (customSystemPrompt ? `\n项目业务规则:\n${customSystemPrompt}` : '')
|
|
200
|
+
const content = await requestChat({
|
|
201
|
+
fetchImpl,
|
|
202
|
+
url,
|
|
203
|
+
apiKey,
|
|
204
|
+
model,
|
|
205
|
+
systemPrompt: chatSystemPrompt,
|
|
206
|
+
userPrompt: `${context}\n用户:${message}\n请直接回答用户。`,
|
|
207
|
+
thinking: false,
|
|
208
|
+
maxTokens: 220,
|
|
209
|
+
temperature: 0.4,
|
|
210
|
+
timeoutMs: 0,
|
|
211
|
+
responseFormat: null,
|
|
212
|
+
stream: false
|
|
213
|
+
})
|
|
214
|
+
return { type: 'chat', reply: content, steps: [] }
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (mode === 'analysis') {
|
|
218
|
+
const analysisManifest = JSON.stringify(
|
|
219
|
+
compactManifest(manifest, { includeDataSources: true, includeSchemas: true }),
|
|
220
|
+
null,
|
|
221
|
+
0
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
const analysisPrompt = `用户要分析:${message}\n\n实时数据:${JSON.stringify(dataResults || {}, null, 0)}\n\n根据真实数据给出最终分析。不要编造数据,不暴露内部实现,适合语音播报。只输出 JSON:{"type":"analysis","reply":"最终分析结果","analysis":"最终分析结果"}`
|
|
225
|
+
const analysisSystemPrompt = `你是农业大屏助手“小园”的分析引擎。只根据提供的真实数据回答。允许深度推理,但不要暴露内部 Function、DOM、DataSource 名称。输出简洁、准确、适合语音播报的 JSON。` + (customSystemPrompt ? `\n项目业务规则:\n${customSystemPrompt}` : '')
|
|
226
|
+
|
|
227
|
+
const content = await requestChat({
|
|
228
|
+
fetchImpl,
|
|
229
|
+
url,
|
|
230
|
+
apiKey,
|
|
231
|
+
model,
|
|
232
|
+
systemPrompt: analysisSystemPrompt,
|
|
233
|
+
userPrompt: `${analysisManifest}\n${analysisPrompt}`,
|
|
234
|
+
thinking: true,
|
|
235
|
+
thinkingBudget: 4096,
|
|
236
|
+
maxTokens: 700,
|
|
237
|
+
temperature: 0.2,
|
|
238
|
+
timeoutMs: 0,
|
|
239
|
+
responseFormat: { type: 'json_object' },
|
|
240
|
+
stream: false
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
return extractJson(content) || {
|
|
244
|
+
type: 'analysis',
|
|
245
|
+
reply: content,
|
|
246
|
+
analysis: content
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const needsData = looksLikeAnalysis(message)
|
|
251
|
+
const plannerManifest = JSON.stringify(
|
|
252
|
+
compactManifest(manifest, {
|
|
253
|
+
includeDataSources: needsData,
|
|
254
|
+
includeSchemas: false
|
|
255
|
+
}),
|
|
256
|
+
null,
|
|
257
|
+
0
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
const intentPrompt = `${plannerManifest}\n用户:${message}\n立即逐行输出 workflow step JSON。`
|
|
261
|
+
const streamedSteps = []
|
|
262
|
+
|
|
263
|
+
const content = await requestChat({
|
|
264
|
+
fetchImpl,
|
|
265
|
+
url,
|
|
266
|
+
apiKey,
|
|
267
|
+
model,
|
|
268
|
+
systemPrompt: PLANNER_PROMPT,
|
|
269
|
+
userPrompt: intentPrompt,
|
|
270
|
+
thinking: false,
|
|
271
|
+
maxTokens: 180,
|
|
272
|
+
temperature: 0,
|
|
273
|
+
timeoutMs: 0,
|
|
274
|
+
responseFormat: null,
|
|
275
|
+
stream: true,
|
|
276
|
+
onStep: (step) => {
|
|
277
|
+
streamedSteps.push(step)
|
|
278
|
+
onPlanStep?.(step)
|
|
279
|
+
}
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
if (streamedSteps.length) {
|
|
283
|
+
return {
|
|
284
|
+
type: 'workflow',
|
|
285
|
+
steps: streamedSteps,
|
|
286
|
+
reply: ''
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const fallback = extractJson(content)
|
|
291
|
+
if (fallback?.steps) return { ...fallback, type: fallback.type || 'workflow' }
|
|
292
|
+
return { type: 'chat', reply: '我暂时没有准确理解这条指令,请换一种说法。', steps: [] }
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export { extractJson }
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
.xy-root {
|
|
2
|
+
--xy-bg: rgba(8, 13, 29, .985);
|
|
3
|
+
--xy-panel: rgba(16, 22, 42, .95);
|
|
4
|
+
--xy-border: rgba(255,255,255,.10);
|
|
5
|
+
--xy-border-strong: rgba(255,255,255,.16);
|
|
6
|
+
--xy-text: #f5f7ff;
|
|
7
|
+
--xy-muted: rgba(231,236,255,.62);
|
|
8
|
+
--xy-faint: rgba(231,236,255,.34);
|
|
9
|
+
--xy-accent: #6d5dfc;
|
|
10
|
+
--xy-accent-2: #9487ff;
|
|
11
|
+
--xy-green: #68e2a2;
|
|
12
|
+
position: fixed;
|
|
13
|
+
top: 22px;
|
|
14
|
+
right: 22px;
|
|
15
|
+
z-index: 2147483000;
|
|
16
|
+
font-family: Inter, "PingFang SC", "Microsoft YaHei", system-ui, sans-serif;
|
|
17
|
+
color: var(--xy-text);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.xy-root * { box-sizing: border-box; }
|
|
21
|
+
.xy-root button,
|
|
22
|
+
.xy-root textarea { font: inherit; }
|
|
23
|
+
|
|
24
|
+
.xy-fab {
|
|
25
|
+
position: relative;
|
|
26
|
+
width: 62px;
|
|
27
|
+
height: 62px;
|
|
28
|
+
border: 1px solid rgba(255,255,255,.20);
|
|
29
|
+
border-radius: 20px;
|
|
30
|
+
cursor: pointer;
|
|
31
|
+
color: #fff;
|
|
32
|
+
background: linear-gradient(145deg, #877aff 0%, #6d5dfc 46%, #5142d0 100%);
|
|
33
|
+
box-shadow: 0 14px 38px rgba(41,31,127,.42), 0 0 0 1px rgba(255,255,255,.05) inset;
|
|
34
|
+
transition: transform .18s ease, box-shadow .18s ease;
|
|
35
|
+
}
|
|
36
|
+
.xy-fab:hover { transform: translateY(-2px); box-shadow: 0 18px 44px rgba(41,31,127,.52), 0 0 0 1px rgba(255,255,255,.08) inset; }
|
|
37
|
+
.xy-fab-glow { position: absolute; inset: -14px; border-radius: 26px; background: radial-gradient(circle, rgba(109,93,252,.34), transparent 68%); z-index: -1; }
|
|
38
|
+
.xy-avatar { display: inline-flex; align-items: center; justify-content: center; width: 100%; height: 100%; font-size: 23px; font-weight: 800; letter-spacing: 1px; }
|
|
39
|
+
.xy-avatar.small { width: 42px; height: 42px; flex: 0 0 42px; font-size: 18px; border-radius: 14px; background: linear-gradient(145deg,#9388ff,#5948dc); box-shadow: 0 7px 18px rgba(87,71,220,.35); }
|
|
40
|
+
.xy-live-dot { position: absolute; top: 4px; right: 4px; width: 9px; height: 9px; background: var(--xy-green); border-radius: 50%; box-shadow: 0 0 0 4px rgba(104,226,162,.10), 0 0 14px rgba(104,226,162,.75); }
|
|
41
|
+
|
|
42
|
+
.xy-panel {
|
|
43
|
+
position: relative;
|
|
44
|
+
width: min(430px, calc(100vw - 28px));
|
|
45
|
+
height: min(680px, calc(100vh - 44px));
|
|
46
|
+
min-height: 500px;
|
|
47
|
+
background: linear-gradient(180deg, rgba(17,24,47,.985), rgba(8,13,29,.99));
|
|
48
|
+
border: 1px solid var(--xy-border-strong);
|
|
49
|
+
border-radius: 22px;
|
|
50
|
+
overflow: hidden;
|
|
51
|
+
box-shadow: 0 30px 100px rgba(0,0,0,.50), 0 8px 30px rgba(33,25,89,.22), 0 0 0 1px rgba(255,255,255,.025) inset;
|
|
52
|
+
backdrop-filter: blur(22px);
|
|
53
|
+
display: grid;
|
|
54
|
+
grid-template-rows: auto auto 1fr auto;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.xy-panel::before {
|
|
58
|
+
content: "";
|
|
59
|
+
position: absolute;
|
|
60
|
+
inset: 0 0 auto 0;
|
|
61
|
+
height: 110px;
|
|
62
|
+
pointer-events: none;
|
|
63
|
+
background: radial-gradient(circle at 16% 0%, rgba(116,99,255,.16), transparent 34%), radial-gradient(circle at 90% 0%, rgba(109,93,252,.10), transparent 28%);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.xy-header { position: relative; display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 16px 17px 14px; border-bottom: 1px solid rgba(255,255,255,.07); }
|
|
67
|
+
.xy-title-wrap { display: flex; align-items: center; gap: 11px; }
|
|
68
|
+
.xy-title { font-size: 17px; font-weight: 750; letter-spacing: .2px; }
|
|
69
|
+
.xy-status { display: flex; align-items: center; gap: 7px; margin-top: 3px; color: var(--xy-muted); font-size: 11px; }
|
|
70
|
+
.xy-status-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--xy-green); box-shadow: 0 0 10px rgba(104,226,162,.56); }
|
|
71
|
+
.xy-status-dot.state-processing,.xy-status-dot.state-listening { background: #aa9eff; box-shadow: 0 0 12px rgba(170,158,255,.72); }
|
|
72
|
+
.xy-header-actions { display: flex; gap: 7px; }
|
|
73
|
+
.xy-icon-btn { display: inline-flex; align-items: center; justify-content: center; width: 36px; height: 36px; border: 1px solid rgba(255,255,255,.07); border-radius: 11px; color: rgba(255,255,255,.68); background: rgba(255,255,255,.045); cursor: pointer; transition: .18s ease; }
|
|
74
|
+
.xy-icon-btn:hover { color: #fff; background: rgba(255,255,255,.08); transform: translateY(-1px); }
|
|
75
|
+
.xy-icon-btn svg {
|
|
76
|
+
display: block !important;
|
|
77
|
+
visibility: visible !important;
|
|
78
|
+
opacity: 1 !important;
|
|
79
|
+
width: 18px !important;
|
|
80
|
+
height: 18px !important;
|
|
81
|
+
min-width: 18px;
|
|
82
|
+
min-height: 18px;
|
|
83
|
+
fill: none !important;
|
|
84
|
+
stroke: #ffffff !important;
|
|
85
|
+
stroke-width: 2 !important;
|
|
86
|
+
stroke-linecap: round !important;
|
|
87
|
+
stroke-linejoin: round !important;
|
|
88
|
+
pointer-events: none;
|
|
89
|
+
}
|
|
90
|
+
/* 某些大屏项目会给 svg/path 设置全局样式,这里给小园自己的图标强制恢复显示。 */
|
|
91
|
+
.xy-icon-btn::before { display: none; }
|
|
92
|
+
.xy-mic svg {
|
|
93
|
+
display: block !important;
|
|
94
|
+
visibility: visible !important;
|
|
95
|
+
opacity: 1 !important;
|
|
96
|
+
width: 21px !important;
|
|
97
|
+
height: 21px !important;
|
|
98
|
+
min-width: 21px;
|
|
99
|
+
min-height: 21px;
|
|
100
|
+
fill: none !important;
|
|
101
|
+
stroke: #ffffff !important;
|
|
102
|
+
stroke-width: 2 !important;
|
|
103
|
+
stroke-linecap: round !important;
|
|
104
|
+
stroke-linejoin: round !important;
|
|
105
|
+
pointer-events: none;
|
|
106
|
+
}
|
|
107
|
+
.xy-mic { position: relative; }
|
|
108
|
+
.xy-mic .xy-mic-label { display: inline-flex; align-items: center; justify-content: center; color: #fff; font-size: 11px; font-weight: 650; }
|
|
109
|
+
.xy-send { white-space: nowrap; overflow: hidden; }
|
|
110
|
+
|
|
111
|
+
.xy-subtitle { min-height: 0; max-height: 0; padding: 0 15px; overflow: hidden; opacity: 0; transition: .22s ease; color: #e6e1ff; font-size: 12px; line-height: 1.5; background: linear-gradient(180deg, rgba(109,93,252,.10), rgba(109,93,252,.045)); border-bottom: 1px solid transparent; }
|
|
112
|
+
.xy-subtitle.show { min-height: 38px; max-height: 100px; padding-top: 9px; padding-bottom: 9px; opacity: 1; border-bottom-color: rgba(109,93,252,.12); }
|
|
113
|
+
|
|
114
|
+
.xy-messages { min-height: 0; padding: 18px 16px 14px; overflow: auto; scrollbar-width: thin; scrollbar-color: rgba(255,255,255,.12) transparent; }
|
|
115
|
+
.xy-messages::-webkit-scrollbar { width: 6px; }
|
|
116
|
+
.xy-messages::-webkit-scrollbar-thumb { background: rgba(255,255,255,.10); border-radius: 999px; }
|
|
117
|
+
.xy-empty { min-height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 40px 16px; text-align: center; }
|
|
118
|
+
.xy-empty-orb { width: 72px; height: 72px; display: grid; place-items: center; border-radius: 22px; margin-bottom: 16px; background: linear-gradient(145deg,#8174ff,#5947d8); box-shadow: 0 16px 34px rgba(82,66,213,.26); }
|
|
119
|
+
.xy-empty-orb span { font-size: 28px; font-weight: 800; }
|
|
120
|
+
.xy-empty-title { font-size: 18px; font-weight: 750; letter-spacing: .2px; }
|
|
121
|
+
.xy-empty-text { margin-top: 8px; max-width: 290px; color: var(--xy-muted); font-size: 12px; line-height: 1.75; }
|
|
122
|
+
.xy-quick-row { display: flex; flex-wrap: wrap; justify-content: center; gap: 7px; margin-top: 18px; }
|
|
123
|
+
.xy-quick-row span { padding: 7px 10px; border: 1px solid rgba(255,255,255,.07); border-radius: 999px; color: rgba(238,242,255,.62); background: rgba(255,255,255,.035); font-size: 10px; }
|
|
124
|
+
|
|
125
|
+
.xy-msg { margin-bottom: 17px; }
|
|
126
|
+
.xy-msg-head { display: flex; align-items: center; gap: 7px; margin-bottom: 6px; }
|
|
127
|
+
.xy-msg-avatar { width: 21px; height: 21px; display: grid; place-items: center; border-radius: 7px; color: #fff; font-size: 9px; font-weight: 700; background: rgba(255,255,255,.10); }
|
|
128
|
+
.xy-msg-avatar.assistant { background: linear-gradient(145deg,#7d70ff,#5b49d5); }
|
|
129
|
+
.xy-msg-avatar.user { color: #ddd8ff; background: rgba(109,93,252,.20); }
|
|
130
|
+
.xy-msg-label { color: rgba(238,242,255,.48); font-size: 10px; }
|
|
131
|
+
.xy-msg-bubble { width: fit-content; max-width: 90%; padding: 10px 13px; border: 1px solid rgba(255,255,255,.055); border-radius: 4px 14px 14px 14px; white-space: pre-wrap; line-height: 1.7; font-size: 13px; color: #edf0ff; background: rgba(255,255,255,.055); box-shadow: 0 8px 22px rgba(0,0,0,.09); }
|
|
132
|
+
.xy-msg.user .xy-msg-head { justify-content: flex-end; }
|
|
133
|
+
.xy-msg.user .xy-msg-bubble { margin-left: auto; border-color: rgba(120,105,255,.24); border-radius: 14px 4px 14px 14px; background: linear-gradient(135deg, rgba(109,93,252,.88), rgba(85,71,214,.86)); box-shadow: 0 10px 26px rgba(71,57,178,.18); }
|
|
134
|
+
|
|
135
|
+
.xy-footer { padding: 11px 12px 13px; border-top: 1px solid rgba(255,255,255,.07); background: linear-gradient(180deg, rgba(8,13,29,.40), rgba(8,13,29,.92)); }
|
|
136
|
+
.xy-interim { margin-bottom: 8px; padding: 8px 10px; border: 1px solid rgba(255,255,255,.05); border-radius: 10px; background: rgba(255,255,255,.035); color: var(--xy-muted); font-size: 11px; }
|
|
137
|
+
.xy-input-row { display: grid; grid-template-columns: 1fr 46px 54px; gap: 8px; align-items: stretch; }
|
|
138
|
+
.xy-input { width: 100%; min-height: 54px; max-height: 110px; resize: none; border: 1px solid rgba(255,255,255,.09); border-radius: 13px; outline: none; color: var(--xy-text); background: rgba(255,255,255,.045); padding: 11px 12px; line-height: 1.55; font-size: 12px; box-shadow: inset 0 1px 0 rgba(255,255,255,.02); transition: .18s ease; }
|
|
139
|
+
.xy-input::placeholder { color: rgba(231,236,255,.34); }
|
|
140
|
+
.xy-input:focus { border-color: rgba(109,93,252,.64); background: rgba(255,255,255,.06); box-shadow: 0 0 0 3px rgba(109,93,252,.10); }
|
|
141
|
+
.xy-mic,.xy-send { height: 54px; border: 0; border-radius: 13px; cursor: pointer; transition: .18s ease; }
|
|
142
|
+
.xy-mic { display: flex; align-items: center; justify-content: center; color: #fff; background: rgba(255,255,255,.065); border: 1px solid rgba(255,255,255,.06); }
|
|
143
|
+
.xy-mic:hover { background: rgba(255,255,255,.10); }
|
|
144
|
+
.xy-mic svg { width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
|
|
145
|
+
.xy-mic.active { background: linear-gradient(145deg,#8072ff,#5c4add); box-shadow: 0 0 24px rgba(109,93,252,.25); }
|
|
146
|
+
.xy-mic-bars { display: flex; align-items: center; justify-content: center; gap: 3px; height: 20px; }
|
|
147
|
+
.xy-mic-bars i { display: block; width: 3px; border-radius: 3px; background: #fff; animation: xy-bars .9s ease-in-out infinite; }
|
|
148
|
+
.xy-mic-bars i:nth-child(1) { height: 9px; animation-delay: -.18s; }
|
|
149
|
+
.xy-mic-bars i:nth-child(2) { height: 16px; }
|
|
150
|
+
.xy-mic-bars i:nth-child(3) { height: 11px; animation-delay: -.32s; }
|
|
151
|
+
@keyframes xy-bars { 0%,100% { transform: scaleY(.7); opacity: .65; } 50% { transform: scaleY(1.1); opacity: 1; } }
|
|
152
|
+
.xy-send { color: #fff; background: linear-gradient(145deg,#7465ff,#5746d7); font-size: 12px; font-weight: 650; box-shadow: 0 8px 20px rgba(84,69,210,.18); }
|
|
153
|
+
.xy-send:not(:disabled):hover { transform: translateY(-1px); filter: brightness(1.04); }
|
|
154
|
+
.xy-send:disabled, .xy-mic:disabled { opacity: .42; cursor: not-allowed; }
|
|
155
|
+
.xy-hint { display: flex; align-items: center; justify-content: center; margin-top: 8px; color: rgba(238,242,255,.30); font-size: 10px; }
|
|
156
|
+
|
|
157
|
+
.xy-pop-enter-active,.xy-pop-leave-active { transition: opacity .18s ease, transform .18s ease; }
|
|
158
|
+
.xy-pop-enter-from,.xy-pop-leave-to { opacity: 0; transform: translateY(-8px) scale(.985); }
|
|
159
|
+
|
|
160
|
+
@media (max-width: 720px) {
|
|
161
|
+
.xy-root { top: 12px; right: 12px; }
|
|
162
|
+
.xy-panel { width: calc(100vw - 24px); height: calc(100vh - 24px); min-height: 0; border-radius: 18px; }
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
.xy-icon-btn { overflow: hidden; }
|
|
166
|
+
.xy-icon-btn svg path, .xy-icon-btn svg rect, .xy-icon-btn svg line, .xy-icon-btn svg polyline, .xy-icon-btn svg circle { vector-effect: non-scaling-stroke; }
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import http from 'node:http'
|
|
2
|
+
import https from 'node:https'
|
|
3
|
+
|
|
4
|
+
function fetchImpl(url, options = {}) {
|
|
5
|
+
if (typeof globalThis.fetch === 'function') return globalThis.fetch(url, options)
|
|
6
|
+
return new Promise((resolve, reject) => {
|
|
7
|
+
const target = new URL(url)
|
|
8
|
+
const lib = target.protocol === 'https:' ? https : http
|
|
9
|
+
const req = lib.request(target, {
|
|
10
|
+
method: options.method || 'GET',
|
|
11
|
+
headers: options.headers || {}
|
|
12
|
+
}, (res) => {
|
|
13
|
+
const chunks = []
|
|
14
|
+
res.on('data', (chunk) => chunks.push(chunk))
|
|
15
|
+
res.on('end', () => {
|
|
16
|
+
const body = Buffer.concat(chunks)
|
|
17
|
+
resolve(new Response(body, { status: res.statusCode, headers: res.headers }))
|
|
18
|
+
})
|
|
19
|
+
})
|
|
20
|
+
req.on('error', reject)
|
|
21
|
+
req.end()
|
|
22
|
+
})
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function xiaoyuanVitePlugin(options = {}) {
|
|
26
|
+
const prefix = options.prefix || '/__xiaoyuan/hewoyi-tts'
|
|
27
|
+
const upstream = options.upstream || 'https://api.hewoyi.com/api/ai/audio/speech'
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
name: 'xiaoyuan-hewoyi-tts-proxy',
|
|
31
|
+
configureServer(server) {
|
|
32
|
+
server.middlewares.use(prefix, async (req, res, next) => {
|
|
33
|
+
if (req.method !== 'GET') {
|
|
34
|
+
next()
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const query = req.url?.includes('?') ? req.url.slice(req.url.indexOf('?') + 1) : ''
|
|
40
|
+
const upstreamUrl = `${upstream}?${query}`
|
|
41
|
+
const response = await fetchImpl(upstreamUrl, {
|
|
42
|
+
method: 'GET',
|
|
43
|
+
headers: {
|
|
44
|
+
Accept: 'application/json,audio/mpeg,audio/*'
|
|
45
|
+
}
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
const buffer = Buffer.from(await response.arrayBuffer())
|
|
49
|
+
const contentType = response.headers.get('content-type') || 'application/octet-stream'
|
|
50
|
+
|
|
51
|
+
res.statusCode = response.status
|
|
52
|
+
res.setHeader('Content-Type', contentType)
|
|
53
|
+
res.setHeader('Cache-Control', 'no-store')
|
|
54
|
+
res.end(buffer)
|
|
55
|
+
} catch (error) {
|
|
56
|
+
res.statusCode = 500
|
|
57
|
+
res.setHeader('Content-Type', 'application/json; charset=utf-8')
|
|
58
|
+
res.end(JSON.stringify({
|
|
59
|
+
message: '小园合我意 TTS 代理异常',
|
|
60
|
+
detail: error?.message || String(error)
|
|
61
|
+
}))
|
|
62
|
+
}
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|