This commit is contained in:
xiongxiao
2026-03-24 02:57:13 +08:00
committed by cnb
parent 36a54652c1
commit 8c8cf1aadf
14 changed files with 514 additions and 119 deletions

View File

@@ -7,7 +7,7 @@ const external = ['bun'];
await Bun.build({ await Bun.build({
target: 'node', target: 'node',
format: 'esm', format: 'esm',
entrypoints: ['./src/index.ts'], entrypoints: ['./src/oldindex.ts'],
outdir: './dist', outdir: './dist',
naming: { naming: {
entry: 'envision.js', entry: 'envision.js',

View File

@@ -26,12 +26,12 @@
"files": [ "files": [
"dist", "dist",
"bin", "bin",
"bun.config.mjs" "bun.config.ts"
], ],
"scripts": { "scripts": {
"dev": "bun src/run.ts ", "dev": "bun src/cli.ts ",
"dev:server": "cd assistant && bun --watch src/run-server.ts ", "dev:server": "cd assistant && bun --watch src/run-server.ts ",
"build": "rimraf dist && bun run bun.config.mjs", "build": "rimraf dist && bun run bun.config.ts",
"deploy": "ev pack -u -p -m no", "deploy": "ev pack -u -p -m no",
"postbuild": "cd assistant && pnpm build " "postbuild": "cd assistant && pnpm build "
}, },
@@ -58,7 +58,8 @@
"nanoid": "^5.1.7", "nanoid": "^5.1.7",
"pm2": "latest", "pm2": "latest",
"semver": "^7.7.4", "semver": "^7.7.4",
"unstorage": "^1.17.4" "unstorage": "^1.17.4",
"zod": "^4.3.6"
}, },
"devDependencies": { "devDependencies": {
"@kevisual/api": "^0.0.65", "@kevisual/api": "^0.0.65",
@@ -76,13 +77,14 @@
"chalk": "^5.6.2", "chalk": "^5.6.2",
"commander": "^14.0.3", "commander": "^14.0.3",
"crypto-js": "^4.2.0", "crypto-js": "^4.2.0",
"es-toolkit": "^1.45.1",
"fast-glob": "^3.3.3", "fast-glob": "^3.3.3",
"filesize": "^11.0.13", "filesize": "^11.0.13",
"form-data": "^4.0.5", "form-data": "^4.0.5",
"ignore": "^7.0.5", "ignore": "^7.0.5",
"jsonwebtoken": "^9.0.3", "jsonwebtoken": "^9.0.3",
"pm2": "^6.0.14", "pm2": "^6.0.14",
"tar": "^7.5.12", "tar": "^7.5.13",
"zustand": "^5.0.12" "zustand": "^5.0.12"
}, },
"engines": { "engines": {

16
pnpm-lock.yaml generated
View File

@@ -62,6 +62,9 @@ importers:
unstorage: unstorage:
specifier: ^1.17.4 specifier: ^1.17.4
version: 1.17.4(idb-keyval@6.2.2) version: 1.17.4(idb-keyval@6.2.2)
zod:
specifier: ^4.3.6
version: 4.3.6
devDependencies: devDependencies:
'@kevisual/api': '@kevisual/api':
specifier: ^0.0.65 specifier: ^0.0.65
@@ -108,6 +111,9 @@ importers:
crypto-js: crypto-js:
specifier: ^4.2.0 specifier: ^4.2.0
version: 4.2.0 version: 4.2.0
es-toolkit:
specifier: ^1.45.1
version: 1.45.1
fast-glob: fast-glob:
specifier: ^3.3.3 specifier: ^3.3.3
version: 3.3.3 version: 3.3.3
@@ -124,8 +130,8 @@ importers:
specifier: ^9.0.3 specifier: ^9.0.3
version: 9.0.3 version: 9.0.3
tar: tar:
specifier: ^7.5.12 specifier: ^7.5.13
version: 7.5.12 version: 7.5.13
zustand: zustand:
specifier: ^5.0.12 specifier: ^5.0.12
version: 5.0.12(react@19.2.4) version: 5.0.12(react@19.2.4)
@@ -2257,8 +2263,8 @@ packages:
resolution: {integrity: sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==} resolution: {integrity: sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==}
engines: {node: '>=10.0.0'} engines: {node: '>=10.0.0'}
tar@7.5.12: tar@7.5.13:
resolution: {integrity: sha512-9TsuLcdhOn4XztcQqhNyq1KOwOOED/3k58JAvtULiYqbO8B/0IBAAIE1hj0Svmm58k27TmcigyDI0deMlgG3uw==} resolution: {integrity: sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng==}
engines: {node: '>=18'} engines: {node: '>=18'}
to-regex-range@5.0.1: to-regex-range@5.0.1:
@@ -4928,7 +4934,7 @@ snapshots:
string-width: 4.2.3 string-width: 4.2.3
strip-ansi: 6.0.1 strip-ansi: 6.0.1
tar@7.5.12: tar@7.5.13:
dependencies: dependencies:
'@isaacs/fs-minipass': 4.0.1 '@isaacs/fs-minipass': 4.0.1
chownr: 3.0.0 chownr: 3.0.0

View File

@@ -1,4 +0,0 @@
import { App } from '@kevisual/app/mod.ts';
import { storage } from '../module/query.ts';
import { sessionStorage } from '../module/cache.ts';
export const app = new App({ token: storage.getItem('token') || '', storage: sessionStorage });

View File

@@ -1,6 +0,0 @@
import { app } from './ai.ts'
import './routes/cmd-run.ts'
export {
app
}

View File

@@ -1,62 +0,0 @@
import { app } from '../ai.ts';
import { execSync } from 'node:child_process'
import { logger } from '@/module/logger.ts';
const promptTemplate = `# CMD 结果判断器
分析上一条 CMD 命令的执行结果,判断是否需要执行下一条命令。
- 若结果中隐含或明确指示需继续执行 → 返回:\`{"cmd": "推断出的下一条命令", "type": "cmd"}\`
- 若无后续操作,甚至上一次执行的返回为空或者成功 → 返回:\`{"type": "none"}\`
1. 仅输出合法 JSON无任何额外文本。
2. \`cmd\` 必须从执行结果中合理推断得出,非预设或猜测。
3. 禁止解释、注释、换行或格式错误。`
app.router.route({
path: 'cmd-run',
description: '执行 CMD 命令并判断下一步操作, 参数是 cmd 字符串',
}).define(async (ctx) => {
const cmd = ctx.query.cmd || '';
if (!cmd) {
ctx.throw(400, 'cmd is required');
}
let result = '';
ctx.state.steps = ctx.state?.steps || [];
try {
logger.info('执行命令:', cmd);
result = execSync(cmd, { encoding: 'utf-8' });
ctx.state.steps.push({ cmd, result });
logger.info(result);
} catch (error: any) {
result = error.message || '';
ctx.state.steps.push({ cmd, result, error: true });
ctx.body = {
steps: ctx.state.steps,
}
return;
}
await app.loadAI()
const prompt = `${promptTemplate}\n上一条命令:\n${cmd}\n执行结果:\n${result}\n`;
const response = await app.ai.question(prompt);
const msg = app.ai.utils.extractJsonFromMarkdown(app.ai.responseText);
try {
logger.debug('AI Prompt', prompt);
logger.debug('AI 分析结果:', msg);
const { cmd, type } = msg;
if (type === 'cmd' && cmd) {
await app.router.call({ path: 'cmd-run', payload: { cmd } }, { state: ctx.state });
} else {
logger.info('无后续命令,结束执行');
ctx.state.steps.push({ type: 'none' });
}
} catch (error) {
result = '执行错误,无法解析返回结果为合法 JSON' + app.ai.responseText
logger.error(result);
ctx.state.steps.push({ cmd, result, parseError: true });
}
ctx.body = {
steps: ctx.state.steps,
}
}).addTo(app.router);

6
src/app.ts Normal file
View File

@@ -0,0 +1,6 @@
import { App } from '@kevisual/router';
import { useContextKey } from '@kevisual/context';
export const app = useContextKey<App>('app', () => {
return new App()
});

4
src/cli.ts Normal file
View File

@@ -0,0 +1,4 @@
import { app } from './index.ts';
import { parse } from "@kevisual/router/commander"
parse({ app });

View File

@@ -1,6 +1,6 @@
import { program, Command } from '@/program.ts'; import { program, Command } from '@/program.ts';
import { getConfig, getEnvToken } from '@/module/get-config.ts'; import { getConfig, getEnvToken } from '@/module/get-config.ts';
import { input, password } from '@inquirer/prompts'; import { input } from '@inquirer/prompts';
import { loginInCommand } from '@/module/login/login-by-web.ts'; import { loginInCommand } from '@/module/login/login-by-web.ts';
import { queryLogin, storage } from '@/module/query.ts'; import { queryLogin, storage } from '@/module/query.ts';
import chalk from 'chalk'; import chalk from 'chalk';

View File

@@ -1,34 +1,7 @@
import { program } from '@/program.ts'; import { app } from './app.ts'
import './command/login.ts'; import './routes/login.ts'
import './command/ls-token.ts';
import './command/deploy.ts';
import './command/config.ts';
import './command/router.ts';
import './command/npm.ts';
import './command/publish.ts';
import './command/proxy.ts';
import './command/update.ts';
import './command/sync/sync.ts'; export { app };
import './command/app/index.ts'; app.createAuth(() => { })
app.createRouteList()
import './command/gist/index.ts';
import './command/config-remote.ts';
import './command/config-secret-remote.ts';
// import './command/ai.ts';
import './command/coding-plan/cc.ts'
import './command/coding-plan/oc.ts'
import './command/docker.ts';
import './command/jwks.ts';
import './command/cnb/index.ts';
import './command/download.ts';
// program.parse(process.argv);
export const runParser = async (argv: string[]) => {
// program.parse(process.argv);
// console.log('argv', argv);
program.parse(argv);
};

33
src/oldindex.ts Normal file
View File

@@ -0,0 +1,33 @@
import { program } from '@/program.ts';
import './command/login.ts';
import './command/ls-token.ts';
import './command/deploy.ts';
import './command/config.ts';
import './command/router.ts';
import './command/npm.ts';
import './command/publish.ts';
import './command/proxy.ts';
import './command/update.ts';
import './command/sync/sync.ts';
import './command/app/index.ts';
import './command/gist/index.ts';
import './command/config-remote.ts';
import './command/config-secret-remote.ts';
import './command/coding-plan/cc.ts'
import './command/coding-plan/oc.ts'
import './command/docker.ts';
import './command/jwks.ts';
import './command/cnb/index.ts';
import './command/download.ts';
// program.parse(process.argv);
export const runParser = async (argv: string[]) => {
// program.parse(process.argv);
// console.log('argv', argv);
program.parse(argv);
};

194
src/routes/login.ts Normal file
View File

@@ -0,0 +1,194 @@
import { app } from '../app.ts';
import { z } from 'zod';
import { getConfig, getEnvToken } from '@/module/get-config.ts';
import { input, password as inputPassword } from '@inquirer/prompts';
import { loginInCommand } from '@/module/login/login-by-web.ts';
import { queryLogin, storage } from '@/module/query.ts';
import chalk from 'chalk';
import util from 'util';
import { pick } from 'es-toolkit'
export const getUsername = async () => {
const token = getEnvToken();
const localToken = storage.getItem('token');
if (!token && !localToken) {
console.log('请先登录');
return null;
}
let me = await queryLogin.getMe(localToken || token);
if (me?.code === 401) {
me = await queryLogin.getMe();
}
if (me?.code === 200) {
return me.data?.username;
}
return null;
}
const showMe = async (show = true) => {
const token = getEnvToken();
const localToken = storage.getItem('token');
if (!token && !localToken) {
console.log('请先登录');
return { code: 40400, message: '请先登录' };
}
let me = await queryLogin.getMe(localToken || token);
if (me?.code === 401) {
me = await queryLogin.getMe();
}
if (show) {
console.log('Me', me.data);
}
return me;
};
app.route({
path: 'user',
key: 'login',
description: '登录',
metadata: {
args: {
username: z.string().optional().describe('用户名'),
password: z.string().optional().describe('密码'),
force: z.boolean().optional().describe('强制登录'),
web: z.boolean().optional().describe('是否通过web登录'),
env: z.boolean().optional().describe('是否通过环境变量KEVISUAL_TOKEN登录'),
}
}
}).define(async (ctx) => {
let { username, password, force, web, env } = ctx.args;
if (web) {
await loginInCommand();
return;
}
// 从环境变量登录
if (env) {
const envToken = getEnvToken();
if (!envToken) {
console.log('环境变量 KEVISUAL_TOKEN 未设置');
return;
}
const res = await showMe(false);
if (res.code === 200) {
console.log('Login success:', res.data?.username || res.data?.email);
} else {
console.log('Login failed:', res.message || 'Invalid token');
}
return;
}
// 如果没有传递参数,则通过交互式输入
if (!username) {
username = await input({
message: 'Enter your username:',
});
}
if (!password) {
password = await inputPassword({
message: 'Enter your password:',
});
}
const token = storage.getItem('token');
if (token) {
const res = await showMe(false);
if (res.code === 200) {
const data = res.data;
if (data.username === username) {
console.log('Already Login For', data.username);
return;
}
}
}
const res = await queryLogin.login({
username,
password,
}).catch((err) => {
return { code: 500, message: err.message || '' };
});
if (res.code === 200) {
console.log('welcome', username);
} else {
console.log('登录失败', res.message || '');
}
}).addTo(app)
app.route({
path: 'user',
key: 'me',
description: '查看当前登录用户信息',
metadata: {
args: {
all: z.boolean().optional().describe('是否显示全部信息默认为false'),
}
}
}).define(async (ctx) => {
const options = ctx.args;
try {
let res = await showMe(false);
let isRefresh = false;
if (res.code === 200 && res.data?.accessToken) {
res = await showMe(false);
isRefresh = true;
}
if (res.code === 40400) {
return
}
if (res.code === 200) {
if (isRefresh) {
console.log(chalk.green('refresh token success'), '\n');
}
} else {
console.log(
isRefresh ? chalk.red('refresh token failed, please login again.') : chalk.red('you need login first. \n run `envision login` to login'),
'\n',
);
return;
}
const baseURL = getConfig().baseURL;
const pickData = pick(res?.data, ['username', 'type', 'orgs']);
console.log(chalk.blue('baseURL', baseURL));
if (options.all) {
console.log(chalk.blue(util.inspect(res?.data, { colors: true, depth: 4 })));
} else {
// 打印pickData
console.log(chalk.blue(util.inspect(pickData, { colors: true, depth: 4 })));
}
} catch (error) {
console.log('me error', error);
}
}).addTo(app)
app.route({
path: 'user',
key: 'switch',
description: '切换到其他组织或用户',
metadata: {
args: {
username: z.string().describe('用户名或组织名'),
}
}
}).define(async (ctx) => {
const { username } = ctx.args;
const res = await queryLogin.switchUser(username);
if (res.code === 200) {
console.log('success switch to', username);
} else {
console.log('switch to', username, 'failed', res.message || '');
}
}).addTo(app)
app.route({
path: 'user',
key: 'logout',
description: '退出登录',
metadata: {}
}).define(async () => {
try {
await queryLogin.logout();
storage.removeItem('token');
console.log('退出成功');
} catch (error) {
console.log('退出失败', error);
}
}).addTo(app)

252
src/routes/token-ls.ts Normal file
View File

@@ -0,0 +1,252 @@
import { app } from '../app.ts';
import { z } from 'zod';
import { getConfig, getEnvToken, writeConfig } from '@/module/get-config.ts';
import { queryLogin, storage } from '@/module/query.ts';
import { Kevisual } from '@/module/kevisual.ts';
import { showMore } from '@/uitls/show-more.ts';
function isNumeric(str: string) {
return /^-?\d+\.?\d*$/.test(str);
}
const showList = (list: string[]) => {
if (list.length === 0) {
console.log('expand baseURLList is empty');
return;
}
const config = getConfig();
console.log('----current baseURL:' + config.baseURL + '----\n');
list.forEach((item, index) => {
console.log(`${index + 1}: ${item}`);
});
};
app.route({
path: 'token',
key: 'ls',
description: '显示 token 列表',
metadata: {
args: {}
}
}).define(async () => {
console.log('show token list');
queryLogin.cacheStore.init();
console.log(queryLogin.cacheStore.cacheData);
}).addTo(app)
app.route({
path: 'token',
key: 'info',
description: '显示 token 信息',
metadata: {
args: {
env: z.boolean().optional().describe('显示环境变量中的 token'),
}
}
}).define(async (ctx) => {
const { env } = ctx.args;
const token = storage.getItem('token');
if (env) {
console.log('token in env', getEnvToken());
} else {
console.log('token', token);
}
}).addTo(app)
app.route({
path: 'token',
key: 'create',
description: '创建 jwks token',
metadata: {
args: {}
}
}).define(async () => {
const kevisual = new Kevisual();
const res = await kevisual.getAdminToken();
if (res.code === 200) {
const jwtToken = res.data?.accessToken;
console.log('============jwt token============\n\n');
console.log(jwtToken);
} else {
console.log('create token failed', showMore(res));
}
}).addTo(app)
app.route({
path: 'baseURL',
key: 'info',
description: '显示 baseURL',
metadata: {
args: {
add: z.string().optional().describe('添加 baseURL'),
remove: z.number().optional().describe('按编号移除 baseURL'),
set: z.union([z.number(), z.string()]).optional().describe('设置 baseURL'),
list: z.boolean().optional().describe('列出 baseURL'),
clear: z.boolean().optional().describe('清除 baseURL'),
}
}
}).define(async (ctx) => {
let { add, remove, set, list, clear } = ctx.args;
let config = getConfig();
let baseList = (config.baseURLList as Array<string>) || [];
if (!config.baseURL) {
baseList = ['https://kevisual.cn'];
writeConfig({ ...config, baseURL: 'https://kevisual.cn', baseURLList: baseList });
config = getConfig();
}
const quineList = (list: string[]) => {
const newList = new Set(list);
return Array.from(newList);
};
if (add || set) {
let change = false;
if (add) {
change = true;
baseList.push(add);
} else if (set) {
if (!isNumeric(String(set))) {
change = true;
baseList.push(String(set));
writeConfig({ ...config, baseURL: String(set) });
config = getConfig();
}
}
if (change) {
baseList = quineList(baseList);
writeConfig({ ...config, baseURLList: baseList });
config = getConfig();
showList(baseList);
}
}
if (remove !== undefined) {
const index = remove - 1;
if (index < 0 || index >= baseList.length) {
console.log('index out of range');
return;
}
const removeBase = baseList.splice(index, 1);
baseList = quineList(baseList);
showList(baseList);
writeConfig({ ...config, baseURLList: baseList });
removeBase[0];
return;
}
if (set !== undefined) {
const isNumber = isNumeric(String(set));
if (isNumber) {
const index = Number(set) - 1;
if (index < 0 || index >= baseList.length) {
console.log('index out of range');
return;
}
writeConfig({ ...config, baseURL: baseList[index] });
showList(baseList);
}
return;
}
if (list) {
showList(baseList);
return;
}
if (clear) {
writeConfig({ ...config, baseURLList: [] });
return;
}
if (!config.baseURL) {
config = getConfig();
writeConfig({ ...config, baseURL: 'https://kevisual.cn' });
config = getConfig();
}
console.log('current baseURL:', config.baseURL);
}).addTo(app)
app.route({
path: 'baseURL',
key: 'set',
description: '设置 baseURL',
metadata: {
args: {
baseURL: z.string().optional().describe('baseURL 地址'),
}
}
}).define(async (ctx) => {
const config = getConfig();
let baseURL = ctx.args.baseURL;
if (!baseURL) {
console.log('baseURL is required');
return;
}
writeConfig({ ...config, baseURL });
}).addTo(app)
app.route({
path: 'registry',
key: 'manage',
description: 'registry 管理',
metadata: {
args: {
list: z.boolean().optional().describe('列出 registry'),
set: z.string().optional().describe('设置 registry'),
}
}
}).define(async (ctx) => {
const { list, set } = ctx.args;
const config = getConfig();
const defaultRegistry = ['https://kevisual.cn', 'https://kevisual.silkyai.cn', 'https://kevisual.xiongxiao.me', 'http://localhost:3005'];
if (list) {
showList(defaultRegistry);
return;
}
if (set) {
const isNumber = isNumeric(set);
if (isNumber) {
const index = Number(set) - 1;
if (index < 0 || index >= defaultRegistry.length) {
console.log('index out of range');
return;
}
writeConfig({ ...config, baseURL: defaultRegistry[index] });
console.log('set registry', defaultRegistry[index]);
} else {
writeConfig({ ...config, baseURL: set });
console.log('set registry', set);
}
}
}).addTo(app)
app.route({
path: 'baseURL',
key: 'kevisual',
description: 'kevisual registry',
metadata: { args: {} }
}).define(async () => {
const config = getConfig();
const defaultRegistry = ['https://kevisual.cn'];
writeConfig({ ...config, baseURL: defaultRegistry[0] });
showList(defaultRegistry);
}).addTo(app)
app.route({
path: 'baseURL',
key: 'silky',
description: 'silky registry',
metadata: { args: {} }
}).define(async () => {
const config = getConfig();
const defaultRegistry = ['https://kevisual.silkyai.cn'];
writeConfig({ ...config, baseURL: defaultRegistry[0] });
showList(defaultRegistry);
}).addTo(app)
app.route({
path: 'baseURL',
key: 'local',
description: 'local registry',
metadata: { args: {} }
}).define(async () => {
const config = getConfig();
const defaultRegistry = ['http://localhost:3005'];
writeConfig({ ...config, baseURL: defaultRegistry[0] });
showList(defaultRegistry);
}).addTo(app)

View File

@@ -1,3 +0,0 @@
import { runParser } from './index.ts';
runParser(process.argv);