更新依赖,增强ASR功能,优化代理路由处理,添加登录页面渲染

This commit is contained in:
2026-01-15 12:42:37 +08:00
parent 2011ea818c
commit a5ce44ba70
9 changed files with 188 additions and 82 deletions

View File

@@ -47,7 +47,7 @@
"@kevisual/logger": "^0.0.4", "@kevisual/logger": "^0.0.4",
"@kevisual/query": "0.0.33", "@kevisual/query": "0.0.33",
"@kevisual/query-login": "0.0.7", "@kevisual/query-login": "0.0.7",
"@kevisual/router": "^0.0.51", "@kevisual/router": "^0.0.52",
"@kevisual/types": "^0.0.10", "@kevisual/types": "^0.0.10",
"@kevisual/use-config": "^1.0.21", "@kevisual/use-config": "^1.0.21",
"@types/bun": "^1.3.5", "@types/bun": "^1.3.5",
@@ -76,7 +76,7 @@
"access": "public" "access": "public"
}, },
"dependencies": { "dependencies": {
"@kevisual/ha-api": "^0.0.5", "@kevisual/ha-api": "^0.0.6",
"@kevisual/video-tools": "^0.0.13", "@kevisual/video-tools": "^0.0.13",
"eventemitter3": "^5.0.1", "eventemitter3": "^5.0.1",
"lowdb": "^7.0.1", "lowdb": "^7.0.1",

View File

@@ -68,6 +68,9 @@ app.route({
key: 'list', key: 'list',
description: '获取路由列表', description: '获取路由列表',
}).define(async (ctx) => { }).define(async (ctx) => {
const list = ctx.app.getList() const list = ctx.app.getList((item) => {
if (item.id === 'auth') return false;
return true;
})
ctx.body = { list } ctx.body = { list }
}).addTo(app); }).addTo(app);

View File

@@ -0,0 +1,52 @@
export const renderNoAuthAndLogin = (text: string) => {
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login Required</title>
<style>
body {
font-family: Arial, sans-serif;
background-color: #f4f4f4;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
}
.container {
background: white;
padding: 20px;
border-radius: 5px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
text-align: center;
}
h1 {
color: #333;
}
p {
color: #666;
}
a {
display: inline-block;
margin-top: 15px;
padding: 10px 15px;
background-color: #007BFF;
color: white;
text-decoration: none;
border-radius: 5px;
}
a:hover {
background-color: #0056b3;
}
</style>
</head>
<body>
<div class="container">
<p>${text}</p>
<a href="/root/home">转到首页</a>
</div>
</body>
</html>`;
}

View File

@@ -453,7 +453,6 @@ export const LoadApp = async (app: AppInfo, opts?: { mainApp?: any, pm2Connect?:
console.log('gateway app not support'); console.log('gateway app not support');
} else if (app.type === AppType.Pm2SystemApp) { } else if (app.type === AppType.Pm2SystemApp) {
const pathEntry = path.join(app.path, app.entry); const pathEntry = path.join(app.path, app.entry);
console.log('pm2 system app start', pathEntry);
const pm2Manager = new Pm2Manager({ const pm2Manager = new Pm2Manager({
appName: app.key, appName: app.key,
script: pathEntry, script: pathEntry,
@@ -473,7 +472,6 @@ export const LoadApp = async (app: AppInfo, opts?: { mainApp?: any, pm2Connect?:
if (!pm2Options.cwd) { if (!pm2Options.cwd) {
pm2Options.cwd = path.join(app.path, '../..'); pm2Options.cwd = path.join(app.path, '../..');
} }
console.log('pm2 start options', pm2Options);
await pm2Manager.start(pm2Options); await pm2Manager.start(pm2Options);
} else if (app.type === AppType.ScriptApp) { } else if (app.type === AppType.ScriptApp) {
// console.log('script app 直接运行,不需要启动'); // console.log('script app 直接运行,不需要启动');

View File

@@ -0,0 +1,25 @@
import { QwenAsrRelatime } from "@kevisual/video-tools/src/asr/index.ts";
export class AsrManger {
map: Map<string, QwenAsrRelatime>;
constructor() {
this.map = new Map();
}
getAsr(id: string) {
const asr = this.map.get(id);
return asr;
}
setAsr(id: string, asr: QwenAsrRelatime) {
this.map.set(id, asr);
return asr;
}
deleteAsr(id: string) {
this.map.delete(id);
}
}
export const arrManager = new AsrManger();

View File

@@ -4,7 +4,7 @@ import { Listener, WebSocketListenerFun, WebSocketReq } from "@kevisual/router";
import { lightHA } from "@/routes/ha-api/ha.ts"; import { lightHA } from "@/routes/ha-api/ha.ts";
import { assistantConfig } from "@/app.ts"; import { assistantConfig } from "@/app.ts";
const func: WebSocketListenerFun = async (req: WebSocketReq<{ asr: QwenAsrRelatime, msgId: string, startTime?: number }>, res) => { const func: WebSocketListenerFun = async (req: WebSocketReq<{ asr: QwenAsrRelatime, msgId: string, startTime?: number, loading?: boolean }>, res) => {
const { ws, emitter, id, data } = req; const { ws, emitter, id, data } = req;
let asr = ws.data.asr; let asr = ws.data.asr;
@@ -14,6 +14,9 @@ const func: WebSocketListenerFun = async (req: WebSocketReq<{ asr: QwenAsrRelati
return; return;
} }
if (!asr) { if (!asr) {
if (ws.data.loading) return;
ws.data.loading = true;
const confg = assistantConfig.getConfig(); const confg = assistantConfig.getConfig();
const asrConfig = confg?.asr; const asrConfig = confg?.asr;
if (!asrConfig?.enabled) { if (!asrConfig?.enabled) {
@@ -27,77 +30,84 @@ const func: WebSocketListenerFun = async (req: WebSocketReq<{ asr: QwenAsrRelati
ws.close(); ws.close();
return; return;
} }
const onConnect = () => {
const asr = ws.data.asr as QwenAsrRelatime;
ws.send(JSON.stringify({ type: 'asr', code: 200, message: 'asr服务已连接', time: Date.now() }));
if (!asr) return;
asr.emitter.on('message', (message) => {
// console.log('ASR message', message);
});
asr.emitter.on('partial', (message) => {
// console.log('ASR message', message);
let msgId = ws.data.msgId || Math.random().toString(36).substring(2, 10);
ws.data.msgId = msgId;
ws.send(JSON.stringify({
type: 'partial',
msgId: msgId,
time: Date.now(),
text: message?.text,
raw: message?.raw,
}));
});
asr.emitter.on('result', async ({ text, raw }) => {
let msgId = ws.data.msgId;
ws.data.msgId = Math.random().toString(36).substring(2, 10);
const endTime = Date.now();
console.log('cost time', ws.data.startTime ? (endTime - ws.data.startTime) : 0);
ws.send(JSON.stringify({
type: 'result',
msgId: msgId,
time: Date.now(),
text,
}));
if (!text) return;
const command = text?.trim().slice(0, 20);
type ParseCommand = {
type?: '打开' | '关闭',
appName?: string,
command?: string,
}
let obj: ParseCommand = {};
if (command.startsWith('打开')) {
obj.appName = command.replace('打开', '').trim();
obj.type = '打开';
} else if (command.startsWith('关闭')) {
obj.appName = command.replace('关闭', '').trim();
obj.type = '关闭';
}
if (obj.type) {
try {
const search = await lightHA.searchLight(obj.appName || '');
console.log('searchTime', Date.now() - endTime);
if (search.id) {
await lightHA.runService({ entity_id: search.id, service: obj.type === '打开' ? 'turn_on' : 'turn_off' });
} else if (search.hasMore) {
const [first] = search.result;
await lightHA.runService({ entity_id: first.entity_id, service: obj.type === '打开' ? 'turn_on' : 'turn_off' });
} else {
console.log('未找到对应设备:', obj.appName);
}
console.log('解析到控制指令', obj);
} catch (e) {
console.error('控制失败', e);
}
}
console.log('toogle light time', Date.now() - endTime);
});
asr.start();
}
// 第一次请求 // 第一次请求
asr = new QwenAsrRelatime({ asr = new QwenAsrRelatime({
token, token,
onConnect: () => { emitter,
const asr = ws.data.asr as QwenAsrRelatime; onConnect,
ws.send(JSON.stringify({ type: 'asr', code: 200, message: 'asr服务已连接', time: Date.now() }));
if (!asr) return;
asr.emitter.on('message', (message) => {
// console.log('ASR message', message);
});
asr.emitter.on('partial', (message) => {
// console.log('ASR message', message);
let msgId = ws.data.msgId || Math.random().toString(36).substring(2, 10);
ws.data.msgId = msgId;
ws.send(JSON.stringify({
type: 'partial',
msgId: msgId,
time: Date.now(),
text: message?.text,
raw: message?.raw,
}));
});
asr.emitter.on('result', async ({ text, raw }) => {
let msgId = ws.data.msgId;
ws.data.msgId = Math.random().toString(36).substring(2, 10);
const endTime = Date.now();
console.log('cost time', ws.data.startTime ? (endTime - ws.data.startTime) : 0);
ws.send(JSON.stringify({
type: 'result',
msgId: msgId,
time: Date.now(),
text,
}));
if (!text) return;
const command = text?.trim().slice(0, 20);
type ParseCommand = {
type?: '打开' | '关闭',
appName?: string,
command?: string,
}
let obj: ParseCommand = {};
if (command.startsWith('打开')) {
obj.appName = command.replace('打开', '').trim();
obj.type = '打开';
} else if (command.startsWith('关闭')) {
obj.appName = command.replace('关闭', '').trim();
obj.type = '关闭';
}
if (obj.type) {
try {
const search = await lightHA.searchLight(obj.appName || '');
console.log('searchTime', Date.now() - endTime);
if (search.id) {
await lightHA.runService({ entity_id: search.id, service: obj.type === '打开' ? 'turn_on' : 'turn_off' });
} else if (search.hasMore) {
const [first] = search.result;
await lightHA.runService({ entity_id: first.entity_id, service: obj.type === '打开' ? 'turn_on' : 'turn_off' });
} else {
console.log('未找到对应设备:', obj.appName);
}
console.log('解析到控制指令', obj);
} catch (e) {
console.error('控制失败', e);
}
}
console.log('toogle light time', Date.now() - endTime);
});
asr.start();
}
}) })
ws.data.asr = asr; ws.data.asr = asr;
ws.data.loading = false;
emitter.on('close--' + id, () => {
console.log('ASR websocket 关闭, 释放资源');
asr?.close?.();
});
} }
const isConnected = await asr.checkConnected(); const isConnected = await asr.checkConnected();
if (!isConnected) return; if (!isConnected) return;

View File

@@ -7,6 +7,7 @@ import { getToken } from '@/module/http-token.ts';
import { getTokenUserCache } from '@/routes/index.ts'; import { getTokenUserCache } from '@/routes/index.ts';
import type { WebSocketListenerFun } from "@kevisual/router"; import type { WebSocketListenerFun } from "@kevisual/router";
import WebSocket from 'ws'; import WebSocket from 'ws';
import { renderNoAuthAndLogin } from '@/module/assistant/html/login.ts';
const localProxy = new LocalProxy({}); const localProxy = new LocalProxy({});
localProxy.initFromAssistantConfig(assistantConfig); localProxy.initFromAssistantConfig(assistantConfig);
@@ -161,7 +162,8 @@ export const proxyRoute = async (req: http.IncomingMessage, res: http.ServerResp
} }
const filter = await authFilter(req, res); const filter = await authFilter(req, res);
if (filter) { if (filter) {
return res.end('Not Authorized Proxy'); res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
return res.end(renderNoAuthAndLogin('Not Authorized Proxy'));
} }
const localProxyProxyList = localProxy.getLocalProxyList(); const localProxyProxyList = localProxy.getLocalProxyList();
const localProxyProxy = localProxyProxyList.find((item) => pathname.startsWith(item.path)); const localProxyProxy = localProxyProxyList.find((item) => pathname.startsWith(item.path));

26
pnpm-lock.yaml generated
View File

@@ -115,8 +115,8 @@ importers:
assistant: assistant:
dependencies: dependencies:
'@kevisual/ha-api': '@kevisual/ha-api':
specifier: ^0.0.5 specifier: ^0.0.6
version: 0.0.5 version: 0.0.6
'@kevisual/video-tools': '@kevisual/video-tools':
specifier: ^0.0.13 specifier: ^0.0.13
version: 0.0.13(dotenv@17.2.3)(supports-color@10.2.2) version: 0.0.13(dotenv@17.2.3)(supports-color@10.2.2)
@@ -155,8 +155,8 @@ importers:
specifier: 0.0.7 specifier: 0.0.7
version: 0.0.7(@kevisual/query@0.0.33) version: 0.0.7(@kevisual/query@0.0.33)
'@kevisual/router': '@kevisual/router':
specifier: ^0.0.51 specifier: ^0.0.52
version: 0.0.51(supports-color@10.2.2) version: 0.0.52(supports-color@10.2.2)
'@kevisual/types': '@kevisual/types':
specifier: ^0.0.10 specifier: ^0.0.10
version: 0.0.10 version: 0.0.10
@@ -1308,8 +1308,8 @@ packages:
resolution: {integrity: sha512-4T/m2LqhtwWEW+lWmg7jLxKFW7VtIAftsWFDDZvh10bZunqFf8iXxChHcVSQWikghJb4cq1IkWzPkvc2l+Asdw==} resolution: {integrity: sha512-4T/m2LqhtwWEW+lWmg7jLxKFW7VtIAftsWFDDZvh10bZunqFf8iXxChHcVSQWikghJb4cq1IkWzPkvc2l+Asdw==}
hasBin: true hasBin: true
'@kevisual/ha-api@0.0.5': '@kevisual/ha-api@0.0.6':
resolution: {integrity: sha512-0A4Hmw797yqB9B92G2vPIn4sxIoNYWBm8D3+yWkUkH+XjEBOE65ScK2j4aJVBfwBNd470QqGlK4DcZ5NmQT5iw==} resolution: {integrity: sha512-pZwcE4XYCDItTpMhIP0dIuo2+C07YmhWukVMgTvUuUQBgNo4KJmpItYjeGIvBGsvEM4AjsDGV1mCjTOB1zLu3Q==}
'@kevisual/hot-api@0.0.3': '@kevisual/hot-api@0.0.3':
resolution: {integrity: sha512-qZ4CNK08StZP4+DR1vWwJhKVDoSXXC+PBFG4ZxtkXF5vO2rybE055zp1n3dg5jo8GwW5wxpqMIG3KBp3pYSTkg==} resolution: {integrity: sha512-qZ4CNK08StZP4+DR1vWwJhKVDoSXXC+PBFG4ZxtkXF5vO2rybE055zp1n3dg5jo8GwW5wxpqMIG3KBp3pYSTkg==}
@@ -1364,6 +1364,9 @@ packages:
'@kevisual/router@0.0.51': '@kevisual/router@0.0.51':
resolution: {integrity: sha512-i9qYBeS/um78oC912oWJD3iElB+5NTKyTrz1Hzf4DckiUFnjLL81UPwjIh5I2l9+ul0IZ/Pxx+sFSF99fJkzKg==} resolution: {integrity: sha512-i9qYBeS/um78oC912oWJD3iElB+5NTKyTrz1Hzf4DckiUFnjLL81UPwjIh5I2l9+ul0IZ/Pxx+sFSF99fJkzKg==}
'@kevisual/router@0.0.52':
resolution: {integrity: sha512-Qiv3P1XjzD813Tm79S+atrDb2eickGCI9tuy/aCu512LcoYYJqZhwwkeT4ES0DinnA13Ckqd43QWBR6UmuYkHQ==}
'@kevisual/types@0.0.10': '@kevisual/types@0.0.10':
resolution: {integrity: sha512-Q73uzzjk9UidumnmCvOpgzqDDvQxsblz22bIFuoiioUFJWwaparx8bpd8ArRyFojicYL1YJoFDzDZ9j9NN8grA==} resolution: {integrity: sha512-Q73uzzjk9UidumnmCvOpgzqDDvQxsblz22bIFuoiioUFJWwaparx8bpd8ArRyFojicYL1YJoFDzDZ9j9NN8grA==}
@@ -6615,7 +6618,7 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- typescript - typescript
'@kevisual/ha-api@0.0.5': '@kevisual/ha-api@0.0.6':
dependencies: dependencies:
'@kevisual/cache': 0.0.4 '@kevisual/cache': 0.0.4
fuse.js: 7.1.0 fuse.js: 7.1.0
@@ -6759,6 +6762,15 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@kevisual/router@0.0.52(supports-color@10.2.2)':
dependencies:
eventemitter3: 5.0.1
path-to-regexp: 8.3.0
selfsigned: 5.4.0
send: 1.2.1(supports-color@10.2.2)
transitivePeerDependencies:
- supports-color
'@kevisual/types@0.0.10': {} '@kevisual/types@0.0.10': {}
'@kevisual/use-config@1.0.21(dotenv@17.2.3)': '@kevisual/use-config@1.0.21(dotenv@17.2.3)':

View File

@@ -1,3 +1,7 @@
packages: packages:
- 'assistant' - assistant
- 'cli-center' - cli-center
onlyBuiltDependencies:
- esbuild
- sharp