feat: restructure command for Claude models and add new remote routes

- Deleted the old cc.ts command and created a new cc.ts under src/command/claude for better organization.
- Added support for a new model 'bailian' in the command.
- Implemented remote app connection status and connection routes in assistant/src/routes/remote/index.ts.
- Updated index.ts to reflect the new path for the cc command.
- Added a placeholder for future management of plugin operations in src/command/opencode/plugin.ts.
This commit is contained in:
2026-01-21 23:22:58 +08:00
parent a911334459
commit 028a6ac726
15 changed files with 469 additions and 276 deletions

View File

@@ -94,6 +94,15 @@ export type AssistantConfigData = {
* 例子: { path: '/root/home', target: 'https://kevisual.cn', pathname: '/root/home' }
*/
proxy?: ProxyInfo[];
/**
* Router代理, 会自动获取 {path: 'router', key: 'list'}的路由信息,然后注入到整个router应用当中.
* 例子: { proxy: [ { type: 'router', api: 'https://localhost:50002/api/router' } ] }
* base: 是否使用 /api/router的基础路径默认false
*/
router?: {
proxy: ProxyInfo[];
base?: boolean;
}
/**
* API 代理配置, 比如api开头的v1开头的等等
*/

View File

@@ -7,12 +7,16 @@ import glob from 'fast-glob';
import type { App } from '@kevisual/router';
import { RemoteApp } from '@/module/remote-app/remote-app.ts';
import { logger } from '@/module/logger.ts';
import { getEnvToken } from '@/module/http-token.ts';
import { initApi } from '@kevisual/api/proxy'
import { Query } from '@kevisual/query';
export class AssistantApp extends Manager {
config: AssistantConfig;
pagesPath: string;
remoteIsConnected = false;
attemptedConnectTimes = 0;
remoteApp: RemoteApp | null = null;
remoteUrl: string | null = null;
constructor(config: AssistantConfig, mainApp?: App) {
config.checkMounted();
const appsPath = config?.configPath?.appsDir || path.join(process.cwd(), 'apps');
@@ -71,11 +75,17 @@ export class AssistantApp extends Manager {
return pagesParse;
}
async initRemoteApp() {
async initRemoteApp(opts?: { token?: string, enabled?: boolean }) {
const config = this.config.getConfig();
const share = config?.share;
if (share && share.enabled !== false) {
const token = config?.token;
const enabled = opts?.enabled ?? share?.enabled ?? false;
if (share && enabled !== false) {
if (this.remoteApp) {
this.remoteApp.ws?.close();
this.remoteApp = null;
this.remoteIsConnected = false;
}
const token = config?.token || opts?.token || getEnvToken() as string;
const url = new URL(share.url || 'https://kevisual.cn/ws/proxy');
const id = config?.app?.id;
if (token && url && id) {
@@ -99,12 +109,63 @@ export class AssistantApp extends Manager {
}, 5 * 1000); // 第一次断开5秒后重连
});
logger.debug('链接到了远程应用服务器');
const appId = id;
const username = config?.auth.username || 'unknown';
const url = new URL(`/${username}/v1/${appId}`, 'https://kevisual.cn/');
this.remoteUrl = url.toString();
console.log('远程地址', this.remoteUrl);
} else {
console.log('Not connected to remote app server');
}
this.remoteApp = remoteApp;
} else {
//
if (!token) {
logger.error('Token是远程应用连接必须的参数');
}
}
}
}
async initRouterApp() {
const config = this.config.getConfig();
const routerProxy = config.router.proxy || [];
const base = config.router.base ?? false;
if (base) {
routerProxy.push({
type: 'router',
router: {
url: `${this.config.getRegistry()}/api/router`,
}
})
}
if (routerProxy.length === 0) {
return
}
for (const proxyInfo of routerProxy) {
if (proxyInfo.type !== 'router') {
console.warn('路由的type必须是"router"');
continue;
}
const url = proxyInfo.router!.url;
if (!url) {
console.warn('路由的api地址不能为空', proxyInfo.router);
continue;
}
const query = new Query({ url });
try {
initApi({
router: this.mainApp,
item: {
type: 'api',
api: {
url,
query: query as any,
}
},
exclude: "WHERE path = 'auth' OR path = 'router' OR path = 'call'",
})
console.log('Router API 已初始化', url.toString());
} catch (err) {
console.error('Router API 初始化失败', url.toString(), err);
}
}
}
@@ -119,9 +180,10 @@ export class AssistantApp extends Manager {
remoteApp.listenProxy();
this.attemptedConnectTimes = 0;
console.log('重新连接到了远程应用服务器');
this.reconnectRemoteApp();
} else {
setTimeout(() => {
this.reconnectRemoteApp();
this.initRouterApp()
}, 30 * 1000 + this.attemptedConnectTimes * 10 * 1000); // 30秒后重连 + 每次增加10秒
}
}

View File

@@ -14,7 +14,7 @@ export type ProxyInfo = {
/**
* 类型
*/
type?: 'file' | 'dynamic' | 'minio' | 'http' | 's3';
type?: 'file' | 'dynamic' | 'minio' | 'http' | 's3' | 'router';
/**
* 目标的 pathname 默认为请求的url.pathname, 设置了pathname则会使用pathname作为请求的url.pathname
* @default undefined
@@ -41,6 +41,10 @@ export type ProxyInfo = {
id?: string;
indexPath?: string;
rootPath?: string;
},
router?: {
id?: string;
url?: string;
}
};

View File

@@ -1,3 +1,4 @@
import { useKey } from '@kevisual/use-config';
import http from 'node:http';
export const error = (msg: string, code = 500) => {
return JSON.stringify({ code, message: msg });
@@ -32,3 +33,7 @@ export const getToken = async (req: http.IncomingMessage) => {
return { token };
};
export const getEnvToken = () => {
const envTokne = useKey('KEVISUAL_TOKEN') || '';
return envTokne;
}

View File

@@ -10,10 +10,12 @@ import './call/index.ts'
// TODO: 移除
// import './hot-api/key-sender/index.ts';
import './opencode/index.ts';
import './remote/index.ts';
import os from 'node:os';
import { authCache } from '@/module/cache/auth.ts';
import { createSkill } from '@kevisual/router';
import { logger } from '@/module/logger.ts';
const getTokenUser = async (token: string) => {
const query = assistantConfig.query
const res = await query.post({
@@ -41,7 +43,7 @@ export const checkAuth = async (ctx: any, isAdmin = false) => {
const config = assistantConfig.getConfig();
const { auth = {} } = config;
const token = ctx.query.token;
console.log('checkAuth', ctx.query, { token });
logger.debug('checkAuth', ctx.query, { token });
if (!token) {
return {
code: 401,
@@ -120,7 +122,7 @@ app
description: '管理员鉴权, 获取用户信息,并验证是否为管理员。',
})
.define(async (ctx) => {
console.log('query', ctx.query);
logger.debug('query', ctx.query);
if (!ctx.query?.token && ctx.appId === app.appId) {
return;
}

View File

@@ -0,0 +1,54 @@
import { useContextKey } from "@kevisual/context";
import { app } from "../../app.ts";
import { AssistantApp } from "@/lib.ts";
app.route({
path: 'remote',
key: 'status',
middleware: ['admin-auth'],
description: '获取远程app连接状态',
}).define(async (ctx) => {
const manager = useContextKey('manager') as AssistantApp;
if (manager?.remoteApp?.isConnect()) {
const url = manager.remoteUrl || ''
ctx.body = {
content: `远程app已经链接, 访问地址:${url}`,
}
} else {
ctx.body = {
content: '远程app未连接',
}
}
}).addTo(app);
app.route({
path: 'remote',
key: 'connect',
middleware: ['admin-auth'],
description: '连接远程app',
}).define(async (ctx) => {
const manager = useContextKey('manager') as AssistantApp;
if (!manager) {
ctx.body = {
content: '远程app管理器未初始化',
}
return;
}
if (manager?.remoteApp?.isConnect()) {
const url = manager.remoteUrl || ''
ctx.body = {
content: `远程app已经链接, 访问地址:${url}`,
}
return;
}
await manager.initRemoteApp({ enabled: true, token: ctx.query?.token }).then(() => {
ctx.body = {
content: '远程app连接成功',
}
}).catch((err) => {
ctx.body = {
content: `远程app连接失败: ${err.message}`,
}
});
}).addTo(app);

View File

@@ -1,3 +1,4 @@
import { useContextKey } from '@kevisual/context';
import { app, assistantConfig } from './app.ts';
import { proxyRoute, proxyWs } from './services/proxy/proxy-page-index.ts';
import './routes/index.ts';
@@ -50,12 +51,13 @@ export const runServer = async (port: number = 51515, listenPath = '127.0.0.1')
...proxyWs(),
qwenAsr,
]);
const manager = new AssistantApp(assistantConfig, app);
const manager = useContextKey('manager', new AssistantApp(assistantConfig, app));
setTimeout(() => {
manager.load({ runtime: 'client' }).then(() => {
console.log('Assistant App Loaded');
});
manager.initRemoteApp()
manager.initRouterApp()
}, 1000);
return {

View File

@@ -36,9 +36,9 @@ export class AssistantInit extends AssistantConfig {
}
// 1. 检查助手路径是否存在
if (!this.checkConfigPath()) {
console.log(chalk.blue('助手路径不存在,正在创建...'));
super.init(configDir);
if (!this.initWorkspace) { return }
console.log(chalk.blue('助手路径不存在,正在创建...'));
} else {
super.init(configDir);
if (!this.initWorkspace) { return }

View File

@@ -1,7 +1,7 @@
import { createApiProxy, ProxyInfo, proxy } from '@/module/assistant/index.ts';
import { createApiProxy, ProxyInfo, proxy } from '@/module/assistant/index.ts';
import http from 'node:http';
import { LocalProxy } from './local-proxy.ts';
import { assistantConfig, app, simpleRouter } from '@/app.ts';
import { assistantConfig, simpleRouter } from '@/app.ts';
import { log, logger } from '@/module/logger.ts';
import { getToken } from '@/module/http-token.ts';
import { getTokenUserCache } from '@/routes/index.ts';
@@ -12,7 +12,7 @@ const localProxy = new LocalProxy({});
localProxy.initFromAssistantConfig(assistantConfig);
const isOpenPath = (pathname: string): boolean => {
const openPaths = ['/root/home', '/root/cli'];
const openPaths = ['/root/home', '/root/cli', '/root/login'];
for (const openPath of openPaths) {
if (pathname.startsWith(openPath)) {
return true;
@@ -31,7 +31,7 @@ const authFilter = async (req: http.IncomingMessage, res: http.ServerResponse) =
const auth = _assistantConfig?.auth || {};
const share = auth.share || 'protected';
const noAdmin = !auth.username;
if (noAdmin) return { code: 500, message: '没有管理员' };
if (noAdmin) return { code: 200, message: '没有管理员, 直接放过, 让管理登录和自己设置' };
const admin = auth.username;
const admins = auth.admin || [];
if (admin) {
@@ -160,7 +160,7 @@ export const proxyRoute = async (req: http.IncomingMessage, res: http.ServerResp
return;
}
const isOpen = isOpenPath(pathname)
log.debug('proxyRoute', { _user, _app, pathname, noAdmin, isOpen });
logger.debug('proxyRoute', { _user, _app, pathname, noAdmin, isOpen });
if (noAdmin && !isOpen) {
return toSetting();
}
@@ -172,19 +172,20 @@ export const proxyRoute = async (req: http.IncomingMessage, res: http.ServerResp
const proxyApiList = _assistantConfig?.proxy || [];
const proxyApi = proxyApiList.find((item) => pathname.startsWith(item.path));
if (proxyApi) {
log.debug('proxyPage', { proxyApi, pathname });
logger.debug('proxyPage', { proxyApi, pathname });
return proxyFn(req, res, proxyApi);
}
const filter = await authFilter(req, res);
if (filter.code !== 200) {
console.log('auth filter deny', filter);
logger.debug('auth filter deny', filter);
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
// TODO: 这里可以做成可配置的登录页面
return res.end(renderNoAuthAndLogin('Not Authorized Proxy'));
}
const localProxyProxyList = localProxy.getLocalProxyList();
const localProxyProxy = localProxyProxyList.find((item) => pathname.startsWith(item.path));
if (localProxyProxy) {
log.log('localProxyProxy', { localProxyProxy, url: req.url });
logger.debug('localProxyProxy', { localProxyProxy, url: req.url });
return proxyFn(req, res, {
path: localProxyProxy.path,
"type": 'file',
@@ -203,7 +204,7 @@ export const proxyRoute = async (req: http.IncomingMessage, res: http.ServerResp
type: 'http',
});
}
log.debug('handle by router 404', req.url);
logger.debug('handle by router 404', req.url);
res.statusCode = 404;
res.end('Not Found Proxy');