udpate
This commit is contained in:
@@ -2,10 +2,12 @@ import { App } from '@kevisual/router';
|
||||
import { SimpleRouter } from '@kevisual/router/simple'
|
||||
// import { App } from '@kevisual/router/src/app.ts';
|
||||
// import { AssistantConfig } from '@/module/assistant/index.ts';
|
||||
|
||||
import { AssistantInit, parseHomeArg } from '@/services/init/index.ts';
|
||||
import { configDir as HomeConfigDir } from '@/module/assistant/config/index.ts';
|
||||
import { useContextKey } from '@kevisual/use-config/context';
|
||||
import { AssistantQuery } from '@/module/assistant/query/index.ts';
|
||||
|
||||
const manualParse = parseHomeArg(HomeConfigDir);
|
||||
const _configDir = manualParse.configDir;
|
||||
export const configDir = AssistantInit.detectConfigDir(_configDir);
|
||||
@@ -14,6 +16,7 @@ export const assistantConfig = useContextKey<AssistantInit>('assistantConfig', (
|
||||
return new AssistantInit({
|
||||
path: configDir,
|
||||
init: isInit,
|
||||
initWorkspace: manualParse.isOpencode ? false : true,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -28,19 +31,16 @@ export const runtime = useContextKey('runtime', () => {
|
||||
});
|
||||
|
||||
export const app: App = useContextKey<App>('app', () => {
|
||||
const init = isInit;
|
||||
if (init) {
|
||||
return new App({
|
||||
serverOptions: {
|
||||
path: '/client/router',
|
||||
httpType: 'http',
|
||||
cors: {
|
||||
origin: '*',
|
||||
},
|
||||
io: true
|
||||
return new App({
|
||||
serverOptions: {
|
||||
path: '/client/router',
|
||||
httpType: 'http',
|
||||
cors: {
|
||||
origin: '*',
|
||||
},
|
||||
});
|
||||
}
|
||||
io: true
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
export const simpleRouter = useContextKey('simpleRouter', () => {
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { app, assistantConfig } from './app.ts';
|
||||
|
||||
import { app } from './app.ts';
|
||||
import { type Plugin } from "@opencode-ai/plugin"
|
||||
import { createRouterAgentPluginFn } from '@kevisual/router/opencode';
|
||||
import './routes/index.ts';
|
||||
import './routes-simple/index.ts';
|
||||
|
||||
export { app, assistantConfig };
|
||||
export const AgentPlugin: Plugin = createRouterAgentPluginFn({
|
||||
router: app.router,
|
||||
})
|
||||
@@ -384,6 +384,7 @@ export function parseArgs(args: string[]) {
|
||||
*/
|
||||
export const parseHomeArg = (homedir?: string) => {
|
||||
const args = process.argv.slice(2);
|
||||
const execPath = process.execPath;
|
||||
const options = parseArgs(args);
|
||||
let _configDir = undefined;
|
||||
if (options.home && homedir) {
|
||||
@@ -391,7 +392,9 @@ export const parseHomeArg = (homedir?: string) => {
|
||||
} else if (options.root) {
|
||||
_configDir = options.root;
|
||||
}
|
||||
const isOpencode = execPath.includes('.opencode') || execPath.includes('opencode.exe');
|
||||
return {
|
||||
isOpencode,
|
||||
options,
|
||||
configDir: _configDir,
|
||||
};
|
||||
|
||||
32
assistant/src/routes/call/index.ts
Normal file
32
assistant/src/routes/call/index.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
|
||||
import { app } from '../../app.ts';
|
||||
import { createSkill, tool } from '@kevisual/router';
|
||||
|
||||
app.route({
|
||||
path: 'call',
|
||||
key: '',
|
||||
description: '调用',
|
||||
middleware: ['auth'],
|
||||
metadata: {
|
||||
tags: ['opencode'],
|
||||
...createSkill({
|
||||
skill: 'call-app',
|
||||
title: '调用app应用',
|
||||
summary: '调用router的应用, 参数path, key, payload',
|
||||
args: {
|
||||
path: tool.schema.string().describe('应用路径,例如 cnb'),
|
||||
key: tool.schema.string().optional().describe('应用key,例如 list-repos'),
|
||||
payload: tool.schema.object({}).optional().describe('调用参数'),
|
||||
}
|
||||
})
|
||||
},
|
||||
}).define(async (ctx) => {
|
||||
const { path, key = '' } = ctx.query;
|
||||
if (!path) {
|
||||
ctx.throw('路径path不能为空');
|
||||
}
|
||||
const res = await ctx.run({ path, key, payload: ctx.query.payload || {} }, {
|
||||
...ctx
|
||||
});
|
||||
ctx.forward(res);
|
||||
}).addTo(app)
|
||||
@@ -1,2 +1,37 @@
|
||||
import { LightHA } from "@kevisual/ha-api";
|
||||
export const lightHA = new LightHA({ token: process.env.HAAS_TOKEN || '', homeassistantURL: process.env.HAAS_URL });
|
||||
|
||||
export const callText = async (text: string) => {
|
||||
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 = '关闭';
|
||||
}
|
||||
let endTime = Date.now();
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,10 +5,10 @@ import './ai/index.ts';
|
||||
// TODO:
|
||||
// import './light-code/index.ts';
|
||||
import './user/index.ts';
|
||||
import './call/index.ts'
|
||||
|
||||
// TODO: 移除
|
||||
// import './hot-api/key-sender/index.ts';
|
||||
|
||||
import './opencode/index.ts';
|
||||
|
||||
import os from 'node:os';
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { useKey } from "@kevisual/use-config";
|
||||
import { app } from '@/app.ts'
|
||||
import { createSkill } from "@kevisual/router";
|
||||
import { opencodeManager } from './module/open.js'
|
||||
import { createSkill, tool } from "@kevisual/router";
|
||||
import { opencodeManager } from './module/open.ts'
|
||||
import path from "node:path";
|
||||
import { execSync } from "node:child_process";
|
||||
|
||||
// 创建一个opencode 客户端
|
||||
app.route({
|
||||
path: 'opencode',
|
||||
key: 'create',
|
||||
@@ -21,9 +23,52 @@ app.route({
|
||||
},
|
||||
}).define(async (ctx) => {
|
||||
const client = await opencodeManager.getClient();
|
||||
ctx.body = { success: true, url: opencodeManager.url, message: 'OpenCode 客户端已就绪' };
|
||||
ctx.body = { content: `${opencodeManager.url} OpenCode 客户端已就绪` };
|
||||
}).addTo(app);
|
||||
|
||||
// 关闭 opencode 客户端
|
||||
app.route({
|
||||
path: 'opencode',
|
||||
key: 'close',
|
||||
middleware: ['auth'],
|
||||
description: '关闭 OpenCode 客户端',
|
||||
metadata: {
|
||||
tags: ['opencode'],
|
||||
...createSkill({
|
||||
skill: 'close-opencode-client',
|
||||
title: '关闭 OpenCode 客户端',
|
||||
summary: '关闭 OpenCode 客户端',
|
||||
args: {
|
||||
|
||||
}
|
||||
})
|
||||
},
|
||||
}).define(async (ctx) => {
|
||||
await opencodeManager.close();
|
||||
ctx.body = { content: 'OpenCode 客户端已关闭' };
|
||||
}).addTo(app);
|
||||
|
||||
// 调用 path: opencode key: getUrl
|
||||
app.route({
|
||||
path: 'opencode',
|
||||
key: 'getUrl',
|
||||
middleware: ['auth'],
|
||||
description: '获取 OpenCode 服务 URL',
|
||||
metadata: {
|
||||
tags: ['opencode'],
|
||||
...createSkill({
|
||||
skill: 'get-opencode-url',
|
||||
title: '获取 OpenCode 服务 URL',
|
||||
summary: '获取当前 OpenCode 服务的 URL 地址',
|
||||
args: {
|
||||
|
||||
}
|
||||
})
|
||||
},
|
||||
}).define(async (ctx) => {
|
||||
const url = opencodeManager.getUrl();
|
||||
ctx.body = { content: url };
|
||||
}).addTo(app);
|
||||
// 调用 path: opencode key: ls-projects
|
||||
app.route({
|
||||
path: 'opencode',
|
||||
@@ -38,3 +83,39 @@ app.route({
|
||||
};
|
||||
}).addTo(app);
|
||||
|
||||
// 调用 path: opencode key: runProject 参数 /home/ubuntu/cli/assistant
|
||||
app.route({
|
||||
path: 'opencode',
|
||||
key: 'runProject',
|
||||
middleware: ['auth'],
|
||||
metadata: {
|
||||
tags: ['opencode'],
|
||||
...createSkill({
|
||||
skill: 'run-opencode-project',
|
||||
title: '运行 OpenCode 项目',
|
||||
summary: '运行一个已有的 OpenCode 项目',
|
||||
args: {
|
||||
projectPath: tool.schema.string().optional().describe('OpenCode 项目的路径, 默认为 /workspace')
|
||||
}
|
||||
})
|
||||
}
|
||||
}).define(async (ctx) => {
|
||||
const { projectPath = '/workspace' } = ctx.query;
|
||||
try {
|
||||
|
||||
// const directory = path.resolve(projectPath);
|
||||
// const runOpencodeCli = 'opencode run hello';
|
||||
// execSync(runOpencodeCli, { cwd: directory, stdio: 'inherit' });
|
||||
// ctx.body = { content: `OpenCode 项目已在路径 ${directory} 运行` };
|
||||
const client = await opencodeManager.getClient();
|
||||
const session = await client.session.create({
|
||||
query: {
|
||||
directory: projectPath
|
||||
}
|
||||
})
|
||||
console.log('Created session:', session.data.id);
|
||||
ctx.body = { content: `OpenCode 项目已在路径 ${projectPath} 运行` };
|
||||
} catch (error) {
|
||||
ctx.body = { content: `运行 OpenCode 项目失败, 请手动运行命令初始化: opencode run hello` };
|
||||
}
|
||||
}).addTo(app);
|
||||
|
||||
@@ -1,13 +1,21 @@
|
||||
import { createOpencode, OpencodeClient } from "@opencode-ai/sdk";
|
||||
import { createOpencode, createOpencodeClient, OpencodeClient, } from "@opencode-ai/sdk";
|
||||
import { randomInt } from "es-toolkit";
|
||||
import getPort from "get-port";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import fs from "node:fs";
|
||||
import { execSync } from "node:child_process";
|
||||
|
||||
|
||||
export class OpencodeManager {
|
||||
private static instance: OpencodeManager | null = null;
|
||||
private client: OpencodeClient | null = null;
|
||||
private server: { url: string; close(): void } | null = null;
|
||||
private isInitializing: boolean = false;
|
||||
private currentPort: number | null = null;
|
||||
|
||||
public url: string = '';
|
||||
private constructor() {}
|
||||
private constructor() { }
|
||||
|
||||
static getInstance(): OpencodeManager {
|
||||
if (!OpencodeManager.instance) {
|
||||
@@ -31,26 +39,89 @@ export class OpencodeManager {
|
||||
// 开始初始化
|
||||
this.isInitializing = true;
|
||||
try {
|
||||
const result = await createOpencode({
|
||||
hostname: '0.0.0.0',
|
||||
});
|
||||
console.log('OpencodeManager: OpenCode 服务已启动', result.server.url);
|
||||
this.url = result.server.url;
|
||||
this.client = result.client;
|
||||
this.server = result.server;
|
||||
return this.client;
|
||||
const port = 5000;
|
||||
const currentPort = await getPort({ port: port });
|
||||
if (port === currentPort) {
|
||||
const result = await createOpencode({
|
||||
hostname: '0.0.0.0',
|
||||
port: port
|
||||
});
|
||||
this.url = result.server.url;
|
||||
this.client = result.client;
|
||||
this.server = result.server;
|
||||
return this.client;
|
||||
} else {
|
||||
this.client = await this.createOpencodeProject({ port });
|
||||
this.url = `http://localhost:${port}`;
|
||||
return this.client;
|
||||
}
|
||||
} finally {
|
||||
this.isInitializing = false;
|
||||
}
|
||||
}
|
||||
|
||||
close(): void {
|
||||
async createOpencodeProject({
|
||||
directory,
|
||||
port = 5000
|
||||
}: { directory?: string, port?: number }): Promise<OpencodeClient> {
|
||||
const client = createOpencodeClient({
|
||||
baseUrl: `http://localhost:${port}`,
|
||||
directory
|
||||
});
|
||||
return client;
|
||||
}
|
||||
async killPort(port: number): Promise<void> {
|
||||
try {
|
||||
// 尝试 使用命令行去关闭 port为5000的服务
|
||||
if (os.platform() === 'win32') {
|
||||
// Windows 平台
|
||||
execSync(`netstat -ano | findstr :${port} | findstr LISTENING`).toString().split('\n').forEach(line => {
|
||||
const parts = line.trim().split(/\s+/);
|
||||
const pid = parts[parts.length - 1];
|
||||
if (pid) {
|
||||
execSync(`taskkill /PID ${pid} /F`);
|
||||
console.log(`OpencodeManager: 已关闭占用端口 ${port} 的进程 PID ${pid}`);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Unix-like 平台
|
||||
const result = execSync(`lsof -i :${port} -t`).toString();
|
||||
result.split('\n').forEach(pid => {
|
||||
if (pid) {
|
||||
execSync(`kill -9 ${pid}`);
|
||||
console.log(`OpencodeManager: 已关闭占用端口 ${port} 的进程 PID ${pid}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to close OpenCode server:', error);
|
||||
}
|
||||
}
|
||||
async close(): Promise<void> {
|
||||
if (this.server) {
|
||||
this.server.close();
|
||||
this.server = null;
|
||||
return
|
||||
}
|
||||
const port = 5000;
|
||||
const currentPort = await getPort({ port: port });
|
||||
if (port === currentPort) {
|
||||
this.client = null;
|
||||
return;
|
||||
} else {
|
||||
await this.killPort(port);
|
||||
}
|
||||
|
||||
this.client = null;
|
||||
}
|
||||
async getUrl(): Promise<string> {
|
||||
if (this.url) {
|
||||
return this.url;
|
||||
}
|
||||
if (!this.url) {
|
||||
await this.getClient();
|
||||
}
|
||||
return 'http://localhost:5000';
|
||||
}
|
||||
}
|
||||
|
||||
export const opencodeManager = OpencodeManager.getInstance();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { QwenAsrRelatime } from "@kevisual/video-tools/src/asr/index.ts";
|
||||
|
||||
import { Listener, WebSocketListenerFun, WebSocketReq } from "@kevisual/router";
|
||||
import { lightHA } from "@/routes/ha-api/ha.ts";
|
||||
import { callText } from "@/routes/ha-api/ha.ts";
|
||||
import { assistantConfig } from "@/app.ts";
|
||||
|
||||
const func: WebSocketListenerFun = async (req: WebSocketReq<{ asr: QwenAsrRelatime, msgId: string, startTime?: number, loading?: boolean }>, res) => {
|
||||
@@ -61,37 +61,7 @@ const func: WebSocketListenerFun = async (req: WebSocketReq<{ asr: QwenAsrRelati
|
||||
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);
|
||||
}
|
||||
}
|
||||
await callText(text);
|
||||
console.log('toogle light time', Date.now() - endTime);
|
||||
});
|
||||
asr.start();
|
||||
|
||||
@@ -8,6 +8,7 @@ export { parseHomeArg, parseHelpArg };
|
||||
export type AssistantInitOptions = {
|
||||
path?: string;
|
||||
init?: boolean;
|
||||
initWorkspace?: boolean;
|
||||
};
|
||||
const randomId = () => Math.random().toString(36).substring(2, 8);
|
||||
/**
|
||||
@@ -16,12 +17,14 @@ const randomId = () => Math.random().toString(36).substring(2, 8);
|
||||
*/
|
||||
export class AssistantInit extends AssistantConfig {
|
||||
#query: Query
|
||||
initWorkspace: boolean = false;
|
||||
constructor(opts?: AssistantInitOptions) {
|
||||
const configDir = opts?.path || process.cwd();
|
||||
super({
|
||||
configDir,
|
||||
init: false,
|
||||
});
|
||||
this.initWorkspace = opts?.initWorkspace ?? true;
|
||||
if (opts?.init) {
|
||||
this.init();
|
||||
}
|
||||
@@ -35,8 +38,10 @@ export class AssistantInit extends AssistantConfig {
|
||||
if (!this.checkConfigPath()) {
|
||||
console.log(chalk.blue('助手路径不存在,正在创建...'));
|
||||
super.init(configDir);
|
||||
if (!this.initWorkspace) { return }
|
||||
} else {
|
||||
super.init(configDir);
|
||||
if (!this.initWorkspace) { return }
|
||||
const assistantConfig = this;
|
||||
console.log(chalk.yellow('助手路径已存在'), chalk.green(assistantConfig.configDir));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user