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,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
+ }