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
package/example/App.vue
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { getCurrentInstance, onMounted, ref } from 'vue'
|
|
3
|
+
|
|
4
|
+
const dataList = ref([
|
|
5
|
+
{ name: 'A01', humidity: 63, temperature: 24.5 },
|
|
6
|
+
{ name: 'A02', humidity: 31, temperature: 28.2 }
|
|
7
|
+
])
|
|
8
|
+
|
|
9
|
+
const dialogVisible = ref(false)
|
|
10
|
+
|
|
11
|
+
const api = getCurrentInstance().appContext.config.globalProperties.$xiaoyuan
|
|
12
|
+
|
|
13
|
+
onMounted(() => {
|
|
14
|
+
api.registerData({
|
|
15
|
+
name: 'plotData',
|
|
16
|
+
description: '当前页面全部地块监测数据,用于分析地块环境与玉米长势',
|
|
17
|
+
schema: {
|
|
18
|
+
name: { type: 'string', description: '地块名称' },
|
|
19
|
+
humidity: { type: 'number', description: '土壤湿度百分比' },
|
|
20
|
+
temperature: { type: 'number', description: '当前温度,℃' }
|
|
21
|
+
},
|
|
22
|
+
get: () => dataList.value
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
api.setContext({ crop: '玉米', growthStage: '拔节期' })
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
function openDialog() {
|
|
29
|
+
dialogVisible.value = true
|
|
30
|
+
}
|
|
31
|
+
</script>
|
|
32
|
+
|
|
33
|
+
<template>
|
|
34
|
+
<div class="dashboard-demo">
|
|
35
|
+
<button data-ai-function="openDialog" @click="openDialog">打开详情</button>
|
|
36
|
+
<button data-ai-function="openPlot" data-ai-id="A02">打开A02</button>
|
|
37
|
+
|
|
38
|
+
<div v-if="dialogVisible" class="dialog">
|
|
39
|
+
当前详情弹窗
|
|
40
|
+
<button @click="dialogVisible = false">关闭</button>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
</template>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { createApp } from 'vue'
|
|
2
|
+
import App from '../example/App.vue'
|
|
3
|
+
import Xiaoyuan from '../src/index.js'
|
|
4
|
+
import '../src/styles/index.css'
|
|
5
|
+
|
|
6
|
+
const app = createApp(App)
|
|
7
|
+
|
|
8
|
+
app.use(Xiaoyuan, {
|
|
9
|
+
model: 'THUDM/GLM-Z1-9B-0414',
|
|
10
|
+
aiUrl: 'https://api.siliconflow.cn/v1/chat/completions',
|
|
11
|
+
apiKey: 'YOUR_SILICONFLOW_KEY',
|
|
12
|
+
tts: {
|
|
13
|
+
provider: 'hewoyi',
|
|
14
|
+
apiKey: 'YOUR_HEWOYI_TTS_KEY',
|
|
15
|
+
apiUrl: 'https://api.hewoyi.com/api/ai/audio/speech',
|
|
16
|
+
voice: 'zh-CN-XiaoyiNeural',
|
|
17
|
+
format: 'mp3',
|
|
18
|
+
speed: '',
|
|
19
|
+
model: '',
|
|
20
|
+
type: 'speech'
|
|
21
|
+
}
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
app.mount('#app')
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 推荐生产环境:Node/Express 后端代理。
|
|
3
|
+
* 环境变量:
|
|
4
|
+
* SILICONFLOW_API_KEY=你的新Key
|
|
5
|
+
* SILICONFLOW_MODEL=THUDM/GLM-Z1-9B-0414
|
|
6
|
+
* FREETTS_API_KEY=你的FreeTTS Key
|
|
7
|
+
*/
|
|
8
|
+
const express = require('express')
|
|
9
|
+
const { spawn } = require('node:child_process')
|
|
10
|
+
|
|
11
|
+
const app = express()
|
|
12
|
+
app.use(express.json({ limit: '2mb' }))
|
|
13
|
+
|
|
14
|
+
function parseSrtTime(value) {
|
|
15
|
+
const match = String(value || '').trim().match(/^(\d{2}):(\d{2}):(\d{2}),(\d{3})$/)
|
|
16
|
+
if (!match) return null
|
|
17
|
+
const [, hh, mm, ss, ms] = match
|
|
18
|
+
return Number(hh) * 3600000 + Number(mm) * 60000 + Number(ss) * 1000 + Number(ms)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function getLastSrtEndMs(srtText) {
|
|
22
|
+
const matches = String(srtText || '').matchAll(
|
|
23
|
+
/(\d{2}:\d{2}:\d{2},\d{3})\s+-->\s+(\d{2}:\d{2}:\d{2},\d{3})/g
|
|
24
|
+
)
|
|
25
|
+
let lastEnd = null
|
|
26
|
+
for (const match of matches) {
|
|
27
|
+
const end = parseSrtTime(match[2])
|
|
28
|
+
if (end != null) lastEnd = end
|
|
29
|
+
}
|
|
30
|
+
return lastEnd
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function trimMp3(buffer, durationMs) {
|
|
34
|
+
if (!durationMs || durationMs <= 0) return buffer
|
|
35
|
+
|
|
36
|
+
return await new Promise((resolve, reject) => {
|
|
37
|
+
const child = spawn('ffmpeg', [
|
|
38
|
+
'-hide_banner', '-loglevel', 'error',
|
|
39
|
+
'-i', 'pipe:0',
|
|
40
|
+
'-t', (durationMs / 1000).toFixed(3),
|
|
41
|
+
'-vn',
|
|
42
|
+
'-c:a', 'libmp3lame',
|
|
43
|
+
'-b:a', '128k',
|
|
44
|
+
'-f', 'mp3',
|
|
45
|
+
'pipe:1'
|
|
46
|
+
])
|
|
47
|
+
|
|
48
|
+
const chunks = []
|
|
49
|
+
const errors = []
|
|
50
|
+
child.stdout.on('data', chunk => chunks.push(chunk))
|
|
51
|
+
child.stderr.on('data', chunk => errors.push(chunk))
|
|
52
|
+
child.on('error', reject)
|
|
53
|
+
child.on('close', code => {
|
|
54
|
+
if (code !== 0 || !chunks.length) {
|
|
55
|
+
reject(new Error(Buffer.concat(errors).toString() || `ffmpeg exit ${code}`))
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
resolve(Buffer.concat(chunks))
|
|
59
|
+
})
|
|
60
|
+
child.stdin.end(buffer)
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
app.post('/api/xiaoyuan/ai', async (req, res) => {
|
|
65
|
+
try {
|
|
66
|
+
const upstream = await fetch('https://api.siliconflow.cn/v1/chat/completions', {
|
|
67
|
+
method: 'POST',
|
|
68
|
+
headers: {
|
|
69
|
+
'Content-Type': 'application/json',
|
|
70
|
+
Authorization: `Bearer ${process.env.SILICONFLOW_API_KEY}`
|
|
71
|
+
},
|
|
72
|
+
body: JSON.stringify({
|
|
73
|
+
model: process.env.SILICONFLOW_MODEL || 'THUDM/GLM-Z1-9B-0414',
|
|
74
|
+
messages: req.body.messages || [],
|
|
75
|
+
temperature: req.body.temperature ?? 0.2
|
|
76
|
+
})
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
const text = await upstream.text()
|
|
80
|
+
res.status(upstream.status)
|
|
81
|
+
res.type('application/json').send(text)
|
|
82
|
+
} catch (error) {
|
|
83
|
+
res.status(500).json({ error: error.message })
|
|
84
|
+
}
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
app.get('/api/xiaoyuan/tts', async (req, res) => {
|
|
88
|
+
try {
|
|
89
|
+
const apiKey = process.env.HEWOYI_TTS_KEY
|
|
90
|
+
if (!apiKey) return res.status(500).json({ message: '缺少 HEWOYI_TTS_KEY' })
|
|
91
|
+
|
|
92
|
+
const params = new URLSearchParams({
|
|
93
|
+
key: apiKey,
|
|
94
|
+
text: String(req.query.text || '').slice(0, 4096),
|
|
95
|
+
voice: req.query.voice || 'zh-CN-XiaoyiNeural',
|
|
96
|
+
format: req.query.format || 'mp3',
|
|
97
|
+
speed: req.query.speed || '',
|
|
98
|
+
model: req.query.model || '',
|
|
99
|
+
type: req.query.type || 'speech'
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
const upstream = await fetch(`https://api.hewoyi.com/api/ai/audio/speech?${params.toString()}`)
|
|
103
|
+
const contentType = upstream.headers.get('content-type') || 'application/json'
|
|
104
|
+
const body = Buffer.from(await upstream.arrayBuffer())
|
|
105
|
+
|
|
106
|
+
res.status(upstream.status)
|
|
107
|
+
res.setHeader('Content-Type', contentType)
|
|
108
|
+
res.setHeader('Cache-Control', 'no-store')
|
|
109
|
+
res.send(body)
|
|
110
|
+
} catch (error) {
|
|
111
|
+
res.status(500).json({ error: error.message })
|
|
112
|
+
}
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
app.listen(process.env.PORT || 3001, () => {
|
|
116
|
+
console.log('Xiaoyuan proxy listening')
|
|
117
|
+
})
|
package/package.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "xiaoyuan-assistant",
|
|
3
|
+
"version": "0.5.45",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./src/index.js",
|
|
6
|
+
"module": "./src/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"import": "./src/index.js",
|
|
10
|
+
"default": "./src/index.js"
|
|
11
|
+
},
|
|
12
|
+
"./style.css": "./src/styles/index.css"
|
|
13
|
+
},
|
|
14
|
+
"peerDependencies": {
|
|
15
|
+
"vue": "^3.3.0"
|
|
16
|
+
},
|
|
17
|
+
"scripts": {}
|
|
18
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises'
|
|
2
|
+
import { dirname, resolve } from 'node:path'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
import https from 'node:https'
|
|
5
|
+
|
|
6
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
7
|
+
const root = resolve(__dirname, '..')
|
|
8
|
+
const output = resolve(root, 'src/assets/received-command.mp3')
|
|
9
|
+
const flagFile = resolve(root, 'src/assets/received-command.local.js')
|
|
10
|
+
|
|
11
|
+
const API_KEY = 'XA8sVN8G43Cj8vc7MmXbf6nFGb'
|
|
12
|
+
const text = '收到指令,请您稍等'
|
|
13
|
+
const apiUrl = `https://api.hewoyi.com/api/ai/audio/speech?key=${encodeURIComponent(API_KEY)}&text=${encodeURIComponent(text)}&voice=zh-CN-XiaoyiNeural&format=&speed=&model=&type=speech`
|
|
14
|
+
|
|
15
|
+
function request(url) {
|
|
16
|
+
return new Promise((resolvePromise, reject) => {
|
|
17
|
+
https.get(url, (res) => {
|
|
18
|
+
const chunks = []
|
|
19
|
+
res.on('data', (chunk) => chunks.push(chunk))
|
|
20
|
+
res.on('end', () => {
|
|
21
|
+
const buffer = Buffer.concat(chunks)
|
|
22
|
+
resolvePromise({ status: res.statusCode || 0, contentType: res.headers['content-type'] || '', buffer })
|
|
23
|
+
})
|
|
24
|
+
}).on('error', reject)
|
|
25
|
+
})
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function extractAudioUrl(html) {
|
|
29
|
+
const match = String(html).match(/<source[^>]+src\s*=\s*["']([^"']+)["']/i)
|
|
30
|
+
|| String(html).match(/<audio[^>]+src\s*=\s*["']([^"']+)["']/i)
|
|
31
|
+
if (!match?.[1]) return null
|
|
32
|
+
return match[1]
|
|
33
|
+
.replace(/&/gi, '&')
|
|
34
|
+
.replace(/"/gi, '"')
|
|
35
|
+
.replace(/'/gi, "'")
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function main() {
|
|
39
|
+
try {
|
|
40
|
+
await mkdir(resolve(root, 'src/assets'), { recursive: true })
|
|
41
|
+
const first = await request(apiUrl)
|
|
42
|
+
if (first.status < 200 || first.status >= 300) throw new Error(`TTS API status ${first.status}`)
|
|
43
|
+
|
|
44
|
+
let audioBuffer = null
|
|
45
|
+
const contentType = first.contentType.toLowerCase()
|
|
46
|
+
if (contentType.startsWith('audio/')) {
|
|
47
|
+
audioBuffer = first.buffer
|
|
48
|
+
} else {
|
|
49
|
+
const sourceUrl = extractAudioUrl(first.buffer.toString('utf8'))
|
|
50
|
+
if (!sourceUrl) throw new Error('TTS HTML 中没有找到 audio/source 地址')
|
|
51
|
+
const second = await request(sourceUrl)
|
|
52
|
+
if (second.status < 200 || second.status >= 300) throw new Error(`audio URL status ${second.status}`)
|
|
53
|
+
audioBuffer = second.buffer
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (!audioBuffer || audioBuffer.length < 10000) {
|
|
57
|
+
throw new Error(`下载到的音频文件异常,大小 ${audioBuffer?.length || 0} bytes`)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
await writeFile(output, audioBuffer)
|
|
61
|
+
await writeFile(flagFile, `// generated by npm postinstall\nexport const receivedCommandLocalReady = true\n`)
|
|
62
|
+
console.log(`[小园] 已将“收到指令,请您稍等。”缓存到本地:${output} (${audioBuffer.length} bytes)`)
|
|
63
|
+
} catch (error) {
|
|
64
|
+
console.warn('[小园] 本地 TTS 预缓存失败,将保留浏览器 TTS/网络 TTS 兼容链路:', error?.message || error)
|
|
65
|
+
// 保留 false,运行时会继续走现有兼容逻辑。
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
await main()
|
|
Binary file
|
|
Binary file
|
|
Binary file
|