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:
@@ -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开头的等等
|
||||
*/
|
||||
|
||||
@@ -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秒
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
54
assistant/src/routes/remote/index.ts
Normal file
54
assistant/src/routes/remote/index.ts
Normal 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);
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user