temp
This commit is contained in:
parent
f084492ed9
commit
7e167fd4a1
3
packages/api/.npmrc
Normal file
3
packages/api/.npmrc
Normal file
@ -0,0 +1,3 @@
|
||||
//npm.xiongxiao.me/:_authToken=${ME_NPM_TOKEN}
|
||||
//registry.npmjs.org/:_authToken=${NPM_TOKEN}
|
||||
ignore-workspace-root-check=true
|
22
packages/api/kevisual.json
Normal file
22
packages/api/kevisual.json
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"$schema": "https://kevisual.xiongxiao.me/root/ai/kevisual/tools/kevisual-sync/schema.json?v=2",
|
||||
"metadata": {
|
||||
"share": "public"
|
||||
},
|
||||
"checkDir": {
|
||||
"query": {
|
||||
"url": "https://kevisual.xiongxiao.me/root/ai/code/registry/query",
|
||||
"enabled": true
|
||||
}
|
||||
},
|
||||
"syncDirectory": [
|
||||
{
|
||||
"files": [
|
||||
"query/**/*"
|
||||
],
|
||||
"ignore": [],
|
||||
"registry": "https://kevisual.xiongxiao.me/root/ai/code/registry"
|
||||
}
|
||||
],
|
||||
"sync": {}
|
||||
}
|
32
packages/api/package.json
Normal file
32
packages/api/package.json
Normal file
@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "@kevisual/api",
|
||||
"version": "0.0.1",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"build2": "bun bun.config.mjs",
|
||||
"download": "ev sync download",
|
||||
"upload": "ev sync upload"
|
||||
},
|
||||
"keywords": [],
|
||||
"files": [
|
||||
"src",
|
||||
"query",
|
||||
"dist"
|
||||
],
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"author": "abearxiong <xiongxiao@xiongxiao.me> (https://www.xiongxiao.me)",
|
||||
"license": "MIT",
|
||||
"packageManager": "pnpm@10.6.2",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@kevisual/query": "^0.0.18",
|
||||
"@kevisual/router": "^0.0.20"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@kevisual/types": "^0.0.10",
|
||||
"@types/node": "^22.15.27"
|
||||
}
|
||||
}
|
7
packages/api/query/index.ts
Normal file
7
packages/api/query/index.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { Query } from '@kevisual/query';
|
||||
|
||||
export const query = new Query();
|
||||
|
||||
export const clientQuery = new Query({ url: '/client/router' });
|
||||
|
||||
export { QueryUtil } from '@kevisual/router/define';
|
25
packages/api/query/kevisual.json
Normal file
25
packages/api/query/kevisual.json
Normal file
@ -0,0 +1,25 @@
|
||||
{
|
||||
"$schema": "https://kevisual.xiongxiao.me/root/ai/kevisual/tools/kevisual-sync/schema.json?v=2",
|
||||
"metadata": {
|
||||
"share": "public"
|
||||
},
|
||||
"checkDir": {
|
||||
"src/query": {
|
||||
"url": "https://kevisual.xiongxiao.me/root/ai/code/registry/query",
|
||||
"enabled": true
|
||||
}
|
||||
},
|
||||
"syncDirectory": [
|
||||
{
|
||||
"files": [
|
||||
"src/query/**/*"
|
||||
],
|
||||
"ignore": [],
|
||||
"registry": "https://kevisual.xiongxiao.me/root/ai/code/registry",
|
||||
"replace": {
|
||||
"src/": ""
|
||||
}
|
||||
}
|
||||
],
|
||||
"sync": {}
|
||||
}
|
42
packages/api/query/query-ai/defines/ai.ts
Normal file
42
packages/api/query/query-ai/defines/ai.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import { QueryUtil } from '@/query/index.ts';
|
||||
|
||||
type Message = {
|
||||
role?: 'user' | 'assistant' | 'system' | 'tool';
|
||||
content?: string;
|
||||
name?: string;
|
||||
};
|
||||
export type PostChat = {
|
||||
messages?: Message[];
|
||||
model?: string;
|
||||
group?: string;
|
||||
user?: string;
|
||||
};
|
||||
|
||||
export type ChatDataOpts = {
|
||||
id?: string;
|
||||
title?: string;
|
||||
messages?: any[];
|
||||
data?: any;
|
||||
type?: 'temp' | 'keep' | string;
|
||||
};
|
||||
export type ChatOpts = {
|
||||
username: string;
|
||||
model: string;
|
||||
/**
|
||||
* 获取完整消息回复
|
||||
*/
|
||||
getFull?: boolean;
|
||||
group: string;
|
||||
/**
|
||||
* openai的参数
|
||||
*/
|
||||
options?: any;
|
||||
};
|
||||
|
||||
export const appDefine = QueryUtil.create({
|
||||
chat: {
|
||||
path: 'ai',
|
||||
key: 'chat',
|
||||
description: '与 AI 进行对话, 调用 GPT 的AI 服务,生成结果,并返回。',
|
||||
},
|
||||
});
|
101
packages/api/query/query-ai/query-ai.ts
Normal file
101
packages/api/query/query-ai/query-ai.ts
Normal file
@ -0,0 +1,101 @@
|
||||
import { appDefine } from './defines/ai.ts';
|
||||
import { PostChat, ChatOpts, ChatDataOpts } from './defines/ai.ts';
|
||||
|
||||
import { BaseQuery, DataOpts, Query } from '@kevisual/query/query';
|
||||
|
||||
export { appDefine };
|
||||
|
||||
export class QueryApp<T extends Query = Query> extends BaseQuery<T, typeof appDefine> {
|
||||
constructor(opts?: { query: T }) {
|
||||
super({
|
||||
...opts,
|
||||
query: opts?.query!,
|
||||
queryDefine: appDefine,
|
||||
});
|
||||
}
|
||||
/**
|
||||
* 与 AI 进行对话, 调用 GPT 的AI 服务,生成结果,并返回。
|
||||
* @param data
|
||||
* @param opts
|
||||
* @returns
|
||||
*/
|
||||
postChat(data: PostChat, opts?: DataOpts) {
|
||||
return this.chain('chat').post(data, opts);
|
||||
}
|
||||
/**
|
||||
* 获取模型列表
|
||||
* @param opts
|
||||
* @returns
|
||||
*/
|
||||
getModelList(data?: { usernames?: string[] }, opts?: DataOpts) {
|
||||
return this.query.post(
|
||||
{
|
||||
path: 'ai',
|
||||
key: 'get-model-list',
|
||||
data,
|
||||
},
|
||||
opts,
|
||||
);
|
||||
}
|
||||
/**
|
||||
* 聊天对话模型
|
||||
* @param data
|
||||
* @param chatOpts
|
||||
* @param opts
|
||||
* @returns
|
||||
*/
|
||||
chat(data: ChatDataOpts, chatOpts: ChatOpts, opts?: DataOpts) {
|
||||
const { username, model, group, getFull = true } = chatOpts;
|
||||
if (!username || !model || !group) {
|
||||
throw new Error('username, model, group is required');
|
||||
}
|
||||
return this.query.post(
|
||||
{
|
||||
path: 'ai',
|
||||
key: 'chat',
|
||||
...chatOpts,
|
||||
getFull,
|
||||
data,
|
||||
},
|
||||
opts,
|
||||
);
|
||||
}
|
||||
clearConfigCache(opts?: DataOpts) {
|
||||
return this.query.post(
|
||||
{
|
||||
path: 'ai',
|
||||
key: 'clear-cache',
|
||||
},
|
||||
opts,
|
||||
);
|
||||
}
|
||||
/**
|
||||
* 获取聊天使用情况
|
||||
* @param opts
|
||||
* @returns
|
||||
*/
|
||||
getChatUsage(opts?: DataOpts) {
|
||||
return this.query.post(
|
||||
{
|
||||
path: 'ai',
|
||||
key: 'get-chat-usage',
|
||||
},
|
||||
opts,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除当前用户模型自己的统计
|
||||
* @param opts
|
||||
* @returns
|
||||
*/
|
||||
clearSelfUsage(opts?: DataOpts) {
|
||||
return this.query.post(
|
||||
{
|
||||
path: 'ai',
|
||||
key: 'clear-chat-limit',
|
||||
},
|
||||
opts,
|
||||
);
|
||||
}
|
||||
}
|
3
packages/api/query/query-app/defines/index.ts
Normal file
3
packages/api/query/query-app/defines/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import { appDefine } from './user-app-list';
|
||||
import { userAppDefine } from './user-app';
|
||||
export { appDefine, userAppDefine };
|
62
packages/api/query/query-app/defines/user-app-list.ts
Normal file
62
packages/api/query/query-app/defines/user-app-list.ts
Normal file
@ -0,0 +1,62 @@
|
||||
import { QueryUtil } from '@/query/index.ts';
|
||||
|
||||
export const appDefine = QueryUtil.create({
|
||||
getApp: {
|
||||
path: 'app',
|
||||
key: 'get',
|
||||
description: '获取应用信息',
|
||||
},
|
||||
|
||||
updateApp: {
|
||||
path: 'app',
|
||||
key: 'update',
|
||||
description: '更新应用信息',
|
||||
},
|
||||
|
||||
deleteApp: {
|
||||
path: 'app',
|
||||
key: 'delete',
|
||||
description: '删除应用信息',
|
||||
},
|
||||
|
||||
listApps: {
|
||||
path: 'app',
|
||||
key: 'list',
|
||||
description: '列出所有应用信息',
|
||||
},
|
||||
|
||||
canUploadFiles: {
|
||||
path: 'app',
|
||||
key: 'canUploadFiles',
|
||||
description: '检查是否可以上传文件',
|
||||
},
|
||||
|
||||
uploadFiles: {
|
||||
path: 'app',
|
||||
key: 'uploadFiles',
|
||||
description: '上传文件',
|
||||
},
|
||||
|
||||
publishApp: {
|
||||
path: 'app',
|
||||
key: 'publish',
|
||||
description: '发布应用',
|
||||
},
|
||||
|
||||
getMinioList: {
|
||||
path: 'app',
|
||||
key: 'get-minio-list',
|
||||
description: '获取 MinIO 文件列表',
|
||||
},
|
||||
|
||||
detectVersionList: {
|
||||
path: 'app',
|
||||
key: 'detectVersionList',
|
||||
description: '检测版本列表并同步 MinIO 数据',
|
||||
},
|
||||
publicList: {
|
||||
path: 'app',
|
||||
key: 'public-list',
|
||||
description: '获取公开应用列表',
|
||||
},
|
||||
});
|
33
packages/api/query/query-app/defines/user-app.ts
Normal file
33
packages/api/query/query-app/defines/user-app.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import { QueryUtil } from '@/query/index.ts';
|
||||
|
||||
export const userAppDefine = QueryUtil.create({
|
||||
listUserApps: {
|
||||
path: 'user-app',
|
||||
key: 'list',
|
||||
description: '列出当前用户的所有应用(不包含 data 字段)',
|
||||
},
|
||||
|
||||
getUserApp: {
|
||||
path: 'user-app',
|
||||
key: 'get',
|
||||
description: '获取用户应用信息,可以指定 id 或 key',
|
||||
},
|
||||
|
||||
updateUserApp: {
|
||||
path: 'user-app',
|
||||
key: 'update',
|
||||
description: '更新或创建用户应用',
|
||||
},
|
||||
|
||||
deleteUserApp: {
|
||||
path: 'user-app',
|
||||
key: 'delete',
|
||||
description: '删除用户应用及关联数据',
|
||||
},
|
||||
|
||||
testUserApp: {
|
||||
path: 'user-app',
|
||||
key: 'test',
|
||||
description: '对 user-app 的数据进行测试,获取版本信息',
|
||||
},
|
||||
});
|
1
packages/api/query/query-app/query-app-define.ts
Normal file
1
packages/api/query/query-app/query-app-define.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './defines/index.ts';
|
18
packages/api/query/query-app/query-app.ts
Normal file
18
packages/api/query/query-app/query-app.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { appDefine, userAppDefine } from './defines/index.ts';
|
||||
|
||||
import { BaseQuery, DataOpts, Query } from '@kevisual/query/query';
|
||||
|
||||
export { appDefine, userAppDefine };
|
||||
|
||||
export class QueryApp extends BaseQuery {
|
||||
appDefine = appDefine;
|
||||
userAppDefine = userAppDefine;
|
||||
constructor(opts?: { query: Query }) {
|
||||
super(opts!);
|
||||
this.appDefine.query = this.query;
|
||||
this.userAppDefine.query = this.query;
|
||||
}
|
||||
getList(data: any, opts?: DataOpts) {
|
||||
return this.appDefine.queryChain('listApps').post(data, opts);
|
||||
}
|
||||
}
|
204
packages/api/query/query-login/login-cache.ts
Normal file
204
packages/api/query/query-login/login-cache.ts
Normal file
@ -0,0 +1,204 @@
|
||||
export interface Cache {
|
||||
/**
|
||||
* @update 获取缓存
|
||||
*/
|
||||
get(key: string): Promise<any>;
|
||||
/**
|
||||
* @update 设置缓存
|
||||
*/
|
||||
set(key: string, value: any): Promise<any>;
|
||||
/**
|
||||
* @update 删除缓存
|
||||
*/
|
||||
del(): Promise<void>;
|
||||
/**
|
||||
* 初始化
|
||||
*/
|
||||
init?: () => Promise<any>;
|
||||
}
|
||||
type User = {
|
||||
avatar?: string;
|
||||
description?: string;
|
||||
id?: string;
|
||||
needChangePassword?: boolean;
|
||||
orgs?: string[];
|
||||
type?: string;
|
||||
username?: string;
|
||||
};
|
||||
|
||||
export type CacheLoginUser = {
|
||||
user?: User;
|
||||
id?: string;
|
||||
accessToken?: string;
|
||||
refreshToken?: string;
|
||||
};
|
||||
type CacheLogin = {
|
||||
loginUsers: CacheLoginUser[];
|
||||
} & CacheLoginUser;
|
||||
|
||||
export type CacheStore<T = Cache> = {
|
||||
name: string;
|
||||
/**
|
||||
* 缓存数据
|
||||
* @important 需要先调用init
|
||||
*/
|
||||
cacheData: CacheLogin;
|
||||
/**
|
||||
* 实际操作的cache, 需要先调用init
|
||||
*/
|
||||
cache: T;
|
||||
|
||||
/**
|
||||
* 设置当前用户
|
||||
*/
|
||||
setLoginUser(user: CacheLoginUser): Promise<void>;
|
||||
/**
|
||||
* 获取当前用户
|
||||
*/
|
||||
getCurrentUser(): Promise<User>;
|
||||
/**
|
||||
* 获取当前用户列表
|
||||
*/
|
||||
getCurrentUserList(): Promise<CacheLoginUser[]>;
|
||||
/**
|
||||
* 获取缓存的refreshToken
|
||||
*/
|
||||
getRefreshToken(): Promise<string>;
|
||||
/**
|
||||
* 获取缓存的accessToken
|
||||
*/
|
||||
getAccessToken(): Promise<string>;
|
||||
/**
|
||||
* 清除当前用户
|
||||
*/
|
||||
clearCurrentUser(): Promise<void>;
|
||||
/**
|
||||
* 清除所有用户
|
||||
*/
|
||||
clearAll(): Promise<void>;
|
||||
|
||||
getValue(): Promise<CacheLogin>;
|
||||
setValue(value: CacheLogin): Promise<CacheLogin>;
|
||||
delValue(): Promise<void>;
|
||||
init(): Promise<any>;
|
||||
};
|
||||
|
||||
export type LoginCacheStoreOpts = {
|
||||
name: string;
|
||||
cache: Cache;
|
||||
};
|
||||
export class LoginCacheStore implements CacheStore<any> {
|
||||
cache: Cache;
|
||||
name: string;
|
||||
cacheData: CacheLogin;
|
||||
constructor(opts: LoginCacheStoreOpts) {
|
||||
if (!opts.cache) {
|
||||
throw new Error('cache is required');
|
||||
}
|
||||
// @ts-ignore
|
||||
this.cache = opts.cache;
|
||||
this.cacheData = {
|
||||
loginUsers: [],
|
||||
user: undefined,
|
||||
id: undefined,
|
||||
accessToken: undefined,
|
||||
refreshToken: undefined,
|
||||
};
|
||||
this.name = opts.name;
|
||||
}
|
||||
/**
|
||||
* 设置缓存
|
||||
* @param key
|
||||
* @param value
|
||||
* @returns
|
||||
*/
|
||||
async setValue(value: CacheLogin) {
|
||||
await this.cache.set(this.name, value);
|
||||
this.cacheData = value;
|
||||
return value;
|
||||
}
|
||||
/**
|
||||
* 删除缓存
|
||||
*/
|
||||
async delValue() {
|
||||
await this.cache.del();
|
||||
}
|
||||
getValue(): Promise<CacheLogin> {
|
||||
return this.cache.get(this.name);
|
||||
}
|
||||
/**
|
||||
* 初始化,设置默认值
|
||||
*/
|
||||
async init() {
|
||||
const defaultData = {
|
||||
loginUsers: [],
|
||||
user: null,
|
||||
id: null,
|
||||
accessToken: null,
|
||||
refreshToken: null,
|
||||
};
|
||||
if (this.cache.init) {
|
||||
try {
|
||||
const cacheData = await this.cache.init();
|
||||
this.cacheData = cacheData || defaultData;
|
||||
} catch (error) {
|
||||
console.log('cacheInit error', error);
|
||||
}
|
||||
} else {
|
||||
this.cacheData = (await this.getValue()) || defaultData;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 设置当前用户
|
||||
* @param user
|
||||
*/
|
||||
async setLoginUser(user: CacheLoginUser) {
|
||||
const has = this.cacheData.loginUsers.find((u) => u.id === user.id);
|
||||
if (has) {
|
||||
this.cacheData.loginUsers = this.cacheData?.loginUsers?.filter((u) => u?.id && u.id !== user.id);
|
||||
}
|
||||
this.cacheData.loginUsers.push(user);
|
||||
this.cacheData.user = user.user;
|
||||
this.cacheData.id = user.id;
|
||||
this.cacheData.accessToken = user.accessToken;
|
||||
this.cacheData.refreshToken = user.refreshToken;
|
||||
await this.setValue(this.cacheData);
|
||||
}
|
||||
|
||||
getCurrentUser(): Promise<CacheLoginUser> {
|
||||
const cacheData = this.cacheData;
|
||||
return Promise.resolve(cacheData.user!);
|
||||
}
|
||||
getCurrentUserList(): Promise<CacheLoginUser[]> {
|
||||
return Promise.resolve(this.cacheData.loginUsers.filter((u) => u?.id));
|
||||
}
|
||||
getRefreshToken(): Promise<string> {
|
||||
const cacheData = this.cacheData;
|
||||
return Promise.resolve(cacheData.refreshToken || '');
|
||||
}
|
||||
getAccessToken(): Promise<string> {
|
||||
const cacheData = this.cacheData;
|
||||
return Promise.resolve(cacheData.accessToken || '');
|
||||
}
|
||||
|
||||
async clearCurrentUser() {
|
||||
const user = await this.getCurrentUser();
|
||||
const has = this.cacheData.loginUsers.find((u) => u.id === user.id);
|
||||
if (has) {
|
||||
this.cacheData.loginUsers = this.cacheData?.loginUsers?.filter((u) => u?.id && u.id !== user.id);
|
||||
}
|
||||
this.cacheData.user = undefined;
|
||||
this.cacheData.id = undefined;
|
||||
this.cacheData.accessToken = undefined;
|
||||
this.cacheData.refreshToken = undefined;
|
||||
await this.setValue(this.cacheData);
|
||||
}
|
||||
async clearAll() {
|
||||
this.cacheData.loginUsers = [];
|
||||
this.cacheData.user = undefined;
|
||||
this.cacheData.id = undefined;
|
||||
this.cacheData.accessToken = undefined;
|
||||
this.cacheData.refreshToken = undefined;
|
||||
await this.setValue(this.cacheData);
|
||||
}
|
||||
}
|
132
packages/api/query/query-login/login-node-cache.ts
Normal file
132
packages/api/query/query-login/login-node-cache.ts
Normal file
@ -0,0 +1,132 @@
|
||||
import { Cache } from './login-cache.ts';
|
||||
import { homedir } from 'node:os';
|
||||
import { join, dirname } from 'node:path';
|
||||
import fs from 'node:fs';
|
||||
import { readFileSync, writeFileSync, accessSync } from 'node:fs';
|
||||
import { readFile, writeFile, unlink, mkdir } from 'node:fs/promises';
|
||||
export const fileExists = async (
|
||||
filePath: string,
|
||||
{ createIfNotExists = true, isFile = true, isDir = false }: { createIfNotExists?: boolean; isFile?: boolean; isDir?: boolean } = {},
|
||||
) => {
|
||||
try {
|
||||
accessSync(filePath, fs.constants.F_OK);
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (createIfNotExists && isDir) {
|
||||
await mkdir(filePath, { recursive: true });
|
||||
return true;
|
||||
} else if (createIfNotExists && isFile) {
|
||||
await mkdir(dirname(filePath), { recursive: true });
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
export const readConfigFile = (filePath: string) => {
|
||||
try {
|
||||
const data = readFileSync(filePath, 'utf-8');
|
||||
const jsonData = JSON.parse(data);
|
||||
return jsonData;
|
||||
} catch (error) {
|
||||
return {};
|
||||
}
|
||||
};
|
||||
export const writeConfigFile = (filePath: string, data: any) => {
|
||||
writeFileSync(filePath, JSON.stringify(data, null, 2));
|
||||
};
|
||||
export const getHostName = () => {
|
||||
const configDir = join(homedir(), '.config', 'envision');
|
||||
const configFile = join(configDir, 'config.json');
|
||||
const config = readConfigFile(configFile);
|
||||
const baseURL = config.baseURL || 'https://kevisual.cn';
|
||||
const hostname = new URL(baseURL).hostname;
|
||||
return hostname;
|
||||
};
|
||||
export class StorageNode implements Storage {
|
||||
cacheData: any;
|
||||
filePath: string;
|
||||
constructor() {
|
||||
this.cacheData = {};
|
||||
const configDir = join(homedir(), '.config', 'envision');
|
||||
const hostname = getHostName();
|
||||
this.filePath = join(configDir, 'config', `${hostname}-storage.json`);
|
||||
fileExists(this.filePath, { isFile: true });
|
||||
}
|
||||
async loadCache() {
|
||||
const filePath = this.filePath;
|
||||
try {
|
||||
const data = await readConfigFile(filePath);
|
||||
this.cacheData = data;
|
||||
} catch (error) {
|
||||
this.cacheData = {};
|
||||
await writeFile(filePath, JSON.stringify(this.cacheData, null, 2));
|
||||
}
|
||||
}
|
||||
get length() {
|
||||
return Object.keys(this.cacheData).length;
|
||||
}
|
||||
getItem(key: string) {
|
||||
return this.cacheData[key];
|
||||
}
|
||||
setItem(key: string, value: any) {
|
||||
this.cacheData[key] = value;
|
||||
writeFile(this.filePath, JSON.stringify(this.cacheData, null, 2));
|
||||
}
|
||||
removeItem(key: string) {
|
||||
delete this.cacheData[key];
|
||||
writeFile(this.filePath, JSON.stringify(this.cacheData, null, 2));
|
||||
}
|
||||
clear() {
|
||||
this.cacheData = {};
|
||||
writeFile(this.filePath, JSON.stringify(this.cacheData, null, 2));
|
||||
}
|
||||
key(index: number) {
|
||||
return Object.keys(this.cacheData)[index];
|
||||
}
|
||||
}
|
||||
export class LoginNodeCache implements Cache {
|
||||
filepath: string;
|
||||
|
||||
constructor(filepath?: string) {
|
||||
this.filepath = filepath || join(homedir(), '.config', 'envision', 'config', `${getHostName()}-login.json`);
|
||||
fileExists(this.filepath, { isFile: true });
|
||||
}
|
||||
async get(_key: string) {
|
||||
try {
|
||||
const filePath = this.filepath;
|
||||
const data = readConfigFile(filePath);
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.log('get error', error);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
async set(_key: string, value: any) {
|
||||
try {
|
||||
const data = readConfigFile(this.filepath);
|
||||
const newData = { ...data, ...value };
|
||||
writeConfigFile(this.filepath, newData);
|
||||
} catch (error) {
|
||||
console.log('set error', error);
|
||||
}
|
||||
}
|
||||
async del() {
|
||||
await unlink(this.filepath);
|
||||
}
|
||||
async loadCache(filePath: string) {
|
||||
try {
|
||||
const data = await readFile(filePath, 'utf-8');
|
||||
const jsonData = JSON.parse(data);
|
||||
return jsonData;
|
||||
} catch (error) {
|
||||
// console.log('loadCache error', error);
|
||||
console.log('create new cache file:', filePath);
|
||||
const defaultData = { loginUsers: [] };
|
||||
writeConfigFile(filePath, defaultData);
|
||||
return defaultData;
|
||||
}
|
||||
}
|
||||
async init() {
|
||||
return await this.loadCache(this.filepath);
|
||||
}
|
||||
}
|
12
packages/api/query/query-login/query-login-browser.ts
Normal file
12
packages/api/query/query-login/query-login-browser.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { QueryLogin, QueryLoginOpts } from './query-login.ts';
|
||||
import { MyCache } from '@kevisual/cache';
|
||||
type QueryLoginNodeOptsWithoutCache = Omit<QueryLoginOpts, 'cache'>;
|
||||
|
||||
export class QueryLoginBrowser extends QueryLogin {
|
||||
constructor(opts: QueryLoginNodeOptsWithoutCache) {
|
||||
super({
|
||||
...opts,
|
||||
cache: new MyCache('login'),
|
||||
});
|
||||
}
|
||||
}
|
14
packages/api/query/query-login/query-login-node.ts
Normal file
14
packages/api/query/query-login/query-login-node.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { QueryLogin, QueryLoginOpts } from './query-login.ts';
|
||||
import { LoginNodeCache, StorageNode } from './login-node-cache.ts';
|
||||
type QueryLoginNodeOptsWithoutCache = Omit<QueryLoginOpts, 'cache'>;
|
||||
export const storage = new StorageNode();
|
||||
await storage.loadCache();
|
||||
export class QueryLoginNode extends QueryLogin {
|
||||
constructor(opts: QueryLoginNodeOptsWithoutCache) {
|
||||
super({
|
||||
...opts,
|
||||
storage,
|
||||
cache: new LoginNodeCache(),
|
||||
});
|
||||
}
|
||||
}
|
434
packages/api/query/query-login/query-login.ts
Normal file
434
packages/api/query/query-login/query-login.ts
Normal file
@ -0,0 +1,434 @@
|
||||
import { Query, BaseQuery } from '@kevisual/query';
|
||||
import type { Result, DataOpts } from '@kevisual/query/query';
|
||||
import { setBaseResponse } from '@kevisual/query/query';
|
||||
import { LoginCacheStore, CacheStore } from './login-cache.ts';
|
||||
import { Cache } from './login-cache.ts';
|
||||
|
||||
export type QueryLoginOpts = {
|
||||
query?: Query;
|
||||
isBrowser?: boolean;
|
||||
onLoad?: () => void;
|
||||
storage?: Storage;
|
||||
cache: Cache;
|
||||
};
|
||||
export type QueryLoginData = {
|
||||
username?: string;
|
||||
password: string;
|
||||
email?: string;
|
||||
};
|
||||
export type QueryLoginResult = {
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
};
|
||||
|
||||
export class QueryLogin extends BaseQuery {
|
||||
/**
|
||||
* query login cache, 非实际操作, 一个cache的包裹模块
|
||||
*/
|
||||
cacheStore: CacheStore;
|
||||
isBrowser: boolean;
|
||||
load?: boolean;
|
||||
storage: Storage;
|
||||
onLoad?: () => void;
|
||||
|
||||
constructor(opts?: QueryLoginOpts) {
|
||||
super({
|
||||
query: opts?.query || new Query(),
|
||||
});
|
||||
this.cacheStore = new LoginCacheStore({ name: 'login', cache: opts?.cache! });
|
||||
this.isBrowser = opts?.isBrowser ?? true;
|
||||
this.init();
|
||||
this.onLoad = opts?.onLoad;
|
||||
this.storage = opts?.storage || localStorage;
|
||||
}
|
||||
setQuery(query: Query) {
|
||||
this.query = query;
|
||||
}
|
||||
private async init() {
|
||||
await this.cacheStore.init();
|
||||
this.load = true;
|
||||
this.onLoad?.();
|
||||
}
|
||||
async post<T = any>(data: any, opts?: DataOpts) {
|
||||
try {
|
||||
return this.query.post<T>({ path: 'user', ...data }, opts);
|
||||
} catch (error) {
|
||||
console.log('error', error);
|
||||
return {
|
||||
code: 400,
|
||||
} as any;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 登录,
|
||||
* @param data
|
||||
* @returns
|
||||
*/
|
||||
async login(data: QueryLoginData) {
|
||||
const res = await this.post<QueryLoginResult>({ key: 'login', ...data });
|
||||
if (res.code === 200) {
|
||||
const { accessToken, refreshToken } = res?.data || {};
|
||||
this.storage.setItem('token', accessToken || '');
|
||||
await this.beforeSetLoginUser({ accessToken, refreshToken });
|
||||
}
|
||||
return res;
|
||||
}
|
||||
/**
|
||||
* 手机号登录
|
||||
* @param data
|
||||
* @returns
|
||||
*/
|
||||
async loginByCode(data: { phone: string; code: string }) {
|
||||
const res = await this.post<QueryLoginResult>({ path: 'sms', key: 'login', data });
|
||||
if (res.code === 200) {
|
||||
const { accessToken, refreshToken } = res?.data || {};
|
||||
this.storage.setItem('token', accessToken || '');
|
||||
await this.beforeSetLoginUser({ accessToken, refreshToken });
|
||||
}
|
||||
return res;
|
||||
}
|
||||
/**
|
||||
* 设置token
|
||||
* @param token
|
||||
*/
|
||||
async setLoginToken(token: { accessToken: string; refreshToken: string }) {
|
||||
const { accessToken, refreshToken } = token;
|
||||
this.storage.setItem('token', accessToken || '');
|
||||
await this.beforeSetLoginUser({ accessToken, refreshToken });
|
||||
}
|
||||
async loginByWechat(data: { code: string }) {
|
||||
const res = await this.post<QueryLoginResult>({ path: 'wx', key: 'open-login', code: data.code });
|
||||
if (res.code === 200) {
|
||||
const { accessToken, refreshToken } = res?.data || {};
|
||||
this.storage.setItem('token', accessToken || '');
|
||||
await this.beforeSetLoginUser({ accessToken, refreshToken });
|
||||
}
|
||||
return res;
|
||||
}
|
||||
/**
|
||||
* 检测微信登录,登陆成功后,调用onSuccess,否则调用onError
|
||||
* @param param0
|
||||
*/
|
||||
async checkWechat({ onSuccess, onError }: { onSuccess?: (res: QueryLoginResult) => void; onError?: (res: any) => void }) {
|
||||
const url = new URL(window.location.href);
|
||||
const code = url.searchParams.get('code');
|
||||
const state = url.searchParams.get('state');
|
||||
if (code && state) {
|
||||
const res = await this.loginByWechat({ code });
|
||||
if (res.code === 200) {
|
||||
onSuccess?.(res.data);
|
||||
} else {
|
||||
onError?.(res);
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 登陆成功,需要获取用户信息进行缓存
|
||||
* @param param0
|
||||
*/
|
||||
async beforeSetLoginUser({ accessToken, refreshToken, check401 }: { accessToken?: string; refreshToken?: string; check401?: boolean }) {
|
||||
if (accessToken && refreshToken) {
|
||||
const resUser = await this.getMe(accessToken, check401);
|
||||
if (resUser.code === 200) {
|
||||
const user = resUser.data;
|
||||
if (user) {
|
||||
this.cacheStore.setLoginUser({
|
||||
user,
|
||||
id: user.id,
|
||||
accessToken,
|
||||
refreshToken,
|
||||
});
|
||||
} else {
|
||||
console.error('登录失败');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 刷新token
|
||||
* @param refreshToken
|
||||
* @returns
|
||||
*/
|
||||
async queryRefreshToken(refreshToken?: string) {
|
||||
const _refreshToken = refreshToken || this.cacheStore.getRefreshToken();
|
||||
let data = { refreshToken: _refreshToken };
|
||||
if (!_refreshToken) {
|
||||
await this.cacheStore.clearCurrentUser();
|
||||
return {
|
||||
code: 401,
|
||||
message: '请先登录',
|
||||
data: {} as any,
|
||||
};
|
||||
}
|
||||
return this.post(
|
||||
{ key: 'refreshToken', data },
|
||||
{
|
||||
afterResponse: async (response, ctx) => {
|
||||
setBaseResponse(response);
|
||||
return response as any;
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
/**
|
||||
* 检查401错误,并刷新token, 如果refreshToken存在,则刷新token, 否则返回401
|
||||
* 拦截请求,请使用run401Action, 不要直接使用 afterCheck401ToRefreshToken
|
||||
* @param response
|
||||
* @param ctx
|
||||
* @param refetch
|
||||
* @returns
|
||||
*/
|
||||
async afterCheck401ToRefreshToken(response: Result, ctx?: { req?: any; res?: any; fetch?: any }, refetch?: boolean) {
|
||||
const that = this;
|
||||
if (response?.code === 401) {
|
||||
const hasRefreshToken = await that.cacheStore.getRefreshToken();
|
||||
if (hasRefreshToken) {
|
||||
const res = await that.queryRefreshToken(hasRefreshToken);
|
||||
if (res.code === 200) {
|
||||
const { accessToken, refreshToken } = res?.data || {};
|
||||
that.storage.setItem('token', accessToken || '');
|
||||
await that.beforeSetLoginUser({ accessToken, refreshToken, check401: false });
|
||||
if (refetch && ctx && ctx.req && ctx.req.url && ctx.fetch) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1500));
|
||||
const url = ctx.req?.url;
|
||||
const body = ctx.req?.body;
|
||||
const headers = ctx.req?.headers;
|
||||
const res = await ctx.fetch(url, {
|
||||
method: 'POST',
|
||||
body: body,
|
||||
headers: { ...headers, Authorization: `Bearer ${accessToken}` },
|
||||
});
|
||||
setBaseResponse(res);
|
||||
return res;
|
||||
}
|
||||
} else {
|
||||
that.storage.removeItem('token');
|
||||
await that.cacheStore.clearCurrentUser();
|
||||
}
|
||||
return res;
|
||||
}
|
||||
}
|
||||
return response as any;
|
||||
}
|
||||
/**
|
||||
* 一个简单的401处理, 如果401,则刷新token, 如果refreshToken不存在,则返回401
|
||||
* refetch 是否重新请求, 会有bug,无限循环,按需要使用
|
||||
* TODO:
|
||||
* @param response
|
||||
* @param ctx
|
||||
* @param opts
|
||||
* @returns
|
||||
*/
|
||||
async run401Action(
|
||||
response: Result,
|
||||
ctx?: { req?: any; res?: any; fetch?: any },
|
||||
opts?: {
|
||||
/**
|
||||
* 是否重新请求, 会有bug,无限循环,按需要使用
|
||||
*/
|
||||
refetch?: boolean;
|
||||
/**
|
||||
* check之后的回调
|
||||
*/
|
||||
afterCheck?: (res: Result) => any;
|
||||
/**
|
||||
* 401处理后, 还是401, 则回调
|
||||
*/
|
||||
afterAlso401?: (res: Result) => any;
|
||||
},
|
||||
) {
|
||||
const that = this;
|
||||
const refetch = opts?.refetch ?? false;
|
||||
if (response?.code === 401) {
|
||||
if (that.query.stop === true) {
|
||||
return { code: 500, success: false, message: 'refresh token loading...' };
|
||||
}
|
||||
that.query.stop = true;
|
||||
const res = await that.afterCheck401ToRefreshToken(response, ctx, refetch);
|
||||
that.query.stop = false;
|
||||
opts?.afterCheck?.(res);
|
||||
if (res.code === 401) {
|
||||
opts?.afterAlso401?.(res);
|
||||
}
|
||||
return res;
|
||||
} else {
|
||||
return response as any;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 获取用户信息
|
||||
* @param token
|
||||
* @returns
|
||||
*/
|
||||
async getMe(token?: string, check401: boolean = true) {
|
||||
const _token = token || this.storage.getItem('token');
|
||||
const that = this;
|
||||
return that.post(
|
||||
{ key: 'me' },
|
||||
{
|
||||
beforeRequest: async (config) => {
|
||||
if (config.headers) {
|
||||
config.headers['Authorization'] = `Bearer ${_token}`;
|
||||
}
|
||||
if (!_token) {
|
||||
return false;
|
||||
}
|
||||
return config;
|
||||
},
|
||||
afterResponse: async (response, ctx) => {
|
||||
if (response?.code === 401 && check401 && !token) {
|
||||
return await that.afterCheck401ToRefreshToken(response, ctx);
|
||||
}
|
||||
return response as any;
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
/**
|
||||
* 检查本地用户,如果本地用户存在,则返回本地用户,否则返回null
|
||||
* @returns
|
||||
*/
|
||||
async checkLocalUser() {
|
||||
const user = await this.cacheStore.getCurrentUser();
|
||||
if (user) {
|
||||
return user;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
/**
|
||||
* 检查本地token是否存在,简单的判断是否已经属于登陆状态
|
||||
* @returns
|
||||
*/
|
||||
async checkLocalToken() {
|
||||
const token = this.storage.getItem('token');
|
||||
return !!token;
|
||||
}
|
||||
/**
|
||||
* 检查本地用户列表
|
||||
* @returns
|
||||
*/
|
||||
async getToken() {
|
||||
const token = this.storage.getItem('token');
|
||||
return token || '';
|
||||
}
|
||||
async beforeRequest(opts: any = {}) {
|
||||
const token = this.storage.getItem('token');
|
||||
if (token) {
|
||||
opts.headers = { ...opts.headers, Authorization: `Bearer ${token}` };
|
||||
}
|
||||
return opts;
|
||||
}
|
||||
/**
|
||||
* 请求更新,切换用户, 使用switchUser
|
||||
* @param username
|
||||
* @returns
|
||||
*/
|
||||
private async postSwitchUser(username: string) {
|
||||
return this.post({ key: 'switchCheck', data: { username } });
|
||||
}
|
||||
/**
|
||||
* 切换用户
|
||||
* @param username
|
||||
* @returns
|
||||
*/
|
||||
async switchUser(username: string) {
|
||||
const localUserList = await this.cacheStore.getCurrentUserList();
|
||||
const user = localUserList.find((userItem) => userItem.user!.username === username);
|
||||
if (user) {
|
||||
this.storage.setItem('token', user.accessToken || '');
|
||||
await this.beforeSetLoginUser({ accessToken: user.accessToken, refreshToken: user.refreshToken });
|
||||
return {
|
||||
code: 200,
|
||||
data: {
|
||||
accessToken: user.accessToken,
|
||||
refreshToken: user.refreshToken,
|
||||
},
|
||||
success: true,
|
||||
message: '切换用户成功',
|
||||
};
|
||||
}
|
||||
const res = await this.postSwitchUser(username);
|
||||
|
||||
if (res.code === 200) {
|
||||
const { accessToken, refreshToken } = res?.data || {};
|
||||
this.storage.setItem('token', accessToken || '');
|
||||
await this.beforeSetLoginUser({ accessToken, refreshToken });
|
||||
}
|
||||
return res;
|
||||
}
|
||||
/**
|
||||
* 退出登陆,去掉token, 并删除缓存
|
||||
* @returns
|
||||
*/
|
||||
async logout() {
|
||||
this.storage.removeItem('token');
|
||||
const users = await this.cacheStore.getCurrentUserList();
|
||||
const tokens = users
|
||||
.map((user) => {
|
||||
return user?.accessToken;
|
||||
})
|
||||
.filter(Boolean);
|
||||
this.cacheStore.delValue();
|
||||
return this.post<Result>({ key: 'logout', data: { tokens } });
|
||||
}
|
||||
/**
|
||||
* 检查用户名的组,这个用户是否存在
|
||||
* @param username
|
||||
* @returns
|
||||
*/
|
||||
async hasUser(username: string) {
|
||||
const that = this;
|
||||
return this.post<Result>(
|
||||
{
|
||||
path: 'org',
|
||||
key: 'hasUser',
|
||||
data: {
|
||||
username,
|
||||
},
|
||||
},
|
||||
{
|
||||
afterResponse: async (response, ctx) => {
|
||||
if (response?.code === 401) {
|
||||
const res = await that.afterCheck401ToRefreshToken(response, ctx, true);
|
||||
return res;
|
||||
}
|
||||
return response as any;
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
/**
|
||||
* 检查登录状态
|
||||
* @param token
|
||||
* @returns
|
||||
*/
|
||||
async checkLoginStatus(token: string) {
|
||||
const res = await this.post({
|
||||
path: 'user',
|
||||
key: 'checkLoginStatus',
|
||||
loginToken: token,
|
||||
});
|
||||
if (res.code === 200) {
|
||||
const accessToken = res.data?.accessToken;
|
||||
this.storage.setItem('token', accessToken || '');
|
||||
await this.beforeSetLoginUser({ accessToken, refreshToken: res.data?.refreshToken });
|
||||
return res;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* 使用web登录,创建url地址, 需要MD5和jsonwebtoken
|
||||
*/
|
||||
loginWithWeb(baseURL: string, { MD5, jsonwebtoken }: { MD5: any; jsonwebtoken: any }) {
|
||||
const randomId = Math.random().toString(36).substring(2, 15);
|
||||
const timestamp = Date.now();
|
||||
const tokenSecret = 'xiao' + randomId;
|
||||
const sign = MD5(`${tokenSecret}${timestamp}`).toString();
|
||||
const token = jsonwebtoken.sign({ randomId, timestamp, sign }, tokenSecret, {
|
||||
// 10分钟过期
|
||||
expiresIn: 60 * 10, // 10分钟
|
||||
});
|
||||
const url = `${baseURL}/api/router?path=user&key=webLogin&p&loginToken=${token}&sign=${sign}&randomId=${randomId}`;
|
||||
return { url, token, tokenSecret };
|
||||
}
|
||||
}
|
154
packages/api/query/query-mark/query-mark.ts
Normal file
154
packages/api/query/query-mark/query-mark.ts
Normal file
@ -0,0 +1,154 @@
|
||||
import { Query } from '@kevisual/query';
|
||||
import type { Result, DataOpts } from '@kevisual/query/query';
|
||||
|
||||
export type SimpleObject = Record<string, any>;
|
||||
export const markType = ['simple', 'md', 'mdx', 'wallnote', 'excalidraw', 'chat'] as const;
|
||||
export type MarkType = (typeof markType)[number];
|
||||
export type MarkData = {
|
||||
nodes?: any[];
|
||||
edges?: any[];
|
||||
elements?: any[];
|
||||
permission?: any;
|
||||
|
||||
[key: string]: any;
|
||||
};
|
||||
export type Mark = {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
markType: MarkType;
|
||||
link: string;
|
||||
data?: MarkData;
|
||||
uid: string;
|
||||
puid: string;
|
||||
summary: string;
|
||||
thumbnail?: string;
|
||||
tags: string[];
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
version: number;
|
||||
};
|
||||
export type ShowMarkPick = Pick<Mark, 'id' | 'title' | 'description' | 'summary' | 'link' | 'tags' | 'thumbnail' | 'updatedAt'>;
|
||||
|
||||
export type SearchOpts = {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
search?: string;
|
||||
sort?: string; // DESC, ASC
|
||||
markType?: MarkType; // 类型
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
export type QueryMarkOpts<T extends SimpleObject = SimpleObject> = {
|
||||
query?: Query;
|
||||
isBrowser?: boolean;
|
||||
onLoad?: () => void;
|
||||
} & T;
|
||||
|
||||
export type ResultMarkList = {
|
||||
list: Mark[];
|
||||
pagination: {
|
||||
pageSize: number;
|
||||
current: number;
|
||||
total: number;
|
||||
};
|
||||
};
|
||||
export type QueryMarkData = {
|
||||
id?: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
[key: string]: any;
|
||||
};
|
||||
export type QueryMarkResult = {
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
};
|
||||
|
||||
export class QueryMarkBase<T extends SimpleObject = SimpleObject> {
|
||||
query: Query;
|
||||
isBrowser: boolean;
|
||||
load?: boolean;
|
||||
storage?: Storage;
|
||||
onLoad?: () => void;
|
||||
|
||||
constructor(opts?: QueryMarkOpts<T>) {
|
||||
this.query = opts?.query || new Query();
|
||||
this.isBrowser = opts?.isBrowser ?? true;
|
||||
this.init();
|
||||
this.onLoad = opts?.onLoad;
|
||||
}
|
||||
setQuery(query: Query) {
|
||||
this.query = query;
|
||||
}
|
||||
private async init() {
|
||||
this.load = true;
|
||||
this.onLoad?.();
|
||||
}
|
||||
|
||||
async post<T = Result<any>>(data: any, opts?: DataOpts): Promise<T> {
|
||||
try {
|
||||
return this.query.post({ path: 'mark', ...data }, opts) as Promise<T>;
|
||||
} catch (error) {
|
||||
console.log('error', error);
|
||||
return {
|
||||
code: 400,
|
||||
} as any;
|
||||
}
|
||||
}
|
||||
|
||||
async getMarkList(search: SearchOpts, opts?: DataOpts) {
|
||||
return this.post<Result<ResultMarkList>>({ key: 'list', ...search }, opts);
|
||||
}
|
||||
|
||||
async getMark(id: string, opts?: DataOpts) {
|
||||
return this.post<Result<Mark>>({ key: 'get', id }, opts);
|
||||
}
|
||||
async getVersion(id: string, opts?: DataOpts) {
|
||||
return this.post<Result<{ version: number; id: string }>>({ key: 'getVersion', id }, opts);
|
||||
}
|
||||
/**
|
||||
* 检查版本
|
||||
* 当需要更新时,返回true
|
||||
* @param id
|
||||
* @param version
|
||||
* @param opts
|
||||
* @returns
|
||||
*/
|
||||
async checkVersion(id: string, version?: number, opts?: DataOpts) {
|
||||
if (!version) {
|
||||
return true;
|
||||
}
|
||||
const res = await this.getVersion(id, opts);
|
||||
if (res.code === 200) {
|
||||
if (res.data!.version > version) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async updateMark(data: any, opts?: DataOpts) {
|
||||
return this.post<Result<Mark>>({ key: 'update', data }, opts);
|
||||
}
|
||||
|
||||
async deleteMark(id: string, opts?: DataOpts) {
|
||||
return this.post<Result<Mark>>({ key: 'delete', id }, opts);
|
||||
}
|
||||
}
|
||||
export class QueryMark extends QueryMarkBase<SimpleObject> {
|
||||
markType: string;
|
||||
constructor(opts?: QueryMarkOpts & { markType?: MarkType }) {
|
||||
super(opts);
|
||||
this.markType = opts?.markType || 'simple';
|
||||
}
|
||||
async getMarkList(search?: SearchOpts, opts?: DataOpts) {
|
||||
return this.post<Result<ResultMarkList>>({ key: 'list', ...search, markType: this.markType }, opts);
|
||||
}
|
||||
async updateMark(data: any, opts?: DataOpts) {
|
||||
if (!data.id) {
|
||||
data.markType = this.markType || 'simple';
|
||||
}
|
||||
return super.updateMark(data, opts);
|
||||
}
|
||||
}
|
71
packages/api/query/query-resources/index.ts
Normal file
71
packages/api/query/query-resources/index.ts
Normal file
@ -0,0 +1,71 @@
|
||||
import { adapter, DataOpts, Result } from '@kevisual/query';
|
||||
|
||||
type QueryResourcesOptions = {
|
||||
prefix?: string;
|
||||
storage?: Storage;
|
||||
username?: string;
|
||||
[key: string]: any;
|
||||
};
|
||||
export class QueryResources {
|
||||
prefix: string; // root/resources
|
||||
storage: Storage;
|
||||
constructor(opts: QueryResourcesOptions) {
|
||||
if (opts.username) {
|
||||
this.prefix = `/${opts.username}/resources/`;
|
||||
} else {
|
||||
this.prefix = opts.prefix || '';
|
||||
}
|
||||
this.storage = opts.storage || localStorage;
|
||||
}
|
||||
setUsername(username: string) {
|
||||
this.prefix = `/${username}/resources/`;
|
||||
}
|
||||
header(headers?: Record<string, string>, json = true): Record<string, string> {
|
||||
const token = this.storage.getItem('token');
|
||||
const _headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
...headers,
|
||||
};
|
||||
if (!json) {
|
||||
delete _headers['Content-Type'];
|
||||
}
|
||||
if (!token) {
|
||||
return _headers;
|
||||
}
|
||||
return {
|
||||
..._headers,
|
||||
Authorization: `Bearer ${token}`,
|
||||
};
|
||||
}
|
||||
async get(data: any, opts: DataOpts): Promise<any> {
|
||||
return adapter({
|
||||
url: opts.url!,
|
||||
method: 'GET',
|
||||
body: data,
|
||||
...opts,
|
||||
headers: this.header(opts?.headers),
|
||||
});
|
||||
}
|
||||
async getList(prefix: string, data?: { recursive?: boolean }, opts?: DataOpts): Promise<Result<any[]>> {
|
||||
return this.get(data, {
|
||||
url: `${this.prefix}${prefix}`,
|
||||
body: data,
|
||||
...opts,
|
||||
});
|
||||
}
|
||||
async fetchFile(filepath: string, opts?: DataOpts): Promise<Result<any>> {
|
||||
return fetch(`${this.prefix}${filepath}`, {
|
||||
method: 'GET',
|
||||
headers: this.header(opts?.headers, false),
|
||||
}).then(async (res) => {
|
||||
if (!res.ok) {
|
||||
return {
|
||||
code: 500,
|
||||
success: false,
|
||||
message: `Failed to fetch file: ${res.status} ${res.statusText}`,
|
||||
} as Result<any>;
|
||||
}
|
||||
return { code: 200, data: await res.text(), success: true } as Result<any>;
|
||||
});
|
||||
}
|
||||
}
|
27
packages/api/query/query-shop/defines/query-shop-define.ts
Normal file
27
packages/api/query/query-shop/defines/query-shop-define.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { QueryUtil } from '@/query/index.ts';
|
||||
|
||||
export const shopDefine = QueryUtil.create({
|
||||
getRegistry: {
|
||||
path: 'shop',
|
||||
key: 'get-registry',
|
||||
description: '获取应用商店注册表信息',
|
||||
},
|
||||
|
||||
listInstalled: {
|
||||
path: 'shop',
|
||||
key: 'list-installed',
|
||||
description: '列出当前已安装的所有应用',
|
||||
},
|
||||
|
||||
install: {
|
||||
path: 'shop',
|
||||
key: 'install',
|
||||
description: '安装指定的应用,可以指定 id、type、force 和 yes 参数',
|
||||
},
|
||||
|
||||
uninstall: {
|
||||
path: 'shop',
|
||||
key: 'uninstall',
|
||||
description: '卸载指定的应用,可以指定 id 和 type 参数',
|
||||
},
|
||||
});
|
17
packages/api/query/query-shop/query-shop.ts
Normal file
17
packages/api/query/query-shop/query-shop.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { shopDefine } from './defines/query-shop-define.ts';
|
||||
|
||||
import { BaseQuery, DataOpts, Query } from '@kevisual/query/query';
|
||||
|
||||
export { shopDefine };
|
||||
|
||||
export class QueryShop<T extends Query = Query> extends BaseQuery<T, typeof shopDefine> {
|
||||
constructor(opts?: { query: T }) {
|
||||
super({
|
||||
query: opts?.query!,
|
||||
queryDefine: shopDefine,
|
||||
});
|
||||
}
|
||||
getInstall(data: any, opts?: DataOpts) {
|
||||
return this.queryDefine.queryChain('install').post(data, opts);
|
||||
}
|
||||
}
|
134
packages/api/query/query-upload/core/upload-chunk.ts
Normal file
134
packages/api/query/query-upload/core/upload-chunk.ts
Normal file
@ -0,0 +1,134 @@
|
||||
import { randomId } from '../utils/random-id.ts';
|
||||
import { UploadProgress } from './upload-progress.ts';
|
||||
export type ConvertOpts = {
|
||||
appKey?: string;
|
||||
version?: string;
|
||||
username?: string;
|
||||
directory?: string;
|
||||
isPublic?: boolean;
|
||||
filename?: string;
|
||||
/**
|
||||
* 是否不检查应用文件, 默认 true,默认不检测
|
||||
*/
|
||||
noCheckAppFiles?: boolean;
|
||||
};
|
||||
|
||||
// createEventSource: (baseUrl: string, searchParams: URLSearchParams) => {
|
||||
// return new EventSource(baseUrl + '/api/s1/events?' + searchParams.toString());
|
||||
// },
|
||||
export type UploadOpts = {
|
||||
uploadProgress: UploadProgress;
|
||||
/**
|
||||
* 创建 EventSource 兼容 nodejs
|
||||
* @param baseUrl 基础 URL
|
||||
* @param searchParams 查询参数
|
||||
* @returns EventSource
|
||||
*/
|
||||
createEventSource: (baseUrl: string, searchParams: URLSearchParams) => EventSource;
|
||||
baseUrl?: string;
|
||||
token: string;
|
||||
FormDataFn: any;
|
||||
};
|
||||
export const uploadFileChunked = async (file: File, opts: ConvertOpts, opts2: UploadOpts) => {
|
||||
const { directory, appKey, version, username, isPublic, noCheckAppFiles = true } = opts;
|
||||
const { uploadProgress, createEventSource, baseUrl = '', token, FormDataFn } = opts2 || {};
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const taskId = randomId();
|
||||
const filename = opts.filename || file.name;
|
||||
uploadProgress?.start(`${filename} 上传中...`);
|
||||
|
||||
const searchParams = new URLSearchParams();
|
||||
searchParams.set('taskId', taskId);
|
||||
if (isPublic) {
|
||||
searchParams.set('public', 'true');
|
||||
}
|
||||
if (noCheckAppFiles) {
|
||||
searchParams.set('noCheckAppFiles', '1');
|
||||
}
|
||||
const eventSource = createEventSource(baseUrl + '/api/s1/events', searchParams);
|
||||
let isError = false;
|
||||
// 监听服务器推送的进度更新
|
||||
eventSource.onmessage = function (event) {
|
||||
console.log('Progress update:', event.data);
|
||||
const parseIfJson = (data: string) => {
|
||||
try {
|
||||
return JSON.parse(data);
|
||||
} catch (e) {
|
||||
return data;
|
||||
}
|
||||
};
|
||||
const receivedData = parseIfJson(event.data);
|
||||
if (typeof receivedData === 'string') return;
|
||||
const progress = Number(receivedData.progress);
|
||||
const progressFixed = progress.toFixed(2);
|
||||
uploadProgress?.set(progress, { ...receivedData, progressFixed, filename, taskId });
|
||||
};
|
||||
eventSource.onerror = function (event) {
|
||||
console.log('eventSource.onerror', event);
|
||||
isError = true;
|
||||
reject(event);
|
||||
};
|
||||
|
||||
const chunkSize = 1 * 1024 * 1024; // 1MB
|
||||
const totalChunks = Math.ceil(file.size / chunkSize);
|
||||
|
||||
for (let currentChunk = 0; currentChunk < totalChunks; currentChunk++) {
|
||||
const start = currentChunk * chunkSize;
|
||||
const end = Math.min(start + chunkSize, file.size);
|
||||
const chunk = file.slice(start, end);
|
||||
|
||||
const formData = new FormDataFn();
|
||||
formData.append('file', chunk, filename);
|
||||
formData.append('chunkIndex', currentChunk.toString());
|
||||
formData.append('totalChunks', totalChunks.toString());
|
||||
const isLast = currentChunk === totalChunks - 1;
|
||||
if (directory) {
|
||||
formData.append('directory', directory);
|
||||
}
|
||||
if (appKey && version) {
|
||||
formData.append('appKey', appKey);
|
||||
formData.append('version', version);
|
||||
}
|
||||
if (username) {
|
||||
formData.append('username', username);
|
||||
}
|
||||
try {
|
||||
const res = await fetch(baseUrl + '/api/s1/resources/upload/chunk?taskId=' + taskId, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: {
|
||||
'task-id': taskId,
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
}).then((response) => response.json());
|
||||
|
||||
if (res?.code !== 200) {
|
||||
console.log('uploadChunk error', res);
|
||||
uploadProgress?.error(res?.message || '上传失败');
|
||||
isError = true;
|
||||
eventSource.close();
|
||||
|
||||
uploadProgress?.done();
|
||||
reject(new Error(res?.message || '上传失败'));
|
||||
return;
|
||||
}
|
||||
if (isLast) {
|
||||
fetch(baseUrl + '/api/s1/events/close?taskId=' + taskId);
|
||||
eventSource.close();
|
||||
uploadProgress?.done();
|
||||
resolve(res);
|
||||
}
|
||||
// console.log(`Chunk ${currentChunk + 1}/${totalChunks} uploaded`, res);
|
||||
} catch (error) {
|
||||
console.log('Error uploading chunk', error);
|
||||
fetch(baseUrl + '/api/s1/events/close?taskId=' + taskId);
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
// 循环结束
|
||||
if (!uploadProgress?.end) {
|
||||
uploadProgress?.done();
|
||||
}
|
||||
});
|
||||
};
|
103
packages/api/query/query-upload/core/upload-progress.ts
Normal file
103
packages/api/query/query-upload/core/upload-progress.ts
Normal file
@ -0,0 +1,103 @@
|
||||
interface UploadNProgress {
|
||||
start: (msg?: string) => void;
|
||||
done: () => void;
|
||||
set: (progress: number) => void;
|
||||
}
|
||||
export type UploadProgressData = {
|
||||
progress: number;
|
||||
progressFixed: number;
|
||||
filename?: string;
|
||||
taskId?: string;
|
||||
};
|
||||
type UploadProgressOpts = {
|
||||
onStart?: () => void;
|
||||
onDone?: () => void;
|
||||
onProgress?: (progress: number, data?: UploadProgressData) => void;
|
||||
};
|
||||
export class UploadProgress implements UploadNProgress {
|
||||
/**
|
||||
* 进度
|
||||
*/
|
||||
progress: number;
|
||||
/**
|
||||
* 开始回调
|
||||
*/
|
||||
onStart: (() => void) | undefined;
|
||||
/**
|
||||
* 结束回调
|
||||
*/
|
||||
onDone: (() => void) | undefined;
|
||||
/**
|
||||
* 消息回调
|
||||
*/
|
||||
onProgress: ((progress: number, data?: UploadProgressData) => void) | undefined;
|
||||
/**
|
||||
* 数据
|
||||
*/
|
||||
data: any;
|
||||
/**
|
||||
* 是否结束
|
||||
*/
|
||||
end: boolean;
|
||||
constructor(uploadOpts: UploadProgressOpts) {
|
||||
this.progress = 0;
|
||||
this.end = false;
|
||||
const mockFn = () => {};
|
||||
this.onStart = uploadOpts.onStart || mockFn;
|
||||
this.onDone = uploadOpts.onDone || mockFn;
|
||||
this.onProgress = uploadOpts.onProgress || mockFn;
|
||||
}
|
||||
start(msg?: string) {
|
||||
this.progress = 0;
|
||||
msg && this.info(msg);
|
||||
this.end = false;
|
||||
this.onStart?.();
|
||||
}
|
||||
done() {
|
||||
this.progress = 100;
|
||||
this.end = true;
|
||||
this.onDone?.();
|
||||
}
|
||||
set(progress: number, data?: UploadProgressData) {
|
||||
this.progress = progress;
|
||||
this.data = data;
|
||||
this.onProgress?.(progress, data);
|
||||
console.log('uploadProgress set', progress, data);
|
||||
}
|
||||
/**
|
||||
* 开始回调
|
||||
*/
|
||||
setOnStart(callback: () => void) {
|
||||
this.onStart = callback;
|
||||
}
|
||||
/**
|
||||
* 结束回调
|
||||
*/
|
||||
setOnDone(callback: () => void) {
|
||||
this.onDone = callback;
|
||||
}
|
||||
/**
|
||||
* 消息回调
|
||||
*/
|
||||
setOnProgress(callback: (progress: number, data?: UploadProgressData) => void) {
|
||||
this.onProgress = callback;
|
||||
}
|
||||
/**
|
||||
* 打印信息
|
||||
*/
|
||||
info(msg: string) {
|
||||
console.log(msg);
|
||||
}
|
||||
/**
|
||||
* 打印错误
|
||||
*/
|
||||
error(msg: string) {
|
||||
console.error(msg);
|
||||
}
|
||||
/**
|
||||
* 打印警告
|
||||
*/
|
||||
warn(msg: string) {
|
||||
console.warn(msg);
|
||||
}
|
||||
}
|
113
packages/api/query/query-upload/core/upload.ts
Normal file
113
packages/api/query/query-upload/core/upload.ts
Normal file
@ -0,0 +1,113 @@
|
||||
import { randomId } from '../utils/random-id.ts';
|
||||
import type { UploadOpts } from './upload-chunk.ts';
|
||||
type ConvertOpts = {
|
||||
appKey?: string;
|
||||
version?: string;
|
||||
username?: string;
|
||||
directory?: string;
|
||||
/**
|
||||
* 文件大小限制
|
||||
*/
|
||||
maxSize?: number;
|
||||
/**
|
||||
* 文件数量限制
|
||||
*/
|
||||
maxCount?: number;
|
||||
/**
|
||||
* 是否不检查应用文件, 默认 true,默认不检测
|
||||
*/
|
||||
noCheckAppFiles?: boolean;
|
||||
};
|
||||
|
||||
export const uploadFiles = async (files: File[], opts: ConvertOpts, opts2: UploadOpts) => {
|
||||
const { directory, appKey, version, username, noCheckAppFiles = true } = opts;
|
||||
const { uploadProgress, createEventSource, baseUrl = '', token, FormDataFn } = opts2 || {};
|
||||
const length = files.length;
|
||||
const maxSize = opts.maxSize || 20 * 1024 * 1024; // 20MB
|
||||
const totalSize = files.reduce((acc, file) => acc + file.size, 0);
|
||||
if (totalSize > maxSize) {
|
||||
const maxSizeMB = maxSize / 1024 / 1024;
|
||||
uploadProgress?.error('有文件大小不能超过' + maxSizeMB + 'MB');
|
||||
return;
|
||||
}
|
||||
const maxCount = opts.maxCount || 10;
|
||||
if (length > maxCount) {
|
||||
uploadProgress?.error(`最多只能上传${maxCount}个文件`);
|
||||
return;
|
||||
}
|
||||
uploadProgress?.info(`上传中,共${length}个文件`);
|
||||
return new Promise((resolve, reject) => {
|
||||
const formData = new FormDataFn();
|
||||
const webkitRelativePath = files[0]?.webkitRelativePath;
|
||||
const keepDirectory = webkitRelativePath !== '';
|
||||
const root = keepDirectory ? webkitRelativePath.split('/')[0] : '';
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const file = files[i];
|
||||
if (keepDirectory) {
|
||||
// relativePath 去除第一级
|
||||
const webkitRelativePath = file.webkitRelativePath.replace(root + '/', '');
|
||||
formData.append('file', file, webkitRelativePath); // 保留文件夹路径
|
||||
} else {
|
||||
formData.append('file', files[i], files[i].name);
|
||||
}
|
||||
}
|
||||
if (directory) {
|
||||
formData.append('directory', directory);
|
||||
}
|
||||
if (appKey && version) {
|
||||
formData.append('appKey', appKey);
|
||||
formData.append('version', version);
|
||||
}
|
||||
if (username) {
|
||||
formData.append('username', username);
|
||||
}
|
||||
const searchParams = new URLSearchParams();
|
||||
const taskId = randomId();
|
||||
searchParams.set('taskId', taskId);
|
||||
|
||||
if (noCheckAppFiles) {
|
||||
searchParams.set('noCheckAppFiles', '1');
|
||||
}
|
||||
const eventSource = new EventSource('/api/s1/events?taskId=' + taskId);
|
||||
|
||||
uploadProgress?.start('上传中...');
|
||||
eventSource.onopen = async function (event) {
|
||||
const res = await fetch('/api/s1/resources/upload?' + searchParams.toString(), {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: {
|
||||
'task-id': taskId,
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
}).then((response) => response.json());
|
||||
|
||||
console.log('upload success', res);
|
||||
fetch('/api/s1/events/close?taskId=' + taskId);
|
||||
eventSource.close();
|
||||
uploadProgress?.done();
|
||||
resolve(res);
|
||||
};
|
||||
// 监听服务器推送的进度更新
|
||||
eventSource.onmessage = function (event) {
|
||||
console.log('Progress update:', event.data);
|
||||
const parseIfJson = (data: string) => {
|
||||
try {
|
||||
return JSON.parse(data);
|
||||
} catch (e) {
|
||||
return data;
|
||||
}
|
||||
};
|
||||
const receivedData = parseIfJson(event.data);
|
||||
if (typeof receivedData === 'string') return;
|
||||
const progress = Number(receivedData.progress);
|
||||
const progressFixed = progress.toFixed(2);
|
||||
console.log('progress', progress);
|
||||
uploadProgress?.set(progress, { ...receivedData, taskId, progressFixed });
|
||||
};
|
||||
|
||||
eventSource.onerror = function (event) {
|
||||
console.log('eventSource.onerror', event);
|
||||
reject(event);
|
||||
};
|
||||
});
|
||||
};
|
51
packages/api/query/query-upload/query-upload-browser.ts
Normal file
51
packages/api/query/query-upload/query-upload-browser.ts
Normal file
@ -0,0 +1,51 @@
|
||||
import { UploadProgress, UploadProgressData } from './core/upload-progress.ts';
|
||||
import { uploadFileChunked } from './core/upload-chunk.ts';
|
||||
import { toFile, uploadFiles, randomId } from './query-upload.ts';
|
||||
|
||||
export { toFile, randomId };
|
||||
export { uploadFiles, uploadFileChunked, UploadProgress };
|
||||
|
||||
type UploadFileProps = {
|
||||
onStart?: () => void;
|
||||
onDone?: () => void;
|
||||
onProgress?: (progress: number, data: UploadProgressData) => void;
|
||||
onSuccess?: (res: any) => void;
|
||||
onError?: (err: any) => void;
|
||||
token?: string;
|
||||
};
|
||||
export type ConvertOpts = {
|
||||
appKey?: string;
|
||||
version?: string;
|
||||
username?: string;
|
||||
directory?: string;
|
||||
isPublic?: boolean;
|
||||
filename?: string;
|
||||
/**
|
||||
* 是否不检查应用文件, 默认 true,默认不检测
|
||||
*/
|
||||
noCheckAppFiles?: boolean;
|
||||
};
|
||||
|
||||
export const uploadChunk = async (file: File, opts: ConvertOpts, props?: UploadFileProps) => {
|
||||
const uploadProgress = new UploadProgress({
|
||||
onStart: function () {
|
||||
props?.onStart?.();
|
||||
},
|
||||
onDone: () => {
|
||||
props?.onDone?.();
|
||||
},
|
||||
onProgress: (progress, data) => {
|
||||
props?.onProgress?.(progress, data!);
|
||||
},
|
||||
});
|
||||
const result = await uploadFileChunked(file, opts, {
|
||||
uploadProgress,
|
||||
token: props?.token!,
|
||||
createEventSource: (url: string, searchParams: URLSearchParams) => {
|
||||
return new EventSource(url + '?' + searchParams.toString());
|
||||
},
|
||||
FormDataFn: FormData,
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
1
packages/api/query/query-upload/query-upload-node.ts
Normal file
1
packages/api/query/query-upload/query-upload-node.ts
Normal file
@ -0,0 +1 @@
|
||||
// console.log('upload)
|
11
packages/api/query/query-upload/query-upload.ts
Normal file
11
packages/api/query/query-upload/query-upload.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { uploadFiles } from './core/upload.ts';
|
||||
|
||||
import { uploadFileChunked } from './core/upload-chunk.ts';
|
||||
import { UploadProgress } from './core/upload-progress.ts';
|
||||
|
||||
export { uploadFiles, uploadFileChunked, UploadProgress };
|
||||
|
||||
export * from './utils/to-file.ts';
|
||||
export { randomId } from './utils/random-id.ts';
|
||||
|
||||
export { filterFiles } from './utils/filter-files.ts';
|
23
packages/api/query/query-upload/utils/filter-files.ts
Normal file
23
packages/api/query/query-upload/utils/filter-files.ts
Normal file
@ -0,0 +1,23 @@
|
||||
/**
|
||||
* 过滤文件, 过滤 .DS_Store, node_modules, 以.开头的文件, 过滤 __开头的文件
|
||||
* @param files
|
||||
* @returns
|
||||
*/
|
||||
export const filterFiles = (files: File[]) => {
|
||||
files = files.filter((file) => {
|
||||
if (file.webkitRelativePath.startsWith('__MACOSX')) {
|
||||
return false;
|
||||
}
|
||||
// 过滤node_modules
|
||||
if (file.webkitRelativePath.includes('node_modules')) {
|
||||
return false;
|
||||
}
|
||||
// 过滤文件 .DS_Store
|
||||
if (file.name === '.DS_Store') {
|
||||
return false;
|
||||
}
|
||||
// 过滤以.开头的文件
|
||||
return !file.name.startsWith('.');
|
||||
});
|
||||
return files;
|
||||
};
|
3
packages/api/query/query-upload/utils/index.ts
Normal file
3
packages/api/query/query-upload/utils/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from './to-file.ts';
|
||||
export * from './filter-files.ts';
|
||||
export * from './random-id.ts';
|
3
packages/api/query/query-upload/utils/random-id.ts
Normal file
3
packages/api/query/query-upload/utils/random-id.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export const randomId = () => {
|
||||
return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
|
||||
};
|
105
packages/api/query/query-upload/utils/to-file.ts
Normal file
105
packages/api/query/query-upload/utils/to-file.ts
Normal file
@ -0,0 +1,105 @@
|
||||
const getFileExtension = (filename: string) => {
|
||||
return filename.split('.').pop();
|
||||
};
|
||||
const getFileType = (extension: string) => {
|
||||
switch (extension) {
|
||||
case 'js':
|
||||
return 'text/javascript';
|
||||
case 'css':
|
||||
return 'text/css';
|
||||
case 'html':
|
||||
return 'text/html';
|
||||
case 'json':
|
||||
return 'application/json';
|
||||
case 'png':
|
||||
return 'image/png';
|
||||
case 'jpg':
|
||||
return 'image/jpeg';
|
||||
case 'jpeg':
|
||||
return 'image/jpeg';
|
||||
case 'gif':
|
||||
return 'image/gif';
|
||||
case 'svg':
|
||||
return 'image/svg+xml';
|
||||
case 'webp':
|
||||
return 'image/webp';
|
||||
case 'ico':
|
||||
return 'image/x-icon';
|
||||
default:
|
||||
return 'text/plain';
|
||||
}
|
||||
};
|
||||
const checkIsBase64 = (content: string) => {
|
||||
return content.startsWith('data:');
|
||||
};
|
||||
/**
|
||||
* 获取文件的目录和文件名
|
||||
* @param filename 文件名
|
||||
* @returns 目录和文件名
|
||||
*/
|
||||
export const getDirectoryAndName = (filename: string) => {
|
||||
if (!filename) {
|
||||
return null;
|
||||
}
|
||||
if (filename.startsWith('.')) {
|
||||
return null;
|
||||
} else {
|
||||
filename = filename.replace(/^\/+/, ''); // Remove all leading slashes
|
||||
}
|
||||
const hasDirectory = filename.includes('/');
|
||||
if (!hasDirectory) {
|
||||
return { directory: '', name: filename };
|
||||
}
|
||||
const parts = filename.split('/');
|
||||
const name = parts.pop()!; // Get the last part as the file name
|
||||
const directory = parts.join('/'); // Join the remaining parts as the directory
|
||||
return { directory, name };
|
||||
};
|
||||
/**
|
||||
* 把字符串转为文件流,并返回文件流,根据filename的扩展名,自动设置文件类型.
|
||||
* 当不是文本类型,自动需要把base64的字符串转为blob
|
||||
* @param content 字符串
|
||||
* @param filename 文件名
|
||||
* @returns 文件流
|
||||
*/
|
||||
export const toFile = (content: string, filename: string) => {
|
||||
// 如果文件名是 a/d/a.js 格式的,则需要把d作为目录,a.js作为文件名
|
||||
const directoryAndName = getDirectoryAndName(filename);
|
||||
if (!directoryAndName) {
|
||||
throw new Error('Invalid filename');
|
||||
}
|
||||
const { name } = directoryAndName;
|
||||
const extension = getFileExtension(name);
|
||||
if (!extension) {
|
||||
throw new Error('Invalid filename');
|
||||
}
|
||||
const isBase64 = checkIsBase64(content);
|
||||
const type = getFileType(extension);
|
||||
|
||||
if (isBase64) {
|
||||
// Decode base64 string
|
||||
const base64Data = content.split(',')[1]; // Remove the data URL prefix
|
||||
const byteCharacters = atob(base64Data);
|
||||
const byteNumbers = new Array(byteCharacters.length);
|
||||
for (let i = 0; i < byteCharacters.length; i++) {
|
||||
byteNumbers[i] = byteCharacters.charCodeAt(i);
|
||||
}
|
||||
const byteArray = new Uint8Array(byteNumbers);
|
||||
const blob = new Blob([byteArray], { type });
|
||||
return new File([blob], filename, { type });
|
||||
} else {
|
||||
const blob = new Blob([content], { type });
|
||||
return new File([blob], filename, { type });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 把字符串转为文本文件
|
||||
* @param content 字符串
|
||||
* @param filename 文件名
|
||||
* @returns 文件流
|
||||
*/
|
||||
export const toTextFile = (content: string = 'keep directory exist', filename: string = 'keep.txt') => {
|
||||
const file = toFile(content, filename);
|
||||
return file;
|
||||
};
|
19
packages/api/tsconfig.json
Normal file
19
packages/api/tsconfig.json
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"extends": "@kevisual/types/json/frontend.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"typeRoots": [
|
||||
"./node_modules/@types",
|
||||
"./node_modules/@kevisual"
|
||||
],
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"src/*"
|
||||
]
|
||||
},
|
||||
},
|
||||
"include": [
|
||||
"src/**/*",
|
||||
"query/**/*",
|
||||
],
|
||||
}
|
44
pnpm-lock.yaml
generated
44
pnpm-lock.yaml
generated
@ -27,6 +27,22 @@ importers:
|
||||
specifier: ^8.4.0
|
||||
version: 8.4.0(typescript@5.8.3)
|
||||
|
||||
packages/api:
|
||||
dependencies:
|
||||
'@kevisual/query':
|
||||
specifier: ^0.0.18
|
||||
version: 0.0.18
|
||||
'@kevisual/router':
|
||||
specifier: ^0.0.20
|
||||
version: 0.0.20
|
||||
devDependencies:
|
||||
'@kevisual/types':
|
||||
specifier: ^0.0.10
|
||||
version: 0.0.10
|
||||
'@types/node':
|
||||
specifier: ^22.15.27
|
||||
version: 22.15.27
|
||||
|
||||
packages/list: {}
|
||||
|
||||
packages/query-app: {}
|
||||
@ -67,7 +83,7 @@ importers:
|
||||
devDependencies:
|
||||
'@types/node':
|
||||
specifier: ^22.14.1
|
||||
version: 22.15.18
|
||||
version: 22.15.27
|
||||
tsup:
|
||||
specifier: ^8.4.0
|
||||
version: 8.4.0(typescript@5.8.3)
|
||||
@ -86,7 +102,7 @@ importers:
|
||||
devDependencies:
|
||||
'@types/node':
|
||||
specifier: ^22.13.11
|
||||
version: 22.15.18
|
||||
version: 22.15.27
|
||||
tsup:
|
||||
specifier: ^8.4.0
|
||||
version: 8.4.0(typescript@5.8.3)
|
||||
@ -95,7 +111,7 @@ importers:
|
||||
devDependencies:
|
||||
'@types/node':
|
||||
specifier: ^22.13.14
|
||||
version: 22.15.18
|
||||
version: 22.15.27
|
||||
eventsource:
|
||||
specifier: ^3.0.6
|
||||
version: 3.0.7
|
||||
@ -407,56 +423,67 @@ packages:
|
||||
resolution: {integrity: sha512-de6TFZYIvJwRNjmW3+gaXiZ2DaWL5D5yGmSYzkdzjBDS3W+B9JQ48oZEsmMvemqjtAFzE16DIBLqd6IQQRuG9Q==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm-musleabihf@4.40.2':
|
||||
resolution: {integrity: sha512-urjaEZubdIkacKc930hUDOfQPysezKla/O9qV+O89enqsqUmQm8Xj8O/vh0gHg4LYfv7Y7UsE3QjzLQzDYN1qg==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-arm64-gnu@4.40.2':
|
||||
resolution: {integrity: sha512-KlE8IC0HFOC33taNt1zR8qNlBYHj31qGT1UqWqtvR/+NuCVhfufAq9fxO8BMFC22Wu0rxOwGVWxtCMvZVLmhQg==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm64-musl@4.40.2':
|
||||
resolution: {integrity: sha512-j8CgxvfM0kbnhu4XgjnCWJQyyBOeBI1Zq91Z850aUddUmPeQvuAy6OiMdPS46gNFgy8gN1xkYyLgwLYZG3rBOg==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-loongarch64-gnu@4.40.2':
|
||||
resolution: {integrity: sha512-Ybc/1qUampKuRF4tQXc7G7QY9YRyeVSykfK36Y5Qc5dmrIxwFhrOzqaVTNoZygqZ1ZieSWTibfFhQ5qK8jpWxw==}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-powerpc64le-gnu@4.40.2':
|
||||
resolution: {integrity: sha512-3FCIrnrt03CCsZqSYAOW/k9n625pjpuMzVfeI+ZBUSDT3MVIFDSPfSUgIl9FqUftxcUXInvFah79hE1c9abD+Q==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-riscv64-gnu@4.40.2':
|
||||
resolution: {integrity: sha512-QNU7BFHEvHMp2ESSY3SozIkBPaPBDTsfVNGx3Xhv+TdvWXFGOSH2NJvhD1zKAT6AyuuErJgbdvaJhYVhVqrWTg==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-riscv64-musl@4.40.2':
|
||||
resolution: {integrity: sha512-5W6vNYkhgfh7URiXTO1E9a0cy4fSgfE4+Hl5agb/U1sa0kjOLMLC1wObxwKxecE17j0URxuTrYZZME4/VH57Hg==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-s390x-gnu@4.40.2':
|
||||
resolution: {integrity: sha512-B7LKIz+0+p348JoAL4X/YxGx9zOx3sR+o6Hj15Y3aaApNfAshK8+mWZEf759DXfRLeL2vg5LYJBB7DdcleYCoQ==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-x64-gnu@4.40.2':
|
||||
resolution: {integrity: sha512-lG7Xa+BmBNwpjmVUbmyKxdQJ3Q6whHjMjzQplOs5Z+Gj7mxPtWakGHqzMqNER68G67kmCX9qX57aRsW5V0VOng==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-x64-musl@4.40.2':
|
||||
resolution: {integrity: sha512-tD46wKHd+KJvsmije4bUskNuvWKFcTOIM9tZ/RrmIvcXnbi0YK/cKS9FzFtAm7Oxi2EhV5N2OpfFB348vSQRXA==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-win32-arm64-msvc@4.40.2':
|
||||
resolution: {integrity: sha512-Bjv/HG8RRWLNkXwQQemdsWw4Mg+IJ29LK+bJPW2SCzPKOUaMmPEppQlu/Fqk1d7+DX3V7JbFdbkh/NMmurT6Pg==}
|
||||
@ -488,6 +515,9 @@ packages:
|
||||
'@types/node@22.15.18':
|
||||
resolution: {integrity: sha512-v1DKRfUdyW+jJhZNEI1PYy29S2YRxMV5AOO/x/SjKmW0acCIOqmbj6Haf9eHAhsPmrhlHSxEhv/1WszcLWV4cg==}
|
||||
|
||||
'@types/node@22.15.27':
|
||||
resolution: {integrity: sha512-5fF+eu5mwihV2BeVtX5vijhdaZOfkQTATrePEaXTcKqI16LhJ7gi2/Vhd9OZM0UojcdmiOCVg5rrax+i1MdoQQ==}
|
||||
|
||||
'@types/resolve@1.20.2':
|
||||
resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==}
|
||||
|
||||
@ -1382,12 +1412,12 @@ snapshots:
|
||||
|
||||
'@types/node-fetch@2.6.12':
|
||||
dependencies:
|
||||
'@types/node': 22.15.18
|
||||
'@types/node': 22.15.27
|
||||
form-data: 4.0.2
|
||||
|
||||
'@types/node-forge@1.3.11':
|
||||
dependencies:
|
||||
'@types/node': 22.15.18
|
||||
'@types/node': 22.15.27
|
||||
|
||||
'@types/node@18.19.100':
|
||||
dependencies:
|
||||
@ -1397,6 +1427,10 @@ snapshots:
|
||||
dependencies:
|
||||
undici-types: 6.21.0
|
||||
|
||||
'@types/node@22.15.27':
|
||||
dependencies:
|
||||
undici-types: 6.21.0
|
||||
|
||||
'@types/resolve@1.20.2': {}
|
||||
|
||||
abort-controller@3.0.0:
|
||||
|
Loading…
x
Reference in New Issue
Block a user