Compare commits
7 Commits
327f115ef2
...
2cb12644ea
| Author | SHA1 | Date | |
|---|---|---|---|
| 2cb12644ea | |||
| e1b86aa809 | |||
| 2f79925e3d | |||
| 0b5a0557ee | |||
| b9b4c993f4 | |||
| 99f01e2b94 | |||
| cc043bfd7e |
44
.cnb.yml
Normal file
44
.cnb.yml
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
# .cnb.yml
|
||||||
|
include:
|
||||||
|
- https://cnb.cool/kevisual/cnb/-/blob/main/.cnb/template.yml
|
||||||
|
|
||||||
|
.common_env: &common_env
|
||||||
|
env:
|
||||||
|
TO_REPO: kevisual/cli
|
||||||
|
TO_URL: git.xiongxiao.me
|
||||||
|
imports:
|
||||||
|
- https://cnb.cool/kevisual/env/-/blob/main/.env.development
|
||||||
|
|
||||||
|
$:
|
||||||
|
vscode:
|
||||||
|
- docker:
|
||||||
|
image: docker.cnb.cool/kevisual/dev-env:latest
|
||||||
|
services:
|
||||||
|
- vscode
|
||||||
|
- docker
|
||||||
|
imports: !reference [.common_env, imports]
|
||||||
|
# 开发环境启动后会执行的任务
|
||||||
|
# stages:
|
||||||
|
# - name: pnpm install
|
||||||
|
# script: pnpm install
|
||||||
|
stages: !reference [.dev_tempalte, stages]
|
||||||
|
|
||||||
|
.common_sync_to_gitea: &common_sync_to_gitea
|
||||||
|
- <<: *common_env
|
||||||
|
services: !reference [.common_sync_to_gitea_template, services]
|
||||||
|
stages: !reference [.common_sync_to_gitea_template, stages]
|
||||||
|
|
||||||
|
.common_sync_from_gitea: &common_sync_from_gitea
|
||||||
|
- <<: *common_env
|
||||||
|
services: !reference [.common_sync_from_gitea_template, services]
|
||||||
|
stages: !reference [.common_sync_from_gitea_template, stages]
|
||||||
|
|
||||||
|
main:
|
||||||
|
web_trigger_sync_to_gitea:
|
||||||
|
- <<: *common_sync_to_gitea
|
||||||
|
web_trigger_sync_from_gitea:
|
||||||
|
- <<: *common_sync_from_gitea
|
||||||
|
api_trigger_sync_to_gitea:
|
||||||
|
- <<: *common_sync_to_gitea
|
||||||
|
api_trigger_sync_from_gitea:
|
||||||
|
- <<: *common_sync_from_gitea
|
||||||
11
.cnb/web_trigger.yml
Normal file
11
.cnb/web_trigger.yml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
# .cnb/web_trigger.yml
|
||||||
|
branch:
|
||||||
|
# 如下按钮在分支名以 release 开头的分支详情页面显示
|
||||||
|
- reg: "^main"
|
||||||
|
buttons:
|
||||||
|
- name: 同步代码到gitea
|
||||||
|
description: 同步代码到gitea
|
||||||
|
event: web_trigger_sync_to_gitea
|
||||||
|
- name: 同步gitea代码到当前仓库
|
||||||
|
description: 同步gitea代码到当前仓库
|
||||||
|
event: web_trigger_sync_from_gitea
|
||||||
1
.npmrc
1
.npmrc
@@ -1,2 +1,3 @@
|
|||||||
//npm.xiongxiao.me/:_authToken=${ME_NPM_TOKEN}
|
//npm.xiongxiao.me/:_authToken=${ME_NPM_TOKEN}
|
||||||
|
//npm.cnb.cool/kevisual/registry/-/packages/:_authToken=${CNB_API_KEY}
|
||||||
//registry.npmjs.org/:_authToken=${NPM_TOKEN}
|
//registry.npmjs.org/:_authToken=${NPM_TOKEN}
|
||||||
4
assistant/.gitignore
vendored
4
assistant/.gitignore
vendored
@@ -10,4 +10,6 @@ assistant-app
|
|||||||
.env*
|
.env*
|
||||||
!.env*example
|
!.env*example
|
||||||
|
|
||||||
libs
|
libs
|
||||||
|
|
||||||
|
cache-file
|
||||||
@@ -6,7 +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'];
|
const external = ['pm2', '@kevisual/hot-api', '@nut-tree-fork/nut-js', 'bun'];
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {string} p
|
* @param {string} p
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@kevisual/assistant-cli",
|
"name": "@kevisual/assistant-cli",
|
||||||
"version": "0.0.6",
|
"version": "0.0.7",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "dist/assistant.mjs",
|
"main": "dist/assistant.mjs",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
@@ -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.26.2",
|
"packageManager": "pnpm@10.28.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"files": [
|
"files": [
|
||||||
"dist",
|
"dist",
|
||||||
@@ -41,18 +41,18 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@kevisual/ai": "^0.0.19",
|
"@kevisual/ai": "^0.0.20",
|
||||||
"@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.33",
|
"@kevisual/query": "0.0.35",
|
||||||
"@kevisual/query-login": "0.0.7",
|
"@kevisual/query-login": "0.0.7",
|
||||||
"@kevisual/router": "^0.0.52",
|
"@kevisual/router": "^0.0.55",
|
||||||
"@kevisual/types": "^0.0.10",
|
"@kevisual/types": "^0.0.11",
|
||||||
"@kevisual/use-config": "^1.0.21",
|
"@kevisual/use-config": "^1.0.28",
|
||||||
"@types/bun": "^1.3.5",
|
"@types/bun": "^1.3.6",
|
||||||
"@types/lodash-es": "^4.17.12",
|
"@types/lodash-es": "^4.17.12",
|
||||||
"@types/node": "^25.0.3",
|
"@types/node": "^25.0.9",
|
||||||
"@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,7 +61,7 @@
|
|||||||
"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.1.0",
|
"inquirer": "^13.2.0",
|
||||||
"lodash-es": "^4.17.22",
|
"lodash-es": "^4.17.22",
|
||||||
"nanoid": "^5.1.6",
|
"nanoid": "^5.1.6",
|
||||||
"send": "^1.2.1",
|
"send": "^1.2.1",
|
||||||
@@ -76,7 +76,7 @@
|
|||||||
"access": "public"
|
"access": "public"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-s3": "^3.969.0",
|
"@aws-sdk/client-s3": "^3.971.0",
|
||||||
"@kevisual/ha-api": "^0.0.6",
|
"@kevisual/ha-api": "^0.0.6",
|
||||||
"@kevisual/oss": "^0.0.16",
|
"@kevisual/oss": "^0.0.16",
|
||||||
"@kevisual/video-tools": "^0.0.13",
|
"@kevisual/video-tools": "^0.0.13",
|
||||||
@@ -84,6 +84,6 @@
|
|||||||
"lowdb": "^7.0.1",
|
"lowdb": "^7.0.1",
|
||||||
"lru-cache": "^11.2.4",
|
"lru-cache": "^11.2.4",
|
||||||
"pm2": "^6.0.14",
|
"pm2": "^6.0.14",
|
||||||
"unstorage": "^1.17.3"
|
"unstorage": "^1.17.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,19 @@
|
|||||||
# assistant cli
|
# assistant cli
|
||||||
|
|
||||||
## 初始化路径
|
## 安装
|
||||||
|
|
||||||
## 启动服务
|
```bash
|
||||||
|
npm i -g @kevisual/cli --registry=https://npm.cnb.cool/kevisual/registry/-/packages/
|
||||||
|
```
|
||||||
|
## 环境变量配置项
|
||||||
|
|
||||||
## 配置
|
- ASSISTANT_CONFIG_DIR
|
||||||
|
- 说明:指定 assistant 的配置文件目录
|
||||||
|
- 示例:`export ASSISTANT_CONFIG_DIR=/path/to/your/config/dir`
|
||||||
|
|
||||||
|
|
||||||
|
## 启动命令
|
||||||
|
|
||||||
|
```bash
|
||||||
|
asst server -s -p 8686
|
||||||
|
```
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { App } from '@kevisual/router';
|
import { App } from '@kevisual/router';
|
||||||
|
import { SimpleRouter } from '@kevisual/router/simple'
|
||||||
// import { App } from '@kevisual/router/src/app.ts';
|
// import { App } from '@kevisual/router/src/app.ts';
|
||||||
import { HttpsPem } from '@/module/assistant/https/sign.ts';
|
|
||||||
// import { AssistantConfig } from '@/module/assistant/index.ts';
|
// import { AssistantConfig } from '@/module/assistant/index.ts';
|
||||||
import { AssistantInit, parseHomeArg } from '@/services/init/index.ts';
|
import { AssistantInit, parseHomeArg } from '@/services/init/index.ts';
|
||||||
import { configDir as HomeConfigDir } from '@/module/assistant/config/index.ts';
|
import { configDir as HomeConfigDir } from '@/module/assistant/config/index.ts';
|
||||||
@@ -21,8 +21,6 @@ export const assistantQuery = useContextKey('assistantQuery', () => {
|
|||||||
return new AssistantQuery(assistantConfig);
|
return new AssistantQuery(assistantConfig);
|
||||||
});
|
});
|
||||||
|
|
||||||
const httpsPem = new HttpsPem(assistantConfig);
|
|
||||||
|
|
||||||
export const runtime = useContextKey('runtime', () => {
|
export const runtime = useContextKey('runtime', () => {
|
||||||
return {
|
return {
|
||||||
type: 'client',
|
type: 'client',
|
||||||
@@ -32,36 +30,22 @@ export const runtime = useContextKey('runtime', () => {
|
|||||||
export const app: App = useContextKey<App>('app', () => {
|
export const app: App = useContextKey<App>('app', () => {
|
||||||
const init = isInit;
|
const init = isInit;
|
||||||
if (init) {
|
if (init) {
|
||||||
const config = assistantConfig.getConfig();
|
return new App({
|
||||||
|
serverOptions: {
|
||||||
if (config?.https?.type !== 'https') {
|
path: '/client/router',
|
||||||
console.log('http模式', 'http');
|
httpType: 'http',
|
||||||
return new App({
|
cors: {
|
||||||
serverOptions: {
|
origin: '*',
|
||||||
path: '/client/router',
|
|
||||||
httpType: 'http',
|
|
||||||
cors: {
|
|
||||||
origin: '*',
|
|
||||||
},
|
|
||||||
io: true
|
|
||||||
},
|
},
|
||||||
});
|
io: true
|
||||||
}
|
|
||||||
}
|
|
||||||
return new App({
|
|
||||||
serverOptions: {
|
|
||||||
path: '/client/router',
|
|
||||||
httpType: 'https',
|
|
||||||
httpsCert: httpsPem.cert,
|
|
||||||
httpsKey: httpsPem.key,
|
|
||||||
cors: {
|
|
||||||
origin: '*',
|
|
||||||
},
|
},
|
||||||
io: true
|
});
|
||||||
},
|
}
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const simpleRouter = useContextKey('simpleRouter', () => {
|
||||||
|
return new SimpleRouter();
|
||||||
|
});
|
||||||
|
|
||||||
app.route({
|
app.route({
|
||||||
path: 'router',
|
path: 'router',
|
||||||
|
|||||||
@@ -155,15 +155,6 @@ export type AssistantConfigData = {
|
|||||||
* share 是对外共享 pages 目录下的页面
|
* share 是对外共享 pages 目录下的页面
|
||||||
*/
|
*/
|
||||||
auth?: AuthPermission;
|
auth?: AuthPermission;
|
||||||
/**
|
|
||||||
* HTTPS 证书配置, 启用后,助手服务会启用 HTTPS 服务, 默认 HTTP
|
|
||||||
* 理论上也不需要https,因为可以通过反向代理实现https
|
|
||||||
*/
|
|
||||||
https?: {
|
|
||||||
type?: 'https' | 'http';
|
|
||||||
keyPath?: string; // 证书私钥路径
|
|
||||||
certPath?: string; // 证书路径
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
let assistantConfig: AssistantConfigData;
|
let assistantConfig: AssistantConfigData;
|
||||||
type AssistantConfigOptions = {
|
type AssistantConfigOptions = {
|
||||||
|
|||||||
@@ -1,180 +0,0 @@
|
|||||||
import { createCert } from '@kevisual/router/sign';
|
|
||||||
import path from 'node:path';
|
|
||||||
import fs from 'node:fs';
|
|
||||||
import { AssistantConfig } from '../config/index.ts';
|
|
||||||
import { checkFileExists } from '../file/index.ts';
|
|
||||||
import { chalk } from '@/module/chalk.ts';
|
|
||||||
import dayjs from 'dayjs';
|
|
||||||
|
|
||||||
type Attributes = {
|
|
||||||
name: string;
|
|
||||||
value: string;
|
|
||||||
};
|
|
||||||
type AltNames = {
|
|
||||||
type: number;
|
|
||||||
value?: string;
|
|
||||||
ip?: string;
|
|
||||||
};
|
|
||||||
export class HttpsPem {
|
|
||||||
assistantConfig: AssistantConfig;
|
|
||||||
key: string;
|
|
||||||
cert: string;
|
|
||||||
isHttps = false;
|
|
||||||
constructor(assistantConfig: AssistantConfig) {
|
|
||||||
this.assistantConfig = assistantConfig;
|
|
||||||
this.#initKeyCert();
|
|
||||||
}
|
|
||||||
#initKeyCert() {
|
|
||||||
this.assistantConfig.checkMounted();
|
|
||||||
const config = this.assistantConfig.getConfig();
|
|
||||||
if (config.https) {
|
|
||||||
const httpsType = config.https?.type || 'https';
|
|
||||||
if (httpsType !== 'https') {
|
|
||||||
// console.log(chalk.yellow('当前配置文件 https.type 不是 https, 不使用证书'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.isHttps = true;
|
|
||||||
if (config.https.keyPath) {
|
|
||||||
const keyPath = config.https.keyPath;
|
|
||||||
const certPath = config.https.certPath;
|
|
||||||
if (checkFileExists(keyPath) && checkFileExists(certPath)) {
|
|
||||||
this.key = fs.readFileSync(keyPath, 'utf-8');
|
|
||||||
this.cert = fs.readFileSync(certPath, 'utf-8');
|
|
||||||
console.log(chalk.green('使用配置文件 https.keyPath 和 https.certPath 的证书'), keyPath, certPath);
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
console.log(chalk.red('证书路径不存在,请检查配置文件 https.keyPath 和 https.certPath 是否正确'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if(!this.isHttps) return;
|
|
||||||
const { key, cert } = this.getCert();
|
|
||||||
this.key = key;
|
|
||||||
this.cert = cert;
|
|
||||||
}
|
|
||||||
getPemDir() {
|
|
||||||
const configDir = this.assistantConfig.configPath?.configDir || process.cwd();
|
|
||||||
const pemDir = path.join(configDir, 'pem');
|
|
||||||
if (!checkFileExists(pemDir)) {
|
|
||||||
fs.mkdirSync(pemDir, { recursive: true });
|
|
||||||
}
|
|
||||||
return pemDir;
|
|
||||||
}
|
|
||||||
getCert() {
|
|
||||||
if (!this.assistantConfig.init) return;
|
|
||||||
const pemDir = this.getPemDir();
|
|
||||||
const pemPath = {
|
|
||||||
key: path.join(pemDir, 'https-private-key.pem'),
|
|
||||||
cert: path.join(pemDir, 'https-cert.pem'),
|
|
||||||
config: path.join(pemDir, 'https-config.json'),
|
|
||||||
};
|
|
||||||
const writeCreate = (opts: { key: string; cert: string; data: { createTime: number; expireTime: number } }) => {
|
|
||||||
fs.writeFileSync(pemPath.key, opts.key);
|
|
||||||
fs.writeFileSync(pemPath.cert, opts.cert);
|
|
||||||
fs.writeFileSync(pemPath.config, JSON.stringify(opts.data, null, 2));
|
|
||||||
};
|
|
||||||
if (!checkFileExists(pemPath.key) || !checkFileExists(pemPath.cert)) {
|
|
||||||
const { key, cert, data } = this.createCert();
|
|
||||||
writeCreate({ key, cert, data });
|
|
||||||
console.log(chalk.green('证书创建成功,浏览器需要导入当前证书'));
|
|
||||||
return {
|
|
||||||
key,
|
|
||||||
cert,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!checkFileExists(pemPath.config)) {
|
|
||||||
const data = this.createExpireData();
|
|
||||||
fs.writeFileSync(pemPath.config, JSON.stringify(data, null, 2));
|
|
||||||
}
|
|
||||||
const key = fs.readFileSync(pemPath.key, 'utf-8');
|
|
||||||
const cert = fs.readFileSync(pemPath.cert, 'utf-8');
|
|
||||||
const config = fs.readFileSync(pemPath.config, 'utf-8');
|
|
||||||
let expireTime = 0;
|
|
||||||
try {
|
|
||||||
const data = JSON.parse(config);
|
|
||||||
expireTime = data.expireTime;
|
|
||||||
if (typeof expireTime !== 'number') {
|
|
||||||
throw new Error('expireTime is not a number');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log(chalk.red('证书配置文件损坏,重新生成证书'));
|
|
||||||
}
|
|
||||||
const now = new Date().getTime();
|
|
||||||
if (now > expireTime) {
|
|
||||||
this.removeCert();
|
|
||||||
const { key, cert, data } = this.createCert();
|
|
||||||
writeCreate({ key, cert, data });
|
|
||||||
console.log(chalk.green('证书更新成功, 浏览器需要重新导入当前证书'));
|
|
||||||
return {
|
|
||||||
key,
|
|
||||||
cert,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
key,
|
|
||||||
cert,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
createExpireData() {
|
|
||||||
const expireTime = new Date().getTime() + 365 * 24 * 60 * 60 * 1000;
|
|
||||||
const expireDate = dayjs(expireTime).format('YYYY-MM-DD HH:mm:ss');
|
|
||||||
return {
|
|
||||||
description: '手动导入证书到浏览器, https-cert.pem文件, 具体使用教程访问 https://kevisual.cn/root/pem-docs/',
|
|
||||||
createTime: new Date().getTime(),
|
|
||||||
expireDate,
|
|
||||||
expireTime,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
/*
|
|
||||||
* 重新生成证书
|
|
||||||
*/
|
|
||||||
removeCert() {
|
|
||||||
const pemDir = this.getPemDir();
|
|
||||||
const pemPath = {
|
|
||||||
key: path.join(pemDir, 'https-private-key.pem'),
|
|
||||||
cert: path.join(pemDir, 'https-cert.pem'),
|
|
||||||
};
|
|
||||||
const oldPath = {
|
|
||||||
key: path.join(pemDir, 'https-private-key.pem.bak'),
|
|
||||||
cert: path.join(pemDir, 'https-cert.pem.bak'),
|
|
||||||
};
|
|
||||||
if (checkFileExists(pemPath.key)) {
|
|
||||||
fs.renameSync(pemPath.key, oldPath.key);
|
|
||||||
}
|
|
||||||
if (checkFileExists(pemPath.cert)) {
|
|
||||||
fs.renameSync(pemPath.cert, oldPath.cert);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/*
|
|
||||||
* 创建证书
|
|
||||||
* @param attrs 证书属性
|
|
||||||
* @param altNames 证书备用名称
|
|
||||||
*/
|
|
||||||
createCert(attrs?: Attributes[], altNames?: AltNames[]) {
|
|
||||||
const attributes = attrs || [];
|
|
||||||
const altNamesList = altNames || [];
|
|
||||||
const { key, cert } = createCert(
|
|
||||||
[
|
|
||||||
{
|
|
||||||
name: 'commonName',
|
|
||||||
value: 'localhost',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'organizationName',
|
|
||||||
value: 'kevisual',
|
|
||||||
},
|
|
||||||
...attributes,
|
|
||||||
],
|
|
||||||
altNamesList,
|
|
||||||
);
|
|
||||||
const data = this.createExpireData();
|
|
||||||
return {
|
|
||||||
key,
|
|
||||||
cert,
|
|
||||||
data: {
|
|
||||||
...data,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,6 @@ import * as http from 'http';
|
|||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import { isBun } from './utils.ts';
|
import { isBun } from './utils.ts';
|
||||||
import { Readable } from 'stream';
|
import { Readable } from 'stream';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 文件流管道传输函数
|
* 文件流管道传输函数
|
||||||
* 将指定文件的内容通过流的方式传输给客户端响应
|
* 将指定文件的内容通过流的方式传输给客户端响应
|
||||||
@@ -112,4 +111,17 @@ export const pipeProxyReq = async (req: http.IncomingMessage, proxyReq: http.Cli
|
|||||||
// Node.js标准环境下直接使用pipe进行流传输
|
// Node.js标准环境下直接使用pipe进行流传输
|
||||||
req.pipe(proxyReq, { end: true });
|
req.pipe(proxyReq, { end: true });
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const pipeBusboy = async (req: http.IncomingMessage, res: http.ServerResponse, busboy: any) => {
|
||||||
|
if (isBun) {
|
||||||
|
// @ts-ignore
|
||||||
|
const bunRequest = req.bun.request;
|
||||||
|
const arrayBuffer = await bunRequest.arrayBuffer();
|
||||||
|
const buffer = Buffer.from(arrayBuffer);
|
||||||
|
busboy.end(buffer);
|
||||||
|
} else {
|
||||||
|
req.pipe(busboy);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
22
assistant/src/module/get-header-token.ts
Normal file
22
assistant/src/module/get-header-token.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import http from 'http';
|
||||||
|
import * as cookie from '@kevisual/router/src/server/cookie.ts';
|
||||||
|
|
||||||
|
export const getTokenFromRequest = (req: http.IncomingMessage) => {
|
||||||
|
let token = (req.headers?.['authorization'] as string) || (req.headers?.['Authorization'] as string) || '';
|
||||||
|
const url = new URL(req.url || '', 'http://localhost');
|
||||||
|
if (!token) {
|
||||||
|
token = url.searchParams.get('token') || '';
|
||||||
|
}
|
||||||
|
if (!token) {
|
||||||
|
const parsedCookies = cookie.parse(req.headers.cookie || '');
|
||||||
|
token = parsedCookies.token || '';
|
||||||
|
}
|
||||||
|
if (token) {
|
||||||
|
token = token.replace('Bearer ', '');
|
||||||
|
}
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getTokenFromContext = (ctx: any) => {
|
||||||
|
return ctx.query.token;
|
||||||
|
}
|
||||||
32
assistant/src/module/upload/mv.ts
Normal file
32
assistant/src/module/upload/mv.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import type { AssistantConfig } from '@/module/assistant/index.ts';
|
||||||
|
import fs from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
export class UploadManager {
|
||||||
|
config: AssistantConfig;
|
||||||
|
constructor(config: AssistantConfig) {
|
||||||
|
this.config = config;
|
||||||
|
}
|
||||||
|
mvFile(opts: {
|
||||||
|
temppath: string;
|
||||||
|
type: 'file' | 's3';
|
||||||
|
targetPath: string;
|
||||||
|
}) {
|
||||||
|
const { type, temppath, targetPath } = opts;
|
||||||
|
if (type === 'file') {
|
||||||
|
const pageDir = this.config.configPath?.pagesDir!;
|
||||||
|
const fullTargetPath = targetPath.startsWith('/')
|
||||||
|
? targetPath
|
||||||
|
: path.join(pageDir, targetPath);
|
||||||
|
const targetDir = fullTargetPath.substring(0, fullTargetPath.lastIndexOf('/'));
|
||||||
|
if (!fs.existsSync(targetDir)) {
|
||||||
|
fs.mkdirSync(targetDir, { recursive: true });
|
||||||
|
}
|
||||||
|
fs.renameSync(temppath, fullTargetPath);
|
||||||
|
return fullTargetPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const uploadManager = (config: AssistantConfig) => {
|
||||||
|
return new UploadManager(config);
|
||||||
|
}
|
||||||
32
assistant/src/routes-simple/events.ts
Normal file
32
assistant/src/routes-simple/events.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { simpleRouter, clients, getTaskId, writeEvents, deleteOldClients, error } from './router.ts';
|
||||||
|
|
||||||
|
simpleRouter.get('/client/events', async (req, res) => {
|
||||||
|
res.writeHead(200, {
|
||||||
|
'Content-Type': 'text/event-stream',
|
||||||
|
'Cache-Control': 'no-cache',
|
||||||
|
Connection: 'keep-alive',
|
||||||
|
});
|
||||||
|
const taskId = getTaskId(req);
|
||||||
|
if (!taskId) {
|
||||||
|
res.end(error('task-id is required'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 将客户端连接推送到 clients 数组
|
||||||
|
clients.set(taskId, { client: res, createTime: Date.now() });
|
||||||
|
writeEvents(req, { progress: 0, message: 'start' })
|
||||||
|
// 移除客户端连接
|
||||||
|
req.on('close', () => {
|
||||||
|
clients.delete(taskId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
simpleRouter.get('/client/events/close', async (req, res) => {
|
||||||
|
const taskId = getTaskId(req);
|
||||||
|
if (!taskId) {
|
||||||
|
res.end(error('task-id is required'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
deleteOldClients();
|
||||||
|
clients.delete(taskId);
|
||||||
|
res.end('ok');
|
||||||
|
});
|
||||||
1
assistant/src/routes-simple/index.ts
Normal file
1
assistant/src/routes-simple/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './upload.ts'
|
||||||
87
assistant/src/routes-simple/router.ts
Normal file
87
assistant/src/routes-simple/router.ts
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import { simpleRouter } from '@/app.ts';
|
||||||
|
import http from 'http';
|
||||||
|
import { useContextKey } from '@kevisual/context';
|
||||||
|
import { useFileStore } from '@kevisual/use-config';
|
||||||
|
export { simpleRouter };
|
||||||
|
|
||||||
|
export const cacheFilePath = useFileStore('cache-file', { needExists: true });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 事件客户端
|
||||||
|
*/
|
||||||
|
const eventClientsInit = () => {
|
||||||
|
const clients = new Map<string, { client?: http.ServerResponse; createTime?: number;[key: string]: any }>();
|
||||||
|
return clients;
|
||||||
|
};
|
||||||
|
export const clients = useContextKey('event-clients', () => eventClientsInit());
|
||||||
|
/**
|
||||||
|
* 获取 task-id
|
||||||
|
* @param req
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const getTaskId = (req: http.IncomingMessage) => {
|
||||||
|
const url = new URL(req.url || '', 'http://localhost');
|
||||||
|
const taskId = url.searchParams.get('taskId');
|
||||||
|
if (taskId) {
|
||||||
|
return taskId;
|
||||||
|
}
|
||||||
|
return req.headers['task-id'] as string;
|
||||||
|
};
|
||||||
|
type EventData = {
|
||||||
|
progress: number | string;
|
||||||
|
message: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 写入事件
|
||||||
|
* @param req
|
||||||
|
* @param data
|
||||||
|
*/
|
||||||
|
export const writeEvents = (req: http.IncomingMessage, data: EventData) => {
|
||||||
|
const taskId = getTaskId(req);
|
||||||
|
if (taskId) {
|
||||||
|
const client = clients.get(taskId)?.client;
|
||||||
|
if (client) {
|
||||||
|
client.write(`data: ${JSON.stringify(data)}\n\n`);
|
||||||
|
}
|
||||||
|
if (Number(data.progress) === 100) {
|
||||||
|
clients.delete(taskId);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('taskId is remove.', taskId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* 查找超出2个小时的clients,都删除了
|
||||||
|
*/
|
||||||
|
export const deleteOldClients = () => {
|
||||||
|
const now = Date.now();
|
||||||
|
for (const [taskId, client] of clients) {
|
||||||
|
// 如果创建时间超过2个小时,则删除
|
||||||
|
if (now - client.createTime > 1000 * 60 * 60 * 2) {
|
||||||
|
clients.delete(taskId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* 解析表单数据, 如果表单数据是数组, 则取第一个,appKey, version, username 等
|
||||||
|
* @param fields 表单数据
|
||||||
|
* @param parseKeys 需要解析的键
|
||||||
|
* @returns 解析后的数据
|
||||||
|
*/
|
||||||
|
export const getKey = (fields: Record<string, any>, parseKeys: string[]) => {
|
||||||
|
let value: Record<string, any> = {};
|
||||||
|
for (const key of parseKeys) {
|
||||||
|
const v = fields[key];
|
||||||
|
if (Array.isArray(v)) {
|
||||||
|
value[key] = v[0];
|
||||||
|
} else {
|
||||||
|
value[key] = v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const error = (msg: string, code = 500) => {
|
||||||
|
return JSON.stringify({ code, message: msg });
|
||||||
|
};
|
||||||
179
assistant/src/routes-simple/upload.ts
Normal file
179
assistant/src/routes-simple/upload.ts
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
|
||||||
|
import Busboy from 'busboy';
|
||||||
|
import { assistantConfig, simpleRouter } from '../app.ts'
|
||||||
|
import http from 'http';
|
||||||
|
import path from 'path';
|
||||||
|
import fs from 'fs';
|
||||||
|
|
||||||
|
import { checkAuth } from '@/routes/index.ts';
|
||||||
|
import { getTokenFromRequest } from '@/module/get-header-token.ts';
|
||||||
|
import { pipeBusboy } from '@/module/assistant/proxy/pipe.ts';
|
||||||
|
import { logger } from '@/module/logger.ts';
|
||||||
|
import { cacheFilePath, getKey, writeEvents } from './router.ts';
|
||||||
|
import { getContentType } from '@kevisual/oss/services';
|
||||||
|
import { validateDirectory } from './utils.ts';
|
||||||
|
import { UploadManager } from '@/module/upload/mv.ts';
|
||||||
|
simpleRouter.get('/client/upload', async (req, res) => {
|
||||||
|
if (res.headersSent) return; // 如果响应已发送,不再处理
|
||||||
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({ message: 'Upload endpoint reached' }));
|
||||||
|
})
|
||||||
|
export const error = (msg: string, code = 500) => {
|
||||||
|
return JSON.stringify({ code, message: msg });
|
||||||
|
};
|
||||||
|
export const parseIfJson = (data = '{}') => {
|
||||||
|
try {
|
||||||
|
const _data = JSON.parse(data);
|
||||||
|
if (typeof _data === 'object') return _data;
|
||||||
|
return {};
|
||||||
|
} catch (error) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const uploadManager = new UploadManager(assistantConfig)
|
||||||
|
export const uploadResources = async (req: http.IncomingMessage, res: http.ServerResponse) => {
|
||||||
|
// const { tokenUser, token } = await checkAuth(req, res);
|
||||||
|
const token = getTokenFromRequest(req);
|
||||||
|
let tokenUser: any = null;
|
||||||
|
const authResult = await checkAuth({ query: { token } });
|
||||||
|
if (authResult.code === 200) {
|
||||||
|
tokenUser = authResult.data?.tokenUser;
|
||||||
|
} else {
|
||||||
|
res.end(error(authResult.message, authResult.code));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!tokenUser) {
|
||||||
|
res.end(error('Token is invalid.'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const url = new URL(req.url || '', 'http://localhost');
|
||||||
|
const share = !!url.searchParams.get('public');
|
||||||
|
const meta = parseIfJson(url.searchParams.get('meta'));
|
||||||
|
// 使用 busboy 解析 multipart/form-data
|
||||||
|
const busboy = Busboy({ headers: req.headers, preservePath: true });
|
||||||
|
const fields: any = {};
|
||||||
|
const files: any[] = [];
|
||||||
|
const filePromises: Promise<void>[] = [];
|
||||||
|
let bytesReceived = 0;
|
||||||
|
let bytesExpected = parseInt(req.headers['content-length'] || '0');
|
||||||
|
busboy.on('field', (fieldname, value) => {
|
||||||
|
fields[fieldname] = value;
|
||||||
|
});
|
||||||
|
|
||||||
|
busboy.on('file', (fieldname, fileStream, info) => {
|
||||||
|
const { filename, encoding, mimeType } = info;
|
||||||
|
const tempPath = path.join(cacheFilePath, `${Date.now()}-${Math.random().toString(36).substring(7)}`);
|
||||||
|
const writeStream = fs.createWriteStream(tempPath);
|
||||||
|
const filePromise = new Promise<void>((resolve, reject) => {
|
||||||
|
fileStream.on('data', (chunk) => {
|
||||||
|
bytesReceived += chunk.length;
|
||||||
|
if (bytesExpected > 0) {
|
||||||
|
const progress = (bytesReceived / bytesExpected) * 100;
|
||||||
|
const data = {
|
||||||
|
progress: progress.toFixed(2),
|
||||||
|
message: `Upload progress: ${progress.toFixed(2)}%`,
|
||||||
|
};
|
||||||
|
console.log('progress-upload', JSON.stringify(data, null, 2));
|
||||||
|
writeEvents(req, data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
fileStream.pipe(writeStream);
|
||||||
|
|
||||||
|
writeStream.on('finish', () => {
|
||||||
|
files.push({
|
||||||
|
filepath: tempPath,
|
||||||
|
originalFilename: filename,
|
||||||
|
mimetype: mimeType,
|
||||||
|
});
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
writeStream.on('error', (err) => {
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
filePromises.push(filePromise);
|
||||||
|
});
|
||||||
|
|
||||||
|
busboy.on('finish', async () => {
|
||||||
|
// 等待所有文件写入完成
|
||||||
|
try {
|
||||||
|
await Promise.all(filePromises);
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(`File write error: ${err.message}`);
|
||||||
|
res.end(error(`File write error: ${err.message}`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const clearFiles = () => {
|
||||||
|
files.forEach((file) => {
|
||||||
|
if (file?.filepath && fs.existsSync(file.filepath)) {
|
||||||
|
fs.unlinkSync(file.filepath);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 检查是否有文件上传
|
||||||
|
if (files.length === 0) {
|
||||||
|
res.end(error('files is required'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log('fields', fields);
|
||||||
|
let { appKey, version, username, directory } = getKey(fields, ['appKey', 'version', 'username', 'directory']);
|
||||||
|
if (!appKey || !version) {
|
||||||
|
res.end(error('appKey or version is not found, please check the upload config.'));
|
||||||
|
clearFiles();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { code, message } = validateDirectory(directory);
|
||||||
|
if (code !== 200) {
|
||||||
|
res.end(error(message));
|
||||||
|
clearFiles();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 逐个处理每个上传的文件
|
||||||
|
const uploadedFiles = files;
|
||||||
|
|
||||||
|
const uploadResults = [];
|
||||||
|
for (let i = 0; i < uploadedFiles.length; i++) {
|
||||||
|
const file = uploadedFiles[i];
|
||||||
|
// @ts-ignore
|
||||||
|
const tempPath = file.filepath; // 文件上传时的临时路径
|
||||||
|
const relativePath = file.originalFilename; // 保留表单中上传的文件名 (包含文件夹结构)
|
||||||
|
// 比如 child2/b.txt
|
||||||
|
const showVersion = false;
|
||||||
|
const _version = showVersion ? `${version ? '/' + version : ''}` : '';
|
||||||
|
const _directory = directory ? `/${directory}` : '';
|
||||||
|
const minioPath = `${username || tokenUser.username || 'unknown'}/${appKey}${_version}${_directory}/${relativePath}`;
|
||||||
|
uploadResults.push({
|
||||||
|
name: relativePath,
|
||||||
|
path: minioPath,
|
||||||
|
});
|
||||||
|
const type = 'file';
|
||||||
|
uploadManager.mvFile({
|
||||||
|
type: 'file',
|
||||||
|
temppath: tempPath,
|
||||||
|
targetPath: minioPath,
|
||||||
|
})
|
||||||
|
if (type !== 'file') {
|
||||||
|
fs.unlinkSync(tempPath); // 删除临时文件
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(
|
||||||
|
JSON.stringify({
|
||||||
|
code: 200,
|
||||||
|
data: {
|
||||||
|
detect: [],
|
||||||
|
upload: uploadResults,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
pipeBusboy(req, res, busboy);
|
||||||
|
}
|
||||||
|
|
||||||
|
simpleRouter.post('/client/upload', uploadResources);
|
||||||
30
assistant/src/routes-simple/utils.ts
Normal file
30
assistant/src/routes-simple/utils.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
/**
|
||||||
|
* 校验directory是否合法, 合法返回200, 不合法返回500
|
||||||
|
*
|
||||||
|
* directory 不能以/开头,不能以/结尾。不能以.开头,不能以.结尾。
|
||||||
|
* 把directory的/替换掉后,只能包含数字、字母、下划线、中划线
|
||||||
|
* @param directory 目录
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const validateDirectory = (directory?: string) => {
|
||||||
|
// 对directory进行校验,不能以/开头,不能以/结尾。不能以.开头,不能以.结尾。
|
||||||
|
if (directory && (directory.startsWith('/') || directory.endsWith('/') || directory.startsWith('..') || directory.endsWith('..'))) {
|
||||||
|
return {
|
||||||
|
code: 500,
|
||||||
|
message: 'directory is invalid',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// 把directory的/替换掉后,只能包含数字、字母、下划线、中划线
|
||||||
|
// 可以包含.
|
||||||
|
let _directory = directory?.replace(/\//g, '');
|
||||||
|
if (_directory && !/^[a-zA-Z0-9_.-]+$/.test(_directory)) {
|
||||||
|
return {
|
||||||
|
code: 500,
|
||||||
|
message: 'directory is invalid, only number, letter, underline and hyphen are allowed',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
message: 'directory is valid',
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -7,7 +7,7 @@ import './ai/index.ts';
|
|||||||
import './user/index.ts';
|
import './user/index.ts';
|
||||||
|
|
||||||
// TODO: 移除
|
// TODO: 移除
|
||||||
import './hot-api/key-sender/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';
|
||||||
@@ -34,30 +34,38 @@ export const getTokenUserCache = async (token: string) => {
|
|||||||
}
|
}
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
const checkAuth = async (ctx: any, isAdmin = false) => {
|
export 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 });
|
console.log('checkAuth', ctx.query, { token });
|
||||||
if (!token) {
|
if (!token) {
|
||||||
return ctx.throw(401, 'not login');
|
return {
|
||||||
|
code: 401,
|
||||||
|
message: '未登录',
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// 鉴权代理
|
// 鉴权代理
|
||||||
let tokenUser = await authCache.get(token);
|
let tokenUser = await authCache.get(token);
|
||||||
if (!tokenUser) {
|
if (!tokenUser) {
|
||||||
const tokenUserRes = await getTokenUser(token);
|
const tokenUserRes = await getTokenUser(token);
|
||||||
if (tokenUserRes.code !== 200) {
|
if (tokenUserRes.code !== 200) {
|
||||||
return ctx.throw(tokenUserRes.code, 'not login');
|
return {
|
||||||
|
code: tokenUserRes.code,
|
||||||
|
message: '验证失败' + tokenUserRes.message,
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
tokenUser = tokenUserRes.data;
|
tokenUser = tokenUserRes.data;
|
||||||
}
|
}
|
||||||
authCache.set(token, tokenUser);
|
authCache.set(token, tokenUser);
|
||||||
}
|
}
|
||||||
ctx.state = {
|
if (ctx.state) {
|
||||||
...ctx.state,
|
ctx.state = {
|
||||||
token,
|
...ctx.state,
|
||||||
tokenUser,
|
token,
|
||||||
};
|
tokenUser,
|
||||||
|
};
|
||||||
|
}
|
||||||
const { username } = tokenUser;
|
const { username } = tokenUser;
|
||||||
if (!auth.username) {
|
if (!auth.username) {
|
||||||
// 初始管理员账号
|
// 初始管理员账号
|
||||||
@@ -75,9 +83,16 @@ const checkAuth = async (ctx: any, isAdmin = false) => {
|
|||||||
isCheckAdmin = true;
|
isCheckAdmin = true;
|
||||||
}
|
}
|
||||||
if (!isCheckAdmin) {
|
if (!isCheckAdmin) {
|
||||||
return ctx.throw(403, 'not admin user');
|
return {
|
||||||
|
code: 403,
|
||||||
|
message: '非管理员用户',
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
data: { tokenUser, token }
|
||||||
|
}
|
||||||
};
|
};
|
||||||
app
|
app
|
||||||
.route({
|
.route({
|
||||||
@@ -86,7 +101,10 @@ app
|
|||||||
description: '获取当前登录用户信息, 第一个登录的用户为管理员用户',
|
description: '获取当前登录用户信息, 第一个登录的用户为管理员用户',
|
||||||
})
|
})
|
||||||
.define(async (ctx) => {
|
.define(async (ctx) => {
|
||||||
await checkAuth(ctx);
|
const authResult = await checkAuth(ctx);
|
||||||
|
if (authResult.code !== 200) {
|
||||||
|
ctx.throw(authResult.code, authResult.message);
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.addTo(app);
|
.addTo(app);
|
||||||
app
|
app
|
||||||
@@ -97,7 +115,10 @@ app
|
|||||||
})
|
})
|
||||||
.define(async (ctx) => {
|
.define(async (ctx) => {
|
||||||
console.log('query', ctx.query);
|
console.log('query', ctx.query);
|
||||||
await checkAuth(ctx, true);
|
const authResult = await checkAuth(ctx, true);
|
||||||
|
if (authResult.code !== 200) {
|
||||||
|
ctx.throw(authResult.code, authResult.message);
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.addTo(app);
|
.addTo(app);
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { app, assistantConfig } from './app.ts';
|
import { app, assistantConfig } from './app.ts';
|
||||||
import { proxyRoute, proxyWs } from './services/proxy/proxy-page-index.ts';
|
import { proxyRoute, proxyWs } from './services/proxy/proxy-page-index.ts';
|
||||||
import './routes/index.ts';
|
import './routes/index.ts';
|
||||||
|
import './routes-simple/index.ts';
|
||||||
|
|
||||||
import getPort, { portNumbers } from 'get-port';
|
import getPort, { portNumbers } from 'get-port';
|
||||||
import { program } from 'commander';
|
import { program } from 'commander';
|
||||||
@@ -35,12 +36,11 @@ export const runServer = async (port: number = 51515, listenPath = '127.0.0.1')
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
app.listen(_port, listenPath, () => {
|
app.listen(_port, listenPath, () => {
|
||||||
const protocol = assistantConfig.getHttps().protocol;
|
|
||||||
let showListenPath = listenPath;
|
let showListenPath = listenPath;
|
||||||
if (listenPath === '::') {
|
if (listenPath === '::') {
|
||||||
showListenPath = 'localhost';
|
showListenPath = 'localhost';
|
||||||
}
|
}
|
||||||
console.log(`Server is running on ${protocol}://${showListenPath}:${_port}`);
|
console.log(`Server is running on ${'http'}://${showListenPath}:${_port}`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
app.server.on([{
|
app.server.on([{
|
||||||
@@ -143,7 +143,7 @@ program
|
|||||||
} else if (options.start) {
|
} else if (options.start) {
|
||||||
console.log('启动服务', chalk.green(assistantConfig.configDir));
|
console.log('启动服务', chalk.green(assistantConfig.configDir));
|
||||||
const config = assistantConfig.getCacheAssistantConfig();
|
const config = assistantConfig.getCacheAssistantConfig();
|
||||||
const listenPort = options.port || config?.server?.port;
|
const listenPort = parseInt(options.port || config?.server?.port);
|
||||||
const listenPath = config?.server?.path || '::';
|
const listenPath = config?.server?.path || '::';
|
||||||
const server = await runServer(listenPort, listenPath);
|
const server = await runServer(listenPort, listenPath);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import fs from 'node:fs';
|
|||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { checkFileExists, AssistantConfig, AssistantConfigData, parseHomeArg, parseHelpArg } from '@/module/assistant/index.ts';
|
import { checkFileExists, AssistantConfig, AssistantConfigData, parseHomeArg, parseHelpArg } from '@/module/assistant/index.ts';
|
||||||
import { chalk } from '@/module/chalk.ts';
|
import { chalk } from '@/module/chalk.ts';
|
||||||
import { HttpsPem } from '@/module/assistant/https/sign.ts';
|
|
||||||
import { Query } from '@kevisual/query/query';
|
import { Query } from '@kevisual/query/query';
|
||||||
import { installDeps } from '@/module/npm-install.ts'
|
import { installDeps } from '@/module/npm-install.ts'
|
||||||
export { parseHomeArg, parseHelpArg };
|
export { parseHomeArg, parseHelpArg };
|
||||||
@@ -77,14 +76,6 @@ export class AssistantInit extends AssistantConfig {
|
|||||||
fs.writeFileSync(appsConfig, JSON.stringify({ description: 'apps manager.', list: [] }));
|
fs.writeFileSync(appsConfig, JSON.stringify({ description: 'apps manager.', list: [] }));
|
||||||
console.log(chalk.green('助手应用配置文件 apps.json 创建成功'));
|
console.log(chalk.green('助手应用配置文件 apps.json 创建成功'));
|
||||||
}
|
}
|
||||||
// create pem dir //
|
|
||||||
const pemDir = path.join(this.configPath?.configDir, 'pem');
|
|
||||||
const httpsPem = new HttpsPem(this);
|
|
||||||
if (httpsPem.isHttps) {
|
|
||||||
if (!checkFileExists(pemDir)) {
|
|
||||||
console.log(chalk.green('助手证书目录创建成功'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
createAssistantConfig() {
|
createAssistantConfig() {
|
||||||
const assistantPath = this.configPath?.configPath;
|
const assistantPath = this.configPath?.configPath;
|
||||||
@@ -129,32 +120,38 @@ export class AssistantInit extends AssistantConfig {
|
|||||||
"description": "assistant-app package pnpm, node pkgs projects",
|
"description": "assistant-app package pnpm, node pkgs projects",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "pm2 start apps/root/code-center/app.mjs --name root/code-center",
|
"start": "pm2 start apps/code-center/dist/app.mjs --name code-center",
|
||||||
"cnb": "ASSISTANT_CONFIG_DIR=/workspace asst server -s -p 7878"
|
"proxy": "pm2 start apps/page-proxy/dist/app.mjs --name page-proxy",
|
||||||
|
"preview": "pnpm i && ASSISTANT_CONFIG_DIR=/workspace asst server -s -p 8686"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@aws-sdk/client-s3": "latest",
|
||||||
|
"@kevisual/oss": "latest",
|
||||||
|
"@kevisual/query": "latest",
|
||||||
"@kevisual/router": "latest",
|
"@kevisual/router": "latest",
|
||||||
"@kevisual/use-config": "latest",
|
"@kevisual/use-config": "latest",
|
||||||
"@kevisual/query": "latest",
|
"better-sqlite3": "latest",
|
||||||
|
"crypto-js": "latest",
|
||||||
|
"dayjs": "latest",
|
||||||
|
"dotenv": "latest",
|
||||||
|
"es-toolkit": "latest",
|
||||||
|
"eventemitter3": "latest",
|
||||||
"ioredis": "latest",
|
"ioredis": "latest",
|
||||||
"minio": "latest",
|
"minio": "latest",
|
||||||
|
"node-cron": "latest",
|
||||||
"pg": "latest",
|
"pg": "latest",
|
||||||
"pm2": "latest",
|
"pm2": "latest",
|
||||||
"sequelize": "latest",
|
"sequelize": "latest",
|
||||||
"crypto-js": "latest",
|
"unstorage": "latest"
|
||||||
"better-sqlite3": "latest",
|
|
||||||
"unstorage": "latest",
|
|
||||||
"dayjs": "latest",
|
|
||||||
"es-toolkit": "latest",
|
|
||||||
"node-cron": "latest",
|
|
||||||
"dotenv": "latest"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "latest",
|
"@kevisual/types": "^0.0.11",
|
||||||
"@types/crypto-js": "latest"
|
"@types/bun": "^1.3.6",
|
||||||
|
"@types/crypto-js": "latest",
|
||||||
|
"@types/node": "latest"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
@@ -218,11 +215,4 @@ export class AssistantInit extends AssistantConfig {
|
|||||||
},
|
},
|
||||||
} as AssistantConfigData;
|
} as AssistantConfigData;
|
||||||
}
|
}
|
||||||
getHttps() {
|
|
||||||
const https = this.getConfig()?.https || {};
|
|
||||||
return {
|
|
||||||
https,
|
|
||||||
protocol: https?.type === 'https' ? 'https' : 'http',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,31 +4,37 @@
|
|||||||
"description": "assistant-app package pnpm, node pkgs projects",
|
"description": "assistant-app package pnpm, node pkgs projects",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "pm2 start apps/root/code-center/app.mjs --name root/code-center",
|
"start": "pm2 start apps/code-center/dist/app.mjs --name code-center",
|
||||||
"proxy": "pm2 start apps/root/page-proxy/app.mjs --name root/page-proxy"
|
"proxy": "pm2 start apps/page-proxy/dist/app.mjs --name page-proxy",
|
||||||
|
"preview": "pnpm i && ASSISTANT_CONFIG_DIR=/workspace asst server -s -p 8686"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@aws-sdk/client-s3": "latest",
|
||||||
|
"@kevisual/oss": "latest",
|
||||||
|
"@kevisual/query": "latest",
|
||||||
"@kevisual/router": "latest",
|
"@kevisual/router": "latest",
|
||||||
"@kevisual/use-config": "latest",
|
"@kevisual/use-config": "latest",
|
||||||
"@kevisual/query": "latest",
|
"better-sqlite3": "latest",
|
||||||
|
"crypto-js": "latest",
|
||||||
|
"dayjs": "latest",
|
||||||
|
"dotenv": "latest",
|
||||||
|
"es-toolkit": "latest",
|
||||||
|
"eventemitter3": "latest",
|
||||||
"ioredis": "latest",
|
"ioredis": "latest",
|
||||||
"minio": "latest",
|
"minio": "latest",
|
||||||
|
"node-cron": "latest",
|
||||||
"pg": "latest",
|
"pg": "latest",
|
||||||
"pm2": "latest",
|
"pm2": "latest",
|
||||||
"sequelize": "latest",
|
"sequelize": "latest",
|
||||||
"crypto-js": "latest",
|
"unstorage": "latest"
|
||||||
"better-sqlite3": "latest",
|
|
||||||
"unstorage": "latest",
|
|
||||||
"dayjs": "latest",
|
|
||||||
"es-toolkit": "latest",
|
|
||||||
"node-cron": "latest",
|
|
||||||
"dotenv": "latest"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "latest",
|
"@kevisual/types": "^0.0.11",
|
||||||
"@types/crypto-js": "latest"
|
"@types/bun": "^1.3.6",
|
||||||
|
"@types/crypto-js": "latest",
|
||||||
|
"@types/node": "latest"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { fileProxy, httpProxy, createApiProxy, ProxyInfo, proxy } from '@/module/assistant/index.ts';
|
import { fileProxy, httpProxy, createApiProxy, ProxyInfo, proxy } from '@/module/assistant/index.ts';
|
||||||
import http from 'node:http';
|
import http from 'node:http';
|
||||||
import { LocalProxy } from './local-proxy.ts';
|
import { LocalProxy } from './local-proxy.ts';
|
||||||
import { assistantConfig, app } from '@/app.ts';
|
import { assistantConfig, app, simpleRouter } from '@/app.ts';
|
||||||
import { log, logger } from '@/module/logger.ts';
|
import { log, logger } from '@/module/logger.ts';
|
||||||
import { getToken } from '@/module/http-token.ts';
|
import { getToken } from '@/module/http-token.ts';
|
||||||
import { getTokenUserCache } from '@/routes/index.ts';
|
import { getTokenUserCache } from '@/routes/index.ts';
|
||||||
@@ -103,6 +103,10 @@ export const proxyRoute = async (req: http.IncomingMessage, res: http.ServerResp
|
|||||||
res.end('Not Found Favicon');
|
res.end('Not Found Favicon');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (pathname.startsWith('/client/upload')) {
|
||||||
|
simpleRouter.parse(req, res);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (pathname.startsWith('/client')) {
|
if (pathname.startsWith('/client')) {
|
||||||
logger.debug('handle by router', { url: req.url });
|
logger.debug('handle by router', { url: req.url });
|
||||||
return;
|
return;
|
||||||
|
|||||||
178
assistant/src/test/upload-file.ts
Normal file
178
assistant/src/test/upload-file.ts
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
// 使用 fetch 和 FormData 上传文件的示例代码
|
||||||
|
|
||||||
|
// 示例 1: 使用 File 对象上传
|
||||||
|
async function uploadFileUsingFileObject(file: File) {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
// 可以添加其他字段
|
||||||
|
formData.append('filename', file.name);
|
||||||
|
formData.append('description', '文件描述');
|
||||||
|
formData.append('appKey', 'test');
|
||||||
|
formData.append('version', '1.0.0');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('http://localhost:51516/client/router', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
// 注意:使用 FormData 时不需要手动设置 Content-Type,
|
||||||
|
// 浏览器会自动设置正确的 multipart/form-data 边界
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`上传失败: ${response.status} ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
console.log('上传成功:', result);
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('上传出错:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 示例 2: 使用 FileList(如 input[type="file"])上传
|
||||||
|
async function uploadFilesUsingFileList(files: FileList) {
|
||||||
|
const formData = new FormData();
|
||||||
|
|
||||||
|
// 多个文件使用相同的字段名
|
||||||
|
for (let i = 0; i < files.length; i++) {
|
||||||
|
formData.append('files', files[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('http://localhost:51516/client/router', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`上传失败: ${response.status} ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
console.log('上传成功:', result);
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('上传出错:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 示例 3: 从路径读取文件上传(Node.js 环境,使用 fs 和 Blob)
|
||||||
|
import fs from 'fs';
|
||||||
|
|
||||||
|
async function uploadFileFromPath(fileList: string[]) {
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
for (let m of fileList) {
|
||||||
|
const buffer = fs.readFileSync(m);
|
||||||
|
const file = new File([buffer], m.split('/').pop()!, {
|
||||||
|
type: 'application/octet-stream',
|
||||||
|
});
|
||||||
|
formData.append('file', file);
|
||||||
|
}
|
||||||
|
formData.append('appKey', 'test');
|
||||||
|
formData.append('version', '1.0.0');
|
||||||
|
|
||||||
|
let token = 'st_n9ycynd4m7wdyw3lejb8plnkyi62uejd'; // 如果需要身份验证,添加令牌
|
||||||
|
try {
|
||||||
|
const response = await fetch('http://localhost:51516/client/upload' + `?token=${token}`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`上传失败: ${response.status} ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
console.log('上传成功:', JSON.stringify(result, null, 2));
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('上传出错:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadFileFromPath(['./src/test/remote-app.ts', './src/test/upload-file.ts']);
|
||||||
|
// 示例 4: 完整的 HTML 使用示例(浏览器环境)
|
||||||
|
/*
|
||||||
|
// HTML
|
||||||
|
// <input type="file" id="fileInput" multiple>
|
||||||
|
// <button onclick="handleUpload()">上传</button>
|
||||||
|
|
||||||
|
async function handleUpload() {
|
||||||
|
const fileInput = document.getElementById('fileInput') as HTMLInputElement;
|
||||||
|
const files = fileInput.files;
|
||||||
|
|
||||||
|
if (!files || files.length === 0) {
|
||||||
|
alert('请选择文件');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
|
||||||
|
for (let i = 0; i < files.length; i++) {
|
||||||
|
formData.append('files', files[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加额外数据
|
||||||
|
formData.append('userId', '12345');
|
||||||
|
formData.append('timestamp', Date.now().toString());
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('http://localhost:51516/client/router', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`上传失败: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
alert('上传成功: ' + JSON.stringify(result));
|
||||||
|
} catch (error) {
|
||||||
|
alert('上传出错: ' + error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 示例 5: 带进度监控的上传(浏览器环境)
|
||||||
|
/*
|
||||||
|
async function uploadWithProgress(file: File, onProgress: (percent: number) => void) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
|
||||||
|
xhr.upload.addEventListener('progress', (e) => {
|
||||||
|
if (e.lengthComputable) {
|
||||||
|
const percent = (e.loaded / e.total) * 100;
|
||||||
|
onProgress(percent);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
xhr.addEventListener('load', () => {
|
||||||
|
if (xhr.status === 200) {
|
||||||
|
resolve(JSON.parse(xhr.responseText));
|
||||||
|
} else {
|
||||||
|
reject(new Error(`上传失败: ${xhr.status}`));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
xhr.addEventListener('error', () => reject(new Error('网络错误')));
|
||||||
|
xhr.addEventListener('abort', () => reject(new Error('上传被取消')));
|
||||||
|
|
||||||
|
xhr.open('POST', 'http://localhost:51516/client/router');
|
||||||
|
xhr.send(formData);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用
|
||||||
|
// const file = fileInput.files[0];
|
||||||
|
// uploadWithProgress(file, (percent) => {
|
||||||
|
// console.log(`上传进度: ${percent.toFixed(1)}%`);
|
||||||
|
// }).then(result => console.log('完成', result));
|
||||||
|
*/
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import pkg from './package.json';
|
import pkg from './package.json';
|
||||||
// bun run src/index.ts --
|
// bun run src/index.ts --
|
||||||
|
const external = ['bun'];
|
||||||
await Bun.build({
|
await Bun.build({
|
||||||
target: 'node',
|
target: 'node',
|
||||||
format: 'esm',
|
format: 'esm',
|
||||||
@@ -11,9 +12,8 @@ await Bun.build({
|
|||||||
naming: {
|
naming: {
|
||||||
entry: 'envision.js',
|
entry: 'envision.js',
|
||||||
},
|
},
|
||||||
|
external: external,
|
||||||
define: {
|
define: {
|
||||||
ENVISION_VERSION: JSON.stringify(pkg.version),
|
ENVISION_VERSION: JSON.stringify(pkg.version),
|
||||||
},
|
},
|
||||||
env: 'ENVISION_*',
|
|
||||||
});
|
});
|
||||||
|
|||||||
16
package.json
16
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@kevisual/cli",
|
"name": "@kevisual/cli",
|
||||||
"version": "0.0.80",
|
"version": "0.0.85",
|
||||||
"description": "envision 命令行工具",
|
"description": "envision 命令行工具",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"basename": "/root/cli",
|
"basename": "/root/cli",
|
||||||
@@ -45,15 +45,17 @@
|
|||||||
"@kevisual/app": "^0.0.2",
|
"@kevisual/app": "^0.0.2",
|
||||||
"@kevisual/context": "^0.0.4",
|
"@kevisual/context": "^0.0.4",
|
||||||
"@kevisual/hot-api": "^0.0.3",
|
"@kevisual/hot-api": "^0.0.3",
|
||||||
"@kevisual/use-config": "^1.0.26",
|
"@kevisual/use-config": "^1.0.28",
|
||||||
"@nut-tree-fork/nut-js": "^4.2.6",
|
|
||||||
|
"@types/busboy": "^1.5.4",
|
||||||
|
"busboy": "^1.6.0",
|
||||||
"eventemitter3": "^5.0.1",
|
"eventemitter3": "^5.0.1",
|
||||||
"lowdb": "^7.0.1",
|
"lowdb": "^7.0.1",
|
||||||
"lru-cache": "^11.2.4",
|
"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"
|
"unstorage": "^1.17.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@kevisual/dts": "^0.0.3",
|
"@kevisual/dts": "^0.0.3",
|
||||||
@@ -65,7 +67,7 @@
|
|||||||
"@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": "^25.0.8",
|
"@types/node": "^25.0.9",
|
||||||
"@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",
|
||||||
@@ -75,7 +77,7 @@
|
|||||||
"form-data": "^4.0.5",
|
"form-data": "^4.0.5",
|
||||||
"ignore": "^7.0.5",
|
"ignore": "^7.0.5",
|
||||||
"jsonwebtoken": "^9.0.3",
|
"jsonwebtoken": "^9.0.3",
|
||||||
"tar": "^7.5.2",
|
"tar": "^7.5.3",
|
||||||
"zustand": "^5.0.10"
|
"zustand": "^5.0.10"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -84,4 +86,4 @@
|
|||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public"
|
"access": "public"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
1023
pnpm-lock.yaml
generated
1023
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -14,7 +14,7 @@ const parseIfJson = (str: string) => {
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const command = new Command('npm').description('npm command show publish and set .npmrc').action(async (options) => {});
|
const command = new Command('npm').description('npm command show publish and set .npmrc').action(async (options) => { });
|
||||||
const publish = new Command('publish')
|
const publish = new Command('publish')
|
||||||
.argument('[registry]')
|
.argument('[registry]')
|
||||||
.option('-p --proxy', 'proxy')
|
.option('-p --proxy', 'proxy')
|
||||||
@@ -33,6 +33,10 @@ const publish = new Command('publish')
|
|||||||
name: 'npm',
|
name: 'npm',
|
||||||
value: 'npm',
|
value: 'npm',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'cnb',
|
||||||
|
value: 'cnb'
|
||||||
|
}
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -60,6 +64,9 @@ const publish = new Command('publish')
|
|||||||
case 'npm':
|
case 'npm':
|
||||||
cmd = 'npm publish --registry https://registry.npmjs.org';
|
cmd = 'npm publish --registry https://registry.npmjs.org';
|
||||||
break;
|
break;
|
||||||
|
case 'cnb':
|
||||||
|
cmd = 'npm publish --registry https://npm.cnb.cool/kevisual/registry/-/packages/';
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
cmd = 'npm publish --registry https://npm.xiongxiao.me';
|
cmd = 'npm publish --registry https://npm.xiongxiao.me';
|
||||||
break;
|
break;
|
||||||
@@ -136,6 +143,7 @@ const npmrc = new Command('set')
|
|||||||
const npmrcContent =
|
const npmrcContent =
|
||||||
config?.npmrc ||
|
config?.npmrc ||
|
||||||
`//npm.xiongxiao.me/:_authToken=\${ME_NPM_TOKEN}
|
`//npm.xiongxiao.me/:_authToken=\${ME_NPM_TOKEN}
|
||||||
|
//npm.cnb.cool/kevisual/registry/-/packages/:_authToken=\${CNB_API_KEY}
|
||||||
//registry.npmjs.org/:_authToken=\${NPM_TOKEN}
|
//registry.npmjs.org/:_authToken=\${NPM_TOKEN}
|
||||||
`;
|
`;
|
||||||
const execPath = process.cwd();
|
const execPath = process.cwd();
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ 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';
|
||||||
|
import { confirm } from '@inquirer/prompts'
|
||||||
const command = new Command('sync')
|
const command = new Command('sync')
|
||||||
.option('-d --dir <dir>')
|
.option('-d --dir <dir>')
|
||||||
.description('同步项目')
|
.description('同步项目')
|
||||||
@@ -33,7 +33,19 @@ const syncUpload = new Command('upload')
|
|||||||
};
|
};
|
||||||
const filepath = sync.getRelativePath(opts.file);
|
const filepath = sync.getRelativePath(opts.file);
|
||||||
const newInfos = [];
|
const newInfos = [];
|
||||||
|
const uploadLength = syncList.length;
|
||||||
|
logger.info(`开始上传文件,总计 ${uploadLength} 个文件`);
|
||||||
|
if (uploadLength > 100) {
|
||||||
|
// 提示用户确认
|
||||||
|
const shouldContinue = await confirm({
|
||||||
|
message: `即将上传 ${uploadLength} 个文件,是否继续?`,
|
||||||
|
default: false,
|
||||||
|
});
|
||||||
|
if (!shouldContinue) {
|
||||||
|
logger.info('已取消上传');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
for (const item of syncList) {
|
for (const item of syncList) {
|
||||||
if (!item.auth || !item.exist) {
|
if (!item.auth || !item.exist) {
|
||||||
nodonwArr.push(item);
|
nodonwArr.push(item);
|
||||||
|
|||||||
Reference in New Issue
Block a user