Compare commits
5 Commits
f33d4c293d
...
c0edd2cbbf
| Author | SHA1 | Date | |
|---|---|---|---|
| c0edd2cbbf | |||
| 048da38c53 | |||
| 13b0f45d3e | |||
| 68c1976754 | |||
| e53c166c29 |
@@ -3,30 +3,7 @@
|
||||
"metadata": {
|
||||
"share": "public"
|
||||
},
|
||||
"checkDir": {
|
||||
"src/query/query-login": {
|
||||
"url": "https://kevisual.xiongxiao.me/root/ai/code/registry/query/query-login",
|
||||
"enabled": true
|
||||
},
|
||||
"src/query/query-ai": {
|
||||
"url": "https://kevisual.xiongxiao.me/root/ai/code/registry/query/query-ai",
|
||||
"enabled": true
|
||||
}
|
||||
},
|
||||
"syncDirectory": [
|
||||
{
|
||||
"files": [
|
||||
"src/query/query-login/**/*",
|
||||
"src/query/query-ai/**/*"
|
||||
],
|
||||
"ignore": [],
|
||||
"registry": "https://kevisual.xiongxiao.me/root/ai/code/registry",
|
||||
"replace": {
|
||||
"src/": ""
|
||||
}
|
||||
}
|
||||
],
|
||||
"sync": {
|
||||
"src/query/index.ts": "https://kevisual.xiongxiao.me/root/ai/code/registry/query/index.ts"
|
||||
}
|
||||
"checkDir": {},
|
||||
"syncDirectory": [],
|
||||
"sync": {}
|
||||
}
|
||||
@@ -10,7 +10,7 @@
|
||||
],
|
||||
"author": "abearxiong <xiongxiao@xiongxiao.me> (https://www.xiongxiao.me)",
|
||||
"license": "MIT",
|
||||
"packageManager": "pnpm@10.30.0",
|
||||
"packageManager": "pnpm@10.30.1",
|
||||
"type": "module",
|
||||
"files": [
|
||||
"dist",
|
||||
@@ -44,18 +44,17 @@
|
||||
"devDependencies": {
|
||||
"@inquirer/prompts": "^8.2.1",
|
||||
"@kevisual/ai": "^0.0.24",
|
||||
"@kevisual/api": "^0.0.51",
|
||||
"@kevisual/api": "^0.0.57",
|
||||
"@kevisual/load": "^0.0.6",
|
||||
"@kevisual/local-app-manager": "^0.1.32",
|
||||
"@kevisual/logger": "^0.0.4",
|
||||
"@kevisual/query": "0.0.44",
|
||||
"@kevisual/query-login": "0.0.7",
|
||||
"@kevisual/router": "^0.0.74",
|
||||
"@kevisual/query": "0.0.49",
|
||||
"@kevisual/router": "^0.0.83",
|
||||
"@kevisual/types": "^0.0.12",
|
||||
"@kevisual/use-config": "^1.0.30",
|
||||
"@opencode-ai/plugin": "^1.2.6",
|
||||
"@opencode-ai/plugin": "^1.2.10",
|
||||
"@types/bun": "^1.3.9",
|
||||
"@types/node": "^25.2.3",
|
||||
"@types/node": "^25.3.0",
|
||||
"@types/send": "^1.2.1",
|
||||
"@types/ws": "^8.18.1",
|
||||
"chalk": "^5.6.2",
|
||||
@@ -77,11 +76,11 @@
|
||||
"access": "public"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.991.0",
|
||||
"@aws-sdk/client-s3": "^3.995.0",
|
||||
"@kevisual/js-filter": "^0.0.5",
|
||||
"@kevisual/oss": "^0.0.19",
|
||||
"@kevisual/video-tools": "^0.0.13",
|
||||
"@opencode-ai/sdk": "^1.2.6",
|
||||
"@opencode-ai/sdk": "^1.2.10",
|
||||
"es-toolkit": "^1.44.0",
|
||||
"eventemitter3": "^5.0.4",
|
||||
"lowdb": "^7.0.1",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { program, Command, assistantConfig } from '@/program.ts';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { parseHomeArg, HomeConfigDir } from '@/module/assistant/config/args.ts';
|
||||
import { execCommand } from '@/module/npm-install.ts';
|
||||
|
||||
|
||||
14
assistant/src/command/user/me.ts
Normal file
14
assistant/src/command/user/me.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { program, Command, assistantConfig } from '@/program.ts';
|
||||
import { AssistantQuery } from '@/lib.ts';
|
||||
import { logger } from '@/module/logger.ts';
|
||||
|
||||
const me = new Command('me')
|
||||
.description('查看当前用户信息')
|
||||
.action(async () => {
|
||||
const aq = new AssistantQuery(assistantConfig);
|
||||
await aq.init()
|
||||
const info = await aq.queryLogin.checkLocalUser()
|
||||
logger.info(info);
|
||||
});
|
||||
|
||||
program.addCommand(me);
|
||||
@@ -6,6 +6,7 @@ import './command/app/index.ts';
|
||||
import './command/run-scripts/index.ts';
|
||||
import './command/ai/index.ts';
|
||||
import './command/plugins/install.ts';
|
||||
import './command/user/me.ts';
|
||||
|
||||
/**
|
||||
* 通过命令行解析器解析参数
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import fs from 'node:fs';
|
||||
import { useKey } from '@kevisual/use-config';
|
||||
export const getFileConfig = (filePath: string): any => {
|
||||
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
||||
}
|
||||
@@ -1,15 +1,11 @@
|
||||
import path from 'path';
|
||||
import { homedir } from 'os';
|
||||
import fs from 'fs';
|
||||
import { checkFileExists, createDir } from '../file/index.ts';
|
||||
import { ProxyInfo } from '../proxy/proxy.ts';
|
||||
import dotenv from 'dotenv';
|
||||
import { logger } from '@/module/logger.ts';
|
||||
import { z } from 'zod'
|
||||
import { HomeConfigDir } from './args.ts'
|
||||
import { getFileConfig } from './get-assistan-config.ts';
|
||||
import { useKey } from '@kevisual/use-config';
|
||||
import { env } from 'pm2';
|
||||
|
||||
/**
|
||||
* 助手配置文件路径, 全局配置文件目录
|
||||
@@ -95,7 +91,6 @@ export type AssistantConfigData = {
|
||||
*/
|
||||
url?: string;
|
||||
}
|
||||
token?: string;
|
||||
registry?: string; // https://kevisual.cn
|
||||
/**
|
||||
* 前端代理,比如/root/home 转到https://kevisual.cn/root/home
|
||||
@@ -231,25 +226,15 @@ export class AssistantConfig {
|
||||
getConfig(): AssistantConfigData {
|
||||
try {
|
||||
if (!checkFileExists(this.configPath.configPath)) {
|
||||
fs.writeFileSync(this.configPath.configPath, JSON.stringify({ proxy: [] }, null, 2));
|
||||
return {
|
||||
app: {
|
||||
url: 'https://kevisual.cn',
|
||||
},
|
||||
proxy: [],
|
||||
};
|
||||
const defaultConfig = this.getDefaultInitAssistantConfig();
|
||||
fs.writeFileSync(this.configPath.configPath, JSON.stringify(defaultConfig, null, 2));
|
||||
return defaultConfig;
|
||||
}
|
||||
assistantConfig = getFileConfig(this.configPath.configPath);
|
||||
return assistantConfig;
|
||||
} catch (error) {
|
||||
console.error('file read', error.message);
|
||||
process.exit(1);
|
||||
return {
|
||||
app: {
|
||||
url: 'https://kevisual.cn',
|
||||
},
|
||||
proxy: [],
|
||||
};
|
||||
}
|
||||
}
|
||||
getCacheAssistantConfig() {
|
||||
@@ -393,6 +378,23 @@ export class AssistantConfig {
|
||||
// 如果没有找到助手配置文件目录,则返回当前目录, 执行默认创建助手配置文件目录
|
||||
return checkConfigDir;
|
||||
}
|
||||
protected getDefaultInitAssistantConfig() {
|
||||
const id = randomId();
|
||||
return {
|
||||
app: {
|
||||
url: 'https://kevisual.cn',
|
||||
id,
|
||||
},
|
||||
description: '助手配置文件',
|
||||
docs: "https://kevisual.cn/root/cli/docs/",
|
||||
home: '/root/home',
|
||||
proxy: [],
|
||||
share: {
|
||||
enabled: false,
|
||||
url: 'https://kevisual.cn/ws/proxy',
|
||||
},
|
||||
} as AssistantConfigData;
|
||||
}
|
||||
}
|
||||
|
||||
type AppConfig = {
|
||||
@@ -409,3 +411,5 @@ export const parseIfJson = (content: string) => {
|
||||
};
|
||||
|
||||
export * from './args.ts';
|
||||
|
||||
const randomId = () => Math.random().toString(36).substring(2, 8);
|
||||
@@ -7,12 +7,13 @@ 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';
|
||||
import { initLightCode } from '@/module/light-code/index.ts';
|
||||
import { ModuleResolver } from './assistant-app-resolve.ts';
|
||||
import z from 'zod';
|
||||
import { assistantQuery } from '@/app.ts';
|
||||
import { useKey } from '@kevisual/context';
|
||||
export class AssistantApp extends Manager {
|
||||
config: AssistantConfig;
|
||||
pagesPath: string;
|
||||
@@ -23,8 +24,8 @@ export class AssistantApp extends Manager {
|
||||
|
||||
constructor(config: AssistantConfig, mainApp?: App) {
|
||||
config.checkMounted();
|
||||
const appsPath = config?.configPath?.appsDir || path.join(process.cwd(), 'apps');
|
||||
const pagesPath = config?.configPath?.pagesDir || path.join(process.cwd(), 'pages');
|
||||
const appsPath = config?.configPath?.appsDir
|
||||
const pagesPath = config?.configPath?.pagesDir;
|
||||
const appsConfigPath = config.configPath?.appsConfigPath;
|
||||
const configFimename = path.basename(appsConfigPath || '');
|
||||
super({
|
||||
@@ -80,6 +81,12 @@ export class AssistantApp extends Manager {
|
||||
return pagesParse;
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化远程应用连接,如果配置了远程应用且启用,则尝试连接远程应用服务器,并设置自动重连机制.
|
||||
* 应用暴露
|
||||
* @info 需要登录权限
|
||||
* @param opts
|
||||
*/
|
||||
async initRemoteApp(opts?: { token?: string, enabled?: boolean }) {
|
||||
const config = this.config.getConfig();
|
||||
const share = config?.share;
|
||||
@@ -90,8 +97,17 @@ export class AssistantApp extends Manager {
|
||||
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');
|
||||
let token = opts?.token;
|
||||
if (!token) {
|
||||
token = await assistantQuery.queryLogin.getToken();
|
||||
}
|
||||
let shareUrl = share?.url;
|
||||
if (!shareUrl) {
|
||||
const _url = new URL(config?.registry || 'https://kevisual.cn/');
|
||||
_url.pathname = '/ws/proxy';
|
||||
shareUrl = _url.toString();
|
||||
}
|
||||
const url = new URL(shareUrl);
|
||||
const id = config?.app?.id;
|
||||
if (token && url && id) {
|
||||
const remoteApp = new RemoteApp({
|
||||
@@ -129,11 +145,22 @@ export class AssistantApp extends Manager {
|
||||
this.remoteApp = remoteApp;
|
||||
} else {
|
||||
if (!token) {
|
||||
logger.error('Token是远程应用连接必须的参数');
|
||||
logger.error('[remote-app] cli当前未登录,无法连接远程app');
|
||||
} else if (!id) {
|
||||
logger.error('[remote-app] app id不存在,无法连接远程app');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 本地路由初始化,根据配置加载应用的模块。
|
||||
* 加载局域网或者某一个链接的远程路由, 或者代码仓库的动态加载的light-code模块(实时同步远程文件执行)
|
||||
* @info 不需要登录
|
||||
* 配置项说明:
|
||||
* - router.proxy: 数组,包含需要代理的路由信息,每项可以是以下两种类型:
|
||||
* - lightcode: 轻代码模块配置
|
||||
* @returns
|
||||
*/
|
||||
async initRouterApp() {
|
||||
const config = this.config.getConfig();
|
||||
const routerProxy = config?.router?.proxy || [];
|
||||
@@ -209,6 +236,10 @@ export class AssistantApp extends Manager {
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 动态加载文件插件模块的应用模块
|
||||
* @info 不需要登录
|
||||
*/
|
||||
async initRoutes() {
|
||||
const routes = this.config.getConfig().routes || [];
|
||||
for (const route of routes) {
|
||||
@@ -216,10 +247,59 @@ export class AssistantApp extends Manager {
|
||||
const routeStr = typeof route === 'string' ? route : route.path;
|
||||
const resolvedPath = this.resolver.resolve(routeStr);
|
||||
await import(resolvedPath);
|
||||
console.log('[routes] 路由已初始化', route, resolvedPath);
|
||||
console.log('[routes] 路由已初始化', route);
|
||||
} catch (err) {
|
||||
console.error('初始化路由失败', route, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 检查本地用户登录状态,如果未登录且存在 CNB_TOKEN,则尝试使用 CNB_TOKEN 登录并更新用户信息
|
||||
*/
|
||||
async checkLocalUser() {
|
||||
const config = this.config.getConfig();
|
||||
const auth = config?.auth;
|
||||
let checkCNB = false;
|
||||
if (!auth?.username) {
|
||||
checkCNB = true;
|
||||
} else {
|
||||
let temp = await assistantQuery.queryLogin.getToken()
|
||||
if (temp) {
|
||||
const isExpired = await assistantQuery.queryLogin.checkTokenValid()
|
||||
console.log('Token 是否过期', isExpired);
|
||||
}
|
||||
logger.info('[assistant] 当前登录用户', auth.username, 'token有效性检查结果', !!temp);
|
||||
}
|
||||
const cnbToken = useKey('CNB_TOKEN');
|
||||
if (!checkCNB && cnbToken) {
|
||||
const res = await assistantQuery.query.post({
|
||||
path: 'user',
|
||||
key: 'cnb-login',
|
||||
payload: {
|
||||
data: {
|
||||
cnbToken: cnbToken,
|
||||
}
|
||||
}
|
||||
});
|
||||
if (res.code === 200) {
|
||||
logger.info('CNB登录成功,用户信息已更新');
|
||||
const resUser = await assistantQuery.queryLogin.beforeSetLoginUser(res.data)
|
||||
if (resUser.code === 200) {
|
||||
const userInfo = resUser.data;
|
||||
auth.username = userInfo.username;
|
||||
auth.share = 'protected'
|
||||
const app = config?.app || {};
|
||||
if (!app?.id) {
|
||||
app.id = 'dev-cnb'
|
||||
}
|
||||
this.config.setConfig({
|
||||
auth,
|
||||
app
|
||||
});
|
||||
} else {
|
||||
console.error('CNB登录失败,无法获取用户信息', resUser);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,33 @@
|
||||
import { DataOpts, Query } from "@kevisual/query/query";
|
||||
import { AssistantConfig } from "../config/index.ts";
|
||||
|
||||
import { QueryLoginNode } from '@kevisual/api/query-login-node'
|
||||
import { EventEmitter } from 'eventemitter3'
|
||||
|
||||
export class AssistantQuery {
|
||||
config: AssistantConfig;
|
||||
query: Query ;
|
||||
query: Query;
|
||||
queryLogin: QueryLoginNode;
|
||||
emitter = new EventEmitter();
|
||||
isLoad = false;
|
||||
constructor(config: AssistantConfig) {
|
||||
config.checkMounted();
|
||||
this.config = config;
|
||||
this.query = new Query({ url: config.getRegistry() + '/api/router' });
|
||||
this.queryLogin = new QueryLoginNode({
|
||||
query: this.query, onLoad: () => {
|
||||
this.isLoad = true;
|
||||
this.emitter.emit('load')
|
||||
}
|
||||
});
|
||||
}
|
||||
async init() {
|
||||
if (this.isLoad) return true;
|
||||
return new Promise((resolve) => {
|
||||
this.emitter.on('load', () => {
|
||||
this.isLoad = true;
|
||||
resolve(true);
|
||||
})
|
||||
})
|
||||
}
|
||||
post(body: any, options?: DataOpts) {
|
||||
return this.query.post(body, options);
|
||||
@@ -16,4 +35,7 @@ export class AssistantQuery {
|
||||
get(body: any, options?: DataOpts) {
|
||||
return this.query.get(body, options);
|
||||
}
|
||||
getToken() {
|
||||
return this.queryLogin.getToken();
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useKey } from '@kevisual/use-config';
|
||||
import http from 'node:http';
|
||||
import { IncomingMessage } from 'node:http';
|
||||
export const error = (msg: string, code = 500) => {
|
||||
return JSON.stringify({ code, message: msg });
|
||||
};
|
||||
@@ -16,7 +15,12 @@ const cookie = {
|
||||
return cookies;
|
||||
}
|
||||
}
|
||||
export const getToken = async (req: http.IncomingMessage) => {
|
||||
/**
|
||||
* 从请求中获取token,优先级:Authorization header > query parameter > cookie
|
||||
* @param req
|
||||
* @returns
|
||||
*/
|
||||
export const getToken = async (req: IncomingMessage) => {
|
||||
let token = (req.headers?.['authorization'] as string) || (req.headers?.['Authorization'] as string) || '';
|
||||
const url = new URL(req.url || '', 'http://localhost');
|
||||
if (!token) {
|
||||
@@ -32,8 +36,3 @@ export const getToken = async (req: http.IncomingMessage) => {
|
||||
|
||||
return { token };
|
||||
};
|
||||
|
||||
export const getEnvToken = () => {
|
||||
const envTokne = useKey('KEVISUAL_TOKEN') || '';
|
||||
return envTokne;
|
||||
}
|
||||
@@ -8,6 +8,7 @@ const codeDemoId = '0e700dc8-90dd-41b7-91dd-336ea51de3d2'
|
||||
import { filter } from "@kevisual/js-filter";
|
||||
import { getHash, getStringHash } from '../file-hash.ts';
|
||||
import { AssistantConfig } from '@/lib.ts';
|
||||
import { assistantQuery } from '@/app.ts';
|
||||
|
||||
const codeDemo = `// 这是一个示例代码文件
|
||||
import {App} from '@kevisual/router';
|
||||
@@ -48,11 +49,11 @@ export const initLightCode = async (opts: Opts) => {
|
||||
console.log('初始化 light-code 路由');
|
||||
const config = opts.config as AssistantInit;
|
||||
const app = opts.router;
|
||||
const token = config.getConfig()?.token || '';
|
||||
const token = await assistantQuery.getToken();
|
||||
const query = config.query;
|
||||
const sync = opts.sync ?? 'remote';
|
||||
if (!config || !app) {
|
||||
console.error('initLightCode 缺少必要参数, config 或 app');
|
||||
console.error('[light-code] initLightCode 缺少必要参数, config 或 app');
|
||||
return;
|
||||
}
|
||||
const lightcodeDir = opts.rootPath;
|
||||
@@ -126,7 +127,7 @@ export const initLightCode = async (opts: Opts) => {
|
||||
}
|
||||
diffList = findGlob({ cwd: lightcodeDir });
|
||||
} else {
|
||||
console.error('light-code 同步失败', queryRes.message);
|
||||
console.error('[light-code] 同步失败', queryRes.message);
|
||||
diffList = codeFiles;
|
||||
}
|
||||
} else if (sync === 'local') {
|
||||
@@ -191,22 +192,22 @@ export const initLightCode = async (opts: Opts) => {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.error('light-code 路由执行失败', runRes.error);
|
||||
console.error('[light-code] 路由执行失败', runRes.error);
|
||||
}
|
||||
}
|
||||
console.log(`light-code 路由注册成功`, `注册${diffList.length}个路由`);
|
||||
console.log(`[light-code] 路由注册成功`, `注册${diffList.length}个路由`);
|
||||
}
|
||||
|
||||
export const clearLightCodeRoutes = (opts: Pick<Opts, 'router'>) => {
|
||||
const app = opts.router;
|
||||
if (!app) {
|
||||
console.error('clearLightCodeRoutes 缺少必要参数, app');
|
||||
console.error('[light-code] clearLightCodeRoutes 缺少必要参数, app');
|
||||
return;
|
||||
}
|
||||
const routes = app.getList();
|
||||
for (const route of routes) {
|
||||
if (route.metadata?.source === 'light-code') {
|
||||
// console.log(`删除 light-code 路由: ${route.path} ${route.id}`);
|
||||
// console.log(`[light-code] 删除 light-code 路由: ${route.path} ${route.id}`);
|
||||
app.removeById(route.id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,9 +29,9 @@ export const installDeps = async (opts: InstallDepsOptions) => {
|
||||
console.log('installDeps', appPath, params);
|
||||
const syncSpawn = opts.sync ? spawnSync : spawn;
|
||||
if (isPnpm) {
|
||||
syncSpawn('pnpm', params, { cwd: appPath, stdio: 'inherit', env: process.env });
|
||||
syncSpawn('pnpm', params, { cwd: appPath, stdio: 'inherit', env: { ...process.env, CI: 'true' }, shell: true });
|
||||
} else {
|
||||
syncSpawn('npm', params, { cwd: appPath, stdio: 'inherit', env: process.env });
|
||||
syncSpawn('npm', params, { cwd: appPath, stdio: 'inherit', env: process.env, shell: true });
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import { Query } from '@kevisual/query';
|
||||
|
||||
export const query = new Query();
|
||||
|
||||
export const clientQuery = new Query({ url: '/client/router' });
|
||||
|
||||
export { QueryUtil } from '@kevisual/router/define';
|
||||
@@ -1,42 +0,0 @@
|
||||
import { QueryUtil } from '@/query/index.ts';
|
||||
|
||||
type Message = {
|
||||
role?: 'user' | 'assistant' | 'system' | 'tool';
|
||||
content?: string;
|
||||
name?: string;
|
||||
};
|
||||
export type PostChat = {
|
||||
messages?: Message[];
|
||||
model?: string;
|
||||
group?: string;
|
||||
user?: string;
|
||||
};
|
||||
|
||||
export type ChatDataOpts = {
|
||||
id?: string;
|
||||
title?: string;
|
||||
messages?: any[];
|
||||
data?: any;
|
||||
type?: 'temp' | 'keep' | string;
|
||||
};
|
||||
export type ChatOpts = {
|
||||
username: string;
|
||||
model: string;
|
||||
/**
|
||||
* 获取完整消息回复
|
||||
*/
|
||||
getFull?: boolean;
|
||||
group: string;
|
||||
/**
|
||||
* openai的参数
|
||||
*/
|
||||
options?: any;
|
||||
};
|
||||
|
||||
export const appDefine = QueryUtil.create({
|
||||
chat: {
|
||||
path: 'ai',
|
||||
key: 'chat',
|
||||
description: '与 AI 进行对话, 调用 GPT 的AI 服务,生成结果,并返回。',
|
||||
},
|
||||
});
|
||||
@@ -1,101 +0,0 @@
|
||||
import { appDefine } from './defines/ai.ts';
|
||||
import { PostChat, ChatOpts, ChatDataOpts } from './defines/ai.ts';
|
||||
|
||||
import { BaseQuery, DataOpts, Query } from '@kevisual/query/query';
|
||||
|
||||
export { appDefine };
|
||||
|
||||
export class QueryApp<T extends Query = Query> extends BaseQuery<T, typeof appDefine> {
|
||||
constructor(opts?: { query: T }) {
|
||||
super({
|
||||
...opts,
|
||||
query: opts?.query!,
|
||||
queryDefine: appDefine,
|
||||
});
|
||||
}
|
||||
/**
|
||||
* 与 AI 进行对话, 调用 GPT 的AI 服务,生成结果,并返回。
|
||||
* @param data
|
||||
* @param opts
|
||||
* @returns
|
||||
*/
|
||||
postChat(data: PostChat, opts?: DataOpts) {
|
||||
return this.chain('chat').post(data, opts);
|
||||
}
|
||||
/**
|
||||
* 获取模型列表
|
||||
* @param opts
|
||||
* @returns
|
||||
*/
|
||||
getModelList(data?: { usernames?: string[] }, opts?: DataOpts) {
|
||||
return this.query.post(
|
||||
{
|
||||
path: 'ai',
|
||||
key: 'get-model-list',
|
||||
data,
|
||||
},
|
||||
opts,
|
||||
);
|
||||
}
|
||||
/**
|
||||
* 聊天对话模型
|
||||
* @param data
|
||||
* @param chatOpts
|
||||
* @param opts
|
||||
* @returns
|
||||
*/
|
||||
chat(data: ChatDataOpts, chatOpts: ChatOpts, opts?: DataOpts) {
|
||||
const { username, model, group, getFull = true } = chatOpts;
|
||||
if (!username || !model || !group) {
|
||||
throw new Error('username, model, group is required');
|
||||
}
|
||||
return this.query.post(
|
||||
{
|
||||
path: 'ai',
|
||||
key: 'chat',
|
||||
...chatOpts,
|
||||
getFull,
|
||||
data,
|
||||
},
|
||||
opts,
|
||||
);
|
||||
}
|
||||
clearConfigCache(opts?: DataOpts) {
|
||||
return this.query.post(
|
||||
{
|
||||
path: 'ai',
|
||||
key: 'clear-cache',
|
||||
},
|
||||
opts,
|
||||
);
|
||||
}
|
||||
/**
|
||||
* 获取聊天使用情况
|
||||
* @param opts
|
||||
* @returns
|
||||
*/
|
||||
getChatUsage(opts?: DataOpts) {
|
||||
return this.query.post(
|
||||
{
|
||||
path: 'ai',
|
||||
key: 'get-chat-usage',
|
||||
},
|
||||
opts,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除当前用户模型自己的统计
|
||||
* @param opts
|
||||
* @returns
|
||||
*/
|
||||
clearSelfUsage(opts?: DataOpts) {
|
||||
return this.query.post(
|
||||
{
|
||||
path: 'ai',
|
||||
key: 'clear-chat-limit',
|
||||
},
|
||||
opts,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,204 +0,0 @@
|
||||
export interface Cache {
|
||||
/**
|
||||
* @update 获取缓存
|
||||
*/
|
||||
get(key: string): Promise<any>;
|
||||
/**
|
||||
* @update 设置缓存
|
||||
*/
|
||||
set(key: string, value: any): Promise<any>;
|
||||
/**
|
||||
* @update 删除缓存
|
||||
*/
|
||||
del(): Promise<void>;
|
||||
/**
|
||||
* 初始化
|
||||
*/
|
||||
init?: () => Promise<any>;
|
||||
}
|
||||
type User = {
|
||||
avatar?: string;
|
||||
description?: string;
|
||||
id?: string;
|
||||
needChangePassword?: boolean;
|
||||
orgs?: string[];
|
||||
type?: string;
|
||||
username?: string;
|
||||
};
|
||||
|
||||
export type CacheLoginUser = {
|
||||
user?: User;
|
||||
id?: string;
|
||||
accessToken?: string;
|
||||
refreshToken?: string;
|
||||
};
|
||||
type CacheLogin = {
|
||||
loginUsers: CacheLoginUser[];
|
||||
} & CacheLoginUser;
|
||||
|
||||
export type CacheStore<T = Cache> = {
|
||||
name: string;
|
||||
/**
|
||||
* 缓存数据
|
||||
* @important 需要先调用init
|
||||
*/
|
||||
cacheData: CacheLogin;
|
||||
/**
|
||||
* 实际操作的cache, 需要先调用init
|
||||
*/
|
||||
cache: T;
|
||||
|
||||
/**
|
||||
* 设置当前用户
|
||||
*/
|
||||
setLoginUser(user: CacheLoginUser): Promise<void>;
|
||||
/**
|
||||
* 获取当前用户
|
||||
*/
|
||||
getCurrentUser(): Promise<User>;
|
||||
/**
|
||||
* 获取当前用户列表
|
||||
*/
|
||||
getCurrentUserList(): Promise<CacheLoginUser[]>;
|
||||
/**
|
||||
* 获取缓存的refreshToken
|
||||
*/
|
||||
getRefreshToken(): Promise<string>;
|
||||
/**
|
||||
* 获取缓存的accessToken
|
||||
*/
|
||||
getAccessToken(): Promise<string>;
|
||||
/**
|
||||
* 清除当前用户
|
||||
*/
|
||||
clearCurrentUser(): Promise<void>;
|
||||
/**
|
||||
* 清除所有用户
|
||||
*/
|
||||
clearAll(): Promise<void>;
|
||||
|
||||
getValue(): Promise<CacheLogin>;
|
||||
setValue(value: CacheLogin): Promise<CacheLogin>;
|
||||
delValue(): Promise<void>;
|
||||
init(): Promise<any>;
|
||||
};
|
||||
|
||||
export type LoginCacheStoreOpts = {
|
||||
name: string;
|
||||
cache: Cache;
|
||||
};
|
||||
export class LoginCacheStore implements CacheStore<any> {
|
||||
cache: Cache;
|
||||
name: string;
|
||||
cacheData: CacheLogin;
|
||||
constructor(opts: LoginCacheStoreOpts) {
|
||||
if (!opts.cache) {
|
||||
throw new Error('cache is required');
|
||||
}
|
||||
// @ts-ignore
|
||||
this.cache = opts.cache;
|
||||
this.cacheData = {
|
||||
loginUsers: [],
|
||||
user: undefined,
|
||||
id: undefined,
|
||||
accessToken: undefined,
|
||||
refreshToken: undefined,
|
||||
};
|
||||
this.name = opts.name;
|
||||
}
|
||||
/**
|
||||
* 设置缓存
|
||||
* @param key
|
||||
* @param value
|
||||
* @returns
|
||||
*/
|
||||
async setValue(value: CacheLogin) {
|
||||
await this.cache.set(this.name, value);
|
||||
this.cacheData = value;
|
||||
return value;
|
||||
}
|
||||
/**
|
||||
* 删除缓存
|
||||
*/
|
||||
async delValue() {
|
||||
await this.cache.del();
|
||||
}
|
||||
getValue(): Promise<CacheLogin> {
|
||||
return this.cache.get(this.name);
|
||||
}
|
||||
/**
|
||||
* 初始化,设置默认值
|
||||
*/
|
||||
async init() {
|
||||
const defaultData = {
|
||||
loginUsers: [],
|
||||
user: null,
|
||||
id: null,
|
||||
accessToken: null,
|
||||
refreshToken: null,
|
||||
};
|
||||
if (this.cache.init) {
|
||||
try {
|
||||
const cacheData = await this.cache.init();
|
||||
this.cacheData = cacheData || defaultData;
|
||||
} catch (error) {
|
||||
console.log('cacheInit error', error);
|
||||
}
|
||||
} else {
|
||||
this.cacheData = (await this.getValue()) || defaultData;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 设置当前用户
|
||||
* @param user
|
||||
*/
|
||||
async setLoginUser(user: CacheLoginUser) {
|
||||
const has = this.cacheData.loginUsers.find((u) => u.id === user.id);
|
||||
if (has) {
|
||||
this.cacheData.loginUsers = this.cacheData?.loginUsers?.filter((u) => u?.id && u.id !== user.id);
|
||||
}
|
||||
this.cacheData.loginUsers.push(user);
|
||||
this.cacheData.user = user.user;
|
||||
this.cacheData.id = user.id;
|
||||
this.cacheData.accessToken = user.accessToken;
|
||||
this.cacheData.refreshToken = user.refreshToken;
|
||||
await this.setValue(this.cacheData);
|
||||
}
|
||||
|
||||
getCurrentUser(): Promise<CacheLoginUser> {
|
||||
const cacheData = this.cacheData;
|
||||
return Promise.resolve(cacheData.user!);
|
||||
}
|
||||
getCurrentUserList(): Promise<CacheLoginUser[]> {
|
||||
return Promise.resolve(this.cacheData.loginUsers.filter((u) => u?.id));
|
||||
}
|
||||
getRefreshToken(): Promise<string> {
|
||||
const cacheData = this.cacheData;
|
||||
return Promise.resolve(cacheData.refreshToken || '');
|
||||
}
|
||||
getAccessToken(): Promise<string> {
|
||||
const cacheData = this.cacheData;
|
||||
return Promise.resolve(cacheData.accessToken || '');
|
||||
}
|
||||
|
||||
async clearCurrentUser() {
|
||||
const user = await this.getCurrentUser();
|
||||
const has = this.cacheData.loginUsers.find((u) => u.id === user.id);
|
||||
if (has) {
|
||||
this.cacheData.loginUsers = this.cacheData?.loginUsers?.filter((u) => u?.id && u.id !== user.id);
|
||||
}
|
||||
this.cacheData.user = undefined;
|
||||
this.cacheData.id = undefined;
|
||||
this.cacheData.accessToken = undefined;
|
||||
this.cacheData.refreshToken = undefined;
|
||||
await this.setValue(this.cacheData);
|
||||
}
|
||||
async clearAll() {
|
||||
this.cacheData.loginUsers = [];
|
||||
this.cacheData.user = undefined;
|
||||
this.cacheData.id = undefined;
|
||||
this.cacheData.accessToken = undefined;
|
||||
this.cacheData.refreshToken = undefined;
|
||||
await this.setValue(this.cacheData);
|
||||
}
|
||||
}
|
||||
@@ -1,132 +0,0 @@
|
||||
import { Cache } from './login-cache.ts';
|
||||
import { homedir } from 'node:os';
|
||||
import { join, dirname } from 'node:path';
|
||||
import fs from 'node:fs';
|
||||
import { readFileSync, writeFileSync, accessSync } from 'node:fs';
|
||||
import { readFile, writeFile, unlink, mkdir } from 'node:fs/promises';
|
||||
export const fileExists = async (
|
||||
filePath: string,
|
||||
{ createIfNotExists = true, isFile = true, isDir = false }: { createIfNotExists?: boolean; isFile?: boolean; isDir?: boolean } = {},
|
||||
) => {
|
||||
try {
|
||||
accessSync(filePath, fs.constants.F_OK);
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (createIfNotExists && isDir) {
|
||||
await mkdir(filePath, { recursive: true });
|
||||
return true;
|
||||
} else if (createIfNotExists && isFile) {
|
||||
await mkdir(dirname(filePath), { recursive: true });
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
export const readConfigFile = (filePath: string) => {
|
||||
try {
|
||||
const data = readFileSync(filePath, 'utf-8');
|
||||
const jsonData = JSON.parse(data);
|
||||
return jsonData;
|
||||
} catch (error) {
|
||||
return {};
|
||||
}
|
||||
};
|
||||
export const writeConfigFile = (filePath: string, data: any) => {
|
||||
writeFileSync(filePath, JSON.stringify(data, null, 2));
|
||||
};
|
||||
export const getHostName = () => {
|
||||
const configDir = join(homedir(), '.config', 'envision');
|
||||
const configFile = join(configDir, 'config.json');
|
||||
const config = readConfigFile(configFile);
|
||||
const baseURL = config.baseURL || 'https://kevisual.cn';
|
||||
const hostname = new URL(baseURL).hostname;
|
||||
return hostname;
|
||||
};
|
||||
export class StorageNode implements Storage {
|
||||
cacheData: any;
|
||||
filePath: string;
|
||||
constructor() {
|
||||
this.cacheData = {};
|
||||
const configDir = join(homedir(), '.config', 'envision');
|
||||
const hostname = getHostName();
|
||||
this.filePath = join(configDir, 'config', `${hostname}-storage.json`);
|
||||
fileExists(this.filePath, { isFile: true });
|
||||
}
|
||||
async loadCache() {
|
||||
const filePath = this.filePath;
|
||||
try {
|
||||
const data = await readConfigFile(filePath);
|
||||
this.cacheData = data;
|
||||
} catch (error) {
|
||||
this.cacheData = {};
|
||||
await writeFile(filePath, JSON.stringify(this.cacheData, null, 2));
|
||||
}
|
||||
}
|
||||
get length() {
|
||||
return Object.keys(this.cacheData).length;
|
||||
}
|
||||
getItem(key: string) {
|
||||
return this.cacheData[key];
|
||||
}
|
||||
setItem(key: string, value: any) {
|
||||
this.cacheData[key] = value;
|
||||
writeFile(this.filePath, JSON.stringify(this.cacheData, null, 2));
|
||||
}
|
||||
removeItem(key: string) {
|
||||
delete this.cacheData[key];
|
||||
writeFile(this.filePath, JSON.stringify(this.cacheData, null, 2));
|
||||
}
|
||||
clear() {
|
||||
this.cacheData = {};
|
||||
writeFile(this.filePath, JSON.stringify(this.cacheData, null, 2));
|
||||
}
|
||||
key(index: number) {
|
||||
return Object.keys(this.cacheData)[index];
|
||||
}
|
||||
}
|
||||
export class LoginNodeCache implements Cache {
|
||||
filepath: string;
|
||||
|
||||
constructor(filepath?: string) {
|
||||
this.filepath = filepath || join(homedir(), '.config', 'envision', 'config', `${getHostName()}-login.json`);
|
||||
fileExists(this.filepath, { isFile: true });
|
||||
}
|
||||
async get(_key: string) {
|
||||
try {
|
||||
const filePath = this.filepath;
|
||||
const data = readConfigFile(filePath);
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.log('get error', error);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
async set(_key: string, value: any) {
|
||||
try {
|
||||
const data = readConfigFile(this.filepath);
|
||||
const newData = { ...data, ...value };
|
||||
writeConfigFile(this.filepath, newData);
|
||||
} catch (error) {
|
||||
console.log('set error', error);
|
||||
}
|
||||
}
|
||||
async del() {
|
||||
await unlink(this.filepath);
|
||||
}
|
||||
async loadCache(filePath: string) {
|
||||
try {
|
||||
const data = await readFile(filePath, 'utf-8');
|
||||
const jsonData = JSON.parse(data);
|
||||
return jsonData;
|
||||
} catch (error) {
|
||||
// console.log('loadCache error', error);
|
||||
console.log('create new cache file:', filePath);
|
||||
const defaultData = { loginUsers: [] };
|
||||
writeConfigFile(filePath, defaultData);
|
||||
return defaultData;
|
||||
}
|
||||
}
|
||||
async init() {
|
||||
return await this.loadCache(this.filepath);
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import { QueryLogin, QueryLoginOpts } from './query-login.ts';
|
||||
import { MyCache } from '@kevisual/cache';
|
||||
type QueryLoginNodeOptsWithoutCache = Omit<QueryLoginOpts, 'cache'>;
|
||||
|
||||
export class QueryLoginBrowser extends QueryLogin {
|
||||
constructor(opts: QueryLoginNodeOptsWithoutCache) {
|
||||
super({
|
||||
...opts,
|
||||
cache: new MyCache('login'),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import { QueryLogin, QueryLoginOpts } from './query-login.ts';
|
||||
import { LoginNodeCache, StorageNode } from './login-node-cache.ts';
|
||||
type QueryLoginNodeOptsWithoutCache = Omit<QueryLoginOpts, 'cache'>;
|
||||
export const storage = new StorageNode();
|
||||
await storage.loadCache();
|
||||
export class QueryLoginNode extends QueryLogin {
|
||||
constructor(opts: QueryLoginNodeOptsWithoutCache) {
|
||||
super({
|
||||
...opts,
|
||||
storage,
|
||||
cache: new LoginNodeCache(),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,434 +0,0 @@
|
||||
import { Query, BaseQuery } from '@kevisual/query';
|
||||
import type { Result, DataOpts } from '@kevisual/query/query';
|
||||
import { setBaseResponse } from '@kevisual/query/query';
|
||||
import { LoginCacheStore, CacheStore } from './login-cache.ts';
|
||||
import { Cache } from './login-cache.ts';
|
||||
|
||||
export type QueryLoginOpts = {
|
||||
query?: Query;
|
||||
isBrowser?: boolean;
|
||||
onLoad?: () => void;
|
||||
storage?: Storage;
|
||||
cache: Cache;
|
||||
};
|
||||
export type QueryLoginData = {
|
||||
username?: string;
|
||||
password: string;
|
||||
email?: string;
|
||||
};
|
||||
export type QueryLoginResult = {
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
};
|
||||
|
||||
export class QueryLogin extends BaseQuery {
|
||||
/**
|
||||
* query login cache, 非实际操作, 一个cache的包裹模块
|
||||
*/
|
||||
cacheStore: CacheStore;
|
||||
isBrowser: boolean;
|
||||
load?: boolean;
|
||||
storage: Storage;
|
||||
onLoad?: () => void;
|
||||
|
||||
constructor(opts?: QueryLoginOpts) {
|
||||
super({
|
||||
query: opts?.query || new Query(),
|
||||
});
|
||||
this.cacheStore = new LoginCacheStore({ name: 'login', cache: opts?.cache! });
|
||||
this.isBrowser = opts?.isBrowser ?? true;
|
||||
this.init();
|
||||
this.onLoad = opts?.onLoad;
|
||||
this.storage = opts?.storage || localStorage;
|
||||
}
|
||||
setQuery(query: Query) {
|
||||
this.query = query;
|
||||
}
|
||||
private async init() {
|
||||
await this.cacheStore.init();
|
||||
this.load = true;
|
||||
this.onLoad?.();
|
||||
}
|
||||
async post<T = any>(data: any, opts?: DataOpts) {
|
||||
try {
|
||||
return this.query.post<T>({ path: 'user', ...data }, opts);
|
||||
} catch (error) {
|
||||
console.log('error', error);
|
||||
return {
|
||||
code: 400,
|
||||
} as any;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 登录,
|
||||
* @param data
|
||||
* @returns
|
||||
*/
|
||||
async login(data: QueryLoginData) {
|
||||
const res = await this.post<QueryLoginResult>({ key: 'login', ...data });
|
||||
if (res.code === 200) {
|
||||
const { accessToken, refreshToken } = res?.data || {};
|
||||
this.storage.setItem('token', accessToken || '');
|
||||
await this.beforeSetLoginUser({ accessToken, refreshToken });
|
||||
}
|
||||
return res;
|
||||
}
|
||||
/**
|
||||
* 手机号登录
|
||||
* @param data
|
||||
* @returns
|
||||
*/
|
||||
async loginByCode(data: { phone: string; code: string }) {
|
||||
const res = await this.post<QueryLoginResult>({ path: 'sms', key: 'login', data });
|
||||
if (res.code === 200) {
|
||||
const { accessToken, refreshToken } = res?.data || {};
|
||||
this.storage.setItem('token', accessToken || '');
|
||||
await this.beforeSetLoginUser({ accessToken, refreshToken });
|
||||
}
|
||||
return res;
|
||||
}
|
||||
/**
|
||||
* 设置token
|
||||
* @param token
|
||||
*/
|
||||
async setLoginToken(token: { accessToken: string; refreshToken: string }) {
|
||||
const { accessToken, refreshToken } = token;
|
||||
this.storage.setItem('token', accessToken || '');
|
||||
await this.beforeSetLoginUser({ accessToken, refreshToken });
|
||||
}
|
||||
async loginByWechat(data: { code: string }) {
|
||||
const res = await this.post<QueryLoginResult>({ path: 'wx', key: 'open-login', code: data.code });
|
||||
if (res.code === 200) {
|
||||
const { accessToken, refreshToken } = res?.data || {};
|
||||
this.storage.setItem('token', accessToken || '');
|
||||
await this.beforeSetLoginUser({ accessToken, refreshToken });
|
||||
}
|
||||
return res;
|
||||
}
|
||||
/**
|
||||
* 检测微信登录,登陆成功后,调用onSuccess,否则调用onError
|
||||
* @param param0
|
||||
*/
|
||||
async checkWechat({ onSuccess, onError }: { onSuccess?: (res: QueryLoginResult) => void; onError?: (res: any) => void }) {
|
||||
const url = new URL(window.location.href);
|
||||
const code = url.searchParams.get('code');
|
||||
const state = url.searchParams.get('state');
|
||||
if (code && state) {
|
||||
const res = await this.loginByWechat({ code });
|
||||
if (res.code === 200) {
|
||||
onSuccess?.(res.data);
|
||||
} else {
|
||||
onError?.(res);
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 登陆成功,需要获取用户信息进行缓存
|
||||
* @param param0
|
||||
*/
|
||||
async beforeSetLoginUser({ accessToken, refreshToken, check401 }: { accessToken?: string; refreshToken?: string; check401?: boolean }) {
|
||||
if (accessToken && refreshToken) {
|
||||
const resUser = await this.getMe(accessToken, check401);
|
||||
if (resUser.code === 200) {
|
||||
const user = resUser.data;
|
||||
if (user) {
|
||||
this.cacheStore.setLoginUser({
|
||||
user,
|
||||
id: user.id,
|
||||
accessToken,
|
||||
refreshToken,
|
||||
});
|
||||
} else {
|
||||
console.error('登录失败');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 刷新token
|
||||
* @param refreshToken
|
||||
* @returns
|
||||
*/
|
||||
async queryRefreshToken(refreshToken?: string) {
|
||||
const _refreshToken = refreshToken || this.cacheStore.getRefreshToken();
|
||||
let data = { refreshToken: _refreshToken };
|
||||
if (!_refreshToken) {
|
||||
await this.cacheStore.clearCurrentUser();
|
||||
return {
|
||||
code: 401,
|
||||
message: '请先登录',
|
||||
data: {} as any,
|
||||
};
|
||||
}
|
||||
return this.post(
|
||||
{ key: 'refreshToken', data },
|
||||
{
|
||||
afterResponse: async (response, ctx) => {
|
||||
setBaseResponse(response);
|
||||
return response as any;
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
/**
|
||||
* 检查401错误,并刷新token, 如果refreshToken存在,则刷新token, 否则返回401
|
||||
* 拦截请求,请使用run401Action, 不要直接使用 afterCheck401ToRefreshToken
|
||||
* @param response
|
||||
* @param ctx
|
||||
* @param refetch
|
||||
* @returns
|
||||
*/
|
||||
async afterCheck401ToRefreshToken(response: Result, ctx?: { req?: any; res?: any; fetch?: any }, refetch?: boolean) {
|
||||
const that = this;
|
||||
if (response?.code === 401) {
|
||||
const hasRefreshToken = await that.cacheStore.getRefreshToken();
|
||||
if (hasRefreshToken) {
|
||||
const res = await that.queryRefreshToken(hasRefreshToken);
|
||||
if (res.code === 200) {
|
||||
const { accessToken, refreshToken } = res?.data || {};
|
||||
that.storage.setItem('token', accessToken || '');
|
||||
await that.beforeSetLoginUser({ accessToken, refreshToken, check401: false });
|
||||
if (refetch && ctx && ctx.req && ctx.req.url && ctx.fetch) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1500));
|
||||
const url = ctx.req?.url;
|
||||
const body = ctx.req?.body;
|
||||
const headers = ctx.req?.headers;
|
||||
const res = await ctx.fetch(url, {
|
||||
method: 'POST',
|
||||
body: body,
|
||||
headers: { ...headers, Authorization: `Bearer ${accessToken}` },
|
||||
});
|
||||
setBaseResponse(res);
|
||||
return res;
|
||||
}
|
||||
} else {
|
||||
that.storage.removeItem('token');
|
||||
await that.cacheStore.clearCurrentUser();
|
||||
}
|
||||
return res;
|
||||
}
|
||||
}
|
||||
return response as any;
|
||||
}
|
||||
/**
|
||||
* 一个简单的401处理, 如果401,则刷新token, 如果refreshToken不存在,则返回401
|
||||
* refetch 是否重新请求, 会有bug,无限循环,按需要使用
|
||||
* TODO:
|
||||
* @param response
|
||||
* @param ctx
|
||||
* @param opts
|
||||
* @returns
|
||||
*/
|
||||
async run401Action(
|
||||
response: Result,
|
||||
ctx?: { req?: any; res?: any; fetch?: any },
|
||||
opts?: {
|
||||
/**
|
||||
* 是否重新请求, 会有bug,无限循环,按需要使用
|
||||
*/
|
||||
refetch?: boolean;
|
||||
/**
|
||||
* check之后的回调
|
||||
*/
|
||||
afterCheck?: (res: Result) => any;
|
||||
/**
|
||||
* 401处理后, 还是401, 则回调
|
||||
*/
|
||||
afterAlso401?: (res: Result) => any;
|
||||
},
|
||||
) {
|
||||
const that = this;
|
||||
const refetch = opts?.refetch ?? false;
|
||||
if (response?.code === 401) {
|
||||
if (that.query.stop === true) {
|
||||
return { code: 500, success: false, message: 'refresh token loading...' };
|
||||
}
|
||||
that.query.stop = true;
|
||||
const res = await that.afterCheck401ToRefreshToken(response, ctx, refetch);
|
||||
that.query.stop = false;
|
||||
opts?.afterCheck?.(res);
|
||||
if (res.code === 401) {
|
||||
opts?.afterAlso401?.(res);
|
||||
}
|
||||
return res;
|
||||
} else {
|
||||
return response as any;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 获取用户信息
|
||||
* @param token
|
||||
* @returns
|
||||
*/
|
||||
async getMe(token?: string, check401: boolean = true) {
|
||||
const _token = token || this.storage.getItem('token');
|
||||
const that = this;
|
||||
return that.post(
|
||||
{ key: 'me' },
|
||||
{
|
||||
beforeRequest: async (config) => {
|
||||
if (config.headers) {
|
||||
config.headers['Authorization'] = `Bearer ${_token}`;
|
||||
}
|
||||
if (!_token) {
|
||||
return false;
|
||||
}
|
||||
return config;
|
||||
},
|
||||
afterResponse: async (response, ctx) => {
|
||||
if (response?.code === 401 && check401 && !token) {
|
||||
return await that.afterCheck401ToRefreshToken(response, ctx);
|
||||
}
|
||||
return response as any;
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
/**
|
||||
* 检查本地用户,如果本地用户存在,则返回本地用户,否则返回null
|
||||
* @returns
|
||||
*/
|
||||
async checkLocalUser() {
|
||||
const user = await this.cacheStore.getCurrentUser();
|
||||
if (user) {
|
||||
return user;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
/**
|
||||
* 检查本地token是否存在,简单的判断是否已经属于登陆状态
|
||||
* @returns
|
||||
*/
|
||||
async checkLocalToken() {
|
||||
const token = this.storage.getItem('token');
|
||||
return !!token;
|
||||
}
|
||||
/**
|
||||
* 检查本地用户列表
|
||||
* @returns
|
||||
*/
|
||||
async getToken() {
|
||||
const token = this.storage.getItem('token');
|
||||
return token || '';
|
||||
}
|
||||
async beforeRequest(opts: any = {}) {
|
||||
const token = this.storage.getItem('token');
|
||||
if (token) {
|
||||
opts.headers = { ...opts.headers, Authorization: `Bearer ${token}` };
|
||||
}
|
||||
return opts;
|
||||
}
|
||||
/**
|
||||
* 请求更新,切换用户, 使用switchUser
|
||||
* @param username
|
||||
* @returns
|
||||
*/
|
||||
private async postSwitchUser(username: string) {
|
||||
return this.post({ key: 'switchCheck', data: { username } });
|
||||
}
|
||||
/**
|
||||
* 切换用户
|
||||
* @param username
|
||||
* @returns
|
||||
*/
|
||||
async switchUser(username: string) {
|
||||
const localUserList = await this.cacheStore.getCurrentUserList();
|
||||
const user = localUserList.find((userItem) => userItem.user!.username === username);
|
||||
if (user) {
|
||||
this.storage.setItem('token', user.accessToken || '');
|
||||
await this.beforeSetLoginUser({ accessToken: user.accessToken, refreshToken: user.refreshToken });
|
||||
return {
|
||||
code: 200,
|
||||
data: {
|
||||
accessToken: user.accessToken,
|
||||
refreshToken: user.refreshToken,
|
||||
},
|
||||
success: true,
|
||||
message: '切换用户成功',
|
||||
};
|
||||
}
|
||||
const res = await this.postSwitchUser(username);
|
||||
|
||||
if (res.code === 200) {
|
||||
const { accessToken, refreshToken } = res?.data || {};
|
||||
this.storage.setItem('token', accessToken || '');
|
||||
await this.beforeSetLoginUser({ accessToken, refreshToken });
|
||||
}
|
||||
return res;
|
||||
}
|
||||
/**
|
||||
* 退出登陆,去掉token, 并删除缓存
|
||||
* @returns
|
||||
*/
|
||||
async logout() {
|
||||
this.storage.removeItem('token');
|
||||
const users = await this.cacheStore.getCurrentUserList();
|
||||
const tokens = users
|
||||
.map((user) => {
|
||||
return user?.accessToken;
|
||||
})
|
||||
.filter(Boolean);
|
||||
this.cacheStore.delValue();
|
||||
return this.post<Result>({ key: 'logout', data: { tokens } });
|
||||
}
|
||||
/**
|
||||
* 检查用户名的组,这个用户是否存在
|
||||
* @param username
|
||||
* @returns
|
||||
*/
|
||||
async hasUser(username: string) {
|
||||
const that = this;
|
||||
return this.post<Result>(
|
||||
{
|
||||
path: 'org',
|
||||
key: 'hasUser',
|
||||
data: {
|
||||
username,
|
||||
},
|
||||
},
|
||||
{
|
||||
afterResponse: async (response, ctx) => {
|
||||
if (response?.code === 401) {
|
||||
const res = await that.afterCheck401ToRefreshToken(response, ctx, true);
|
||||
return res;
|
||||
}
|
||||
return response as any;
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
/**
|
||||
* 检查登录状态
|
||||
* @param token
|
||||
* @returns
|
||||
*/
|
||||
async checkLoginStatus(token: string) {
|
||||
const res = await this.post({
|
||||
path: 'user',
|
||||
key: 'checkLoginStatus',
|
||||
loginToken: token,
|
||||
});
|
||||
if (res.code === 200) {
|
||||
const accessToken = res.data?.accessToken;
|
||||
this.storage.setItem('token', accessToken || '');
|
||||
await this.beforeSetLoginUser({ accessToken, refreshToken: res.data?.refreshToken });
|
||||
return res;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* 使用web登录,创建url地址, 需要MD5和jsonwebtoken
|
||||
*/
|
||||
loginWithWeb(baseURL: string, { MD5, jsonwebtoken }: { MD5: any; jsonwebtoken: any }) {
|
||||
const randomId = Math.random().toString(36).substring(2, 15);
|
||||
const timestamp = Date.now();
|
||||
const tokenSecret = 'xiao' + randomId;
|
||||
const sign = MD5(`${tokenSecret}${timestamp}`).toString();
|
||||
const token = jsonwebtoken.sign({ randomId, timestamp, sign }, tokenSecret, {
|
||||
// 10分钟过期
|
||||
expiresIn: 60 * 10, // 10分钟
|
||||
});
|
||||
const url = `${baseURL}/api/router?path=user&key=webLogin&p&loginToken=${token}&sign=${sign}&randomId=${randomId}`;
|
||||
return { url, token, tokenSecret };
|
||||
}
|
||||
}
|
||||
@@ -41,14 +41,14 @@ app.route({
|
||||
}
|
||||
return;
|
||||
}
|
||||
await manager.initRemoteApp({ enabled: true, token: ctx.query?.token }).then(() => {
|
||||
try {
|
||||
const res = await manager.initRemoteApp({ enabled: true, token: ctx.query?.token })
|
||||
ctx.body = {
|
||||
content: '远程app连接成功',
|
||||
}
|
||||
}).catch((err) => {
|
||||
} catch (error) {
|
||||
ctx.body = {
|
||||
content: `远程app连接失败: ${err.message}`,
|
||||
content: '远程app连接失败',
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
}).addTo(app);
|
||||
@@ -1,4 +1,5 @@
|
||||
import { app, assistantConfig } from '../../app.ts';
|
||||
import { forwardCookie } from './utils/cookie.ts';
|
||||
|
||||
app.route({
|
||||
path: 'admin',
|
||||
@@ -23,19 +24,7 @@ app.route({
|
||||
password,
|
||||
}),
|
||||
});
|
||||
|
||||
// 转发上游服务器返回的所有 set-cookie(支持多个 cookie)
|
||||
const setCookieHeaders = res.headers.getSetCookie?.() || [];
|
||||
if (setCookieHeaders.length > 0) {
|
||||
// 设置多个 cookie 到原生 http.ServerResponse
|
||||
ctx.res.setHeader('Set-Cookie', setCookieHeaders);
|
||||
} else {
|
||||
// 兼容旧版本,使用 get 方法
|
||||
const setCookieHeader = res.headers.get('set-cookie');
|
||||
if (setCookieHeader) {
|
||||
ctx.res.setHeader('Set-Cookie', setCookieHeader);
|
||||
}
|
||||
}
|
||||
forwardCookie(ctx, res);
|
||||
|
||||
const responseData = await res.json();
|
||||
console.debug('admin login response', { res: responseData });
|
||||
@@ -73,6 +62,9 @@ app.route({
|
||||
key: 'me'
|
||||
}).define(async (ctx) => {
|
||||
const token = ctx.query.token;
|
||||
if (!token) {
|
||||
ctx.throw(401, 'token is required');
|
||||
}
|
||||
const res = await assistantConfig.query.post({
|
||||
path: 'user',
|
||||
key: 'me',
|
||||
|
||||
14
assistant/src/routes/user/utils/cookie.ts
Normal file
14
assistant/src/routes/user/utils/cookie.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
export const forwardCookie = (ctx: any, res: any) => {
|
||||
// 转发上游服务器返回的所有 set-cookie(支持多个 cookie)
|
||||
const setCookieHeaders = res.headers.getSetCookie?.() || [];
|
||||
if (setCookieHeaders.length > 0) {
|
||||
// 设置多个 cookie 到原生 http.ServerResponse
|
||||
ctx.res.setHeader('Set-Cookie', setCookieHeaders);
|
||||
} else {
|
||||
// 兼容旧版本,使用 get 方法
|
||||
const setCookieHeader = res.headers.get('set-cookie');
|
||||
if (setCookieHeader) {
|
||||
ctx.res.setHeader('Set-Cookie', setCookieHeader);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,12 +30,7 @@ export const runServer = async (port: number = 51515, listenPath = '127.0.0.1')
|
||||
}
|
||||
_port = isPortAvailable;
|
||||
}
|
||||
const hasSocket = listenPath.includes('.sock');
|
||||
if (hasSocket) {
|
||||
app.listen(listenPath, () => {
|
||||
console.log(`Server is running on ${listenPath}`);
|
||||
});
|
||||
} else {
|
||||
|
||||
app.listen(_port, listenPath, () => {
|
||||
let showListenPath = listenPath;
|
||||
if (listenPath === '::') {
|
||||
@@ -43,7 +38,6 @@ export const runServer = async (port: number = 51515, listenPath = '127.0.0.1')
|
||||
}
|
||||
console.log(`Server is running on ${'http'}://${showListenPath}:${_port}`);
|
||||
});
|
||||
}
|
||||
app.server.on([{
|
||||
id: 'handle-all',
|
||||
func: proxyRoute as any,
|
||||
@@ -53,14 +47,14 @@ export const runServer = async (port: number = 51515, listenPath = '127.0.0.1')
|
||||
qwenAsr,
|
||||
]);
|
||||
const manager = useContextKey('manager', new AssistantApp(assistantConfig, app));
|
||||
setTimeout(() => {
|
||||
manager.load({ runtime: 'client' }).then(() => {
|
||||
setTimeout(async () => {
|
||||
await manager.load({ runtime: 'client' });
|
||||
console.log('Assistant App Loaded');
|
||||
});
|
||||
manager.initRemoteApp()
|
||||
manager.initRouterApp()
|
||||
await manager.checkLocalUser()
|
||||
await manager.initRemoteApp();
|
||||
await manager.initRouterApp();
|
||||
if (runtime.isServer) {
|
||||
manager.initRoutes();
|
||||
await manager.initRoutes();
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
|
||||
38
assistant/src/services/init/common.ts
Normal file
38
assistant/src/services/init/common.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
export const configJson = `{
|
||||
"name": "assistant-app",
|
||||
"version": "1.0.1",
|
||||
"description": "assistant-app package pnpm, node pkgs projects",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "pm2 start apps/code-center/dist/app.mjs --name code-center",
|
||||
"proxy": "pm2 start apps/page-proxy/dist/app.mjs --name page-proxy",
|
||||
"preview": "pnpm i && ASSISTANT_CONFIG_DIR=/workspace/kevisual asst server -s"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.990.0",
|
||||
"@kevisual/oss": "^0.0.19",
|
||||
"@kevisual/query": "^0.0.40",
|
||||
"eventemitter3": "^5.0.4",
|
||||
"@kevisual/router": "^0.0.70",
|
||||
"@kevisual/use-config": "^1.0.30",
|
||||
"ioredis": "^5.9.3",
|
||||
"pg": "^8.18.0",
|
||||
"pm2": "^6.0.14",
|
||||
"crypto-js": "^4.2.0",
|
||||
"unstorage": "^1.17.4",
|
||||
"dayjs": "^1.11.19",
|
||||
"es-toolkit": "^1.44.0",
|
||||
"node-cron": "^4.2.1",
|
||||
"dotenv": "^17.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@kevisual/types": "^0.0.12",
|
||||
"@types/bun": "^1.3.9",
|
||||
"@types/crypto-js": "^4.2.2",
|
||||
"@types/node": "^25.2.3"
|
||||
}
|
||||
}
|
||||
`
|
||||
@@ -3,7 +3,7 @@ import path from 'node:path';
|
||||
import { checkFileExists, AssistantConfig, AssistantConfigData, parseHomeArg, parseHelpArg } from '@/module/assistant/index.ts';
|
||||
import { chalk } from '@/module/chalk.ts';
|
||||
import { Query } from '@kevisual/query/query';
|
||||
import { installDeps } from '@/module/npm-install.ts'
|
||||
import { checkNpmConfg } from './update-pkgs.ts';
|
||||
export { parseHomeArg, parseHelpArg };
|
||||
export type AssistantInitOptions = {
|
||||
path?: string;
|
||||
@@ -118,56 +118,7 @@ export class AssistantInit extends AssistantConfig {
|
||||
console.log(chalk.green('助手 pnpm-workspace.yaml 文件创建成功'));
|
||||
}
|
||||
const packagePath = path.join(this.configDir, 'assistant-app', 'package.json');
|
||||
if (!checkFileExists(packagePath, true)) {
|
||||
create = true;
|
||||
fs.writeFileSync(
|
||||
packagePath,
|
||||
`{
|
||||
"name": "assistant-app",
|
||||
"version": "1.0.1",
|
||||
"description": "assistant-app package pnpm, node pkgs projects",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "pm2 start apps/code-center/dist/app.mjs --name code-center",
|
||||
"proxy": "pm2 start apps/page-proxy/dist/app.mjs --name page-proxy",
|
||||
"preview": "pnpm i && ASSISTANT_CONFIG_DIR=/workspace/kevisual asst server -s"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.990.0",
|
||||
"@kevisual/oss": "^0.0.19",
|
||||
"@kevisual/query": "^0.0.40",
|
||||
"eventemitter3": "^5.0.4",
|
||||
"@kevisual/router": "^0.0.70",
|
||||
"@kevisual/use-config": "^1.0.30",
|
||||
"ioredis": "^5.9.3",
|
||||
"pg": "^8.18.0",
|
||||
"pm2": "^6.0.14",
|
||||
"crypto-js": "^4.2.0",
|
||||
"unstorage": "^1.17.4",
|
||||
"dayjs": "^1.11.19",
|
||||
"es-toolkit": "^1.44.0",
|
||||
"node-cron": "^4.2.1",
|
||||
"dotenv": "^17.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@kevisual/types": "^0.0.12",
|
||||
"@types/bun": "^1.3.9",
|
||||
"@types/crypto-js": "^4.2.2",
|
||||
"@types/node": "^25.2.3"
|
||||
}
|
||||
}
|
||||
`,
|
||||
);
|
||||
console.log(chalk.green('助手 package.json 文件创建成功, 正在安装依赖...'));
|
||||
installDeps({ appPath: path.dirname(packagePath), isProduction: true }).then(() => {
|
||||
console.log('------------------------------------------------');
|
||||
console.log(chalk.green('助手依赖安装完成'));
|
||||
console.log('------------------------------------------------');
|
||||
});
|
||||
}
|
||||
checkNpmConfg(packagePath);
|
||||
return {
|
||||
create,
|
||||
};
|
||||
@@ -205,21 +156,4 @@ export class AssistantInit extends AssistantConfig {
|
||||
console.log(chalk.green('.gitignore 文件更新成功'));
|
||||
}
|
||||
}
|
||||
protected getDefaultInitAssistantConfig() {
|
||||
const id = randomId();
|
||||
return {
|
||||
app: {
|
||||
url: 'https://kevisual.cn',
|
||||
id,
|
||||
},
|
||||
description: '助手配置文件',
|
||||
docs: "https://kevisual.cn/root/cli/docs/",
|
||||
home: '/root/home',
|
||||
proxy: [],
|
||||
share: {
|
||||
enabled: false,
|
||||
url: 'https://kevisual.cn/ws/proxy',
|
||||
},
|
||||
} as AssistantConfigData;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "assistant-app",
|
||||
"version": "1.0.1",
|
||||
"version": "1.0.3",
|
||||
"description": "assistant-app package pnpm, node pkgs projects",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
@@ -12,26 +12,25 @@
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.990.0",
|
||||
"@aws-sdk/client-s3": "^3.994.0",
|
||||
"@kevisual/oss": "^0.0.19",
|
||||
"@kevisual/query": "^0.0.40",
|
||||
"eventemitter3": "^5.0.4",
|
||||
"@kevisual/router": "^0.0.70",
|
||||
"@kevisual/query": "^0.0.49",
|
||||
"@kevisual/router": "^0.0.83",
|
||||
"@kevisual/use-config": "^1.0.30",
|
||||
"dayjs": "^1.11.19",
|
||||
"dotenv": "^17.3.1",
|
||||
"es-toolkit": "^1.44.0",
|
||||
"eventemitter3": "^5.0.4",
|
||||
"ioredis": "^5.9.3",
|
||||
"node-cron": "^4.2.1",
|
||||
"pg": "^8.18.0",
|
||||
"pm2": "^6.0.14",
|
||||
"crypto-js": "^4.2.0",
|
||||
"unstorage": "^1.17.4",
|
||||
"dayjs": "^1.11.19",
|
||||
"es-toolkit": "^1.44.0",
|
||||
"node-cron": "^4.2.1",
|
||||
"dotenv": "^17.3.1"
|
||||
"unstorage": "^1.17.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@kevisual/types": "^0.0.12",
|
||||
"@types/bun": "^1.3.9",
|
||||
"@types/crypto-js": "^4.2.2",
|
||||
"@types/node": "^25.2.3"
|
||||
"@types/node": "^25.3.0",
|
||||
"semver": "^7.7.4"
|
||||
}
|
||||
}
|
||||
111
assistant/src/services/init/update-pkgs.ts
Normal file
111
assistant/src/services/init/update-pkgs.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import { configJson } from './common.ts';
|
||||
import { checkFileExists } from '@/module/assistant/index.ts';
|
||||
import { chalk } from '@/module/chalk.ts';
|
||||
import { installDeps } from '@/module/npm-install.ts'
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { execSync } from 'node:child_process';
|
||||
import semver from 'semver';
|
||||
|
||||
export const checkNpmConfg = (packagePath: string) => {
|
||||
let create = false;
|
||||
if (!checkFileExists(packagePath, true)) {
|
||||
create = true;
|
||||
fs.writeFileSync(packagePath, configJson);
|
||||
console.log(chalk.green('助手 package.json 文件创建成功, 正在安装依赖...'));
|
||||
installDeps({ appPath: path.dirname(packagePath), isProduction: true }).then(() => {
|
||||
console.log('------------------------------------------------');
|
||||
console.log(chalk.green('助手依赖安装完成'));
|
||||
console.log('------------------------------------------------');
|
||||
});
|
||||
} else {
|
||||
checkNpmFileConfig(packagePath);
|
||||
}
|
||||
|
||||
return {
|
||||
create,
|
||||
}
|
||||
}
|
||||
|
||||
const checkNpmFileConfig = async (packagePath: string) => {
|
||||
const existingConfig = JSON.parse(fs.readFileSync(packagePath, 'utf-8'));
|
||||
const defaultConfig = JSON.parse(configJson);
|
||||
const appPath = path.dirname(packagePath);
|
||||
let needUpdate = false;
|
||||
if (defaultConfig.version !== existingConfig.version) {
|
||||
existingConfig.version = defaultConfig.version;
|
||||
existingConfig.scripts = {
|
||||
...existingConfig.scripts,
|
||||
...defaultConfig.scripts,
|
||||
};
|
||||
needUpdate = true;
|
||||
fs.writeFileSync(packagePath, JSON.stringify(existingConfig, null, 2));
|
||||
console.log(chalk.yellow('助手 package.json 文件版本不匹配,正在更新...'));
|
||||
}
|
||||
// 收集需要更新的依赖列表
|
||||
const depsToUpdate: string[] = [];
|
||||
const devDepsToUpdate: string[] = [];
|
||||
// 收集需要添加的依赖(不存在的)
|
||||
const depsToAdd: string[] = [];
|
||||
const devDepsToAdd: string[] = [];
|
||||
|
||||
// 检查 dependencies - 不存在或版本小于 defaultConfig
|
||||
for (const dep in defaultConfig.dependencies) {
|
||||
const defaultVersion = defaultConfig.dependencies[dep].replace(/^[~^]/, '');
|
||||
const existingVersion = existingConfig.dependencies?.[dep]?.replace(/^[~^]/, '');
|
||||
// console.log(chalk.blue(`检查 dependency: ${dep}, defaultVersion: ${defaultVersion}, existingVersion: ${existingVersion}`));
|
||||
if (!existingVersion) {
|
||||
// 依赖不存在,先添加到 package.json
|
||||
existingConfig.dependencies = existingConfig.dependencies || {};
|
||||
existingConfig.dependencies[dep] = defaultConfig.dependencies[dep];
|
||||
depsToAdd.push(dep);
|
||||
} else if (semver.lt(existingVersion, defaultVersion)) {
|
||||
depsToUpdate.push(dep);
|
||||
}
|
||||
}
|
||||
|
||||
// 检查 devDependencies - 不存在或版本小于 defaultConfig
|
||||
for (const devDep in defaultConfig.devDependencies) {
|
||||
const defaultVersion = defaultConfig.devDependencies[devDep].replace(/^[~^]/, '');
|
||||
const existingVersion = existingConfig.devDependencies?.[devDep]?.replace(/^[~^]/, '');
|
||||
// console.log(chalk.blue(`检查 devDependency: ${devDep}, defaultVersion: ${defaultVersion}, existingVersion: ${existingVersion}`));
|
||||
if (!existingVersion) {
|
||||
// 依赖不存在,先添加到 package.json
|
||||
existingConfig.devDependencies = existingConfig.devDependencies || {};
|
||||
existingConfig.devDependencies[devDep] = defaultConfig.devDependencies[devDep];
|
||||
devDepsToAdd.push(devDep);
|
||||
} else if (semver.lt(existingVersion, defaultVersion)) {
|
||||
devDepsToUpdate.push(devDep);
|
||||
}
|
||||
}
|
||||
|
||||
// 如果有新增的依赖,先写入 package.json
|
||||
if (depsToAdd.length > 0 || devDepsToAdd.length > 0) {
|
||||
fs.writeFileSync(packagePath, JSON.stringify(existingConfig, null, 2));
|
||||
console.log(chalk.yellow(`新增的 dependencies: ${depsToAdd.join(', ') || '无'}`));
|
||||
console.log(chalk.yellow(`新增的 devDependencies: ${devDepsToAdd.join(', ') || '无'}`));
|
||||
}
|
||||
|
||||
if (depsToUpdate.length > 0 || devDepsToUpdate.length > 0 || needUpdate) {
|
||||
console.log(chalk.yellow(`需要更新的 dependencies: ${depsToUpdate.join(', ') || '无'}`));
|
||||
console.log(chalk.yellow(`需要更新的 devDependencies: ${devDepsToUpdate.join(', ') || '无'}`));
|
||||
console.log(chalk.green('正在执行 ncu -u 更新依赖版本...'));
|
||||
|
||||
// 执行 ncu -u 更新 package.json
|
||||
try {
|
||||
execSync('ncu -u', { cwd: appPath, stdio: 'inherit' });
|
||||
console.log(chalk.green('ncu -u 执行完成'));
|
||||
} catch (error) {
|
||||
console.error(chalk.red('ncu -u 执行失败:', error));
|
||||
}
|
||||
|
||||
console.log(chalk.green('正在安装依赖...'));
|
||||
installDeps({ appPath, isProduction: true }).then(() => {
|
||||
console.log('------------------------------------------------');
|
||||
console.log(chalk.green('助手依赖安装完成'));
|
||||
console.log('------------------------------------------------');
|
||||
});
|
||||
} else {
|
||||
console.log(chalk.green('助手 package.json 文件已存在且依赖完整,无需更新'));
|
||||
}
|
||||
}
|
||||
@@ -45,7 +45,7 @@ const authFilter = async (req: http.IncomingMessage, res: http.ServerResponse) =
|
||||
return { code: 200, message: '允许访问根路径' };
|
||||
}
|
||||
// 放开首页
|
||||
if (pathname.startsWith('/root/home') || pathname === '/root/cli/docs/') {
|
||||
if (pathname.startsWith('/root/home') || pathname === '/root/cli-center/') {
|
||||
return { code: 200, message: '允许访问首页' };
|
||||
}
|
||||
// 放开api, 以 /api, /v1, /client, /serve 开头的请求
|
||||
@@ -86,7 +86,7 @@ export const proxyRoute = async (req: http.IncomingMessage, res: http.ServerResp
|
||||
let noAdmin = !auth.username;
|
||||
|
||||
const toSetting = () => {
|
||||
res.writeHead(302, { Location: `/root/cli/setting/` });
|
||||
res.writeHead(302, { Location: `/root/cli-center/` });
|
||||
res.end();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ const main = async () => {
|
||||
const config = assistantConfig?.getConfig();
|
||||
const share = config?.share;
|
||||
if (share && share.enabled !== false) {
|
||||
const token = config?.token;
|
||||
const token = ''
|
||||
const url = new URL(share.url || 'https://kevisual.cn/ws/proxy');
|
||||
const id = config?.app?.id;
|
||||
if (!id) {
|
||||
@@ -14,7 +14,6 @@ const main = async () => {
|
||||
return;
|
||||
}
|
||||
if (!token) {
|
||||
console.error('Token is required for remote app connection.');
|
||||
return;
|
||||
}
|
||||
const remoteApp = new RemoteApp({
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
"@kevisual/context": "^0.0.4",
|
||||
"@kevisual/kv-code": "^0.0.4",
|
||||
"@kevisual/query": "^0.0.38",
|
||||
"@kevisual/query-login": "^0.0.7",
|
||||
"@kevisual/registry": "^0.0.1",
|
||||
"@radix-ui/react-slot": "^1.2.4",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { QueryClient, Query } from '@kevisual/query'
|
||||
import { QueryLoginBrowser } from '@kevisual/query-login'
|
||||
import { Query } from '@kevisual/query'
|
||||
import { QueryLoginBrowser } from '@kevisual/api/query-login'
|
||||
const getUrl = () => {
|
||||
const host = window.location.host
|
||||
const isKevisual = host.includes('kevisual');
|
||||
@@ -10,11 +10,11 @@ const getUrl = () => {
|
||||
return '/client/router'
|
||||
}
|
||||
|
||||
export const query = new QueryClient({
|
||||
export const query = new Query({
|
||||
url: getUrl()
|
||||
});
|
||||
|
||||
export const clientQuery = new QueryClient({
|
||||
export const clientQuery = new Query({
|
||||
url: '/client/router'
|
||||
});
|
||||
|
||||
@@ -23,5 +23,5 @@ export const remoteQuery = new Query({
|
||||
});
|
||||
|
||||
export const queryLogin = new QueryLoginBrowser({
|
||||
query: remoteQuery
|
||||
query: remoteQuery as any,
|
||||
});
|
||||
15
package.json
15
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@kevisual/cli",
|
||||
"version": "0.1.5",
|
||||
"version": "0.1.7",
|
||||
"description": "envision 命令行工具",
|
||||
"type": "module",
|
||||
"basename": "/root/cli",
|
||||
@@ -44,9 +44,9 @@
|
||||
"@inquirer/prompts": "^8.2.1",
|
||||
"@kevisual/app": "^0.0.2",
|
||||
"@kevisual/auth": "^2.0.3",
|
||||
"@kevisual/context": "^0.0.6",
|
||||
"@kevisual/context": "^0.0.8",
|
||||
"@kevisual/use-config": "^1.0.30",
|
||||
"@opencode-ai/sdk": "^1.2.6",
|
||||
"@opencode-ai/sdk": "^1.2.10",
|
||||
"@types/busboy": "^1.5.4",
|
||||
"busboy": "^1.6.0",
|
||||
"eventemitter3": "^5.0.4",
|
||||
@@ -59,18 +59,17 @@
|
||||
"unstorage": "^1.17.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@kevisual/api": "^0.0.51",
|
||||
"@kevisual/cnb": "^0.0.26",
|
||||
"@kevisual/api": "^0.0.57",
|
||||
"@kevisual/cnb": "^0.0.28",
|
||||
"@kevisual/dts": "^0.0.4",
|
||||
"@kevisual/load": "^0.0.6",
|
||||
"@kevisual/logger": "^0.0.4",
|
||||
"@kevisual/query": "0.0.44",
|
||||
"@kevisual/query-login": "0.0.7",
|
||||
"@kevisual/query": "0.0.49",
|
||||
"@types/bun": "^1.3.9",
|
||||
"@types/crypto-js": "^4.2.2",
|
||||
"@types/jsonwebtoken": "^9.0.10",
|
||||
"@types/micromatch": "^4.0.10",
|
||||
"@types/node": "^25.2.3",
|
||||
"@types/node": "^25.3.0",
|
||||
"@types/semver": "^7.7.1",
|
||||
"chalk": "^5.6.2",
|
||||
"commander": "^14.0.3",
|
||||
|
||||
818
pnpm-lock.yaml
generated
818
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
import { program, Command } from '@/program.ts';
|
||||
import { query } from '@/module/query.ts';
|
||||
import { QueryConfig } from '@/query/query-config/query-config.ts';
|
||||
import { QueryConfig } from '@kevisual/api/query-config';
|
||||
import { showMore } from '@/uitls/show-more.ts';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { program, Command } from '@/program.ts';
|
||||
import { query } from '@/module/query.ts';
|
||||
import { QueryConfig } from '@/query/query-secret/query-secret.ts';
|
||||
import { QueryConfig } from '@kevisual/api/query-secret';
|
||||
import { showMore } from '@/uitls/show-more.ts';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
@@ -16,7 +16,7 @@ import './command/app/index.ts';
|
||||
import './command/gist/index.ts';
|
||||
import './command/config-remote.ts';
|
||||
import './command/config-secret-remote.ts';
|
||||
import './command/ai.ts';
|
||||
// import './command/ai.ts';
|
||||
import './command/coding-plan/cc.ts'
|
||||
import './command/coding-plan/oc.ts'
|
||||
import './command/docker.ts';
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Query } from '@kevisual/query/query';
|
||||
import { getConfig, getEnvToken } from './get-config.ts';
|
||||
import { QueryLoginNode, storage } from '@kevisual/query-login/node';
|
||||
import { QueryLoginNode, StorageNode } from '@kevisual/api/login-node';
|
||||
const config = getConfig();
|
||||
export const baseURL = config?.baseURL || 'https://kevisual.cn';
|
||||
export { storage };
|
||||
export const storage = new StorageNode({ load: true });
|
||||
|
||||
export const getBaseURL = () => {
|
||||
return baseURL;
|
||||
};
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
/**
|
||||
* 配置查询
|
||||
* @updatedAt 2025-12-03 10:33:00
|
||||
*/
|
||||
import { Query } from '@kevisual/query';
|
||||
import type { Result } from '@kevisual/query/query';
|
||||
type QueryConfigOpts = {
|
||||
query?: Query;
|
||||
};
|
||||
export type Config<T = any> = {
|
||||
id?: string;
|
||||
title?: string;
|
||||
key?: string;
|
||||
description?: string;
|
||||
data?: T;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
};
|
||||
export type UploadConfig = {
|
||||
key?: string;
|
||||
version?: string;
|
||||
};
|
||||
type PostOpts = {
|
||||
token?: string;
|
||||
payload?: Record<string, any>;
|
||||
};
|
||||
export const defaultConfigKeys = ['upload.json', 'workspace.json', 'ai.json', 'user.json', 'life.json'] as const;
|
||||
type DefaultConfigKey = (typeof defaultConfigKeys)[number];
|
||||
|
||||
export class QueryConfig {
|
||||
query: Query;
|
||||
constructor(opts?: QueryConfigOpts) {
|
||||
this.query = opts?.query || new Query();
|
||||
}
|
||||
async post<T = Config>(data: any) {
|
||||
return this.query.post<T>({ path: 'config', ...data });
|
||||
}
|
||||
async getConfig({ id, key }: { id?: string; key?: string }, opts?: PostOpts) {
|
||||
return this.post({
|
||||
key: 'get',
|
||||
data: {
|
||||
id,
|
||||
key,
|
||||
},
|
||||
...opts,
|
||||
});
|
||||
}
|
||||
async updateConfig(data: Config, opts?: PostOpts) {
|
||||
return this.post({
|
||||
key: 'update',
|
||||
data,
|
||||
...opts,
|
||||
});
|
||||
}
|
||||
async deleteConfig(data: { id?: string, key?: string }, opts?: PostOpts) {
|
||||
console.log('Delete Config Params:', data);
|
||||
return this.post({
|
||||
key: 'delete',
|
||||
data,
|
||||
});
|
||||
}
|
||||
async listConfig(opts?: PostOpts) {
|
||||
return this.post<{ list: Config[] }>({
|
||||
key: 'list',
|
||||
...opts,
|
||||
});
|
||||
}
|
||||
/**
|
||||
* 获取上传配置
|
||||
* @returns
|
||||
*/
|
||||
async getUploadConfig(opts?: PostOpts) {
|
||||
return this.post<Result<Config<UploadConfig>>>({
|
||||
key: 'getUploadConfig',
|
||||
...opts,
|
||||
});
|
||||
}
|
||||
/**
|
||||
* 更新上传配置
|
||||
* @param data
|
||||
* @returns
|
||||
*/
|
||||
async updateUploadConfig(data: Config, opts?: PostOpts) {
|
||||
return this.post<Result<Config<UploadConfig>>>({
|
||||
key: 'updateUploadConfig',
|
||||
data,
|
||||
...opts,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测配置是否存在
|
||||
* @param id
|
||||
* @returns
|
||||
*/
|
||||
async detectConfig(opts?: PostOpts) {
|
||||
return this.post<{ updateList: Config[] }>({
|
||||
key: 'detect',
|
||||
...opts,
|
||||
});
|
||||
}
|
||||
/**
|
||||
* 获取配置, 获取默认的配置项
|
||||
* @param key
|
||||
* @returns
|
||||
*/
|
||||
async getConfigByKey(key: DefaultConfigKey, opts?: PostOpts) {
|
||||
return this.post<Result<Config>>({
|
||||
key: 'defaultConfig',
|
||||
configKey: key,
|
||||
...opts,
|
||||
});
|
||||
}
|
||||
async getByKey<T = any>(key: string, opts?: PostOpts) {
|
||||
return this.post<Result<Config<T>>>({
|
||||
key: 'get',
|
||||
...opts,
|
||||
data: { key },
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
/**
|
||||
* 配置查询
|
||||
* @updatedAt 2025-12-03 10:33:00
|
||||
*/
|
||||
import { Query } from '@kevisual/query';
|
||||
import type { Result } from '@kevisual/query/query';
|
||||
type QueryConfigOpts = {
|
||||
query?: Query;
|
||||
};
|
||||
export type Config<T = any> = {
|
||||
id?: string;
|
||||
title?: string;
|
||||
key?: string;
|
||||
description?: string;
|
||||
data?: T;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
};
|
||||
export type UploadConfig = {
|
||||
key?: string;
|
||||
version?: string;
|
||||
};
|
||||
type PostOpts = {
|
||||
token?: string;
|
||||
payload?: Record<string, any>;
|
||||
};
|
||||
|
||||
export class QueryConfig {
|
||||
query: Query;
|
||||
constructor(opts?: QueryConfigOpts) {
|
||||
this.query = opts?.query || new Query();
|
||||
}
|
||||
async post<T = Config>(data: any) {
|
||||
return this.query.post<T>({ path: 'secret', ...data });
|
||||
}
|
||||
async getItem({ id, key }: { id?: string; key?: string }, opts?: PostOpts) {
|
||||
return this.post({
|
||||
key: 'get',
|
||||
data: {
|
||||
id,
|
||||
key,
|
||||
},
|
||||
...opts,
|
||||
});
|
||||
}
|
||||
async updateItem(data: Config, opts?: PostOpts) {
|
||||
return this.post({
|
||||
key: 'update',
|
||||
data,
|
||||
...opts,
|
||||
});
|
||||
}
|
||||
async deleteItem(data: { id?: string, key?: string }, opts?: PostOpts) {
|
||||
return this.post({
|
||||
key: 'delete',
|
||||
data,
|
||||
});
|
||||
}
|
||||
async listItems(opts?: PostOpts) {
|
||||
return this.post<{ list: Config[] }>({
|
||||
key: 'list',
|
||||
...opts,
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user