Compare commits

...

5 Commits

Author SHA1 Message Date
xiongxiao
c832465d57 Auto commit: 2026-03-18 00:26 2026-03-18 00:26:33 +08:00
xiongxiao
babee3f155 update 2026-03-16 03:25:30 +08:00
xiongxiao
35d3cae9f1 update 2026-03-16 03:23:40 +08:00
xiongxiao
b7e2bdcb1c feat: 添加获取 OpenCode 可用模型列表的功能,并优化创建客户端的参数 2026-03-16 02:13:51 +08:00
xiongxiao
c3bc0d2465 feat: 添加 OpenCode 会话管理功能,支持列出、获取、创建、更新和删除会话 2026-03-16 01:35:36 +08:00
6 changed files with 1542 additions and 15 deletions

View File

@@ -4,6 +4,50 @@ import { getClient } from './module/client.ts';
import dayjs from 'dayjs';
import { Session } from '@opencode-ai/sdk';
app.route({
path: 'opencode-cnb',
key: 'models',
middleware: ['auth-admin'],
description: '获取 OpenCode 可用模型列表,返回 providerID 和 modelID',
metadata: {
args: {
baseUrl: z.string().optional().describe('OpenCode 服务地址,默认为 http://localhost:4096'),
}
}
}).define(async (ctx) => {
const { baseUrl } = ctx.query;
const client = await getClient({ baseUrl });
if (!client) {
ctx.body = { content: '获取 OpenCode 客户端失败' };
return;
}
const res = await client.provider.list();
const providerData = (res as any)?.data ?? res;
const all: any[] = providerData?.all ?? [];
const connected: string[] = providerData?.connected ?? [];
const defaultModels: Record<string, string> = providerData?.default ?? {};
const models: Array<{ providerID: string; providerName: string; modelID: string; modelName: string; isDefault: boolean; isConnected: boolean }> = [];
for (const provider of all) {
const isConnected = connected.includes(provider.id);
for (const [modelKey, model] of Object.entries<any>(provider.models ?? {})) {
models.push({
providerID: provider.id,
providerName: provider.name,
modelID: modelKey,
modelName: model.name ?? modelKey,
isDefault: defaultModels[provider.id] === modelKey,
isConnected,
});
}
}
ctx.body = {
content: `${models.length} 个模型,已连接提供商: ${connected.join(', ') || '无'}`,
data: { models, connected, default: defaultModels },
};
}).addTo(app);
app.route({
path: 'opencode-cnb',
key: 'question',
@@ -14,13 +58,16 @@ app.route({
question: z.string().describe('问题'),
baseUrl: z.string().optional().describe('OpenCode 服务地址,默认为 http://localhost:4096'),
directory: z.string().optional().describe('运行目录,默认为根目录'),
messageID: z.string().optional().describe('消息 ID选填'),
messageId: z.string().optional().describe('消息 ID选填'),
sessionId: z.string().optional().describe('会话 ID选填'),
providerId: z.string().optional().describe('指定使用的提供商 ID默认为空表示使用默认提供商'),
modelId: z.string().optional().describe('指定使用的模型 ID默认为空表示使用默认模型'),
parts: z.array(z.any()).optional().describe('消息内容的分块,优先于 question 参数'),
awaitAnswer: z.boolean().optional().describe('是否等待回答完成,默认为 false开启后会在回答完成后返回完整回答内容')
}
}
}).define(async (ctx) => {
const { question, baseUrl, directory = '/workspace', messageID, sessionId, parts } = ctx.query;
const { question, baseUrl, directory = '/workspace', messageId, sessionId, parts, awaitAnswer = false, providerId, modelId } = ctx.query;
const client = await getClient({ baseUrl: baseUrl });
if (!client) {
ctx.body = { content: `OpenCode 客户端获取失败` };
@@ -30,12 +77,7 @@ app.route({
ctx.body = { content: `问题不能为空` };
return;
}
// const sessionList = await client.session.list()
let session: Session | null = null;
// const hasSession = sessionList.data.find(s => s.directory === directory);
// if (hasSession) {
// session = hasSession;
// } else {
if (sessionId) {
try {
const getSession = await client.session.get({ path: { id: sessionId } });
@@ -53,16 +95,30 @@ app.route({
session = createSession.data;
}
let _parts: any[] = parts ?? [{ type: "text", text: question }];
const message = await client.session.prompt({
let data: any = null;
let model = null;
if (providerId && modelId) {
model = { providerID: providerId, modelID: modelId };
}
const promptPromise = client.session.prompt({
body: {
messageID: messageID,
messageID: messageId,
parts: _parts,
...model ? { model } : {},
},
path: {
id: sessionId || session.id,
},
})
const data = message.data;
ctx.body = { content: `已经启动`, data };
if (awaitAnswer) {
const message = await promptPromise;
data = message.data;
} else {
promptPromise.then(res => {
console.log(`Prompt completed with response:`, res.data.info.id);
}).catch(() => {
// 忽略错误
})
}
ctx.body = { content: awaitAnswer ? `已经完成` : `运行中`, data, sessionId: session.id };
}).addTo(app);

View File

@@ -1,2 +1,3 @@
import './ls.ts'
import './cnb.ts'
import './cnb.ts'
import './session/index.ts'

View File

@@ -0,0 +1,282 @@
import { app } from '@/app.ts'
import { createSkill, tool } from '@kevisual/router';
import { opencodeManager } from '../module/open.ts';
// 查询 - 列出所有 session
app.route({
path: 'opencode-session',
key: 'list',
middleware: ['auth-admin'],
description: '列出所有 OpenCode Session',
metadata: {
tags: ['session'],
...createSkill({
skill: 'list-opencode-sessions',
title: '列出所有 Session',
summary: '列出 OpenCode 中的所有会话,可按目录过滤',
args: {
port: tool.schema.number().optional().describe('OpenCode 服务端口,默认为 4096'),
}
})
}
}).define(async (ctx) => {
const { port } = ctx.query;
const client = await opencodeManager.getClient({ port });
const result = await client.session.list();
ctx.body = { data: result.data, content: `${(result.data as any[])?.length ?? 0} 个 Session` };
}).addTo(app);
// 查询 - 获取单个 session
app.route({
path: 'opencode-session',
key: 'get',
middleware: ['auth-admin'],
description: '获取指定 OpenCode Session',
metadata: {
tags: ['session'],
...createSkill({
skill: 'get-opencode-session',
title: '获取 Session',
summary: '根据 ID 获取指定的 OpenCode 会话信息',
args: {
id: tool.schema.string().describe('Session ID'),
port: tool.schema.number().optional().describe('OpenCode 服务端口,默认为 4096'),
}
})
}
}).define(async (ctx) => {
const { id, port } = ctx.query;
if (!id) {
ctx.throw(400, 'Session ID 不能为空');
return;
}
const client = await opencodeManager.getClient({ port });
const result = await client.session.get({ path: { id } });
ctx.body = { data: result.data, content: `已获取 Session: ${id}` };
}).addTo(app);
// 查询 - 获取 session 状态
app.route({
path: 'opencode-session',
key: 'status',
middleware: ['auth-admin'],
description: '获取 OpenCode Session 状态',
metadata: {
tags: ['session'],
...createSkill({
skill: 'get-opencode-session-status',
title: '获取 Session 状态',
summary: '获取当前 OpenCode 会话的运行状态,可按目录过滤',
args: {
directory: tool.schema.string().optional().describe('工作目录'),
port: tool.schema.number().optional().describe('OpenCode 服务端口,默认为 4096'),
}
})
}
}).define(async (ctx) => {
const { directory, port } = ctx.query;
const client = await opencodeManager.getClient({ port });
const result = await client.session.status(directory ? { query: { directory } } : undefined);
ctx.body = { data: result.data, content: `Session 状态已获取` };
}).addTo(app);
// 查询 - 列出 session 消息
app.route({
path: 'opencode-session',
key: 'messages',
middleware: ['auth-admin'],
description: '列出 OpenCode Session 消息',
metadata: {
tags: ['session'],
...createSkill({
skill: 'list-opencode-session-messages',
title: '列出 Session 消息',
summary: '列出指定 OpenCode 会话的所有消息记录',
args: {
sessionId: tool.schema.string().describe('Session ID'),
port: tool.schema.number().optional().describe('OpenCode 服务端口,默认为 4096'),
}
})
}
}).define(async (ctx) => {
const { sessionId, port } = ctx.query;
if (!sessionId) {
ctx.throw(400, 'Session ID 不能为空');
return;
}
const client = await opencodeManager.getClient({ port });
const result = await client.session.messages({ path: { id: sessionId } });
ctx.body = { data: result.data, content: `Session ${sessionId}${(result.data as any[])?.length ?? 0} 条消息` };
}).addTo(app);
// 新增 - 创建 session
app.route({
path: 'opencode-session',
key: 'create',
middleware: ['auth-admin'],
description: '创建 OpenCode Session',
metadata: {
tags: ['session'],
...createSkill({
skill: 'create-opencode-session',
title: '创建 Session',
summary: '在指定目录创建一个新的 OpenCode 会话',
args: {
directory: tool.schema.string().optional().describe('工作目录,默认为 /workspace'),
port: tool.schema.number().optional().describe('OpenCode 服务端口,默认为 4096'),
}
})
}
}).define(async (ctx) => {
const { directory = '/workspace', port } = ctx.query;
const client = await opencodeManager.getClient({ port });
const result = await client.session.create({ query: { directory } });
ctx.body = { data: result.data, content: `Session 已创建ID: ${(result.data as any)?.id}` };
}).addTo(app);
// 修改 - 更新 session
app.route({
path: 'opencode-session',
key: 'update',
middleware: ['auth-admin'],
description: '更新 OpenCode Session',
metadata: {
tags: ['session'],
...createSkill({
skill: 'update-opencode-session',
title: '更新 Session',
summary: '更新指定 OpenCode 会话的属性,如标题',
args: {
sessionId: tool.schema.string().describe('Session ID'),
title: tool.schema.string().optional().describe('新的会话标题'),
port: tool.schema.number().optional().describe('OpenCode 服务端口,默认为 4096'),
}
})
}
}).define(async (ctx) => {
const { sessionId, title, port } = ctx.query;
if (!sessionId) {
ctx.throw(400, 'Session ID 不能为空');
return;
}
const client = await opencodeManager.getClient({ port });
const result = await client.session.update({ path: { id: sessionId }, body: { title } });
ctx.body = { data: result.data, content: `Session ${sessionId} 已更新` };
}).addTo(app);
// 删除 - 删除 session
app.route({
path: 'opencode-session',
key: 'delete',
middleware: ['auth-admin'],
description: '删除 OpenCode Session',
metadata: {
tags: ['session'],
...createSkill({
skill: 'delete-opencode-session',
title: '删除 Session',
summary: '根据 ID 删除指定的 OpenCode 会话及其所有数据',
args: {
sessionId: tool.schema.string().describe('Session ID'),
port: tool.schema.number().optional().describe('OpenCode 服务端口,默认为 4096'),
}
})
}
}).define(async (ctx) => {
const { sessionId, port } = ctx.query;
if (!sessionId) {
ctx.throw(400, 'Session ID 不能为空');
return;
}
const client = await opencodeManager.getClient({ port });
await client.session.delete({ path: { id: sessionId } });
ctx.body = { content: `Session ${sessionId} 已删除` };
}).addTo(app);
// 操作 - 中止 session
app.route({
path: 'opencode-session',
key: 'abort',
middleware: ['auth-admin'],
description: '中止 OpenCode Session',
metadata: {
tags: ['session'],
...createSkill({
skill: 'abort-opencode-session',
title: '中止 Session',
summary: '中止正在运行的 OpenCode 会话',
args: {
sessionId: tool.schema.string().describe('Session ID'),
port: tool.schema.number().optional().describe('OpenCode 服务端口,默认为 4096'),
}
})
}
}).define(async (ctx) => {
const { sessionId, port } = ctx.query;
if (!sessionId) {
ctx.throw(400, 'Session ID 不能为空');
return;
}
const client = await opencodeManager.getClient({ port });
await client.session.abort({ path: { id: sessionId } });
ctx.body = { content: `Session ${sessionId} 已中止` };
}).addTo(app);
// 操作 - Fork session
app.route({
path: 'opencode-session',
key: 'fork',
middleware: ['auth-admin'],
description: 'Fork OpenCode Session',
metadata: {
tags: ['session'],
...createSkill({
skill: 'fork-opencode-session',
title: 'Fork Session',
summary: '从指定消息处 Fork 一个 OpenCode 会话',
args: {
sessionId: tool.schema.string().describe('Session ID'),
messageId: tool.schema.string().describe('从该消息处开始 Fork'),
port: tool.schema.number().optional().describe('OpenCode 服务端口,默认为 4096'),
}
})
}
}).define(async (ctx) => {
const { sessionId, messageId, port } = ctx.query;
if (!sessionId || !messageId) {
ctx.throw(400, 'Session ID 和 messageId 不能为空');
return;
}
const client = await opencodeManager.getClient({ port });
const result = await client.session.fork({ path: { id: sessionId }, body: { messageID: messageId } });
ctx.body = { data: result.data, content: `Session ${sessionId} 已从消息 ${messageId} Fork` };
}).addTo(app);
// 操作 - 总结 session
app.route({
path: 'opencode-session',
key: 'summarize',
middleware: ['auth-admin'],
description: '总结 OpenCode Session',
metadata: {
tags: ['session'],
...createSkill({
skill: 'summarize-opencode-session',
title: '总结 Session',
summary: '对指定的 OpenCode 会话进行内容总结',
args: {
sessionId: tool.schema.string().describe('Session ID'),
port: tool.schema.number().optional().describe('OpenCode 服务端口,默认为 4096'),
}
})
}
}).define(async (ctx) => {
const { sessionId, port } = ctx.query;
if (!sessionId) {
ctx.throw(400, 'Session ID 不能为空');
return;
}
const client = await opencodeManager.getClient({ port });
const result = await client.session.summarize({ path: { id: sessionId } });
ctx.body = { data: result.data, content: `Session ${sessionId} 总结完成` };
}).addTo(app);

1184
bun.lock Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "@kevisual/cli",
"version": "0.1.25",
"version": "0.1.27",
"description": "envision 命令行工具",
"type": "module",
"basename": "/root/cli",
@@ -90,5 +90,8 @@
},
"publishConfig": {
"access": "public"
}
},
"workspaces": [
"assistant"
]
}

View File

@@ -1,5 +1,6 @@
# 一个简单的cli工具
```
pnpm i
npm i -g @kevisual/cli
```