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,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,7 @@
1
+ {
2
+ "private": true,
3
+ "type": "commonjs",
4
+ "dependencies": {
5
+ "express": "^4.21.2"
6
+ }
7
+ }
@@ -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(/&amp;/gi, '&')
34
+ .replace(/&quot;/gi, '"')
35
+ .replace(/&#39;/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()
@@ -0,0 +1,2 @@
1
+ // generated by npm postinstall
2
+ export const receivedCommandLocalReady = true
Binary file