This commit is contained in:
2026-01-20 15:39:46 +08:00
parent 9f20e149a0
commit 89470346be
20 changed files with 330 additions and 195 deletions

View File

@@ -1 +1 @@
export * from "../../agent/plugin";
export { AgentPlugin } from "../../src/main.ts";

View File

@@ -0,0 +1,35 @@
---
name: kill-opencode
description: 自动查找并杀死所有opencode相关的进程确保系统资源释放。
tags:
- opencode
- process-management
- automation
---
```bash
#!/bin/bash
# kill_opencode.sh - 自动杀死所有opencode进程
echo "正在查找opencode进程..."
ps aux | grep opencode | grep -v grep
if [ $? -eq 0 ]; then
echo "正在杀死所有opencode进程..."
pkill -f opencode
sleep 2
# 检查是否还有残留进程
remaining=$(ps aux | grep opencode | grep -v grep | wc -l)
if [ $remaining -gt 0 ]; then
echo "发现 $remaining 个顽固进程,使用强制杀死模式..."
pkill -9 -f opencode
fi
echo "opencode进程清理完成"
else
echo "未找到opencode进程"
fi
# 验证是否已完全清理
echo "当前opencode进程状态"
ps aux | grep opencode | grep -v grep || echo "没有运行中的opencode进程"
```

View File

@@ -1,3 +0,0 @@
import { app } from '../src/main.ts'
export { app }

View File

@@ -1,64 +0,0 @@
import { tool } from "@opencode-ai/plugin/tool"
import { type Plugin } from "@opencode-ai/plugin"
import { app } from './index.ts';
import { Skill } from "@kevisual/router";
import './call.ts';
import './step.ts';
const routes = app.router.routes.filter(r => {
const metadata = r.metadata as Skill
if (metadata && metadata.tags && metadata.tags.includes('opencode')) {
return !!metadata.skill
}
return false
})
// opencode run "查看系统信息"
export const AgentPlugin: Plugin = async ({ project, client, $, directory, worktree }) => {
return {
'tool': {
...routes.reduce((acc, route) => {
const metadata = route.metadata as Skill
acc[metadata.skill!] = {
name: metadata.title || metadata.skill,
description: metadata.summary || '',
args: metadata.args || {},
async execute(args: Record<string, any>) {
console.log(`Executing skill ${metadata.skill} with args:`, args);
await client.app.log({
body: {
service: 'cnb',
level: 'info',
message: `Executing skill ${metadata.skill} with args: ${JSON.stringify(args)}`
}
});
const res = await app.run({
path: route.path,
key: route.key,
payload: args
},
// @ts-ignore
{ appId: app.appId! });
if (res.code === 200) {
if (res.data?.content) {
return res.data.content;
}
const str = JSON.stringify(res.data || res, null, 2);
if (str.length > 5000) {
return str.slice(0, 5000) + '... (truncated)';
}
return str;
}
return `Error: ${res?.message || '无法获取结果'}`;
}
}
return acc;
}, {} as Record<string, any>)
},
'tool.execute.before': async (opts) => {
// console.log('CnbPlugin: tool.execute.before', opts.tool);
// delete toolSkills['cnb-login-verify']
}
}
}

View File

@@ -1,27 +0,0 @@
import { createSkill } from '@kevisual/router'
import { app } from './index.ts'
import { tool } from '@opencode-ai/plugin/tool'
// "调用 path: test key: step"
app.route({
path: 'test',
key: 'step',
description: '测试步骤调用',
metadata: {
tags: ['opencode'],
...createSkill({
skill: 'test-step',
title: '获取系统信息',
summary: '根据步骤获取系统信息',
args: {
}
})
},
}).define(async (ctx) => {
ctx.body = {
context: `执行步骤
1. 调用 path: client key: system 获取系统信息
2. 调用 path: client key: time 获取当前时间
3. 返回结果`,
};
}).addTo(app)

View File

@@ -40,6 +40,28 @@ await Bun.build({
},
external,
});
await Bun.build({
target: 'node',
format: 'esm',
entrypoints: [w('./src/main.ts')],
outdir: w('./dist'),
naming: {
entry: 'assistant-opencode.js',
},
define: {
ENVISION_VERSION: JSON.stringify(pkg.version),
},
external,
});
const dts = 'dts -i src/main.ts -o assistant-opencode.d.ts';
Bun.spawnSync({
cmd: ['sh', '-c', dts],
cwd: __dirname,
stdout: 'inherit',
stderr: 'inherit',
});
// const copyDist = ['dist', 'bin'];
const copyDist = ['dist'];
export const copyFileToEnvision = async () => {

View File

@@ -47,10 +47,10 @@
"@kevisual/logger": "^0.0.4",
"@kevisual/query": "0.0.35",
"@kevisual/query-login": "0.0.7",
"@kevisual/router": "^0.0.56",
"@kevisual/router": "^0.0.57",
"@kevisual/types": "^0.0.11",
"@kevisual/use-config": "^1.0.28",
"@opencode-ai/plugin": "^1.1.25",
"@opencode-ai/plugin": "^1.1.26",
"@types/bun": "^1.3.6",
"@types/lodash-es": "^4.17.12",
"@types/node": "^25.0.9",
@@ -81,7 +81,7 @@
"@kevisual/ha-api": "^0.0.6",
"@kevisual/oss": "^0.0.16",
"@kevisual/video-tools": "^0.0.13",
"@opencode-ai/sdk": "^1.1.25",
"@opencode-ai/sdk": "^1.1.26",
"es-toolkit": "^1.44.0",
"eventemitter3": "^5.0.4",
"lowdb": "^7.0.1",

View File

@@ -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', () => {

View File

@@ -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,
})

View File

@@ -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,
};

View File

@@ -1,8 +1,7 @@
import { createSkill } from '@kevisual/router'
import { app } from './index.ts'
import { tool } from '@opencode-ai/plugin/tool'
// "调用 path: router key: list"
import { app } from '../../app.ts';
import { createSkill, tool } from '@kevisual/router';
app.route({
path: 'call',
key: '',

View File

@@ -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);
}
}
}

View File

@@ -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';

View File

@@ -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);

View File

@@ -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();

View File

@@ -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();

View File

@@ -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));
}