temp
This commit is contained in:
		
							
								
								
									
										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
 | 
					        specifier: ^8.4.0
 | 
				
			||||||
        version: 8.4.0(typescript@5.8.3)
 | 
					        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/list: {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  packages/query-app: {}
 | 
					  packages/query-app: {}
 | 
				
			||||||
@@ -67,7 +83,7 @@ importers:
 | 
				
			|||||||
    devDependencies:
 | 
					    devDependencies:
 | 
				
			||||||
      '@types/node':
 | 
					      '@types/node':
 | 
				
			||||||
        specifier: ^22.14.1
 | 
					        specifier: ^22.14.1
 | 
				
			||||||
        version: 22.15.18
 | 
					        version: 22.15.27
 | 
				
			||||||
      tsup:
 | 
					      tsup:
 | 
				
			||||||
        specifier: ^8.4.0
 | 
					        specifier: ^8.4.0
 | 
				
			||||||
        version: 8.4.0(typescript@5.8.3)
 | 
					        version: 8.4.0(typescript@5.8.3)
 | 
				
			||||||
@@ -86,7 +102,7 @@ importers:
 | 
				
			|||||||
    devDependencies:
 | 
					    devDependencies:
 | 
				
			||||||
      '@types/node':
 | 
					      '@types/node':
 | 
				
			||||||
        specifier: ^22.13.11
 | 
					        specifier: ^22.13.11
 | 
				
			||||||
        version: 22.15.18
 | 
					        version: 22.15.27
 | 
				
			||||||
      tsup:
 | 
					      tsup:
 | 
				
			||||||
        specifier: ^8.4.0
 | 
					        specifier: ^8.4.0
 | 
				
			||||||
        version: 8.4.0(typescript@5.8.3)
 | 
					        version: 8.4.0(typescript@5.8.3)
 | 
				
			||||||
@@ -95,7 +111,7 @@ importers:
 | 
				
			|||||||
    devDependencies:
 | 
					    devDependencies:
 | 
				
			||||||
      '@types/node':
 | 
					      '@types/node':
 | 
				
			||||||
        specifier: ^22.13.14
 | 
					        specifier: ^22.13.14
 | 
				
			||||||
        version: 22.15.18
 | 
					        version: 22.15.27
 | 
				
			||||||
      eventsource:
 | 
					      eventsource:
 | 
				
			||||||
        specifier: ^3.0.6
 | 
					        specifier: ^3.0.6
 | 
				
			||||||
        version: 3.0.7
 | 
					        version: 3.0.7
 | 
				
			||||||
@@ -407,56 +423,67 @@ packages:
 | 
				
			|||||||
    resolution: {integrity: sha512-de6TFZYIvJwRNjmW3+gaXiZ2DaWL5D5yGmSYzkdzjBDS3W+B9JQ48oZEsmMvemqjtAFzE16DIBLqd6IQQRuG9Q==}
 | 
					    resolution: {integrity: sha512-de6TFZYIvJwRNjmW3+gaXiZ2DaWL5D5yGmSYzkdzjBDS3W+B9JQ48oZEsmMvemqjtAFzE16DIBLqd6IQQRuG9Q==}
 | 
				
			||||||
    cpu: [arm]
 | 
					    cpu: [arm]
 | 
				
			||||||
    os: [linux]
 | 
					    os: [linux]
 | 
				
			||||||
 | 
					    libc: [glibc]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  '@rollup/rollup-linux-arm-musleabihf@4.40.2':
 | 
					  '@rollup/rollup-linux-arm-musleabihf@4.40.2':
 | 
				
			||||||
    resolution: {integrity: sha512-urjaEZubdIkacKc930hUDOfQPysezKla/O9qV+O89enqsqUmQm8Xj8O/vh0gHg4LYfv7Y7UsE3QjzLQzDYN1qg==}
 | 
					    resolution: {integrity: sha512-urjaEZubdIkacKc930hUDOfQPysezKla/O9qV+O89enqsqUmQm8Xj8O/vh0gHg4LYfv7Y7UsE3QjzLQzDYN1qg==}
 | 
				
			||||||
    cpu: [arm]
 | 
					    cpu: [arm]
 | 
				
			||||||
    os: [linux]
 | 
					    os: [linux]
 | 
				
			||||||
 | 
					    libc: [musl]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  '@rollup/rollup-linux-arm64-gnu@4.40.2':
 | 
					  '@rollup/rollup-linux-arm64-gnu@4.40.2':
 | 
				
			||||||
    resolution: {integrity: sha512-KlE8IC0HFOC33taNt1zR8qNlBYHj31qGT1UqWqtvR/+NuCVhfufAq9fxO8BMFC22Wu0rxOwGVWxtCMvZVLmhQg==}
 | 
					    resolution: {integrity: sha512-KlE8IC0HFOC33taNt1zR8qNlBYHj31qGT1UqWqtvR/+NuCVhfufAq9fxO8BMFC22Wu0rxOwGVWxtCMvZVLmhQg==}
 | 
				
			||||||
    cpu: [arm64]
 | 
					    cpu: [arm64]
 | 
				
			||||||
    os: [linux]
 | 
					    os: [linux]
 | 
				
			||||||
 | 
					    libc: [glibc]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  '@rollup/rollup-linux-arm64-musl@4.40.2':
 | 
					  '@rollup/rollup-linux-arm64-musl@4.40.2':
 | 
				
			||||||
    resolution: {integrity: sha512-j8CgxvfM0kbnhu4XgjnCWJQyyBOeBI1Zq91Z850aUddUmPeQvuAy6OiMdPS46gNFgy8gN1xkYyLgwLYZG3rBOg==}
 | 
					    resolution: {integrity: sha512-j8CgxvfM0kbnhu4XgjnCWJQyyBOeBI1Zq91Z850aUddUmPeQvuAy6OiMdPS46gNFgy8gN1xkYyLgwLYZG3rBOg==}
 | 
				
			||||||
    cpu: [arm64]
 | 
					    cpu: [arm64]
 | 
				
			||||||
    os: [linux]
 | 
					    os: [linux]
 | 
				
			||||||
 | 
					    libc: [musl]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  '@rollup/rollup-linux-loongarch64-gnu@4.40.2':
 | 
					  '@rollup/rollup-linux-loongarch64-gnu@4.40.2':
 | 
				
			||||||
    resolution: {integrity: sha512-Ybc/1qUampKuRF4tQXc7G7QY9YRyeVSykfK36Y5Qc5dmrIxwFhrOzqaVTNoZygqZ1ZieSWTibfFhQ5qK8jpWxw==}
 | 
					    resolution: {integrity: sha512-Ybc/1qUampKuRF4tQXc7G7QY9YRyeVSykfK36Y5Qc5dmrIxwFhrOzqaVTNoZygqZ1ZieSWTibfFhQ5qK8jpWxw==}
 | 
				
			||||||
    cpu: [loong64]
 | 
					    cpu: [loong64]
 | 
				
			||||||
    os: [linux]
 | 
					    os: [linux]
 | 
				
			||||||
 | 
					    libc: [glibc]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  '@rollup/rollup-linux-powerpc64le-gnu@4.40.2':
 | 
					  '@rollup/rollup-linux-powerpc64le-gnu@4.40.2':
 | 
				
			||||||
    resolution: {integrity: sha512-3FCIrnrt03CCsZqSYAOW/k9n625pjpuMzVfeI+ZBUSDT3MVIFDSPfSUgIl9FqUftxcUXInvFah79hE1c9abD+Q==}
 | 
					    resolution: {integrity: sha512-3FCIrnrt03CCsZqSYAOW/k9n625pjpuMzVfeI+ZBUSDT3MVIFDSPfSUgIl9FqUftxcUXInvFah79hE1c9abD+Q==}
 | 
				
			||||||
    cpu: [ppc64]
 | 
					    cpu: [ppc64]
 | 
				
			||||||
    os: [linux]
 | 
					    os: [linux]
 | 
				
			||||||
 | 
					    libc: [glibc]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  '@rollup/rollup-linux-riscv64-gnu@4.40.2':
 | 
					  '@rollup/rollup-linux-riscv64-gnu@4.40.2':
 | 
				
			||||||
    resolution: {integrity: sha512-QNU7BFHEvHMp2ESSY3SozIkBPaPBDTsfVNGx3Xhv+TdvWXFGOSH2NJvhD1zKAT6AyuuErJgbdvaJhYVhVqrWTg==}
 | 
					    resolution: {integrity: sha512-QNU7BFHEvHMp2ESSY3SozIkBPaPBDTsfVNGx3Xhv+TdvWXFGOSH2NJvhD1zKAT6AyuuErJgbdvaJhYVhVqrWTg==}
 | 
				
			||||||
    cpu: [riscv64]
 | 
					    cpu: [riscv64]
 | 
				
			||||||
    os: [linux]
 | 
					    os: [linux]
 | 
				
			||||||
 | 
					    libc: [glibc]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  '@rollup/rollup-linux-riscv64-musl@4.40.2':
 | 
					  '@rollup/rollup-linux-riscv64-musl@4.40.2':
 | 
				
			||||||
    resolution: {integrity: sha512-5W6vNYkhgfh7URiXTO1E9a0cy4fSgfE4+Hl5agb/U1sa0kjOLMLC1wObxwKxecE17j0URxuTrYZZME4/VH57Hg==}
 | 
					    resolution: {integrity: sha512-5W6vNYkhgfh7URiXTO1E9a0cy4fSgfE4+Hl5agb/U1sa0kjOLMLC1wObxwKxecE17j0URxuTrYZZME4/VH57Hg==}
 | 
				
			||||||
    cpu: [riscv64]
 | 
					    cpu: [riscv64]
 | 
				
			||||||
    os: [linux]
 | 
					    os: [linux]
 | 
				
			||||||
 | 
					    libc: [musl]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  '@rollup/rollup-linux-s390x-gnu@4.40.2':
 | 
					  '@rollup/rollup-linux-s390x-gnu@4.40.2':
 | 
				
			||||||
    resolution: {integrity: sha512-B7LKIz+0+p348JoAL4X/YxGx9zOx3sR+o6Hj15Y3aaApNfAshK8+mWZEf759DXfRLeL2vg5LYJBB7DdcleYCoQ==}
 | 
					    resolution: {integrity: sha512-B7LKIz+0+p348JoAL4X/YxGx9zOx3sR+o6Hj15Y3aaApNfAshK8+mWZEf759DXfRLeL2vg5LYJBB7DdcleYCoQ==}
 | 
				
			||||||
    cpu: [s390x]
 | 
					    cpu: [s390x]
 | 
				
			||||||
    os: [linux]
 | 
					    os: [linux]
 | 
				
			||||||
 | 
					    libc: [glibc]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  '@rollup/rollup-linux-x64-gnu@4.40.2':
 | 
					  '@rollup/rollup-linux-x64-gnu@4.40.2':
 | 
				
			||||||
    resolution: {integrity: sha512-lG7Xa+BmBNwpjmVUbmyKxdQJ3Q6whHjMjzQplOs5Z+Gj7mxPtWakGHqzMqNER68G67kmCX9qX57aRsW5V0VOng==}
 | 
					    resolution: {integrity: sha512-lG7Xa+BmBNwpjmVUbmyKxdQJ3Q6whHjMjzQplOs5Z+Gj7mxPtWakGHqzMqNER68G67kmCX9qX57aRsW5V0VOng==}
 | 
				
			||||||
    cpu: [x64]
 | 
					    cpu: [x64]
 | 
				
			||||||
    os: [linux]
 | 
					    os: [linux]
 | 
				
			||||||
 | 
					    libc: [glibc]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  '@rollup/rollup-linux-x64-musl@4.40.2':
 | 
					  '@rollup/rollup-linux-x64-musl@4.40.2':
 | 
				
			||||||
    resolution: {integrity: sha512-tD46wKHd+KJvsmije4bUskNuvWKFcTOIM9tZ/RrmIvcXnbi0YK/cKS9FzFtAm7Oxi2EhV5N2OpfFB348vSQRXA==}
 | 
					    resolution: {integrity: sha512-tD46wKHd+KJvsmije4bUskNuvWKFcTOIM9tZ/RrmIvcXnbi0YK/cKS9FzFtAm7Oxi2EhV5N2OpfFB348vSQRXA==}
 | 
				
			||||||
    cpu: [x64]
 | 
					    cpu: [x64]
 | 
				
			||||||
    os: [linux]
 | 
					    os: [linux]
 | 
				
			||||||
 | 
					    libc: [musl]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  '@rollup/rollup-win32-arm64-msvc@4.40.2':
 | 
					  '@rollup/rollup-win32-arm64-msvc@4.40.2':
 | 
				
			||||||
    resolution: {integrity: sha512-Bjv/HG8RRWLNkXwQQemdsWw4Mg+IJ29LK+bJPW2SCzPKOUaMmPEppQlu/Fqk1d7+DX3V7JbFdbkh/NMmurT6Pg==}
 | 
					    resolution: {integrity: sha512-Bjv/HG8RRWLNkXwQQemdsWw4Mg+IJ29LK+bJPW2SCzPKOUaMmPEppQlu/Fqk1d7+DX3V7JbFdbkh/NMmurT6Pg==}
 | 
				
			||||||
@@ -488,6 +515,9 @@ packages:
 | 
				
			|||||||
  '@types/node@22.15.18':
 | 
					  '@types/node@22.15.18':
 | 
				
			||||||
    resolution: {integrity: sha512-v1DKRfUdyW+jJhZNEI1PYy29S2YRxMV5AOO/x/SjKmW0acCIOqmbj6Haf9eHAhsPmrhlHSxEhv/1WszcLWV4cg==}
 | 
					    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':
 | 
					  '@types/resolve@1.20.2':
 | 
				
			||||||
    resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==}
 | 
					    resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -1382,12 +1412,12 @@ snapshots:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  '@types/node-fetch@2.6.12':
 | 
					  '@types/node-fetch@2.6.12':
 | 
				
			||||||
    dependencies:
 | 
					    dependencies:
 | 
				
			||||||
      '@types/node': 22.15.18
 | 
					      '@types/node': 22.15.27
 | 
				
			||||||
      form-data: 4.0.2
 | 
					      form-data: 4.0.2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  '@types/node-forge@1.3.11':
 | 
					  '@types/node-forge@1.3.11':
 | 
				
			||||||
    dependencies:
 | 
					    dependencies:
 | 
				
			||||||
      '@types/node': 22.15.18
 | 
					      '@types/node': 22.15.27
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  '@types/node@18.19.100':
 | 
					  '@types/node@18.19.100':
 | 
				
			||||||
    dependencies:
 | 
					    dependencies:
 | 
				
			||||||
@@ -1397,6 +1427,10 @@ snapshots:
 | 
				
			|||||||
    dependencies:
 | 
					    dependencies:
 | 
				
			||||||
      undici-types: 6.21.0
 | 
					      undici-types: 6.21.0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  '@types/node@22.15.27':
 | 
				
			||||||
 | 
					    dependencies:
 | 
				
			||||||
 | 
					      undici-types: 6.21.0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  '@types/resolve@1.20.2': {}
 | 
					  '@types/resolve@1.20.2': {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  abort-controller@3.0.0:
 | 
					  abort-controller@3.0.0:
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user