Compare commits
22 Commits
c018ffd422
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| c77578805a | |||
| ca1c3706b2 | |||
| 6e1ffe173a | |||
| 5b610fd600 | |||
| b9624e4f6f | |||
| 8f29ddb449 | |||
| 22de8cad52 | |||
| 73d98a1209 | |||
| 6b96a22c7a | |||
| 2393cbefbb | |||
| 0ca5989a40 | |||
| 48f2695367 | |||
| 7d4bc37c09 | |||
| f3f1a1d058 | |||
| 4aeb3637bf | |||
| 5b83f7a6d1 | |||
| 9127df2600 | |||
| 8118daa4e2 | |||
| eca7b42377 | |||
| ee33208e6c | |||
| 94e331e376 | |||
| 77186a02a2 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -4,7 +4,7 @@ node_modules
|
|||||||
dist
|
dist
|
||||||
|
|
||||||
pack-dist
|
pack-dist
|
||||||
apps
|
|
||||||
assistant-app
|
assistant-app
|
||||||
|
|
||||||
build
|
build
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import fs from 'node:fs';
|
|||||||
// bun run src/index.ts --
|
// bun run src/index.ts --
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
const external = ['pm2', '@kevisual/hot-api', '@nut-tree-fork/nut-js'];
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {string} p
|
* @param {string} p
|
||||||
@@ -20,11 +21,10 @@ await Bun.build({
|
|||||||
naming: {
|
naming: {
|
||||||
entry: 'assistant.js',
|
entry: 'assistant.js',
|
||||||
},
|
},
|
||||||
external: ['pm2'],
|
external,
|
||||||
define: {
|
define: {
|
||||||
ENVISION_VERSION: JSON.stringify(pkg.version),
|
ENVISION_VERSION: JSON.stringify(pkg.version),
|
||||||
},
|
},
|
||||||
env: 'ENVISION_*',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await Bun.build({
|
await Bun.build({
|
||||||
@@ -38,8 +38,7 @@ await Bun.build({
|
|||||||
define: {
|
define: {
|
||||||
ENVISION_VERSION: JSON.stringify(pkg.version),
|
ENVISION_VERSION: JSON.stringify(pkg.version),
|
||||||
},
|
},
|
||||||
external: ['pm2'],
|
external,
|
||||||
env: 'ENVISION_*',
|
|
||||||
});
|
});
|
||||||
// const copyDist = ['dist', 'bin'];
|
// const copyDist = ['dist', 'bin'];
|
||||||
const copyDist = ['dist'];
|
const copyDist = ['dist'];
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
],
|
],
|
||||||
"author": "abearxiong <xiongxiao@xiongxiao.me> (https://www.xiongxiao.me)",
|
"author": "abearxiong <xiongxiao@xiongxiao.me> (https://www.xiongxiao.me)",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"packageManager": "pnpm@10.24.0",
|
"packageManager": "pnpm@10.26.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"files": [
|
"files": [
|
||||||
"dist",
|
"dist",
|
||||||
@@ -20,8 +20,9 @@
|
|||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "bun run src/run.ts ",
|
"dev": "bun run src/run.ts ",
|
||||||
"dev:server": "cross-env NODE_TLS_REJECT_UNAUTHORIZED=0 bun --watch src/run-server.ts --home ",
|
"dev:server": "bun --watch src/run-server.ts ",
|
||||||
"dev:share": "cross-env NODE_TLS_REJECT_UNAUTHORIZED=0 bun --watch src/test/remote-app.ts ",
|
"dev:cnb": "ASSISTANT_CONFIG_DIR=/workspace bun --watch src/run-server.ts ",
|
||||||
|
"dev:share": "bun --watch src/test/remote-app.ts ",
|
||||||
"build:lib": "bun run bun-lib.config.mjs",
|
"build:lib": "bun run bun-lib.config.mjs",
|
||||||
"postbuild:lib": "dts -i src/lib.ts -o assistant-lib.d.ts -d libs -t",
|
"postbuild:lib": "dts -i src/lib.ts -o assistant-lib.d.ts -d libs -t",
|
||||||
"build": "rimraf dist && bun run bun.config.mjs",
|
"build": "rimraf dist && bun run bun.config.mjs",
|
||||||
@@ -41,18 +42,18 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@kevisual/ai": "^0.0.12",
|
"@kevisual/ai": "^0.0.19",
|
||||||
"@kevisual/load": "^0.0.6",
|
"@kevisual/load": "^0.0.6",
|
||||||
"@kevisual/local-app-manager": "^0.1.32",
|
"@kevisual/local-app-manager": "^0.1.32",
|
||||||
"@kevisual/logger": "^0.0.4",
|
"@kevisual/logger": "^0.0.4",
|
||||||
"@kevisual/query": "0.0.29",
|
"@kevisual/query": "0.0.32",
|
||||||
"@kevisual/query-login": "0.0.7",
|
"@kevisual/query-login": "0.0.7",
|
||||||
"@kevisual/router": "^0.0.33",
|
"@kevisual/router": "^0.0.39",
|
||||||
"@kevisual/types": "^0.0.10",
|
"@kevisual/types": "^0.0.10",
|
||||||
"@kevisual/use-config": "^1.0.21",
|
"@kevisual/use-config": "^1.0.21",
|
||||||
"@types/bun": "^1.3.3",
|
"@types/bun": "^1.3.4",
|
||||||
"@types/lodash-es": "^4.17.12",
|
"@types/lodash-es": "^4.17.12",
|
||||||
"@types/node": "^24.10.1",
|
"@types/node": "^25.0.3",
|
||||||
"@types/send": "^1.2.1",
|
"@types/send": "^1.2.1",
|
||||||
"@types/ws": "^8.18.1",
|
"@types/ws": "^8.18.1",
|
||||||
"chalk": "^5.6.2",
|
"chalk": "^5.6.2",
|
||||||
@@ -61,10 +62,10 @@
|
|||||||
"dayjs": "^1.11.19",
|
"dayjs": "^1.11.19",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"get-port": "^7.1.0",
|
"get-port": "^7.1.0",
|
||||||
"inquirer": "^13.0.1",
|
"inquirer": "^13.1.0",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.22",
|
||||||
"nanoid": "^5.1.6",
|
"nanoid": "^5.1.6",
|
||||||
"send": "^1.2.0",
|
"send": "^1.2.1",
|
||||||
"supports-color": "^10.2.2",
|
"supports-color": "^10.2.2",
|
||||||
"ws": "npm:@kevisual/ws"
|
"ws": "npm:@kevisual/ws"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ import { spawnSync } from 'node:child_process';
|
|||||||
const command = new Command('server')
|
const command = new Command('server')
|
||||||
.description('启动服务')
|
.description('启动服务')
|
||||||
.option('-d, --daemon', '是否以守护进程方式运行')
|
.option('-d, --daemon', '是否以守护进程方式运行')
|
||||||
.option('-n, --name <name>', '服务名称')
|
.option('-n, --name <name>', '服务名称', 'assistant-server')
|
||||||
.option('-p, --port <port>', '服务端口')
|
.option('-p, --port <port>', '服务端口')
|
||||||
.option('-s, --start', '是否启动服务')
|
.option('-s, --start', '是否启动服务')
|
||||||
.option('-i, --home', '是否以home方式运行')
|
.option('-e, --interpreter <interpreter>', '指定使用的解释器', 'bun')
|
||||||
.action((options) => {
|
.action((options) => {
|
||||||
const { port } = options;
|
const { port } = options;
|
||||||
const [_interpreter, execPath] = process.argv;
|
const [_interpreter, execPath] = process.argv;
|
||||||
@@ -24,8 +24,8 @@ const command = new Command('server')
|
|||||||
if (port) {
|
if (port) {
|
||||||
shellCommands.push(`-p ${port}`);
|
shellCommands.push(`-p ${port}`);
|
||||||
}
|
}
|
||||||
if (options.home) {
|
if (options.interpreter) {
|
||||||
shellCommands.push('--home');
|
shellCommands.push(`-e ${options.interpreter}`);
|
||||||
}
|
}
|
||||||
const basename = _interpreter.split('/').pop();
|
const basename = _interpreter.split('/').pop();
|
||||||
|
|
||||||
|
|||||||
@@ -4,10 +4,17 @@ import fs from 'fs';
|
|||||||
import { checkFileExists, createDir } from '../file/index.ts';
|
import { checkFileExists, createDir } from '../file/index.ts';
|
||||||
import { ProxyInfo } from '../proxy/proxy.ts';
|
import { ProxyInfo } from '../proxy/proxy.ts';
|
||||||
import dotenv from 'dotenv';
|
import dotenv from 'dotenv';
|
||||||
|
|
||||||
|
let kevisualDir = path.join(homedir(), 'kevisual');
|
||||||
|
const envKevisualDir = process.env.ASSISTANT_CONFIG_DIR
|
||||||
|
if (envKevisualDir) {
|
||||||
|
kevisualDir = envKevisualDir;
|
||||||
|
console.log('使用环境变量 ASSISTANT_CONFIG_DIR 作为 kevisual 目录:', kevisualDir);
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* 助手配置文件路径, 全局配置文件目录
|
* 助手配置文件路径, 全局配置文件目录
|
||||||
*/
|
*/
|
||||||
export const configDir = createDir(path.join(homedir(), 'kevisual/assistant-app'));
|
export const configDir = createDir(path.join(kevisualDir, 'assistant-app'));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 助手配置文件初始化
|
* 助手配置文件初始化
|
||||||
@@ -63,7 +70,7 @@ export type ReturnInitConfigType = ReturnType<typeof initConfig>;
|
|||||||
type AuthPermission = {
|
type AuthPermission = {
|
||||||
type?: 'auth-proxy' | 'public' | 'private' | 'project';
|
type?: 'auth-proxy' | 'public' | 'private' | 'project';
|
||||||
username?: string; // 用户名
|
username?: string; // 用户名
|
||||||
admin?: Omit<AuthPermission, 'admin'>;
|
admin?: string[];
|
||||||
};
|
};
|
||||||
export type AssistantConfigData = {
|
export type AssistantConfigData = {
|
||||||
pageApi?: string; // https://kevisual.cn
|
pageApi?: string; // https://kevisual.cn
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
export type ProxyInfo = {
|
export type ProxyInfo = {
|
||||||
/**
|
/**
|
||||||
* 代理路径, 比如/root/center, 匹配的路径
|
* 代理路径, 比如/root/home, 匹配的路径
|
||||||
*/
|
*/
|
||||||
path?: string;
|
path?: string;
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { LocalProxy, LocalProxyOpts } from './index.ts';
|
|||||||
import http from 'node:http';
|
import http from 'node:http';
|
||||||
import { fileProxy } from './proxy/file-proxy.ts';
|
import { fileProxy } from './proxy/file-proxy.ts';
|
||||||
const localProxy = new LocalProxy({});
|
const localProxy = new LocalProxy({});
|
||||||
let home = '/root/center';
|
let home = '/root/home';
|
||||||
export const initProxy = (data: LocalProxyOpts & { home?: string }) => {
|
export const initProxy = (data: LocalProxyOpts & { home?: string }) => {
|
||||||
localProxy.pagesDir = data.pagesDir || '';
|
localProxy.pagesDir = data.pagesDir || '';
|
||||||
localProxy.watch = data.watch ?? false;
|
localProxy.watch = data.watch ?? false;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
export type ProxyInfo = {
|
export type ProxyInfo = {
|
||||||
/**
|
/**
|
||||||
* 代理路径, 比如/root/center, 匹配的路径
|
* 代理路径, 比如/root/home, 匹配的路径
|
||||||
*/
|
*/
|
||||||
path?: string;
|
path?: string;
|
||||||
/**
|
/**
|
||||||
|
|||||||
20
assistant/src/routes/hot-api/key-sender/index.ts
Normal file
20
assistant/src/routes/hot-api/key-sender/index.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { app } from '@/app.ts';
|
||||||
|
// import { Hotkeys } from '@kevisual/hot-api';
|
||||||
|
import { Hotkeys } from './lib.ts';
|
||||||
|
import { useContextKey } from '@kevisual/context';
|
||||||
|
app.route({
|
||||||
|
path: 'key-sender',
|
||||||
|
// middleware: ['admin-auth']
|
||||||
|
}).define(async (ctx) => {
|
||||||
|
let keys = ctx.query.keys;
|
||||||
|
if (keys.includes(' ')) {
|
||||||
|
keys = keys.replace(/\s+/g, '+');
|
||||||
|
}
|
||||||
|
const hotKeys: Hotkeys = useContextKey('hotkeys', () => new Hotkeys());
|
||||||
|
if (typeof keys === 'string') {
|
||||||
|
await hotKeys.pressHotkey({
|
||||||
|
hotkey: keys,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
ctx.body = 'ok';
|
||||||
|
}).addTo(app);
|
||||||
89
assistant/src/routes/hot-api/key-sender/lib.ts
Normal file
89
assistant/src/routes/hot-api/key-sender/lib.ts
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
import { keyboard, Key } from "@nut-tree-fork/nut-js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 控制功能部分的案件映射
|
||||||
|
*/
|
||||||
|
export const keyMap: Record<string, Key> = {
|
||||||
|
'ctrl': Key.LeftControl,
|
||||||
|
'leftctrl': Key.LeftControl,
|
||||||
|
'rightctrl': Key.RightControl,
|
||||||
|
'alt': Key.LeftAlt,
|
||||||
|
'leftalt': Key.LeftAlt,
|
||||||
|
'rightalt': Key.RightAlt,
|
||||||
|
'shift': Key.LeftShift,
|
||||||
|
'leftshift': Key.LeftShift,
|
||||||
|
'rightshift': Key.RightShift,
|
||||||
|
'meta': Key.LeftSuper,
|
||||||
|
'cmd': Key.LeftCmd,
|
||||||
|
'win': Key.LeftWin,
|
||||||
|
// 根据操作系统选择 Ctrl 或 Command 键
|
||||||
|
'ctrlorcommand': process.platform === 'darwin' ? Key.LeftCmd : Key.LeftControl,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将快捷键字符串转换为 Key 枚举值
|
||||||
|
* @param hotkey
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const parseHotkey = (hotkey: string): Key[] => {
|
||||||
|
return hotkey
|
||||||
|
.toLowerCase()
|
||||||
|
.split('+')
|
||||||
|
.map(key => {
|
||||||
|
const trimmed = key.trim().toLowerCase();
|
||||||
|
// 如果是修饰键,从映射表中获取
|
||||||
|
if (keyMap[trimmed]) {
|
||||||
|
return keyMap[trimmed];
|
||||||
|
}
|
||||||
|
// 如果是字母,转换为大写并查找对应的 Key
|
||||||
|
if (trimmed.length === 1 && /[a-z]/.test(trimmed)) {
|
||||||
|
const upperKey = trimmed.toUpperCase();
|
||||||
|
return Key[upperKey as keyof typeof Key] as Key;
|
||||||
|
}
|
||||||
|
// 其他情况直接查找
|
||||||
|
return Key[trimmed as keyof typeof Key] as Key;
|
||||||
|
})
|
||||||
|
.filter((key): key is Key => key !== undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
type PressHostKeysOptions = {
|
||||||
|
hotkey: string;
|
||||||
|
durationMs?: number;
|
||||||
|
}
|
||||||
|
export const pressHotkey = async (opts: PressHostKeysOptions): Promise<boolean> => {
|
||||||
|
const { hotkey, durationMs = 100 } = opts;
|
||||||
|
const keys = parseHotkey(hotkey);
|
||||||
|
|
||||||
|
console.log('准备模拟按下快捷键:', hotkey);
|
||||||
|
// 同时按下所有键
|
||||||
|
await keyboard.pressKey(...keys);
|
||||||
|
// 短暂延迟后释放
|
||||||
|
await new Promise(resolve => setTimeout(resolve, durationMs));
|
||||||
|
// 释放所有键
|
||||||
|
await keyboard.releaseKey(...keys);
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 模拟按下一组快捷键,支持逗号分隔的多个快捷键
|
||||||
|
* @param opts
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const pressHotkeys = async (opts: PressHostKeysOptions): Promise<boolean> => {
|
||||||
|
let { hotkey } = opts;
|
||||||
|
hotkey = hotkey.replace(/\s+/g, ''); // 去除所有空格
|
||||||
|
const hotkeyList = hotkey.split(',').map(hk => hk.trim());
|
||||||
|
if (hotkeyList.length === 0) {
|
||||||
|
return await pressHotkey({ ...opts, hotkey });
|
||||||
|
}
|
||||||
|
for (const hk of hotkeyList) {
|
||||||
|
await pressHotkey({ ...opts, hotkey: hk });
|
||||||
|
// 每个快捷键之间稍作延迟
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 200));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
export class Hotkeys {
|
||||||
|
pressHotkey = pressHotkey;
|
||||||
|
pressHotkeys = pressHotkeys;
|
||||||
|
}
|
||||||
@@ -2,8 +2,10 @@ import { app, assistantConfig } from '../app.ts';
|
|||||||
import './config/index.ts';
|
import './config/index.ts';
|
||||||
import './shop-install/index.ts';
|
import './shop-install/index.ts';
|
||||||
import './ai/index.ts';
|
import './ai/index.ts';
|
||||||
import './light-code/index.ts';
|
// TODO:
|
||||||
|
// import './light-code/index.ts';
|
||||||
import './user/index.ts';
|
import './user/index.ts';
|
||||||
|
import './hot-api/key-sender/index.ts';
|
||||||
|
|
||||||
import os from 'node:os';
|
import os from 'node:os';
|
||||||
import { authCache } from '@/module/cache/auth.ts';
|
import { authCache } from '@/module/cache/auth.ts';
|
||||||
@@ -12,7 +14,7 @@ export const getTokenUser = async (ctx: any) => {
|
|||||||
const res = await query.post({
|
const res = await query.post({
|
||||||
path: 'user',
|
path: 'user',
|
||||||
key: 'me',
|
key: 'me',
|
||||||
token: ctx.state.token,
|
token: ctx.state.token || ctx.query.token,
|
||||||
});
|
});
|
||||||
if (res.code !== 200) {
|
if (res.code !== 200) {
|
||||||
return ctx.throw(401, 'not login');
|
return ctx.throw(401, 'not login');
|
||||||
@@ -24,7 +26,7 @@ const checkAuth = async (ctx: any, isAdmin = false) => {
|
|||||||
const config = assistantConfig.getConfig();
|
const config = assistantConfig.getConfig();
|
||||||
const { auth = {} } = config;
|
const { auth = {} } = config;
|
||||||
const token = ctx.query.token;
|
const token = ctx.query.token;
|
||||||
|
console.log('checkAuth', ctx.query, { token });
|
||||||
if (!token) {
|
if (!token) {
|
||||||
return ctx.throw(401, 'not login');
|
return ctx.throw(401, 'not login');
|
||||||
}
|
}
|
||||||
@@ -45,8 +47,17 @@ const checkAuth = async (ctx: any, isAdmin = false) => {
|
|||||||
auth.username = username;
|
auth.username = username;
|
||||||
assistantConfig.setConfig({ auth });
|
assistantConfig.setConfig({ auth });
|
||||||
}
|
}
|
||||||
if (isAdmin) {
|
if (isAdmin && auth.username) {
|
||||||
if (auth.username && auth.username !== username) {
|
const admins = config.auth?.admin || [];
|
||||||
|
let isCheckAdmin = false;
|
||||||
|
const admin = auth.username;
|
||||||
|
if (admin === username) {
|
||||||
|
isCheckAdmin = true;
|
||||||
|
}
|
||||||
|
if (!isCheckAdmin && admins.length > 0 && admins.includes(username)) {
|
||||||
|
isCheckAdmin = true;
|
||||||
|
}
|
||||||
|
if (!isCheckAdmin) {
|
||||||
return ctx.throw(403, 'not admin user');
|
return ctx.throw(403, 'not admin user');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -68,6 +79,7 @@ app
|
|||||||
description: '管理员鉴权, 获取用户信息,并验证是否为管理员。',
|
description: '管理员鉴权, 获取用户信息,并验证是否为管理员。',
|
||||||
})
|
})
|
||||||
.define(async (ctx) => {
|
.define(async (ctx) => {
|
||||||
|
console.log('query', ctx.query);
|
||||||
await checkAuth(ctx, true);
|
await checkAuth(ctx, true);
|
||||||
})
|
})
|
||||||
.addTo(app);
|
.addTo(app);
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { app, assistantConfig } from '@/app.ts';
|
import { app, assistantConfig } from '@/app.ts';
|
||||||
import { AppDownload } from '@/services/app/index.ts';
|
import { AppDownload } from '@/services/app/index.ts';
|
||||||
import { AssistantApp } from '@/module/assistant/index.ts';
|
import { AssistantApp } from '@/module/assistant/index.ts';
|
||||||
import { shopDefine } from './define.ts';
|
|
||||||
app
|
app
|
||||||
.route({
|
.route({
|
||||||
...shopDefine.get('getRegistry'),
|
path: 'shop',
|
||||||
|
key: 'get-registry',
|
||||||
|
description: '获取应用商店注册表信息',
|
||||||
middleware: ['admin-auth'],
|
middleware: ['admin-auth'],
|
||||||
metadata: {
|
metadata: {
|
||||||
admin: true,
|
admin: true,
|
||||||
@@ -19,7 +20,9 @@ app
|
|||||||
|
|
||||||
app
|
app
|
||||||
.route({
|
.route({
|
||||||
...shopDefine.get('listInstalled'),
|
path: 'shop',
|
||||||
|
key: 'list-installed',
|
||||||
|
description: '列出当前已安装的所有应用',
|
||||||
middleware: ['admin-auth'],
|
middleware: ['admin-auth'],
|
||||||
metadata: {
|
metadata: {
|
||||||
admin: true,
|
admin: true,
|
||||||
@@ -35,7 +38,9 @@ app
|
|||||||
|
|
||||||
app
|
app
|
||||||
.route({
|
.route({
|
||||||
...shopDefine.get('install'),
|
path: 'shop',
|
||||||
|
key: 'install',
|
||||||
|
description: '安装指定的应用,可以指定 id、type、force 和 yes 参数',
|
||||||
middleware: ['admin-auth'],
|
middleware: ['admin-auth'],
|
||||||
metadata: {
|
metadata: {
|
||||||
admin: true,
|
admin: true,
|
||||||
@@ -60,7 +65,9 @@ app
|
|||||||
|
|
||||||
app
|
app
|
||||||
.route({
|
.route({
|
||||||
...shopDefine.get('uninstall'),
|
path: 'shop',
|
||||||
|
key: 'uninstall',
|
||||||
|
description: '卸载指定的应用,可以指定 id 和 type 参数',
|
||||||
middleware: ['admin-auth'],
|
middleware: ['admin-auth'],
|
||||||
metadata: {
|
metadata: {
|
||||||
admin: true,
|
admin: true,
|
||||||
|
|||||||
@@ -6,29 +6,64 @@ app.route({
|
|||||||
description: '管理员用户登录',
|
description: '管理员用户登录',
|
||||||
}).define(async (ctx) => {
|
}).define(async (ctx) => {
|
||||||
const { username, password } = ctx.query;
|
const { username, password } = ctx.query;
|
||||||
const query = assistantConfig.query;
|
const auth = assistantConfig.getConfig().auth || {};
|
||||||
const auth = assistantConfig.getConfig().auth;
|
if (auth && auth.username && auth.username !== username) {
|
||||||
const res = await query.post({
|
return ctx.throw(403, 'login user is not admin user');
|
||||||
|
}
|
||||||
|
// 发起请求,转发客户端 cookie
|
||||||
|
const res = await fetch(`${assistantConfig.baseURL}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
path: 'user',
|
path: 'user',
|
||||||
key: 'login',
|
key: 'login',
|
||||||
data: {
|
|
||||||
username,
|
username,
|
||||||
password,
|
password,
|
||||||
},
|
}),
|
||||||
})
|
});
|
||||||
if (res.code !== 200) {
|
|
||||||
|
// 转发上游服务器返回的所有 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const responseData = await res.json();
|
||||||
|
console.debug('admin login response', { res: responseData });
|
||||||
|
if (responseData.code !== 200) {
|
||||||
|
console.debug('admin login failed', { res: responseData });
|
||||||
return ctx.throw(401, 'login failed');
|
return ctx.throw(401, 'login failed');
|
||||||
}
|
}
|
||||||
const loginUser = res.data.username;
|
const me = await assistantConfig.query.post({
|
||||||
|
path: 'user',
|
||||||
|
key: 'me',
|
||||||
|
token: responseData.data.token,
|
||||||
|
})
|
||||||
|
if (me.code === 200) {
|
||||||
|
const loginUser = me.data.username;
|
||||||
if (auth.username && loginUser !== auth.username) {
|
if (auth.username && loginUser !== auth.username) {
|
||||||
return ctx.throw(403, 'login user is not admin user');
|
return ctx.throw(403, 'login user is not admin user');
|
||||||
}
|
}
|
||||||
if (!auth.username) {
|
if (!auth.username) {
|
||||||
// 初始管理员账号
|
// 初始管理员账号
|
||||||
auth.username = 'admin';
|
auth.username = loginUser;
|
||||||
|
if (!auth.type) {
|
||||||
|
auth.type = 'public';
|
||||||
|
}
|
||||||
assistantConfig.setConfig({ auth });
|
assistantConfig.setConfig({ auth });
|
||||||
|
console.log('set first admin user', { username: loginUser });
|
||||||
}
|
}
|
||||||
// 保存配置
|
// 保存配置
|
||||||
|
}
|
||||||
|
|
||||||
ctx.body = res.data;
|
ctx.body = responseData.data;
|
||||||
}).addTo(app);
|
}).addTo(app);
|
||||||
@@ -9,7 +9,7 @@ import path from 'node:path'
|
|||||||
import chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
import { AssistantApp } from './lib.ts';
|
import { AssistantApp } from './lib.ts';
|
||||||
import { getBunPath } from './module/get-bun-path.ts';
|
import { getBunPath } from './module/get-bun-path.ts';
|
||||||
export const runServer = async (port?: number, listenPath = '127.0.0.1') => {
|
export const runServer = async (port: number = 51015, listenPath = '127.0.0.1') => {
|
||||||
let _port: number | undefined;
|
let _port: number | undefined;
|
||||||
if (port) {
|
if (port) {
|
||||||
_port = await getPort({ port });
|
_port = await getPort({ port });
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export type AssistantInitOptions = {
|
|||||||
path?: string;
|
path?: string;
|
||||||
init?: boolean;
|
init?: boolean;
|
||||||
};
|
};
|
||||||
|
const randomId = () => Math.random().toString(36).substring(2, 8);
|
||||||
/**
|
/**
|
||||||
* 助手初始化类
|
* 助手初始化类
|
||||||
* @class AssistantInit
|
* @class AssistantInit
|
||||||
@@ -41,6 +42,7 @@ export class AssistantInit extends AssistantConfig {
|
|||||||
this.createEnvConfig();
|
this.createEnvConfig();
|
||||||
this.createOtherConfig();
|
this.createOtherConfig();
|
||||||
this.initPnpm();
|
this.initPnpm();
|
||||||
|
this.initIgnore();
|
||||||
}
|
}
|
||||||
get query() {
|
get query() {
|
||||||
if (!this.#query) {
|
if (!this.#query) {
|
||||||
@@ -48,6 +50,9 @@ export class AssistantInit extends AssistantConfig {
|
|||||||
}
|
}
|
||||||
return this.#query;
|
return this.#query;
|
||||||
}
|
}
|
||||||
|
get baseURL() {
|
||||||
|
return `${this.getConfig()?.pageApi || 'https://kevisual.cn'}/api/router`;
|
||||||
|
}
|
||||||
setQuery(query?: Query) {
|
setQuery(query?: Query) {
|
||||||
this.#query = query || new Query({
|
this.#query = query || new Query({
|
||||||
url: `${this.getConfig()?.pageApi || 'https://kevisual.cn'}/api/router`,
|
url: `${this.getConfig()?.pageApi || 'https://kevisual.cn'}/api/router`,
|
||||||
@@ -153,10 +158,46 @@ export class AssistantInit extends AssistantConfig {
|
|||||||
create,
|
create,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
initIgnore() {
|
||||||
|
const gitignorePath = path.join(this.configDir, '.gitignore');
|
||||||
|
let content = '';
|
||||||
|
if (checkFileExists(gitignorePath, true)) {
|
||||||
|
content = fs.readFileSync(gitignorePath, 'utf-8');
|
||||||
|
}
|
||||||
|
const ignoreLines = [
|
||||||
|
'node_modules',
|
||||||
|
'.DS_Store',
|
||||||
|
'dist',
|
||||||
|
'pack-dist',
|
||||||
|
'cache-file',
|
||||||
|
'build',
|
||||||
|
'apps/**/node_modules/',
|
||||||
|
'pages/**/node_modules/',
|
||||||
|
'.env',
|
||||||
|
'!.env*development',
|
||||||
|
'.pnpm-store',
|
||||||
|
'.vite',
|
||||||
|
'.astro'
|
||||||
|
];
|
||||||
|
let updated = false;
|
||||||
|
ignoreLines.forEach((line) => {
|
||||||
|
if (!content.includes(line)) {
|
||||||
|
content += `\n${line}`;
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (updated) {
|
||||||
|
fs.writeFileSync(gitignorePath, content.trim() + '\n');
|
||||||
|
console.log(chalk.green('.gitignore 文件更新成功'));
|
||||||
|
}
|
||||||
|
}
|
||||||
protected getDefaultInitAssistantConfig() {
|
protected getDefaultInitAssistantConfig() {
|
||||||
|
const id = randomId();
|
||||||
return {
|
return {
|
||||||
|
id,
|
||||||
description: '助手配置文件',
|
description: '助手配置文件',
|
||||||
home: '/root/center',
|
docs: "https://kevisual.cn/root/cli-docs/",
|
||||||
|
home: '/root/home',
|
||||||
proxy: [],
|
proxy: [],
|
||||||
apiProxyList: [],
|
apiProxyList: [],
|
||||||
share: {
|
share: {
|
||||||
|
|||||||
@@ -8,10 +8,21 @@ localProxy.initFromAssistantConfig(assistantConfig);
|
|||||||
|
|
||||||
export const proxyRoute = async (req: http.IncomingMessage, res: http.ServerResponse) => {
|
export const proxyRoute = async (req: http.IncomingMessage, res: http.ServerResponse) => {
|
||||||
const _assistantConfig = assistantConfig.getCacheAssistantConfig();
|
const _assistantConfig = assistantConfig.getCacheAssistantConfig();
|
||||||
|
const home = _assistantConfig?.home || '/root/home';
|
||||||
|
const auth = _assistantConfig?.auth || {};
|
||||||
|
let noAdmin = !auth.username;
|
||||||
|
const toSetting = () => {
|
||||||
|
res.writeHead(302, { Location: `/root/cli/setting/` });
|
||||||
|
res.end();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
const url = new URL(req.url, 'http://localhost');
|
const url = new URL(req.url, 'http://localhost');
|
||||||
const pathname = decodeURIComponent(url.pathname);
|
const pathname = decodeURIComponent(url.pathname);
|
||||||
if (pathname === '/' && _assistantConfig?.home) {
|
if (pathname === '/') {
|
||||||
res.writeHead(302, { Location: `${_assistantConfig?.home}/` });
|
if (noAdmin) {
|
||||||
|
return toSetting();
|
||||||
|
}
|
||||||
|
res.writeHead(302, { Location: `${home}/` });
|
||||||
return res.end();
|
return res.end();
|
||||||
}
|
}
|
||||||
if (pathname.startsWith('/favicon.ico')) {
|
if (pathname.startsWith('/favicon.ico')) {
|
||||||
@@ -20,8 +31,7 @@ export const proxyRoute = async (req: http.IncomingMessage, res: http.ServerResp
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (pathname.startsWith('/client')) {
|
if (pathname.startsWith('/client')) {
|
||||||
logger.info('url', { url: req.url });
|
logger.debug('handle by router', { url: req.url });
|
||||||
console.debug('handle by router');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// client, api, v1, serve 开头的拦截
|
// client, api, v1, serve 开头的拦截
|
||||||
@@ -42,6 +52,9 @@ export const proxyRoute = async (req: http.IncomingMessage, res: http.ServerResp
|
|||||||
res.end('Not Found Proxy');
|
res.end('Not Found Proxy');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (noAdmin) {
|
||||||
|
return toSetting();
|
||||||
|
}
|
||||||
if (_app && urls.length === 3) {
|
if (_app && urls.length === 3) {
|
||||||
// 重定向到
|
// 重定向到
|
||||||
res.writeHead(302, { Location: `${req.url}/` });
|
res.writeHead(302, { Location: `${req.url}/` });
|
||||||
@@ -58,7 +71,7 @@ export const proxyRoute = async (req: http.IncomingMessage, res: http.ServerResp
|
|||||||
return res.end(`Not Found [${proxyApi.path}] rootPath`);
|
return res.end(`Not Found [${proxyApi.path}] rootPath`);
|
||||||
}
|
}
|
||||||
return fileProxy(req, res, {
|
return fileProxy(req, res, {
|
||||||
path: proxyApi.path, // 代理路径, 比如/root/center
|
path: proxyApi.path, // 代理路径, 比如/root/home
|
||||||
rootPath: proxyApi.rootPath,
|
rootPath: proxyApi.rootPath,
|
||||||
...proxyApi,
|
...proxyApi,
|
||||||
indexPath: _indexPath, // 首页路径
|
indexPath: _indexPath, // 首页路径
|
||||||
|
|||||||
0
cli-center/.env
Normal file
0
cli-center/.env
Normal file
6
cli-center/.gitignore
vendored
Normal file
6
cli-center/.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
node_modules
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
.astro
|
||||||
|
|
||||||
|
dist
|
||||||
43
cli-center/astro.config.mjs
Normal file
43
cli-center/astro.config.mjs
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { defineConfig } from 'astro/config';
|
||||||
|
import mdx from '@astrojs/mdx';
|
||||||
|
import react from '@astrojs/react';
|
||||||
|
import sitemap from '@astrojs/sitemap';
|
||||||
|
import pkgs from './package.json';
|
||||||
|
import tailwindcss from '@tailwindcss/vite';
|
||||||
|
import dotenv from 'dotenv';
|
||||||
|
// import vue from '@astrojs/vue';
|
||||||
|
|
||||||
|
dotenv.config();
|
||||||
|
const isDev = process.env.NODE_ENV === 'development';
|
||||||
|
|
||||||
|
let target = process.env.VITE_API_URL || 'http://localhost:51015';
|
||||||
|
const apiProxy = { target: target, changeOrigin: true, ws: true, rewriteWsOrigin: true, secure: false, cookieDomainRewrite: 'localhost' };
|
||||||
|
let proxy = {
|
||||||
|
'/root/': apiProxy,
|
||||||
|
'/api': apiProxy,
|
||||||
|
'/client': apiProxy,
|
||||||
|
};
|
||||||
|
|
||||||
|
const basename = isDev ? undefined : `${pkgs.basename}`;
|
||||||
|
export default defineConfig({
|
||||||
|
base: basename,
|
||||||
|
integrations: [
|
||||||
|
mdx(),
|
||||||
|
react(), //
|
||||||
|
// vue(),
|
||||||
|
// sitemap(), // sitemap must be site has a domain
|
||||||
|
],
|
||||||
|
|
||||||
|
vite: {
|
||||||
|
plugins: [tailwindcss()],
|
||||||
|
define: {
|
||||||
|
BASE_NAME: JSON.stringify(basename || ''),
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
port: 7008,
|
||||||
|
host: '0.0.0.0',
|
||||||
|
allowedHosts: true,
|
||||||
|
proxy,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
22
cli-center/components.json
Normal file
22
cli-center/components.json
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://ui.shadcn.com/schema.json",
|
||||||
|
"style": "new-york",
|
||||||
|
"rsc": false,
|
||||||
|
"tsx": true,
|
||||||
|
"tailwind": {
|
||||||
|
"config": "",
|
||||||
|
"css": "src/styles/global.css",
|
||||||
|
"baseColor": "neutral",
|
||||||
|
"cssVariables": true,
|
||||||
|
"prefix": ""
|
||||||
|
},
|
||||||
|
"iconLibrary": "lucide",
|
||||||
|
"aliases": {
|
||||||
|
"components": "@/components",
|
||||||
|
"utils": "@/lib/utils",
|
||||||
|
"ui": "@/components/ui",
|
||||||
|
"lib": "@/lib",
|
||||||
|
"hooks": "@/hooks"
|
||||||
|
},
|
||||||
|
"registries": {}
|
||||||
|
}
|
||||||
25
cli-center/kevisual.json
Normal file
25
cli-center/kevisual.json
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"metadata": {
|
||||||
|
"name": "kevisual",
|
||||||
|
"share": "public"
|
||||||
|
},
|
||||||
|
"registry": "https://kevisual.cn/root/ai/kevisual/frontend/simple-astro-template",
|
||||||
|
"clone": {
|
||||||
|
".": {
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"syncd": [
|
||||||
|
{
|
||||||
|
"files": [
|
||||||
|
"**/*"
|
||||||
|
],
|
||||||
|
"registry": ""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"sync": {
|
||||||
|
".gitignore": {
|
||||||
|
"url": "/gitignore.txt"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
69
cli-center/package.json
Normal file
69
cli-center/package.json
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
{
|
||||||
|
"name": "@kevisual/cli",
|
||||||
|
"version": "0.0.3",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"basename": "/root/cli",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "astro dev",
|
||||||
|
"build": "astro build",
|
||||||
|
"preview": "astro preview",
|
||||||
|
"pub": "envision deploy ./dist -k cli -v 0.0.3 -u",
|
||||||
|
"slide:dev": "slidev --open slides/index.md",
|
||||||
|
"slide:build": "slidev build slides/index.md --base /root/cli-slide/",
|
||||||
|
"slide:pub": "envision deploy ./slides/dist -k cli-slide -v 0.0.3 -u",
|
||||||
|
"ui": "pnpm dlx shadcn@latest add "
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "abearxiong <xiongxiao@xiongxiao.me> (https://www.xiongxiao.me)",
|
||||||
|
"license": "MIT",
|
||||||
|
"type": "module",
|
||||||
|
"dependencies": {
|
||||||
|
"@astrojs/mdx": "^4.3.13",
|
||||||
|
"@astrojs/react": "^4.4.2",
|
||||||
|
"@astrojs/sitemap": "^3.6.0",
|
||||||
|
"@astrojs/vue": "^5.1.3",
|
||||||
|
"@kevisual/context": "^0.0.4",
|
||||||
|
"@kevisual/query": "^0.0.32",
|
||||||
|
"@kevisual/query-login": "^0.0.7",
|
||||||
|
"@kevisual/registry": "^0.0.1",
|
||||||
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
|
"@tailwindcss/vite": "^4.1.18",
|
||||||
|
"@uiw/react-md-editor": "^4.0.11",
|
||||||
|
"antd": "^6.1.1",
|
||||||
|
"astro": "^5.16.6",
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"dayjs": "^1.11.19",
|
||||||
|
"es-toolkit": "^1.43.0",
|
||||||
|
"github-markdown-css": "^5.8.1",
|
||||||
|
"highlight.js": "^11.11.1",
|
||||||
|
"lucide-react": "^0.561.0",
|
||||||
|
"marked": "^17.0.1",
|
||||||
|
"marked-highlight": "^2.2.3",
|
||||||
|
"nanoid": "^5.1.6",
|
||||||
|
"react": "^19.2.3",
|
||||||
|
"react-dom": "^19.2.3",
|
||||||
|
"react-toastify": "^11.0.5",
|
||||||
|
"tailwind-merge": "^3.4.0",
|
||||||
|
"vue": "^3.5.25",
|
||||||
|
"zustand": "^5.0.9"
|
||||||
|
},
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@kevisual/types": "^0.0.10",
|
||||||
|
"@types/react": "^19.2.7",
|
||||||
|
"@types/react-dom": "^19.2.3",
|
||||||
|
"dotenv": "^17.2.3",
|
||||||
|
"tailwindcss": "^4.1.18",
|
||||||
|
"tw-animate-css": "^1.4.0"
|
||||||
|
},
|
||||||
|
"packageManager": "pnpm@10.26.0",
|
||||||
|
"onlyBuiltDependencies": [
|
||||||
|
"@tailwindcss/oxide",
|
||||||
|
"esbuild",
|
||||||
|
"sharp"
|
||||||
|
]
|
||||||
|
}
|
||||||
6922
cli-center/pnpm-lock.yaml
generated
Normal file
6922
cli-center/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
29
cli-center/slides/components/Counter.vue
Normal file
29
cli-center/slides/components/Counter.vue
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
count: {
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const counter = ref(props.count)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex w-min border border-main rounded-md">
|
||||||
|
<button
|
||||||
|
class="border-r border-main p-2 font-mono outline-none hover:bg-gray-400 hover:bg-opacity-20"
|
||||||
|
@click="counter -= 1"
|
||||||
|
>
|
||||||
|
-
|
||||||
|
</button>
|
||||||
|
<span class="m-auto p-2">{{ counter }}</span>
|
||||||
|
<button
|
||||||
|
class="border-l border-main p-2 font-mono outline-none hover:bg-gray-400 hover:bg-opacity-20"
|
||||||
|
@click="counter += 1"
|
||||||
|
>
|
||||||
|
+
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
611
cli-center/slides/demos/contents.md
Normal file
611
cli-center/slides/demos/contents.md
Normal file
@@ -0,0 +1,611 @@
|
|||||||
|
# Welcome to Slidev
|
||||||
|
|
||||||
|
Presentation slides for developers
|
||||||
|
|
||||||
|
<div @click="$slidev.nav.next" class="mt-12 py-1" hover:bg="white op-10">
|
||||||
|
Press Space for next page <carbon:arrow-right />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="abs-br m-6 text-xl">
|
||||||
|
<button @click="$slidev.nav.openInEditor()" title="Open in Editor" class="slidev-icon-btn">
|
||||||
|
<carbon:edit />
|
||||||
|
</button>
|
||||||
|
<a href="https://github.com/slidevjs/slidev" target="_blank" class="slidev-icon-btn">
|
||||||
|
<carbon:logo-github />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
The last comment block of each slide will be treated as slide notes. It will be visible and editable in Presenter Mode along with the slide. [Read more in the docs](https://sli.dev/guide/syntax.html#notes)
|
||||||
|
-->
|
||||||
|
|
||||||
|
---
|
||||||
|
transition: fade-out
|
||||||
|
---
|
||||||
|
|
||||||
|
# What is Slidev?
|
||||||
|
|
||||||
|
Slidev is a slides maker and presenter designed for developers, consist of the following features
|
||||||
|
|
||||||
|
- 📝 **Text-based** - focus on the content with Markdown, and then style them later
|
||||||
|
- 🎨 **Themable** - themes can be shared and re-used as npm packages
|
||||||
|
- 🧑💻 **Developer Friendly** - code highlighting, live coding with autocompletion
|
||||||
|
- 🤹 **Interactive** - embed Vue components to enhance your expressions
|
||||||
|
- 🎥 **Recording** - built-in recording and camera view
|
||||||
|
- 📤 **Portable** - export to PDF, PPTX, PNGs, or even a hostable SPA
|
||||||
|
- 🛠 **Hackable** - virtually anything that's possible on a webpage is possible in Slidev
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
|
||||||
|
Read more about [Why Slidev?](https://sli.dev/guide/why)
|
||||||
|
|
||||||
|
<!--
|
||||||
|
You can have `style` tag in markdown to override the style for the current page.
|
||||||
|
Learn more: https://sli.dev/features/slide-scope-style
|
||||||
|
-->
|
||||||
|
|
||||||
|
<style>
|
||||||
|
h1 {
|
||||||
|
background-color: #2B90B6;
|
||||||
|
background-image: linear-gradient(45deg, #4EC5D4 10%, #146b8c 20%);
|
||||||
|
background-size: 100%;
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-moz-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
-moz-text-fill-color: transparent;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Here is another comment.
|
||||||
|
-->
|
||||||
|
|
||||||
|
---
|
||||||
|
transition: slide-up
|
||||||
|
level: 2
|
||||||
|
---
|
||||||
|
|
||||||
|
# Navigation
|
||||||
|
|
||||||
|
Hover on the bottom-left corner to see the navigation's controls panel, [learn more](https://sli.dev/guide/ui#navigation-bar)
|
||||||
|
|
||||||
|
## Keyboard Shortcuts
|
||||||
|
|
||||||
|
| | |
|
||||||
|
| --------------------------------------------------- | --------------------------- |
|
||||||
|
| <kbd>right</kbd> / <kbd>space</kbd> | next animation or slide |
|
||||||
|
| <kbd>left</kbd> / <kbd>shift</kbd><kbd>space</kbd> | previous animation or slide |
|
||||||
|
| <kbd>up</kbd> | previous slide |
|
||||||
|
| <kbd>down</kbd> | next slide |
|
||||||
|
|
||||||
|
<!-- https://sli.dev/guide/animations.html#click-animation -->
|
||||||
|
<img
|
||||||
|
v-click
|
||||||
|
class="absolute -bottom-9 -left-7 w-80 opacity-50"
|
||||||
|
src="https://sli.dev/assets/arrow-bottom-left.svg"
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
<p v-after class="absolute bottom-23 left-45 opacity-30 transform -rotate-10">Here!</p>
|
||||||
|
|
||||||
|
---
|
||||||
|
layout: two-cols
|
||||||
|
layoutClass: gap-16
|
||||||
|
---
|
||||||
|
|
||||||
|
# Table of contents
|
||||||
|
|
||||||
|
You can use the `Toc` component to generate a table of contents for your slides:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<Toc minDepth="1" maxDepth="1" />
|
||||||
|
```
|
||||||
|
|
||||||
|
The title will be inferred from your slide content, or you can override it with `title` and `level` in your frontmatter.
|
||||||
|
|
||||||
|
::right::
|
||||||
|
|
||||||
|
<Toc text-sm minDepth="1" maxDepth="2" />
|
||||||
|
|
||||||
|
---
|
||||||
|
layout: image-right
|
||||||
|
image: https://cover.sli.dev
|
||||||
|
---
|
||||||
|
|
||||||
|
# Code
|
||||||
|
|
||||||
|
Use code snippets and get the highlighting directly, and even types hover!
|
||||||
|
|
||||||
|
```ts [filename-example.ts] {all|4|6|6-7|9|all} twoslash
|
||||||
|
// TwoSlash enables TypeScript hover information
|
||||||
|
// and errors in markdown code blocks
|
||||||
|
// More at https://shiki.style/packages/twoslash
|
||||||
|
import { computed, ref } from 'vue'
|
||||||
|
|
||||||
|
const count = ref(0)
|
||||||
|
const doubled = computed(() => count.value * 2)
|
||||||
|
|
||||||
|
doubled.value = 2
|
||||||
|
```
|
||||||
|
|
||||||
|
<arrow v-click="[4, 5]" x1="350" y1="310" x2="195" y2="342" color="#953" width="2" arrowSize="1" />
|
||||||
|
|
||||||
|
<!-- This allow you to embed external code blocks -->
|
||||||
|
<!-- <<< @/snippets/external.ts#snippet -->
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
|
||||||
|
[Learn more](https://sli.dev/features/line-highlighting)
|
||||||
|
|
||||||
|
<!-- Inline style -->
|
||||||
|
<style>
|
||||||
|
.footnotes-sep {
|
||||||
|
@apply mt-5 opacity-10;
|
||||||
|
}
|
||||||
|
.footnotes {
|
||||||
|
@apply text-sm opacity-75;
|
||||||
|
}
|
||||||
|
.footnote-backref {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Notes can also sync with clicks
|
||||||
|
|
||||||
|
[click] This will be highlighted after the first click
|
||||||
|
|
||||||
|
[click] Highlighted with `count = ref(0)`
|
||||||
|
|
||||||
|
[click:3] Last click (skip two clicks)
|
||||||
|
-->
|
||||||
|
|
||||||
|
---
|
||||||
|
level: 2
|
||||||
|
---
|
||||||
|
|
||||||
|
# Shiki Magic Move
|
||||||
|
|
||||||
|
Powered by [shiki-magic-move](https://shiki-magic-move.netlify.app/), Slidev supports animations across multiple code snippets.
|
||||||
|
|
||||||
|
Add multiple code blocks and wrap them with <code>````md magic-move</code> (four backticks) to enable the magic move. For example:
|
||||||
|
|
||||||
|
````md magic-move {lines: true}
|
||||||
|
```ts {*|2|*}
|
||||||
|
// step 1
|
||||||
|
const author = reactive({
|
||||||
|
name: 'John Doe',
|
||||||
|
books: [
|
||||||
|
'Vue 2 - Advanced Guide',
|
||||||
|
'Vue 3 - Basic Guide',
|
||||||
|
'Vue 4 - The Mystery'
|
||||||
|
]
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
```ts {*|1-2|3-4|3-4,8}
|
||||||
|
// step 2
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
author: {
|
||||||
|
name: 'John Doe',
|
||||||
|
books: [
|
||||||
|
'Vue 2 - Advanced Guide',
|
||||||
|
'Vue 3 - Basic Guide',
|
||||||
|
'Vue 4 - The Mystery'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// step 3
|
||||||
|
export default {
|
||||||
|
data: () => ({
|
||||||
|
author: {
|
||||||
|
name: 'John Doe',
|
||||||
|
books: [
|
||||||
|
'Vue 2 - Advanced Guide',
|
||||||
|
'Vue 3 - Basic Guide',
|
||||||
|
'Vue 4 - The Mystery'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Non-code blocks are ignored.
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<!-- step 4 -->
|
||||||
|
<script setup>
|
||||||
|
const author = {
|
||||||
|
name: 'John Doe',
|
||||||
|
books: [
|
||||||
|
'Vue 2 - Advanced Guide',
|
||||||
|
'Vue 3 - Basic Guide',
|
||||||
|
'Vue 4 - The Mystery'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
````
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Components
|
||||||
|
|
||||||
|
<div grid="~ cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
|
||||||
|
You can use Vue components directly inside your slides.
|
||||||
|
|
||||||
|
We have provided a few built-in components like `<Tweet/>` and `<Youtube/>` that you can use directly. And adding your custom components is also super easy.
|
||||||
|
|
||||||
|
```html
|
||||||
|
<Counter :count="10" />
|
||||||
|
```
|
||||||
|
|
||||||
|
<!-- ../components/Counter.vue -->
|
||||||
|
<Counter :count="10" m="t-4" />
|
||||||
|
|
||||||
|
Check out [the guides](https://sli.dev/builtin/components.html) for more.
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
|
||||||
|
```html
|
||||||
|
<Tweet id="1390115482657726468" />
|
||||||
|
```
|
||||||
|
|
||||||
|
<Tweet id="1390115482657726468" scale="0.65" />
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Presenter note with **bold**, *italic*, and ~~striked~~ text.
|
||||||
|
|
||||||
|
Also, HTML elements are valid:
|
||||||
|
<div class="flex w-full">
|
||||||
|
<span style="flex-grow: 1;">Left content</span>
|
||||||
|
<span>Right content</span>
|
||||||
|
</div>
|
||||||
|
-->
|
||||||
|
|
||||||
|
---
|
||||||
|
class: px-20
|
||||||
|
---
|
||||||
|
|
||||||
|
# Themes
|
||||||
|
|
||||||
|
Slidev comes with powerful theming support. Themes can provide styles, layouts, components, or even configurations for tools. Switching between themes by just **one edit** in your frontmatter:
|
||||||
|
|
||||||
|
<div grid="~ cols-2 gap-2" m="t-2">
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
theme: default
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
theme: seriph
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|
||||||
|
<img border="rounded" src="https://github.com/slidevjs/themes/blob/main/screenshots/theme-default/01.png?raw=true" alt="">
|
||||||
|
|
||||||
|
<img border="rounded" src="https://github.com/slidevjs/themes/blob/main/screenshots/theme-seriph/01.png?raw=true" alt="">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
Read more about [How to use a theme](https://sli.dev/guide/theme-addon#use-theme) and
|
||||||
|
check out the [Awesome Themes Gallery](https://sli.dev/resources/theme-gallery).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Clicks Animations
|
||||||
|
|
||||||
|
You can add `v-click` to elements to add a click animation.
|
||||||
|
|
||||||
|
<div v-click>
|
||||||
|
|
||||||
|
This shows up when you click the slide:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div v-click>This shows up when you click the slide.</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<v-click>
|
||||||
|
|
||||||
|
The <span v-mark.red="3"><code>v-mark</code> directive</span>
|
||||||
|
also allows you to add
|
||||||
|
<span v-mark.circle.orange="4">inline marks</span>
|
||||||
|
, powered by [Rough Notation](https://roughnotation.com/):
|
||||||
|
|
||||||
|
```html
|
||||||
|
<span v-mark.underline.orange>inline markers</span>
|
||||||
|
```
|
||||||
|
|
||||||
|
</v-click>
|
||||||
|
|
||||||
|
<div mt-20 v-click>
|
||||||
|
|
||||||
|
[Learn more](https://sli.dev/guide/animations#click-animation)
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Motions
|
||||||
|
|
||||||
|
Motion animations are powered by [@vueuse/motion](https://motion.vueuse.org/), triggered by `v-motion` directive.
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div
|
||||||
|
v-motion
|
||||||
|
:initial="{ x: -80 }"
|
||||||
|
:enter="{ x: 0 }"
|
||||||
|
:click-3="{ x: 80 }"
|
||||||
|
:leave="{ x: 1000 }"
|
||||||
|
>
|
||||||
|
Slidev
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
<div class="w-60 relative">
|
||||||
|
<div class="relative w-40 h-40">
|
||||||
|
<img
|
||||||
|
v-motion
|
||||||
|
:initial="{ x: 800, y: -100, scale: 1.5, rotate: -50 }"
|
||||||
|
:enter="final"
|
||||||
|
class="absolute inset-0"
|
||||||
|
src="https://sli.dev/logo-square.png"
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
v-motion
|
||||||
|
:initial="{ y: 500, x: -100, scale: 2 }"
|
||||||
|
:enter="final"
|
||||||
|
class="absolute inset-0"
|
||||||
|
src="https://sli.dev/logo-circle.png"
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
v-motion
|
||||||
|
:initial="{ x: 600, y: 400, scale: 2, rotate: 100 }"
|
||||||
|
:enter="final"
|
||||||
|
class="absolute inset-0"
|
||||||
|
src="https://sli.dev/logo-triangle.png"
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="text-5xl absolute top-14 left-40 text-[#2B90B6] -z-1"
|
||||||
|
v-motion
|
||||||
|
:initial="{ x: -80, opacity: 0}"
|
||||||
|
:enter="{ x: 0, opacity: 1, transition: { delay: 2000, duration: 1000 } }">
|
||||||
|
Slidev
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- vue script setup scripts can be directly used in markdown, and will only affects current page -->
|
||||||
|
<script setup lang="ts">
|
||||||
|
const final = {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
rotate: 0,
|
||||||
|
scale: 1,
|
||||||
|
transition: {
|
||||||
|
type: 'spring',
|
||||||
|
damping: 10,
|
||||||
|
stiffness: 20,
|
||||||
|
mass: 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-motion
|
||||||
|
:initial="{ x:35, y: 30, opacity: 0}"
|
||||||
|
:enter="{ y: 0, opacity: 1, transition: { delay: 3500 } }">
|
||||||
|
|
||||||
|
[Learn more](https://sli.dev/guide/animations.html#motion)
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# LaTeX
|
||||||
|
|
||||||
|
LaTeX is supported out-of-box. Powered by [KaTeX](https://katex.org/).
|
||||||
|
|
||||||
|
<div h-3 />
|
||||||
|
|
||||||
|
Inline $\sqrt{3x-1}+(1+x)^2$
|
||||||
|
|
||||||
|
Block
|
||||||
|
$$ {1|3|all}
|
||||||
|
\begin{aligned}
|
||||||
|
\nabla \cdot \vec{E} &= \frac{\rho}{\varepsilon_0} \\
|
||||||
|
\nabla \cdot \vec{B} &= 0 \\
|
||||||
|
\nabla \times \vec{E} &= -\frac{\partial\vec{B}}{\partial t} \\
|
||||||
|
\nabla \times \vec{B} &= \mu_0\vec{J} + \mu_0\varepsilon_0\frac{\partial\vec{E}}{\partial t}
|
||||||
|
\end{aligned}
|
||||||
|
$$
|
||||||
|
|
||||||
|
[Learn more](https://sli.dev/features/latex)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Diagrams
|
||||||
|
|
||||||
|
You can create diagrams / graphs from textual descriptions, directly in your Markdown.
|
||||||
|
|
||||||
|
<div class="grid grid-cols-4 gap-5 pt-4 -mb-6">
|
||||||
|
|
||||||
|
```mermaid {scale: 0.5, alt: 'A simple sequence diagram'}
|
||||||
|
sequenceDiagram
|
||||||
|
Alice->John: Hello John, how are you?
|
||||||
|
Note over Alice,John: A typical interaction
|
||||||
|
```
|
||||||
|
|
||||||
|
```mermaid {theme: 'neutral', scale: 0.8}
|
||||||
|
graph TD
|
||||||
|
B[Text] --> C{Decision}
|
||||||
|
C -->|One| D[Result 1]
|
||||||
|
C -->|Two| E[Result 2]
|
||||||
|
```
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
mindmap
|
||||||
|
root((mindmap))
|
||||||
|
Origins
|
||||||
|
Long history
|
||||||
|
::icon(fa fa-book)
|
||||||
|
Popularisation
|
||||||
|
British popular psychology author Tony Buzan
|
||||||
|
Research
|
||||||
|
On effectiveness<br/>and features
|
||||||
|
On Automatic creation
|
||||||
|
Uses
|
||||||
|
Creative techniques
|
||||||
|
Strategic planning
|
||||||
|
Argument mapping
|
||||||
|
Tools
|
||||||
|
Pen and paper
|
||||||
|
Mermaid
|
||||||
|
```
|
||||||
|
|
||||||
|
```plantuml {scale: 0.7}
|
||||||
|
@startuml
|
||||||
|
|
||||||
|
package "Some Group" {
|
||||||
|
HTTP - [First Component]
|
||||||
|
[Another Component]
|
||||||
|
}
|
||||||
|
|
||||||
|
node "Other Groups" {
|
||||||
|
FTP - [Second Component]
|
||||||
|
[First Component] --> FTP
|
||||||
|
}
|
||||||
|
|
||||||
|
cloud {
|
||||||
|
[Example 1]
|
||||||
|
}
|
||||||
|
|
||||||
|
database "MySql" {
|
||||||
|
folder "This is my folder" {
|
||||||
|
[Folder 3]
|
||||||
|
}
|
||||||
|
frame "Foo" {
|
||||||
|
[Frame 4]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Another Component] --> [Example 1]
|
||||||
|
[Example 1] --> [Folder 3]
|
||||||
|
[Folder 3] --> [Frame 4]
|
||||||
|
|
||||||
|
@enduml
|
||||||
|
```
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
Learn more: [Mermaid Diagrams](https://sli.dev/features/mermaid) and [PlantUML Diagrams](https://sli.dev/features/plantuml)
|
||||||
|
|
||||||
|
---
|
||||||
|
foo: bar
|
||||||
|
dragPos:
|
||||||
|
square: 691,32,167,_,-16
|
||||||
|
---
|
||||||
|
|
||||||
|
# Draggable Elements
|
||||||
|
|
||||||
|
Double-click on the draggable elements to edit their positions.
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
###### Directive Usage
|
||||||
|
|
||||||
|
```md
|
||||||
|
<img v-drag="'square'" src="https://sli.dev/logo.png">
|
||||||
|
```
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
###### Component Usage
|
||||||
|
|
||||||
|
```md
|
||||||
|
<v-drag text-3xl>
|
||||||
|
<div class="i-carbon:arrow-up" />
|
||||||
|
Use the `v-drag` component to have a draggable container!
|
||||||
|
</v-drag>
|
||||||
|
```
|
||||||
|
|
||||||
|
<v-drag pos="640,212,261,_,-15">
|
||||||
|
<div text-center text-3xl border border-main rounded>
|
||||||
|
Double-click me!
|
||||||
|
</div>
|
||||||
|
</v-drag>
|
||||||
|
|
||||||
|
<img v-drag="'square'" src="https://sli.dev/logo.png">
|
||||||
|
|
||||||
|
###### Draggable Arrow
|
||||||
|
|
||||||
|
```md
|
||||||
|
<v-drag-arrow two-way />
|
||||||
|
```
|
||||||
|
|
||||||
|
<v-drag-arrow pos="360,319,253,46" two-way op70 />
|
||||||
|
|
||||||
|
---
|
||||||
|
src: ./pages/imported-slides.md
|
||||||
|
hide: false
|
||||||
|
---
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Monaco Editor
|
||||||
|
|
||||||
|
Slidev provides built-in Monaco Editor support.
|
||||||
|
|
||||||
|
Add `{monaco}` to the code block to turn it into an editor:
|
||||||
|
|
||||||
|
```ts {monaco}
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { emptyArray } from './external'
|
||||||
|
|
||||||
|
const arr = ref(emptyArray(10))
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `{monaco-run}` to create an editor that can execute the code directly in the slide:
|
||||||
|
|
||||||
|
```ts {monaco-run}
|
||||||
|
import { version } from 'vue'
|
||||||
|
import { emptyArray, sayHello } from './external'
|
||||||
|
|
||||||
|
sayHello()
|
||||||
|
console.log(`vue ${version}`)
|
||||||
|
console.log(emptyArray<number>(10).reduce(fib => [...fib, fib.at(-1)! + fib.at(-2)!], [1, 1]))
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
layout: center
|
||||||
|
class: text-center
|
||||||
|
---
|
||||||
|
|
||||||
|
# Learn More
|
||||||
|
|
||||||
|
[Documentation](https://sli.dev) · [GitHub](https://github.com/slidevjs/slidev) · [Showcases](https://sli.dev/resources/showcases)
|
||||||
|
|
||||||
|
<PoweredBySlidev mt-10 />
|
||||||
28
cli-center/slides/index.md
Normal file
28
cli-center/slides/index.md
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
theme: default
|
||||||
|
# random image from a curated Unsplash collection by Anthony
|
||||||
|
background: https://cover.sli.dev
|
||||||
|
# 介绍文档: https://sli.dev
|
||||||
|
title: Welcome to Slidev
|
||||||
|
info: |
|
||||||
|
## 关于Slidev的介绍
|
||||||
|
演示稿
|
||||||
|
class: text-center
|
||||||
|
# https://sli.dev/features/drawing
|
||||||
|
drawings:
|
||||||
|
persist: false
|
||||||
|
# slide transition: https://sli.dev/guide/animations.html#slide-transitions
|
||||||
|
transition: slide-left
|
||||||
|
# enable MDC Syntax: https://sli.dev/features/mdc
|
||||||
|
mdc: true
|
||||||
|
htmlAttrs:
|
||||||
|
dir: ltr
|
||||||
|
lang: zh-CN
|
||||||
|
# duration of the presentation
|
||||||
|
duration: 35min
|
||||||
|
---
|
||||||
|
# slide 是一个 所见即所得的幻灯片制作工具
|
||||||
|
---
|
||||||
|
src: ./demos/contents.md
|
||||||
|
hide: false
|
||||||
|
---
|
||||||
10
cli-center/slides/pages/contents.md
Normal file
10
cli-center/slides/pages/contents.md
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
---
|
||||||
|
title: '例子'
|
||||||
|
---
|
||||||
|
|
||||||
|
# 常用语法结构
|
||||||
|
|
||||||
|
---
|
||||||
|
---
|
||||||
|
|
||||||
|
# 第二个
|
||||||
41
cli-center/src/apps/footer.tsx
Normal file
41
cli-center/src/apps/footer.tsx
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { wrapBasename } from "@/modules/basename"
|
||||||
|
|
||||||
|
export const Footer = () => {
|
||||||
|
|
||||||
|
const links = [
|
||||||
|
{
|
||||||
|
href: wrapBasename('/'),
|
||||||
|
label: '主页',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: wrapBasename('/docs'),
|
||||||
|
label: '文档',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<footer className="fixed bottom-0 w-full bg-white border-t border-gray-200 shadow-lg">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
||||||
|
{/* 链接区域 */}
|
||||||
|
<nav className="flex flex-wrap justify-center items-center gap-2 sm:gap-4 mb-3">
|
||||||
|
{links.map((link) => (
|
||||||
|
<a
|
||||||
|
key={link.href}
|
||||||
|
href={link.href}
|
||||||
|
className="relative px-4 py-2 text-sm sm:text-base font-medium text-gray-600 hover:text-blue-600 transition-all duration-300 ease-in-out
|
||||||
|
before:absolute before:bottom-0 before:left-0 before:w-0 before:h-0.5 before:bg-blue-600 before:transition-all before:duration-300
|
||||||
|
hover:before:w-full active:scale-95"
|
||||||
|
>
|
||||||
|
{link.label}
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
{/* 版权信息 */}
|
||||||
|
<div className="text-center text-xs sm:text-sm text-gray-500">
|
||||||
|
© 2025 Daily Question
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
)
|
||||||
|
}
|
||||||
62
cli-center/src/apps/menu.tsx
Normal file
62
cli-center/src/apps/menu.tsx
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import { useMemo } from "react";
|
||||||
|
|
||||||
|
export type MenuProps = {
|
||||||
|
items: MenuItem[];
|
||||||
|
basename?: string;
|
||||||
|
};
|
||||||
|
export type MenuItem = {
|
||||||
|
id: string;
|
||||||
|
data: {
|
||||||
|
title: string;
|
||||||
|
tags: string[];
|
||||||
|
hideInMenu?: boolean;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export const Menu = (props: MenuProps) => {
|
||||||
|
const { items, basename = '' } = props;
|
||||||
|
const list = useMemo(() => {
|
||||||
|
return items.filter(item => !item.data?.hideInMenu).sort((a, b) => {
|
||||||
|
return (a.id).localeCompare(b.id)
|
||||||
|
});
|
||||||
|
}, [items]);
|
||||||
|
if (list.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const currentPath = typeof window !== 'undefined' ? window.location.pathname : '';
|
||||||
|
|
||||||
|
const isActive = (itemId: string) => {
|
||||||
|
return currentPath.includes(`/docs/${itemId}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<nav className='flex-1 overflow-y-auto scrollbar bg-white border border-gray-200 rounded-lg shadow-sm'>
|
||||||
|
<div className="sticky top-0 bg-white border-b border-gray-200 px-4 py-3 rounded-t-lg">
|
||||||
|
<h2 className="text-sm font-semibold text-black">文档列表</h2>
|
||||||
|
</div>
|
||||||
|
<div className="p-2 space-y-0.5">
|
||||||
|
{list.map(item => (
|
||||||
|
<a
|
||||||
|
key={item.id}
|
||||||
|
href={`${basename}/docs/${item.id}/`}
|
||||||
|
className={`group block rounded-md transition-all duration-200 ease-in-out border-l-3 ${
|
||||||
|
isActive(item.id)
|
||||||
|
? 'bg-gray-100 border-l-4 border-black shadow-sm'
|
||||||
|
: 'border-transparent hover:bg-gray-50 hover:border-l-4 hover:border-gray-400'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="px-3 py-2.5">
|
||||||
|
<h3 className={`text-sm font-medium transition-colors ${
|
||||||
|
isActive(item.id)
|
||||||
|
? 'text-black font-semibold'
|
||||||
|
: 'text-gray-700 group-hover:text-black'
|
||||||
|
}`}>
|
||||||
|
{item.data?.title}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
78
cli-center/src/apps/setting/index.tsx
Normal file
78
cli-center/src/apps/setting/index.tsx
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import { use, useEffect, useState } from "react";
|
||||||
|
import { Layout } from "./layout"
|
||||||
|
import { useStore } from "./store";
|
||||||
|
export const FirstLogin = () => {
|
||||||
|
const store = useStore();
|
||||||
|
const [username, setUsername] = useState('')
|
||||||
|
const [password, setPassword] = useState('')
|
||||||
|
useEffect(() => {
|
||||||
|
store.initAdmin();
|
||||||
|
}, []);
|
||||||
|
useEffect(() => {
|
||||||
|
if (store.username) {
|
||||||
|
setUsername(store.username);
|
||||||
|
}
|
||||||
|
}, [store.username]);
|
||||||
|
const onClickLogin = async () => {
|
||||||
|
await store.login(username, password);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Layout>
|
||||||
|
<div className='flex items-center justify-center px-4'>
|
||||||
|
<div className='w-full max-w-md space-y-6'>
|
||||||
|
<h1 className='text-2xl font-bold text-black text-center'>管理员设置</h1>
|
||||||
|
<blockquote className="text-gray-500 p-4 mt-4">
|
||||||
|
第一次登录的<a className="text-gray-700 mx-1" href="https://kevisual.cn">kevisual</a> 用户为当前设备管理员。如果已经存在管理员账号,管理员可在"全局设置"中设置。
|
||||||
|
<a className="text-gray-700 mx-1 underline" href="../docs/01-login-first/">文档</a>
|
||||||
|
</blockquote>
|
||||||
|
<form className='space-y-4 mt-8'>
|
||||||
|
<div>
|
||||||
|
<label htmlFor='account' className='block text-sm font-medium text-gray-900 mb-2'> 账号 </label>
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
id='account'
|
||||||
|
onChange={e => setUsername(e.target.value)}
|
||||||
|
value={username}
|
||||||
|
placeholder='请输入账号'
|
||||||
|
className='w-full px-4 py-2 border-2 border-black bg-white text-black placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-black focus:border-transparent'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label htmlFor='password' className='block text-sm font-medium text-gray-900 mb-2'> 密码 </label>
|
||||||
|
<input
|
||||||
|
type='password'
|
||||||
|
id='password'
|
||||||
|
onChange={e => setPassword(e.target.value)}
|
||||||
|
value={password}
|
||||||
|
placeholder='请输入密码'
|
||||||
|
className='w-full px-4 py-2 border-2 border-black bg-white text-black placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-black focus:border-transparent'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
onClick={onClickLogin}
|
||||||
|
className='w-full px-4 py-2 bg-black text-white font-medium hover:bg-gray-800 active:bg-gray-900 transition-colors focus:outline-none focus:ring-2 focus:ring-black focus:ring-offset-2'>
|
||||||
|
确定
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Config = () => {
|
||||||
|
const store = useStore();
|
||||||
|
useEffect(() => {
|
||||||
|
store.initAdmin();
|
||||||
|
}, []);
|
||||||
|
return (
|
||||||
|
<Layout>
|
||||||
|
<div className="p-4">
|
||||||
|
<pre className="bg-gray-100 p-4 rounded-lg overflow-x-auto">
|
||||||
|
{JSON.stringify(store.config, null, 2)}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
)
|
||||||
|
}
|
||||||
22
cli-center/src/apps/setting/layout.tsx
Normal file
22
cli-center/src/apps/setting/layout.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { Nav } from './nav'
|
||||||
|
|
||||||
|
export const Layout = (props) => {
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-screen bg-white">
|
||||||
|
<div className="left-nav w-64 bg-white border-r border-gray-100">
|
||||||
|
<div className="p-4">
|
||||||
|
<h2 className="text-xl font-bold text-black">设置</h2>
|
||||||
|
</div>
|
||||||
|
<div className="p-4">
|
||||||
|
<Nav />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="main flex-1 flex flex-col">
|
||||||
|
<div className="main-header bg-white px-6 py-4 border-b border-gray-100">
|
||||||
|
<h1 className="text-lg font-semibold text-black">配置中心</h1>
|
||||||
|
</div>
|
||||||
|
<main className="flex-1 p-6 bg-gray-50">{props.children}</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
34
cli-center/src/apps/setting/nav.tsx
Normal file
34
cli-center/src/apps/setting/nav.tsx
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { wrapBasename } from "../../modules/basename";
|
||||||
|
import { clsx } from 'clsx';
|
||||||
|
|
||||||
|
export const Nav = () => {
|
||||||
|
const currentPath = typeof window !== 'undefined' ? window.location.pathname : ''
|
||||||
|
|
||||||
|
const navItems = [
|
||||||
|
{ name: '管理员设置', path: wrapBasename('/setting/') },
|
||||||
|
{ name: '全局设置', path: wrapBasename('/setting/all/') },
|
||||||
|
];
|
||||||
|
|
||||||
|
const isActive = (path: string) => {
|
||||||
|
return currentPath === path
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<nav className="space-y-2">
|
||||||
|
{navItems.map((item) => (
|
||||||
|
<a
|
||||||
|
key={item.path}
|
||||||
|
href={item.path}
|
||||||
|
className={clsx(
|
||||||
|
"block px-4 py-2 rounded transition-colors border-l-4",
|
||||||
|
isActive(item.path)
|
||||||
|
? "bg-gray-100 text-black border-black font-medium"
|
||||||
|
: "text-gray-700 border-transparent hover:bg-gray-50 hover:border-gray-300"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{item.name}
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
)
|
||||||
|
}
|
||||||
42
cli-center/src/apps/setting/store.ts
Normal file
42
cli-center/src/apps/setting/store.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { query, queryLogin } from '@/modules/query';
|
||||||
|
import { create } from 'zustand';
|
||||||
|
|
||||||
|
type SettingState = {
|
||||||
|
username?: string;
|
||||||
|
initAdmin: () => any;
|
||||||
|
login: (username: string, password: string) => any;
|
||||||
|
config?: any;
|
||||||
|
}
|
||||||
|
export const useStore = create<SettingState>((set => ({
|
||||||
|
username: undefined,
|
||||||
|
config: undefined,
|
||||||
|
initAdmin: async () => {
|
||||||
|
const res = await query.post({
|
||||||
|
path: 'config'
|
||||||
|
})
|
||||||
|
console.log('initAdmin', res);
|
||||||
|
if (res.code === 200) {
|
||||||
|
const auth = res.data.auth || {}
|
||||||
|
if (auth.username) {
|
||||||
|
set({ username: auth.username });
|
||||||
|
}
|
||||||
|
set({ config: res.data });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
login: async (username: string, password: string) => {
|
||||||
|
const res = await query.post({
|
||||||
|
path: 'admin',
|
||||||
|
key: 'login',
|
||||||
|
username,
|
||||||
|
password
|
||||||
|
});
|
||||||
|
if (res.code === 200) {
|
||||||
|
set({ username });
|
||||||
|
const setToken = await queryLogin.setLoginToken(res.data)
|
||||||
|
console.log('setToken', setToken);
|
||||||
|
}
|
||||||
|
console.log('login res', res);
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
})));
|
||||||
70
cli-center/src/components/MdPreview.tsx
Normal file
70
cli-center/src/components/MdPreview.tsx
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { Marked } from 'marked';
|
||||||
|
import hljs from 'highlight.js';
|
||||||
|
import { markedHighlight } from 'marked-highlight';
|
||||||
|
|
||||||
|
const markedAndHighlight = new Marked(
|
||||||
|
markedHighlight({
|
||||||
|
emptyLangClass: 'hljs',
|
||||||
|
langPrefix: 'hljs language-',
|
||||||
|
highlight(code, lang, info) {
|
||||||
|
const language = hljs.getLanguage(lang) ? lang : 'plaintext';
|
||||||
|
return hljs.highlight(code, { language }).value;
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const md2html = async (md: string) => {
|
||||||
|
const html = markedAndHighlight.parse(md);
|
||||||
|
return html;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const clearMeta = (markdown?: string) => {
|
||||||
|
if (!markdown) return '';
|
||||||
|
// Remove YAML front matter if present
|
||||||
|
const yamlRegex = /^---\n[\s\S]*?\n---\n/;
|
||||||
|
return markdown.replace(yamlRegex, '');
|
||||||
|
};
|
||||||
|
type Props = {
|
||||||
|
children?: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
content?: string; // Optional content prop for markdown text
|
||||||
|
[key: string]: any; // Allow any additional props
|
||||||
|
};
|
||||||
|
export const MarkdownPreview = (props: Props) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'markdown-body scrollbar h-full overflow-auto w-full px-6 py-2 max-w-[800px] border my-4 flex flex-col justify-self-center rounded-md shadow-md',
|
||||||
|
props.className,
|
||||||
|
)}
|
||||||
|
style={props.style}>
|
||||||
|
{props.children ? <WrapperText>{props.children}</WrapperText> : <MarkdownPreviewWrapper content={clearMeta(props.content)} />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WrapperText = (props: { children?: React.ReactNode; html?: string }) => {
|
||||||
|
if (props.html) {
|
||||||
|
return <div className='w-full' dangerouslySetInnerHTML={{ __html: props.html }} />;
|
||||||
|
}
|
||||||
|
return <div className='w-full h-full'>{props.children}</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MarkdownPreviewWrapper = (props: Props) => {
|
||||||
|
const [html, setHtml] = useState<string>('');
|
||||||
|
useEffect(() => {
|
||||||
|
init();
|
||||||
|
}, [props.content]);
|
||||||
|
const init = async () => {
|
||||||
|
if (props.content) {
|
||||||
|
const htmlContent = await md2html(props.content);
|
||||||
|
setHtml(htmlContent);
|
||||||
|
} else {
|
||||||
|
setHtml('');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return <WrapperText html={html} />;
|
||||||
|
};
|
||||||
47
cli-center/src/components/html.astro
Normal file
47
cli-center/src/components/html.astro
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
---
|
||||||
|
import '../styles/global.css';
|
||||||
|
import '../styles/theme.css';
|
||||||
|
export interface Props {
|
||||||
|
title?: string;
|
||||||
|
description?: string;
|
||||||
|
lang?: string;
|
||||||
|
charset?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { title = 'Light Code', description = 'A lightweight code editor', lang = 'zh-CN', charset = 'UTF-8' } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<!doctype html>
|
||||||
|
<html lang={lang}>
|
||||||
|
<head>
|
||||||
|
<meta charset={charset} />
|
||||||
|
<meta name='viewport' content='width=device-width, initial-scale=1.0' />
|
||||||
|
<meta name='description' content={description} />
|
||||||
|
<title>{title}</title>
|
||||||
|
<!-- 样式 -->
|
||||||
|
<slot name='head' />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<slot />
|
||||||
|
|
||||||
|
<!-- 脚本 -->
|
||||||
|
<slot name='scripts' />
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
html {
|
||||||
|
font-family: system-ui, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
60
cli-center/src/components/ui/button.tsx
Normal file
60
cli-center/src/components/ui/button.tsx
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { Slot } from "@radix-ui/react-slot"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const buttonVariants = cva(
|
||||||
|
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||||
|
destructive:
|
||||||
|
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||||
|
outline:
|
||||||
|
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
||||||
|
secondary:
|
||||||
|
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||||
|
ghost:
|
||||||
|
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||||
|
link: "text-primary underline-offset-4 hover:underline",
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
||||||
|
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
||||||
|
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||||
|
icon: "size-9",
|
||||||
|
"icon-sm": "size-8",
|
||||||
|
"icon-lg": "size-10",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
size: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
function Button({
|
||||||
|
className,
|
||||||
|
variant,
|
||||||
|
size,
|
||||||
|
asChild = false,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"button"> &
|
||||||
|
VariantProps<typeof buttonVariants> & {
|
||||||
|
asChild?: boolean
|
||||||
|
}) {
|
||||||
|
const Comp = asChild ? Slot : "button"
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
data-slot="button"
|
||||||
|
className={cn(buttonVariants({ variant, size, className }))}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Button, buttonVariants }
|
||||||
29
cli-center/src/components/vue/Counter.vue
Normal file
29
cli-center/src/components/vue/Counter.vue
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
count: {
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const counter = ref(props.count)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex w-min border border-main rounded-md">
|
||||||
|
<button
|
||||||
|
class="border-r border-main p-2 font-mono outline-none hover:bg-gray-400 hover:bg-opacity-20"
|
||||||
|
@click="counter -= 1"
|
||||||
|
>
|
||||||
|
-
|
||||||
|
</button>
|
||||||
|
<span class="m-auto p-2">{{ counter }}</span>
|
||||||
|
<button
|
||||||
|
class="border-l border-main p-2 font-mono outline-none hover:bg-gray-400 hover:bg-opacity-20"
|
||||||
|
@click="counter += 1"
|
||||||
|
>
|
||||||
|
+
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
24
cli-center/src/content.config.ts
Normal file
24
cli-center/src/content.config.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
// @ts-ignore
|
||||||
|
import { defineCollection, z } from 'astro:content';
|
||||||
|
import { glob, file } from 'astro/loaders'; // 不适用于旧版 API
|
||||||
|
|
||||||
|
const docs = defineCollection({
|
||||||
|
loader: glob({ pattern: '**/[^_]*.md', base: './src/data/docs' }),
|
||||||
|
schema: z.object({
|
||||||
|
title: z.string().optional(),
|
||||||
|
description: z.string().optional(),
|
||||||
|
tags: z.array(z.string()).optional(),
|
||||||
|
// pubDate: z.coerce.date(),
|
||||||
|
createdAt: z.coerce.date().optional(),
|
||||||
|
updatedAt: z.coerce.date().optional(),
|
||||||
|
showMenu: z.boolean().optional().default(true),
|
||||||
|
/**
|
||||||
|
* 在侧边栏隐藏该文档
|
||||||
|
*/
|
||||||
|
hideInMenu: z.boolean().optional().default(false),
|
||||||
|
order: z.number().optional().default(0),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
export const collections = { docs };
|
||||||
15
cli-center/src/data/docs/00-intro.md
Normal file
15
cli-center/src/data/docs/00-intro.md
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
---
|
||||||
|
title: '介绍'
|
||||||
|
tags: ['introduce']
|
||||||
|
createdAt: '2025-12-18 20:00:00'
|
||||||
|
---
|
||||||
|
|
||||||
|
# @kevisual/cli 目的?
|
||||||
|
|
||||||
|
对于每一个人来说,搭建知识库,自动化,数据可视化,都是一件非常复杂的事情。但是,我们希望通过 `@kevisual/cli` 让这件事情变得非常简单。
|
||||||
|
|
||||||
|
AI 时代,人人都可以成为开发者,每一个人的灵感和想法都能变成代码,而把代码运行起来,把自己的知识库运行起来,也应该有一个体系化的解决方案。而这就是基于这个初衷。
|
||||||
|
|
||||||
|
## 数据代理
|
||||||
|
|
||||||
|
https://kevisual.cn 只作为一个用户管理的平台,而每一个人的设备,都需要运行一个cli,而这个cli负责管理所有的程序的运行,但是通过公网去访问到自己的服务。
|
||||||
13
cli-center/src/data/docs/01-login-first.md
Normal file
13
cli-center/src/data/docs/01-login-first.md
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
---
|
||||||
|
title: '第一次登录'
|
||||||
|
tags: ['setting']
|
||||||
|
createdAt: '2025-12-18 20:00:00'
|
||||||
|
---
|
||||||
|
|
||||||
|
## 第一次登录设备
|
||||||
|
|
||||||
|
每一台设备需要有自己的管理员信息,第一次登录的时候,用kevisual.cn 的账号密码登录,就会把这个账号设置为这个设备的管理员账号。
|
||||||
|
|
||||||
|
下一次修改管理员配置,必须管理员才能操作。
|
||||||
|
|
||||||
|
或者自己在 `kevisual/assistant-app/assistant-config.json` 里面修改管理员账号.
|
||||||
24
cli-center/src/layouts/MDXPost.astro
Normal file
24
cli-center/src/layouts/MDXPost.astro
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
---
|
||||||
|
import '../styles/global.css';
|
||||||
|
export interface Props {
|
||||||
|
title?: string;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
import 'github-markdown-css/github-markdown-light.css';
|
||||||
|
import 'highlight.js/styles/github-dark.css';
|
||||||
|
const { title, description } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<title>{title || '文档'}</title>
|
||||||
|
</head>
|
||||||
|
<body class="bg-gray-50 min-h-screen">
|
||||||
|
<div class="container mx-auto px-4 py-8 max-w-4xl">
|
||||||
|
<article class="markdown-body bg-white rounded-lg shadow-lg p-8">
|
||||||
|
<slot />
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
47
cli-center/src/layouts/blank.astro
Normal file
47
cli-center/src/layouts/blank.astro
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
---
|
||||||
|
import '../styles/global.css';
|
||||||
|
import '../styles/theme.css';
|
||||||
|
export interface Props {
|
||||||
|
title?: string;
|
||||||
|
description?: string;
|
||||||
|
lang?: string;
|
||||||
|
charset?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { title = 'Light Code', description = 'A lightweight code editor', lang = 'zh-CN', charset = 'UTF-8' } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<!doctype html>
|
||||||
|
<html lang={lang}>
|
||||||
|
<head>
|
||||||
|
<meta charset={charset} />
|
||||||
|
<meta name='viewport' content='width=device-width, initial-scale=1.0' />
|
||||||
|
<meta name='description' content={description} />
|
||||||
|
<title>{title}</title>
|
||||||
|
<!-- 样式 -->
|
||||||
|
<slot name='head' />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<slot />
|
||||||
|
|
||||||
|
<!-- 脚本 -->
|
||||||
|
<slot name='scripts' />
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
html {
|
||||||
|
font-family: system-ui, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
4
cli-center/src/layouts/docs/pagination.astro
Normal file
4
cli-center/src/layouts/docs/pagination.astro
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
---
|
||||||
|
|
||||||
|
分页组件
|
||||||
73
cli-center/src/layouts/mdx.astro
Normal file
73
cli-center/src/layouts/mdx.astro
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
---
|
||||||
|
export interface Props {
|
||||||
|
children: any;
|
||||||
|
}
|
||||||
|
import '../styles/global.css';
|
||||||
|
import '../styles/theme.css';
|
||||||
|
import 'github-markdown-css/github-markdown-light.css';
|
||||||
|
import { Menu, MenuItem } from '../apps/menu';
|
||||||
|
export interface Props {
|
||||||
|
title?: string;
|
||||||
|
description?: string;
|
||||||
|
lang?: string;
|
||||||
|
charset?: string;
|
||||||
|
showMenu?: boolean;
|
||||||
|
menu?: MenuItem[];
|
||||||
|
basename?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
title = 'Light Code',
|
||||||
|
description = 'A lightweight code editor',
|
||||||
|
lang = 'zh-CN',
|
||||||
|
charset = 'UTF-8',
|
||||||
|
showMenu = true,
|
||||||
|
menu,
|
||||||
|
basename = '',
|
||||||
|
} = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<html lang={lang}>
|
||||||
|
<head>
|
||||||
|
<meta charset={charset} />
|
||||||
|
<meta name='viewport' content='width=device-width, initial-scale=1.0' />
|
||||||
|
<title>{title}</title>
|
||||||
|
<meta name='description' content={description} />
|
||||||
|
<style>
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class='flex flex-col items-center bg-background'>
|
||||||
|
<div class='w-full'>
|
||||||
|
<slot name='header' />
|
||||||
|
</div>
|
||||||
|
<main class='flex-1 flex overflow-hidden w-full max-w-7xl px-4 py-4'>
|
||||||
|
{
|
||||||
|
showMenu && (
|
||||||
|
<aside class='w-64 min-w-64 h-full flex flex-col'>
|
||||||
|
<slot name='menu'>
|
||||||
|
<Menu items={menu!} client:only basename={basename} />
|
||||||
|
</slot>
|
||||||
|
</aside>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
<div class='flex-1 h-full flex items-start justify-center overflow-hidden'>
|
||||||
|
<article class='markdown-body h-full scrollbar overflow-auto px-8 py-6 w-full max-w-4xl border border-border rounded-lg shadow-sm bg-card'>
|
||||||
|
<slot />
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<footer class='w-full border-t border-border bg-card/50 backdrop-blur-sm'>
|
||||||
|
<slot name='footer'>
|
||||||
|
<div class='text-center text-sm text-muted-foreground py-4'>Copyright © 2025</div>
|
||||||
|
</slot>
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
6
cli-center/src/lib/utils.ts
Normal file
6
cli-center/src/lib/utils.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { clsx, type ClassValue } from "clsx"
|
||||||
|
import { twMerge } from "tailwind-merge"
|
||||||
|
|
||||||
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs))
|
||||||
|
}
|
||||||
13
cli-center/src/modules/basename.ts
Normal file
13
cli-center/src/modules/basename.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
// @ts-ignore
|
||||||
|
export const basename = BASE_NAME;
|
||||||
|
|
||||||
|
console.log(basename);
|
||||||
|
|
||||||
|
export const wrapBasename = (path: string) => {
|
||||||
|
const hasEnd = path.endsWith('/')
|
||||||
|
if (basename) {
|
||||||
|
return `${basename}${path}` + (hasEnd ? '' : '/');
|
||||||
|
} else {
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
}
|
||||||
23
cli-center/src/modules/query.ts
Normal file
23
cli-center/src/modules/query.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { QueryClient, Query } from '@kevisual/query'
|
||||||
|
import { QueryLoginBrowser } from '@kevisual/query-login'
|
||||||
|
const getUrl = () => {
|
||||||
|
const host = window.location.host
|
||||||
|
const isKevisual = host.includes('kevisual');
|
||||||
|
if (isKevisual) {
|
||||||
|
return '/api/router'
|
||||||
|
}
|
||||||
|
|
||||||
|
return '/client/router'
|
||||||
|
}
|
||||||
|
|
||||||
|
export const query = new QueryClient({
|
||||||
|
url: getUrl()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const remoteQuery = new Query({
|
||||||
|
url: '/api/router'
|
||||||
|
});
|
||||||
|
|
||||||
|
export const queryLogin = new QueryLoginBrowser({
|
||||||
|
query: remoteQuery
|
||||||
|
});
|
||||||
9
cli-center/src/pages/demos/base.astro
Normal file
9
cli-center/src/pages/demos/base.astro
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
import Html from '@/components/html.astro';
|
||||||
|
---
|
||||||
|
|
||||||
|
<Html>
|
||||||
|
<main>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
</Html>
|
||||||
10
cli-center/src/pages/demos/vue.astro
Normal file
10
cli-center/src/pages/demos/vue.astro
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
---
|
||||||
|
import Html from '@/components/html.astro';
|
||||||
|
// import Counter from '@/components/vue/Counter.vue';
|
||||||
|
---
|
||||||
|
|
||||||
|
<Html>
|
||||||
|
<main>
|
||||||
|
<!-- <Counter count={10} client:only/> -->
|
||||||
|
</main>
|
||||||
|
</Html>
|
||||||
27
cli-center/src/pages/docs/[...id].astro
Normal file
27
cli-center/src/pages/docs/[...id].astro
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
---
|
||||||
|
import { getCollection, render } from 'astro:content';
|
||||||
|
import Main from '@/layouts/mdx.astro';
|
||||||
|
import { basename } from '@/modules/basename';
|
||||||
|
// 1. 为每个集合条目生成一个新路径
|
||||||
|
export async function getStaticPaths() {
|
||||||
|
const posts = await getCollection('docs');
|
||||||
|
return posts.map((post) => ({
|
||||||
|
params: { id: post.id },
|
||||||
|
props: { post },
|
||||||
|
data: post,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
type Post = {
|
||||||
|
data: { title: string; tags: string[]; showMenu?: boolean };
|
||||||
|
};
|
||||||
|
// 2. 对于你的模板,你可以直接从 prop 获取条目
|
||||||
|
const { post } = Astro.props as { post: Post };
|
||||||
|
const { Content } = await render(post);
|
||||||
|
const showMenu = post.data?.showMenu;
|
||||||
|
const staticPaths = await getStaticPaths();
|
||||||
|
const menu = staticPaths.map((item) => item.data);
|
||||||
|
---
|
||||||
|
|
||||||
|
<Main showMenu={showMenu} menu={menu} basename={basename} title={post.data.title}>
|
||||||
|
<Content />
|
||||||
|
</Main>
|
||||||
80
cli-center/src/pages/docs/index.astro
Normal file
80
cli-center/src/pages/docs/index.astro
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
---
|
||||||
|
import { getCollection } from 'astro:content';
|
||||||
|
const posts = await getCollection('docs');
|
||||||
|
import { basename, wrapBasename } from '@/modules/basename';
|
||||||
|
import Blank from '@/layouts/blank.astro';
|
||||||
|
---
|
||||||
|
|
||||||
|
<Blank>
|
||||||
|
<main class='min-h-screen bg-linear-to-br from-slate-50 to-slate-100 dark:from-slate-900 dark:to-slate-800'>
|
||||||
|
<div class='max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12'>
|
||||||
|
{/* 页面标题区域 */}
|
||||||
|
<div class='mb-12'>
|
||||||
|
<h1 class='text-4xl sm:text-5xl font-bold text-slate-900 dark:text-white mb-4 bg-clip-text bg-linear-to-r from-blue-600 to-purple-600'>📚 文档列表</h1>
|
||||||
|
<p class='text-slate-600 dark:text-slate-400 text-lg'>浏览所有可用的文档资源</p>
|
||||||
|
<div class='mt-4 h-1 w-20 bg-linear-to-r from-blue-600 to-purple-600 rounded-full'></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 文档列表 */}
|
||||||
|
<div class='space-y-4'>
|
||||||
|
{
|
||||||
|
posts.map((post) => {
|
||||||
|
const tags = post.data.tags || [];
|
||||||
|
const postUrl = wrapBasename(`/docs/${post.id}`);
|
||||||
|
return (
|
||||||
|
<article class='group bg-white dark:bg-slate-800 rounded-xl shadow-sm hover:shadow-xl transition-all duration-300 overflow-hidden border border-slate-200 dark:border-slate-700 hover:border-blue-500 dark:hover:border-blue-400'>
|
||||||
|
<div class='p-6'>
|
||||||
|
{/* 文档标题 */}
|
||||||
|
<a href={postUrl} class='block'>
|
||||||
|
<h2 class='text-xl sm:text-2xl font-semibold text-slate-900 dark:text-white group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors duration-200 mb-3'>
|
||||||
|
{post.data.title}
|
||||||
|
</h2>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
{/* 文档描述(如果有) */}
|
||||||
|
{post.data.description && <p class='text-slate-600 dark:text-slate-400 mb-4 line-clamp-2'>{post.data.description}</p>}
|
||||||
|
|
||||||
|
{/* 标签列表 */}
|
||||||
|
{tags.length > 0 && (
|
||||||
|
<div class='flex flex-wrap gap-2 mt-4'>
|
||||||
|
{tags.map((tag) => (
|
||||||
|
<div class='inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-blue-50 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 hover:bg-blue-100 dark:hover:bg-blue-900/50 transition-colors duration-200 border border-blue-200 dark:border-blue-800'>
|
||||||
|
<span class='mr-1'>#</span>
|
||||||
|
{tag}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 阅读更多指示器 */}
|
||||||
|
<a
|
||||||
|
href={postUrl}
|
||||||
|
class='mt-4 flex items-center text-blue-600 dark:text-blue-400 text-sm font-medium opacity-0 group-hover:opacity-100 transition-opacity duration-200'>
|
||||||
|
<span>阅读更多</span>
|
||||||
|
<svg
|
||||||
|
class='w-4 h-4 ml-1 transform group-hover:translate-x-1 transition-transform duration-200'
|
||||||
|
fill='none'
|
||||||
|
viewBox='0 0 24 24'
|
||||||
|
stroke='currentColor'>
|
||||||
|
<path stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M9 5l7 7-7 7' />
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 空状态 */}
|
||||||
|
{
|
||||||
|
posts.length === 0 && (
|
||||||
|
<div class='text-center py-16'>
|
||||||
|
<div class='text-6xl mb-4'>📭</div>
|
||||||
|
<p class='text-xl text-slate-600 dark:text-slate-400'>暂无文档</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</Blank>
|
||||||
47
cli-center/src/pages/index.astro
Normal file
47
cli-center/src/pages/index.astro
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
---
|
||||||
|
// import { query } from '@/modules/query.ts';
|
||||||
|
console.log('Hello from index.astro');
|
||||||
|
import '../styles/global.css';
|
||||||
|
---
|
||||||
|
|
||||||
|
<html lang='en'>
|
||||||
|
<head>
|
||||||
|
<title>My Homepage</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1 onclick="{onClick}">Welcome to my website!</h1>
|
||||||
|
<div class='bg-amber-50 w-20 h-20 rounded-full'></div>
|
||||||
|
<div id='root'></div>
|
||||||
|
<script type='importmap' data-vite-ignore is:inline>
|
||||||
|
{
|
||||||
|
"imports": {
|
||||||
|
"react": "https://esm.sh/react@19.1.0",
|
||||||
|
"react-dom": "https://esm.sh/react-dom@19.1.0/client.js",
|
||||||
|
"react-toastify": "https://esm.sh/react-toastify@11.0.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<script type='module' data-vite-ignore is:inline>
|
||||||
|
import { Button, message } from 'https://esm.sh/antd?standalone';
|
||||||
|
import React from 'react';
|
||||||
|
import { ToastContainer, toast } from 'react-toastify';
|
||||||
|
import { createRoot } from 'react-dom';
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.loading('Hello from index.astro');
|
||||||
|
window.toast = toast;
|
||||||
|
console.log('message', toast);
|
||||||
|
}, 1000);
|
||||||
|
console.log('Hello from index.astro', Button);
|
||||||
|
const root = document.getElementById('root');
|
||||||
|
const render = createRoot(root);
|
||||||
|
const App = () => {
|
||||||
|
const button = React.createElement(Button, null, 'Hello');
|
||||||
|
const messageEl = React.createElement(ToastContainer, null, 'Hello');
|
||||||
|
const wrapperMessage = React.createElement('div', null, [button, messageEl]);
|
||||||
|
return wrapperMessage;
|
||||||
|
};
|
||||||
|
// render.render(React.createElement(Button, null, 'Hello'), root);
|
||||||
|
render.render(App(), root);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
8
cli-center/src/pages/setting.astro
Normal file
8
cli-center/src/pages/setting.astro
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
---
|
||||||
|
import Html from '@/components/html.astro';
|
||||||
|
import { FirstLogin } from '@/apps/setting/index.tsx';
|
||||||
|
---
|
||||||
|
|
||||||
|
<Html>
|
||||||
|
<FirstLogin client:only />
|
||||||
|
</Html>
|
||||||
8
cli-center/src/pages/setting/all.astro
Normal file
8
cli-center/src/pages/setting/all.astro
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
---
|
||||||
|
import Html from '@/components/html.astro';
|
||||||
|
import { Config } from '@/apps/setting/index.tsx';
|
||||||
|
---
|
||||||
|
|
||||||
|
<Html>
|
||||||
|
<Config client:only />
|
||||||
|
</Html>
|
||||||
120
cli-center/src/styles/global.css
Normal file
120
cli-center/src/styles/global.css
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
@import 'tailwindcss';
|
||||||
|
@import "tw-animate-css";
|
||||||
|
|
||||||
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
|
@theme inline {
|
||||||
|
--radius-sm: calc(var(--radius) - 4px);
|
||||||
|
--radius-md: calc(var(--radius) - 2px);
|
||||||
|
--radius-lg: var(--radius);
|
||||||
|
--radius-xl: calc(var(--radius) + 4px);
|
||||||
|
--color-background: var(--background);
|
||||||
|
--color-foreground: var(--foreground);
|
||||||
|
--color-card: var(--card);
|
||||||
|
--color-card-foreground: var(--card-foreground);
|
||||||
|
--color-popover: var(--popover);
|
||||||
|
--color-popover-foreground: var(--popover-foreground);
|
||||||
|
--color-primary: var(--primary);
|
||||||
|
--color-primary-foreground: var(--primary-foreground);
|
||||||
|
--color-secondary: var(--secondary);
|
||||||
|
--color-secondary-foreground: var(--secondary-foreground);
|
||||||
|
--color-muted: var(--muted);
|
||||||
|
--color-muted-foreground: var(--muted-foreground);
|
||||||
|
--color-accent: var(--accent);
|
||||||
|
--color-accent-foreground: var(--accent-foreground);
|
||||||
|
--color-destructive: var(--destructive);
|
||||||
|
--color-border: var(--border);
|
||||||
|
--color-input: var(--input);
|
||||||
|
--color-ring: var(--ring);
|
||||||
|
--color-chart-1: var(--chart-1);
|
||||||
|
--color-chart-2: var(--chart-2);
|
||||||
|
--color-chart-3: var(--chart-3);
|
||||||
|
--color-chart-4: var(--chart-4);
|
||||||
|
--color-chart-5: var(--chart-5);
|
||||||
|
--color-sidebar: var(--sidebar);
|
||||||
|
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||||
|
--color-sidebar-primary: var(--sidebar-primary);
|
||||||
|
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||||
|
--color-sidebar-accent: var(--sidebar-accent);
|
||||||
|
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||||
|
--color-sidebar-border: var(--sidebar-border);
|
||||||
|
--color-sidebar-ring: var(--sidebar-ring);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--radius: 0.625rem;
|
||||||
|
--background: oklch(1 0 0);
|
||||||
|
--foreground: oklch(0.145 0 0);
|
||||||
|
--card: oklch(1 0 0);
|
||||||
|
--card-foreground: oklch(0.145 0 0);
|
||||||
|
--popover: oklch(1 0 0);
|
||||||
|
--popover-foreground: oklch(0.145 0 0);
|
||||||
|
--primary: oklch(0.205 0 0);
|
||||||
|
--primary-foreground: oklch(0.985 0 0);
|
||||||
|
--secondary: oklch(0.97 0 0);
|
||||||
|
--secondary-foreground: oklch(0.205 0 0);
|
||||||
|
--muted: oklch(0.97 0 0);
|
||||||
|
--muted-foreground: oklch(0.556 0 0);
|
||||||
|
--accent: oklch(0.97 0 0);
|
||||||
|
--accent-foreground: oklch(0.205 0 0);
|
||||||
|
--destructive: oklch(0.577 0.245 27.325);
|
||||||
|
--border: oklch(0.922 0 0);
|
||||||
|
--input: oklch(0.922 0 0);
|
||||||
|
--ring: oklch(0.708 0 0);
|
||||||
|
--chart-1: oklch(0.646 0.222 41.116);
|
||||||
|
--chart-2: oklch(0.6 0.118 184.704);
|
||||||
|
--chart-3: oklch(0.398 0.07 227.392);
|
||||||
|
--chart-4: oklch(0.828 0.189 84.429);
|
||||||
|
--chart-5: oklch(0.769 0.188 70.08);
|
||||||
|
--sidebar: oklch(0.985 0 0);
|
||||||
|
--sidebar-foreground: oklch(0.145 0 0);
|
||||||
|
--sidebar-primary: oklch(0.205 0 0);
|
||||||
|
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-accent: oklch(0.97 0 0);
|
||||||
|
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||||
|
--sidebar-border: oklch(0.922 0 0);
|
||||||
|
--sidebar-ring: oklch(0.708 0 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--background: oklch(0.145 0 0);
|
||||||
|
--foreground: oklch(0.985 0 0);
|
||||||
|
--card: oklch(0.205 0 0);
|
||||||
|
--card-foreground: oklch(0.985 0 0);
|
||||||
|
--popover: oklch(0.205 0 0);
|
||||||
|
--popover-foreground: oklch(0.985 0 0);
|
||||||
|
--primary: oklch(0.922 0 0);
|
||||||
|
--primary-foreground: oklch(0.205 0 0);
|
||||||
|
--secondary: oklch(0.269 0 0);
|
||||||
|
--secondary-foreground: oklch(0.985 0 0);
|
||||||
|
--muted: oklch(0.269 0 0);
|
||||||
|
--muted-foreground: oklch(0.708 0 0);
|
||||||
|
--accent: oklch(0.269 0 0);
|
||||||
|
--accent-foreground: oklch(0.985 0 0);
|
||||||
|
--destructive: oklch(0.704 0.191 22.216);
|
||||||
|
--border: oklch(1 0 0 / 10%);
|
||||||
|
--input: oklch(1 0 0 / 15%);
|
||||||
|
--ring: oklch(0.556 0 0);
|
||||||
|
--chart-1: oklch(0.488 0.243 264.376);
|
||||||
|
--chart-2: oklch(0.696 0.17 162.48);
|
||||||
|
--chart-3: oklch(0.769 0.188 70.08);
|
||||||
|
--chart-4: oklch(0.627 0.265 303.9);
|
||||||
|
--chart-5: oklch(0.645 0.246 16.439);
|
||||||
|
--sidebar: oklch(0.205 0 0);
|
||||||
|
--sidebar-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||||
|
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-accent: oklch(0.269 0 0);
|
||||||
|
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-border: oklch(1 0 0 / 10%);
|
||||||
|
--sidebar-ring: oklch(0.556 0 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
@apply border-border outline-ring/50;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
@apply bg-background text-foreground;
|
||||||
|
}
|
||||||
|
}
|
||||||
98
cli-center/src/styles/theme.css
Normal file
98
cli-center/src/styles/theme.css
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
@import 'tailwindcss';
|
||||||
|
|
||||||
|
@theme {
|
||||||
|
/* --color-primary: #ffc107;
|
||||||
|
--color-secondary: #ffa000;
|
||||||
|
--color-text-primary: #000000;
|
||||||
|
--color-text-secondary: #000000;
|
||||||
|
--color-success: #28a745; */
|
||||||
|
|
||||||
|
--color-scrollbar-thumb: #999999;
|
||||||
|
--color-scrollbar-track: rgba(0, 0, 0, 0.1);
|
||||||
|
--color-scrollbar-thumb-hover: #666666;
|
||||||
|
--scrollbar-color: #ffc107;
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
font-size: 16px;
|
||||||
|
font-family: 'Montserrat', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* font-family */
|
||||||
|
@utility font-family-mon {
|
||||||
|
font-family: 'Montserrat', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
@utility font-family-rob {
|
||||||
|
font-family: 'Roboto', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
@utility font-family-int {
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
@utility font-family-orb {
|
||||||
|
font-family: 'Orbitron', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
@utility font-family-din {
|
||||||
|
font-family: 'DIN', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
@utility flex-row-center {
|
||||||
|
@apply flex flex-row items-center justify-center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@utility flex-col-center {
|
||||||
|
@apply flex flex-col items-center justify-center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@utility scrollbar {
|
||||||
|
overflow: auto;
|
||||||
|
|
||||||
|
/* 整个滚动条 */
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
width: 3px;
|
||||||
|
height: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-track {
|
||||||
|
background-color: var(--color-scrollbar-track);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 滚动条有滑块的轨道部分 */
|
||||||
|
&::-webkit-scrollbar-track-piece {
|
||||||
|
background-color: transparent;
|
||||||
|
border-radius: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 滚动条滑块(竖向:vertical 横向:horizontal) */
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: var(--color-scrollbar-thumb);
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 滚动条滑块hover */
|
||||||
|
&::-webkit-scrollbar-thumb:hover {
|
||||||
|
background-color: var(--color-scrollbar-thumb-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 同时有垂直和水平滚动条时交汇的部分 */
|
||||||
|
&::-webkit-scrollbar-corner {
|
||||||
|
display: block;
|
||||||
|
/* 修复交汇时出现的白块 */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ul,
|
||||||
|
menu {
|
||||||
|
list-style: disc;
|
||||||
|
}
|
||||||
|
|
||||||
|
ol {
|
||||||
|
list-style: decimal;
|
||||||
|
}
|
||||||
18
cli-center/tsconfig.json
Normal file
18
cli-center/tsconfig.json
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"extends": "@kevisual/types/json/frontend.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": [
|
||||||
|
"./src/*"
|
||||||
|
],
|
||||||
|
"@/agent": [
|
||||||
|
"./src/agent"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src/**/*",
|
||||||
|
"agent/**/*"
|
||||||
|
],
|
||||||
|
}
|
||||||
22
package.json
22
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@kevisual/cli",
|
"name": "@kevisual/cli",
|
||||||
"version": "0.0.71",
|
"version": "0.0.76",
|
||||||
"description": "envision 命令行工具",
|
"description": "envision 命令行工具",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"basename": "/root/cli",
|
"basename": "/root/cli",
|
||||||
@@ -28,6 +28,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "bun src/run.ts ",
|
"dev": "bun src/run.ts ",
|
||||||
"dev:tsx": "tsx src/run.ts ",
|
"dev:tsx": "tsx src/run.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.mjs",
|
||||||
"deploy": "ev pack -u -p -m no",
|
"deploy": "ev pack -u -p -m no",
|
||||||
"pub:me": "npm publish --registry https://npm.xiongxiao.me --tag beta",
|
"pub:me": "npm publish --registry https://npm.xiongxiao.me --tag beta",
|
||||||
@@ -40,22 +41,29 @@
|
|||||||
],
|
],
|
||||||
"author": "abearxiong",
|
"author": "abearxiong",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@kevisual/app": "^0.0.1",
|
||||||
"@kevisual/context": "^0.0.4",
|
"@kevisual/context": "^0.0.4",
|
||||||
|
"@kevisual/hot-api": "^0.0.3",
|
||||||
|
"@nut-tree-fork/nut-js": "^4.2.6",
|
||||||
|
"eventemitter3": "^5.0.1",
|
||||||
|
"lowdb": "^7.0.1",
|
||||||
|
"lru-cache": "^11.2.4",
|
||||||
"micromatch": "^4.0.8",
|
"micromatch": "^4.0.8",
|
||||||
"pm2": "^6.0.14",
|
"pm2": "^6.0.14",
|
||||||
"semver": "^7.7.3"
|
"semver": "^7.7.3",
|
||||||
|
"unstorage": "^1.17.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@kevisual/dts": "^0.0.3",
|
"@kevisual/dts": "^0.0.3",
|
||||||
"@kevisual/load": "^0.0.6",
|
"@kevisual/load": "^0.0.6",
|
||||||
"@kevisual/logger": "^0.0.4",
|
"@kevisual/logger": "^0.0.4",
|
||||||
"@kevisual/query": "0.0.29",
|
"@kevisual/query": "0.0.32",
|
||||||
"@kevisual/query-login": "0.0.7",
|
"@kevisual/query-login": "0.0.7",
|
||||||
"@types/bun": "^1.3.3",
|
"@types/bun": "^1.3.4",
|
||||||
"@types/crypto-js": "^4.2.2",
|
"@types/crypto-js": "^4.2.2",
|
||||||
"@types/jsonwebtoken": "^9.0.10",
|
"@types/jsonwebtoken": "^9.0.10",
|
||||||
"@types/micromatch": "^4.0.10",
|
"@types/micromatch": "^4.0.10",
|
||||||
"@types/node": "^24.10.1",
|
"@types/node": "^25.0.3",
|
||||||
"@types/semver": "^7.7.1",
|
"@types/semver": "^7.7.1",
|
||||||
"chalk": "^5.6.2",
|
"chalk": "^5.6.2",
|
||||||
"commander": "^14.0.2",
|
"commander": "^14.0.2",
|
||||||
@@ -64,8 +72,8 @@
|
|||||||
"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",
|
||||||
"inquirer": "^13.0.2",
|
"inquirer": "^13.1.0",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.3",
|
||||||
"tar": "^7.5.2",
|
"tar": "^7.5.2",
|
||||||
"zustand": "^5.0.9"
|
"zustand": "^5.0.9"
|
||||||
},
|
},
|
||||||
|
|||||||
8544
pnpm-lock.yaml
generated
8544
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,2 +1,3 @@
|
|||||||
packages:
|
packages:
|
||||||
- 'assistant'
|
- 'assistant'
|
||||||
|
- 'cli-center'
|
||||||
4
src/ai/ai.ts
Normal file
4
src/ai/ai.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import { App } from '@kevisual/app/src/app.ts';
|
||||||
|
import { storage } from '../module/query.ts';
|
||||||
|
|
||||||
|
export const app = new App({ token: storage.getItem('token') || '', storage });
|
||||||
6
src/ai/index.ts
Normal file
6
src/ai/index.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { app } from './ai.ts'
|
||||||
|
import './routes/cmd-run.ts'
|
||||||
|
|
||||||
|
export {
|
||||||
|
app
|
||||||
|
}
|
||||||
62
src/ai/routes/cmd-run.ts
Normal file
62
src/ai/routes/cmd-run.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
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);
|
||||||
41
src/command/ai.ts
Normal file
41
src/command/ai.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { program, Command } from '@/program.ts';
|
||||||
|
import { app } from '../ai/index.ts';
|
||||||
|
import util from 'util';
|
||||||
|
import { chalk } from '@/module/chalk.ts';
|
||||||
|
import { logger } from '@/module/logger.ts';
|
||||||
|
const aiCmd = new Command('ai')
|
||||||
|
.description('AI 相关命令')
|
||||||
|
.action(async (opts) => {
|
||||||
|
});
|
||||||
|
|
||||||
|
const runCmd = async (cmd: string) => {
|
||||||
|
const res = await app.router.call({ path: 'cmd-run', payload: { cmd } });
|
||||||
|
const { body } = res;
|
||||||
|
const steps = body?.steps || [];
|
||||||
|
for (const step of steps) {
|
||||||
|
logger.debug(chalk.blue(`\n==== 步骤: ${step.cmd || '结束'} ====`));
|
||||||
|
logger.debug(step.result || 'No result');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const aiRun = new Command('run')
|
||||||
|
.description('执行 AI 命令')
|
||||||
|
.option('-c, --cmd <cmd>', '要执行的 CMD 命令')
|
||||||
|
.action(async (opts) => {
|
||||||
|
if (opts.cmd) {
|
||||||
|
await runCmd(opts.cmd);
|
||||||
|
} else {
|
||||||
|
console.log('请提供要执行的 CMD 命令');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const aiRunDeploy = new Command('deploy')
|
||||||
|
.description('部署 AI 后端应用')
|
||||||
|
.action(async (opts) => {
|
||||||
|
const cmd = 'ev pack -p -u';
|
||||||
|
const res = await runCmd(cmd);
|
||||||
|
});
|
||||||
|
|
||||||
|
aiCmd.addCommand(aiRun);
|
||||||
|
aiCmd.addCommand(aiRunDeploy);
|
||||||
|
|
||||||
|
program.addCommand(aiCmd);
|
||||||
78
src/command/config-secret-remote.ts
Normal file
78
src/command/config-secret-remote.ts
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import { program, Command } from '@/program.ts';
|
||||||
|
import { query } from '@/module/query.ts';
|
||||||
|
import { QueryConfig } from '@/query/query-secret/query-secret.ts';
|
||||||
|
import { showMore } from '@/uitls/show-more.ts';
|
||||||
|
import fs from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
|
||||||
|
const queryConfig = new QueryConfig({ query: query as any });
|
||||||
|
const command = new Command('remote-secret')
|
||||||
|
.alias('rs').description('获取或设置远程配置');
|
||||||
|
|
||||||
|
|
||||||
|
const getCommand = new Command('get')
|
||||||
|
.option('-k, --key <key>', '配置键名')
|
||||||
|
.action(async (options) => {
|
||||||
|
const { key } = options || {};
|
||||||
|
if (!key) {
|
||||||
|
console.log('Please provide a key using -k or --key option.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const res = await queryConfig.getItem({ id: key });
|
||||||
|
console.log('res Config Result:', showMore(res.data));
|
||||||
|
})
|
||||||
|
|
||||||
|
const listCommand = new Command('list')
|
||||||
|
.description('列出所有配置')
|
||||||
|
.action(async () => {
|
||||||
|
const res = await queryConfig.listItems();
|
||||||
|
if (res.code === 200) {
|
||||||
|
const list = res.data?.list || [];
|
||||||
|
list.forEach(item => {
|
||||||
|
console.log(item.id, item.key, showMore(item));
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log('获取错误:', res.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
const updateCommand = new Command('update')
|
||||||
|
.description('更新远程配置')
|
||||||
|
.option('-i, --id <id>', '配置ID')
|
||||||
|
.option('-t, --title <title>', '配置值')
|
||||||
|
.option('-d, --description <description>', '配置数据,JSON格式')
|
||||||
|
.action(async (options) => {
|
||||||
|
const { id, title, description } = options || {};
|
||||||
|
let updateData: any = {};
|
||||||
|
if (title) {
|
||||||
|
updateData.title = title;
|
||||||
|
}
|
||||||
|
if (description) {
|
||||||
|
updateData.description = description;
|
||||||
|
}
|
||||||
|
if (id) {
|
||||||
|
updateData.id = id;
|
||||||
|
}
|
||||||
|
const res = await queryConfig.updateItem(updateData);
|
||||||
|
console.log('修改结果:', showMore(res));
|
||||||
|
});
|
||||||
|
const deleteCommand = new Command('delete')
|
||||||
|
.description('删除远程配置')
|
||||||
|
.option('-i, --id <id>', '配置ID')
|
||||||
|
.option('-k, --key <key>', '配置键名')
|
||||||
|
.action(async (options) => {
|
||||||
|
const { key, id } = options || {};
|
||||||
|
if (!key && !id) {
|
||||||
|
console.log('请提供配置键名或配置ID,使用 -k 或 --key 选项,或 -i 或 --id 选项。');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const res = await queryConfig.deleteItem({ key, id });
|
||||||
|
console.log('Delete Config Result:', showMore(res));
|
||||||
|
});
|
||||||
|
|
||||||
|
command.addCommand(listCommand);
|
||||||
|
command.addCommand(getCommand);
|
||||||
|
command.addCommand(updateCommand);
|
||||||
|
command.addCommand(deleteCommand);
|
||||||
|
|
||||||
|
program.addCommand(command);
|
||||||
@@ -30,7 +30,7 @@ export const getPackageJson = (opts?: { version?: string; appKey?: string }) =>
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const [user, appKey] = userAppArry;
|
const [user, appKey] = userAppArry;
|
||||||
return { basename, version, pkg: packageJson, user, appKey: appKey || opts?.appKey, app };
|
return { basename, version, pkg: packageJson, user, appKey: opts?.appKey || appKey, app };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -52,7 +52,6 @@ const command = new Command('deploy')
|
|||||||
let { version, key, yes, update, org, showBackend } = options;
|
let { version, key, yes, update, org, showBackend } = options;
|
||||||
const noCheck = !options.noCheck;
|
const noCheck = !options.noCheck;
|
||||||
const dot = !!options.dot;
|
const dot = !!options.dot;
|
||||||
// 获取当前目录,是否存在package.json, 如果有,从package.json 获取 version 和basename
|
|
||||||
const pkgInfo = getPackageJson({ version, appKey: key });
|
const pkgInfo = getPackageJson({ version, appKey: key });
|
||||||
if (!version && pkgInfo?.version) {
|
if (!version && pkgInfo?.version) {
|
||||||
version = pkgInfo?.version || '';
|
version = pkgInfo?.version || '';
|
||||||
@@ -60,7 +59,7 @@ const command = new Command('deploy')
|
|||||||
if (!key && pkgInfo?.appKey) {
|
if (!key && pkgInfo?.appKey) {
|
||||||
key = pkgInfo?.appKey || '';
|
key = pkgInfo?.appKey || '';
|
||||||
}
|
}
|
||||||
console.log('start deploy');
|
logger.debug('start deploy');
|
||||||
if (!version || !key) {
|
if (!version || !key) {
|
||||||
const answers = await inquirer.prompt([
|
const answers = await inquirer.prompt([
|
||||||
{
|
{
|
||||||
@@ -107,8 +106,8 @@ const command = new Command('deploy')
|
|||||||
const filename = path.basename(directory);
|
const filename = path.basename(directory);
|
||||||
_relativeFiles = [filename];
|
_relativeFiles = [filename];
|
||||||
}
|
}
|
||||||
console.log('upload Files', _relativeFiles);
|
logger.debug('upload Files', _relativeFiles);
|
||||||
console.log('upload Files Key', key, version);
|
logger.debug('upload Files Key', key, version);
|
||||||
if (!yes) {
|
if (!yes) {
|
||||||
// 确认是否上传
|
// 确认是否上传
|
||||||
const confirm = await inquirer.prompt([
|
const confirm = await inquirer.prompt([
|
||||||
@@ -125,7 +124,6 @@ const command = new Command('deploy')
|
|||||||
const uploadDirectory = isDirectory ? directory : path.dirname(directory);
|
const uploadDirectory = isDirectory ? directory : path.dirname(directory);
|
||||||
const res = await uploadFiles(_relativeFiles, uploadDirectory, { key, version, username: org, noCheckAppFiles: !noCheck, directory: options.directory });
|
const res = await uploadFiles(_relativeFiles, uploadDirectory, { key, version, username: org, noCheckAppFiles: !noCheck, directory: options.directory });
|
||||||
if (res?.code === 200) {
|
if (res?.code === 200) {
|
||||||
console.log('File uploaded successfully!');
|
|
||||||
res.data?.upload?.map?.((d) => {
|
res.data?.upload?.map?.((d) => {
|
||||||
console.log(chalk.green('uploaded file', d?.name, d?.path));
|
console.log(chalk.green('uploaded file', d?.name, d?.path));
|
||||||
});
|
});
|
||||||
@@ -140,7 +138,6 @@ const command = new Command('deploy')
|
|||||||
// const { id, data, ...rest } = res.data?.app || {};
|
// const { id, data, ...rest } = res.data?.app || {};
|
||||||
const { id, data, ...rest } = res2.data || {};
|
const { id, data, ...rest } = res2.data || {};
|
||||||
if (id && !update) {
|
if (id && !update) {
|
||||||
console.log(chalk.green('id: '), id);
|
|
||||||
if (!org) {
|
if (!org) {
|
||||||
console.log(chalk.green(`更新为最新版本: envision deploy-load ${id}`));
|
console.log(chalk.green(`更新为最新版本: envision deploy-load ${id}`));
|
||||||
} else {
|
} else {
|
||||||
@@ -153,9 +150,7 @@ const command = new Command('deploy')
|
|||||||
}
|
}
|
||||||
logger.debug('deploy success', res2.data);
|
logger.debug('deploy success', res2.data);
|
||||||
if (id && showBackend) {
|
if (id && showBackend) {
|
||||||
console.log('\n');
|
console.log(chalk.blue('下一个步骤服务端应用部署:\n'), 'envision pack-deploy', id);
|
||||||
console.log(chalk.blue('服务端应用部署: '), 'envision pack-deploy', id);
|
|
||||||
console.log('\n');
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.error('File upload failed', res?.message);
|
console.error('File upload failed', res?.message);
|
||||||
@@ -177,14 +172,18 @@ const uploadFiles = async (files: string[], directory: string, opts: UploadFileO
|
|||||||
const { key, version, username } = opts || {};
|
const { key, version, username } = opts || {};
|
||||||
const form = new FormData();
|
const form = new FormData();
|
||||||
const data: Record<string, any> = { files: [] };
|
const data: Record<string, any> = { files: [] };
|
||||||
|
let description = '';
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
const filePath = path.join(directory, file);
|
const filePath = path.join(directory, file);
|
||||||
const hash = getHash(filePath);
|
const hash = getHash(filePath);
|
||||||
if (!hash) {
|
if (!hash) {
|
||||||
console.error('文件', filePath, '不存在');
|
logger.error('文件', filePath, '不存在');
|
||||||
console.error('请检查文件是否存在');
|
logger.error('请检查文件是否存在');
|
||||||
}
|
}
|
||||||
data.files.push({ path: file, hash: hash });
|
data.files.push({ path: file, hash: hash });
|
||||||
|
if(filePath.includes('readme.md')) {
|
||||||
|
description = fs.readFileSync(filePath, 'utf-8');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
data.appKey = key;
|
data.appKey = key;
|
||||||
data.version = version;
|
data.version = version;
|
||||||
@@ -221,11 +220,11 @@ const uploadFiles = async (files: string[], directory: string, opts: UploadFileO
|
|||||||
const filePath = path.join(directory, file);
|
const filePath = path.join(directory, file);
|
||||||
const check = checkData.find((d) => d.path === file);
|
const check = checkData.find((d) => d.path === file);
|
||||||
if (check?.isUpload) {
|
if (check?.isUpload) {
|
||||||
console.log('文件已经上传过了', file);
|
logger.debug('文件已经上传过了', file);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const filename = path.basename(filePath);
|
const filename = path.basename(filePath);
|
||||||
console.log('upload file', file, filename);
|
logger.debug('upload file', file, filename);
|
||||||
form.append('file', fs.createReadStream(filePath), {
|
form.append('file', fs.createReadStream(filePath), {
|
||||||
filename: filename,
|
filename: filename,
|
||||||
filepath: file,
|
filepath: file,
|
||||||
@@ -233,7 +232,7 @@ const uploadFiles = async (files: string[], directory: string, opts: UploadFileO
|
|||||||
needUpload = true;
|
needUpload = true;
|
||||||
}
|
}
|
||||||
if (!needUpload) {
|
if (!needUpload) {
|
||||||
console.log('所有文件都上传过了,不需要上传文件');
|
logger.debug('所有文件都上传过了,不需要上传文件');
|
||||||
return {
|
return {
|
||||||
code: 200,
|
code: 200,
|
||||||
};
|
};
|
||||||
@@ -261,16 +260,16 @@ const deployLoadFn = async (id: string, org?: string) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (res.code === 200) {
|
if (res.code === 200) {
|
||||||
console.log(chalk.green('deploy-load success. current version:', res.data?.version));
|
logger.info(chalk.green('deploy-load success. current version:', res.data?.version));
|
||||||
// /:username/:appName
|
// /:username/:appName
|
||||||
try {
|
try {
|
||||||
const { user, key } = res.data;
|
const { user, key } = res.data;
|
||||||
const baseURL = getBaseURL();
|
const baseURL = getBaseURL();
|
||||||
const deployURL = new URL(`/${user}/${key}/`, baseURL);
|
const deployURL = new URL(`/${user}/${key}/`, baseURL);
|
||||||
console.log(chalk.blue('deployURL', deployURL.href));
|
logger.info(chalk.blue('deployURL', deployURL.href));
|
||||||
} catch (error) { }
|
} catch (error) { }
|
||||||
} else {
|
} else {
|
||||||
console.error('deploy-load failed', res.message);
|
logger.error('deploy-load failed', res.message);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -79,10 +79,10 @@ const showMe = async (show = true) => {
|
|||||||
const localToken = storage.getItem('token');
|
const localToken = storage.getItem('token');
|
||||||
if (!token && !localToken) {
|
if (!token && !localToken) {
|
||||||
console.log('请先登录');
|
console.log('请先登录');
|
||||||
return;
|
return { code: 40400, message: '请先登录' };
|
||||||
}
|
}
|
||||||
let me = await queryLogin.getMe(token);
|
let me = await queryLogin.getMe(token);
|
||||||
if (me.code === 401) {
|
if (me?.code === 401) {
|
||||||
me = await queryLogin.getMe();
|
me = await queryLogin.getMe();
|
||||||
}
|
}
|
||||||
if (show) {
|
if (show) {
|
||||||
@@ -113,6 +113,9 @@ const command = new Command('me')
|
|||||||
res = await showMe(false);
|
res = await showMe(false);
|
||||||
isRefresh = true;
|
isRefresh = true;
|
||||||
}
|
}
|
||||||
|
if (res.code === 40400) {
|
||||||
|
return
|
||||||
|
}
|
||||||
if (res.code === 200) {
|
if (res.code === 200) {
|
||||||
if (isRefresh) {
|
if (isRefresh) {
|
||||||
console.log(chalk.green('refresh token success'), '\n');
|
console.log(chalk.green('refresh token success'), '\n');
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { fileIsExist } from '@/uitls/file.ts';
|
|||||||
import { chalk } from '@/module/chalk.ts';
|
import { chalk } from '@/module/chalk.ts';
|
||||||
import * as backServices from '@/query/services/index.ts';
|
import * as backServices from '@/query/services/index.ts';
|
||||||
import inquirer from 'inquirer';
|
import inquirer from 'inquirer';
|
||||||
|
import { logger } from '@/module/logger.ts';
|
||||||
// 查找文件(忽略大小写)
|
// 查找文件(忽略大小写)
|
||||||
async function findFileInsensitive(targetFile: string): Promise<string | null> {
|
async function findFileInsensitive(targetFile: string): Promise<string | null> {
|
||||||
const files = fs.readdirSync('.');
|
const files = fs.readdirSync('.');
|
||||||
@@ -139,9 +140,9 @@ export const pack = async (opts: { packDist?: string, mergeDist?: boolean }) =>
|
|||||||
const allFiles = (await Promise.all(filesToInclude.map((file) => collectFileInfo(file)))).flat();
|
const allFiles = (await Promise.all(filesToInclude.map((file) => collectFileInfo(file)))).flat();
|
||||||
|
|
||||||
// 输出文件详细信息
|
// 输出文件详细信息
|
||||||
console.log('文件列表:');
|
logger.debug('文件列表:');
|
||||||
allFiles.forEach((file) => {
|
allFiles.forEach((file) => {
|
||||||
console.log(`${file.size}B ${file.path}`);
|
logger.debug(`${file.size}B ${file.path}`);
|
||||||
});
|
});
|
||||||
const totalSize = allFiles.reduce((sum, file) => sum + file.size, 0);
|
const totalSize = allFiles.reduce((sum, file) => sum + file.size, 0);
|
||||||
|
|
||||||
@@ -150,10 +151,10 @@ export const pack = async (opts: { packDist?: string, mergeDist?: boolean }) =>
|
|||||||
collection.totalSize = totalSize;
|
collection.totalSize = totalSize;
|
||||||
collection.tags = packageJson.app?.tags || packageJson.keywords || [];
|
collection.tags = packageJson.app?.tags || packageJson.keywords || [];
|
||||||
|
|
||||||
console.log('\n基本信息');
|
logger.debug('\n基本信息');
|
||||||
console.log(`name: ${packageJson.name}`);
|
logger.debug(`name: ${packageJson.name}`);
|
||||||
console.log(`version: ${packageJson.version}`);
|
logger.debug(`version: ${packageJson.version}`);
|
||||||
console.log(`total files: ${allFiles.length}`);
|
logger.debug(`total files: ${allFiles.length}`);
|
||||||
try {
|
try {
|
||||||
copyFilesToPackDist(filesToInclude, cwd, opts.packDist, mergeDist);
|
copyFilesToPackDist(filesToInclude, cwd, opts.packDist, mergeDist);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -188,7 +189,7 @@ const publishCommand = new Command('publish')
|
|||||||
console.log('发布逻辑实现', { key, version, config });
|
console.log('发布逻辑实现', { key, version, config });
|
||||||
});
|
});
|
||||||
|
|
||||||
const deployLoadFn = async (id: string, fileKey: string, force = false, install = false) => {
|
const deployLoadFn = async (id: string, fileKey: string, force = true, install = false) => {
|
||||||
if (!id) {
|
if (!id) {
|
||||||
console.error(chalk.red('id is required'));
|
console.error(chalk.red('id is required'));
|
||||||
return;
|
return;
|
||||||
@@ -222,7 +223,7 @@ const deployLoadFn = async (id: string, fileKey: string, force = false, install
|
|||||||
console.log('deploy-load success. current version:', res.data?.pkg?.version);
|
console.log('deploy-load success. current version:', res.data?.pkg?.version);
|
||||||
console.log('run: ', 'envision services -s', res.data?.showAppInfo?.key);
|
console.log('run: ', 'envision services -s', res.data?.showAppInfo?.key);
|
||||||
} else {
|
} else {
|
||||||
console.error('deploy-load failed', res.message);
|
console.error('deploy-load 失败', res.message);
|
||||||
}
|
}
|
||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
@@ -303,7 +304,7 @@ const packCommand = new Command('pack')
|
|||||||
if (yes) {
|
if (yes) {
|
||||||
deployCommand.push('-y', 'yes');
|
deployCommand.push('-y', 'yes');
|
||||||
}
|
}
|
||||||
console.log(chalk.blue('deploy doing: '), deployCommand.slice(2).join(' '), '\n');
|
logger.debug(chalk.blue('deploy doing: '), deployCommand.slice(2).join(' '), '\n');
|
||||||
// console.log('pack deploy services', chalk.blue('example: '), runDeployCommand);
|
// console.log('pack deploy services', chalk.blue('example: '), runDeployCommand);
|
||||||
|
|
||||||
program.parse(deployCommand);
|
program.parse(deployCommand);
|
||||||
@@ -312,11 +313,10 @@ const packCommand = new Command('pack')
|
|||||||
const packDeployCommand = new Command('pack-deploy')
|
const packDeployCommand = new Command('pack-deploy')
|
||||||
.argument('<id>', 'id')
|
.argument('<id>', 'id')
|
||||||
.option('-k, --key <key>', 'fileKey, 服务器的部署文件夹的列表')
|
.option('-k, --key <key>', 'fileKey, 服务器的部署文件夹的列表')
|
||||||
.option('-f --force', 'force')
|
|
||||||
.option('-i, --install ', 'install dependencies')
|
.option('-i, --install ', 'install dependencies')
|
||||||
.action(async (id, opts) => {
|
.action(async (id, opts) => {
|
||||||
let { force, key, install } = opts || {};
|
let { key, install } = opts || {};
|
||||||
const res = await deployLoadFn(id, key, force, install);
|
const res = await deployLoadFn(id, key, true, install);
|
||||||
});
|
});
|
||||||
|
|
||||||
program.addCommand(packDeployCommand);
|
program.addCommand(packDeployCommand);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
import { Config, SyncList, SyncConfigType } from './type.ts';
|
import { Config, SyncList, SyncConfigType, SyncConfig } from './type.ts';
|
||||||
import { fileIsExist } from '@/uitls/file.ts';
|
import { fileIsExist } from '@/uitls/file.ts';
|
||||||
import { getHash } from '@/uitls/hash.ts';
|
import { getHash } from '@/uitls/hash.ts';
|
||||||
import glob from 'fast-glob';
|
import glob from 'fast-glob';
|
||||||
@@ -32,6 +32,15 @@ export class SyncBase {
|
|||||||
this.baseURL = opts?.baseURL ?? '';
|
this.baseURL = opts?.baseURL ?? '';
|
||||||
this.init();
|
this.init();
|
||||||
}
|
}
|
||||||
|
get dir() {
|
||||||
|
return this.#dir;
|
||||||
|
}
|
||||||
|
get configFilename() {
|
||||||
|
return this.#filename;
|
||||||
|
}
|
||||||
|
get configPath() {
|
||||||
|
return path.join(this.#dir, this.#filename);
|
||||||
|
}
|
||||||
async init() {
|
async init() {
|
||||||
try {
|
try {
|
||||||
const dir = this.#dir;
|
const dir = this.#dir;
|
||||||
@@ -59,7 +68,8 @@ export class SyncBase {
|
|||||||
if (!filename) return false;
|
if (!filename) return false;
|
||||||
const dir = this.#dir;
|
const dir = this.#dir;
|
||||||
const file = path.join(dir, filename);
|
const file = path.join(dir, filename);
|
||||||
return { relative: path.relative(dir, file), absolute: file };
|
const realFilename = path.basename(filename);
|
||||||
|
return { relative: path.relative(dir, file), absolute: file, filename: realFilename };
|
||||||
}
|
}
|
||||||
async canDone(syncType: SyncConfigType, type?: SyncConfigType) {
|
async canDone(syncType: SyncConfigType, type?: SyncConfigType) {
|
||||||
if (syncType === 'sync') return true;
|
if (syncType === 'sync') return true;
|
||||||
@@ -120,38 +130,67 @@ export class SyncBase {
|
|||||||
return syncList;
|
return syncList;
|
||||||
}
|
}
|
||||||
async getCheckList() {
|
async getCheckList() {
|
||||||
const checkDir = this.config?.checkDir || {};
|
const checkDir = this.config?.clone || {};
|
||||||
const dirKeys = Object.keys(checkDir);
|
const dirKeys = Object.keys(checkDir);
|
||||||
|
const registry = this.config?.registry || '';
|
||||||
const files = dirKeys.map((key) => {
|
const files = dirKeys.map((key) => {
|
||||||
return { key, ...this.getRelativePath(key) };
|
return { key, ...this.getRelativePath(key) };
|
||||||
});
|
});
|
||||||
return files
|
return files
|
||||||
.map((item) => {
|
.map((item) => {
|
||||||
if (!item) return;
|
if (!item) return;
|
||||||
let auth = checkAuth(checkDir[item.key]?.url, this.baseURL);
|
let url = checkDir[item.key]?.url || registry;
|
||||||
|
let auth = checkAuth(url, this.baseURL);
|
||||||
return {
|
return {
|
||||||
key: item.key,
|
key: item.key,
|
||||||
...checkDir[item.key],
|
...checkDir[item.key],
|
||||||
|
url: url,
|
||||||
filepath: item?.absolute,
|
filepath: item?.absolute,
|
||||||
auth,
|
auth,
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.filter((item) => item);
|
.filter((item) => item);
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* sync 是已有的,优先级高于 fileSync
|
||||||
|
*
|
||||||
|
* @param sync
|
||||||
|
* @param fileSync
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
getMergeSync(sync: Config['sync'] = {}, fileSync: Config['sync'] = {}) {
|
getMergeSync(sync: Config['sync'] = {}, fileSync: Config['sync'] = {}) {
|
||||||
const syncFileSyncKeys = Object.keys(fileSync);
|
const syncFileSyncKeys = Object.keys(fileSync);
|
||||||
const syncKeys = Object.keys(sync);
|
const syncKeys = Object.keys(sync);
|
||||||
|
const config = this.config!;
|
||||||
|
const registry = config?.registry;
|
||||||
const keys = [...syncKeys, ...syncFileSyncKeys];
|
const keys = [...syncKeys, ...syncFileSyncKeys];
|
||||||
const obj: Config['sync'] = {};
|
const obj: Config['sync'] = {};
|
||||||
|
const wrapperRegistry = (value: SyncConfig | string) => {
|
||||||
|
if (typeof value === 'object') {
|
||||||
|
const url = value.url;
|
||||||
|
if (registry && !url.startsWith('http')) {
|
||||||
|
return {
|
||||||
|
...value,
|
||||||
|
url: registry.replace(/\/+$/g, '') + '/' + url.replace(/^\/+/g, ''),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
const url = value;
|
||||||
|
if (registry && !url.startsWith('http')) {
|
||||||
|
return registry.replace(/\/+$/g, '') + '/' + url.replace(/^\/+/g, '');
|
||||||
|
}
|
||||||
|
return url;
|
||||||
|
}
|
||||||
for (let key of keys) {
|
for (let key of keys) {
|
||||||
const value = sync[key] ?? fileSync[key];
|
const value = sync[key] ?? fileSync[key];
|
||||||
obj[key] = value;
|
obj[key] = wrapperRegistry(value);
|
||||||
}
|
}
|
||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
async getSyncDirectoryList() {
|
async getSyncDirectoryList() {
|
||||||
const config = this.config;
|
const config = this.config;
|
||||||
const syncDirectory = config?.syncDirectory || [];
|
const syncDirectory = config?.syncd || [];
|
||||||
let obj: Record<string, any> = {};
|
let obj: Record<string, any> = {};
|
||||||
const keys: string[] = [];
|
const keys: string[] = [];
|
||||||
for (let item of syncDirectory) {
|
for (let item of syncDirectory) {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export type SyncDirectory = {
|
|||||||
**/
|
**/
|
||||||
ignore?: string[];
|
ignore?: string[];
|
||||||
/**
|
/**
|
||||||
* 合并路径的源地址,https://kevisual.xiongxiao.me/root/ai/kevisual
|
* 合并路径的源地址,https://kevisual.cn/root/ai/kevisual
|
||||||
*/
|
*/
|
||||||
registry?: string;
|
registry?: string;
|
||||||
files?: string[];
|
files?: string[];
|
||||||
@@ -21,13 +21,13 @@ export type SyncDirectory = {
|
|||||||
export interface Config {
|
export interface Config {
|
||||||
name?: string; // 项目名称
|
name?: string; // 项目名称
|
||||||
version?: string; // 项目版本号
|
version?: string; // 项目版本号
|
||||||
registry?: string; // 项目仓库地址
|
registry?: string; // 当前模块的root
|
||||||
metadata?: Record<string, any>; // 元数据, 统一的配置
|
metadata?: Record<string, any>; // 元数据, 统一的配置
|
||||||
syncDirectory?: SyncDirectory[];
|
syncd?: SyncDirectory[];
|
||||||
sync?: {
|
sync?: {
|
||||||
[key: string]: SyncConfig | string;
|
[key: string]: SyncConfig | string;
|
||||||
};
|
};
|
||||||
checkDir?: {
|
clone?: {
|
||||||
[key: string]: {
|
[key: string]: {
|
||||||
url: string; // 需要检查的 url
|
url: string; // 需要检查的 url
|
||||||
replace?: Record<string, string>; // 替换的路径
|
replace?: Record<string, string>; // 替换的路径
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { program as app, Command } from '@/program.ts';
|
import { program as app, Command } from '@/program.ts';
|
||||||
import { SyncBase } from './modules/base.ts';
|
import { SyncBase } from './modules/base.ts';
|
||||||
import { baseURL, storage } from '@/module/query.ts';
|
import { baseURL, query, storage } from '@/module/query.ts';
|
||||||
import { fetchLink, fetchAiList } from '@/module/download/install.ts';
|
import { fetchLink, fetchAiList } from '@/module/download/install.ts';
|
||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
import { upload } from '@/module/download/upload.ts';
|
import { upload } from '@/module/download/upload.ts';
|
||||||
import { logger } from '@/module/logger.ts';
|
import { logger, printClickableLink } from '@/module/logger.ts';
|
||||||
import { chalk } from '@/module/chalk.ts';
|
import { chalk } from '@/module/chalk.ts';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { fileIsExist } from '@/uitls/file.ts';
|
import { fileIsExist } from '@/uitls/file.ts';
|
||||||
@@ -124,7 +124,9 @@ const syncList = new Command('list')
|
|||||||
syncList.forEach((item) => {
|
syncList.forEach((item) => {
|
||||||
if (opts.all) {
|
if (opts.all) {
|
||||||
logger.info(item);
|
logger.info(item);
|
||||||
} else logger.info(chalk.blue(item.key), chalk.gray(item.type), chalk.green(item.url));
|
} else {
|
||||||
|
logger.info(chalk.green(printClickableLink({ url: item.url, text: item.key, print: false })), chalk.gray(item.type));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
const syncCreateList = new Command('create')
|
const syncCreateList = new Command('create')
|
||||||
@@ -154,16 +156,29 @@ const syncCreateList = new Command('create')
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const checkDir = new Command('check')
|
const clone = new Command('clone')
|
||||||
.option('-d --dir <dir>', '配置目录')
|
.option('-d --dir <dir>', '配置目录')
|
||||||
.option('-c --config <config>', '配置文件的名字', 'kevisual.json')
|
.option('-c --config <config>', '配置文件的名字', 'kevisual.json')
|
||||||
|
.option('-i --link <link>', '克隆链接, 比 kevisual.json 优先级更高')
|
||||||
.description('检查目录')
|
.description('检查目录')
|
||||||
.action(async (opts) => {
|
.action(async (opts) => {
|
||||||
|
const link = opts.link || '';
|
||||||
const sync = new SyncBase({ dir: opts.dir, baseURL: baseURL, configFilename: opts.config });
|
const sync = new SyncBase({ dir: opts.dir, baseURL: baseURL, configFilename: opts.config });
|
||||||
|
if (link) {
|
||||||
|
const res = await query.fetchText(link);
|
||||||
|
if (res.code === 200) {
|
||||||
|
fs.writeFileSync(sync.configPath, JSON.stringify(res.data, null, 2));
|
||||||
|
} else {
|
||||||
|
logger.error('下载配置文件失败', link, res);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await sync.init()
|
||||||
|
}
|
||||||
const syncList = await sync.getSyncList();
|
const syncList = await sync.getSyncList();
|
||||||
logger.debug(syncList);
|
logger.debug(syncList);
|
||||||
logger.info('检查目录\n');
|
logger.info('检查目录\n');
|
||||||
const checkList = await sync.getCheckList();
|
const checkList = await sync.getCheckList();
|
||||||
|
logger.info('检查列表', checkList);
|
||||||
for (const item of checkList) {
|
for (const item of checkList) {
|
||||||
if (!item.auth) {
|
if (!item.auth) {
|
||||||
continue;
|
continue;
|
||||||
@@ -184,7 +199,19 @@ const checkDir = new Command('check')
|
|||||||
const matchList = matchObjectList
|
const matchList = matchObjectList
|
||||||
.map((item2) => {
|
.map((item2) => {
|
||||||
const rp = sync.getRelativePath(item2.pathname);
|
const rp = sync.getRelativePath(item2.pathname);
|
||||||
|
|
||||||
if (!rp) return false;
|
if (!rp) return false;
|
||||||
|
if (rp.absolute.endsWith('gitignore.txt')) {
|
||||||
|
// 修改为 .gitignore
|
||||||
|
const newPath = rp.absolute.replace('gitignore.txt', '.gitignore');
|
||||||
|
rp.absolute = newPath;
|
||||||
|
rp.relative = path.relative(sync.dir, newPath);
|
||||||
|
} else if (rp.absolute.endsWith('.dot')) {
|
||||||
|
const filename = path.basename(rp.absolute, '.dot');
|
||||||
|
const newPath = path.join(path.dirname(rp.absolute), `.${filename}`);
|
||||||
|
rp.absolute = newPath;
|
||||||
|
rp.relative = path.relative(sync.dir, newPath);
|
||||||
|
}
|
||||||
return { ...item2, relative: rp.relative, absolute: rp.absolute };
|
return { ...item2, relative: rp.relative, absolute: rp.absolute };
|
||||||
})
|
})
|
||||||
.filter((i) => i);
|
.filter((i) => i);
|
||||||
@@ -226,6 +253,6 @@ command.addCommand(syncUpload);
|
|||||||
command.addCommand(syncDownload);
|
command.addCommand(syncDownload);
|
||||||
command.addCommand(syncList);
|
command.addCommand(syncList);
|
||||||
command.addCommand(syncCreateList);
|
command.addCommand(syncCreateList);
|
||||||
command.addCommand(checkDir);
|
command.addCommand(clone);
|
||||||
|
|
||||||
app.addCommand(command);
|
app.addCommand(command);
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ import './command/app/index.ts';
|
|||||||
|
|
||||||
import './command/gist/index.ts';
|
import './command/gist/index.ts';
|
||||||
import './command/config-remote.ts';
|
import './command/config-remote.ts';
|
||||||
|
import './command/config-secret-remote.ts';
|
||||||
|
import './command/ai.ts';
|
||||||
// program.parse(process.argv);
|
// program.parse(process.argv);
|
||||||
|
|
||||||
export const runParser = async (argv: string[]) => {
|
export const runParser = async (argv: string[]) => {
|
||||||
|
|||||||
@@ -1,6 +1,15 @@
|
|||||||
import { Logger } from '@kevisual/logger/node';
|
import { Logger } from '@kevisual/logger/node';
|
||||||
|
|
||||||
const level = process.env.LOG_LEVEL || 'info';
|
const level = process.env.LOG_LEVEL || 'info';
|
||||||
export const logger = new Logger({
|
export const logger = new Logger({
|
||||||
level: level as any,
|
level: level as any,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export function printClickableLink({ url, text, print = true }: { url: string; text: string, print?: boolean }) {
|
||||||
|
const escape = '\x1B'; // ESC 字符
|
||||||
|
const linkStart = `${escape}]8;;${url}${escape}\\`;
|
||||||
|
const linkEnd = `${escape}]8;;${escape}\\`;
|
||||||
|
if (print) {
|
||||||
|
console.log(`${linkStart}${text}${linkEnd}`);
|
||||||
|
}
|
||||||
|
return `${linkStart}${text}${linkEnd}`;
|
||||||
|
}
|
||||||
|
|||||||
65
src/query/query-secret/query-secret.ts
Normal file
65
src/query/query-secret/query-secret.ts
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
/**
|
||||||
|
* 配置查询
|
||||||
|
* @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