Compare commits

..

16 Commits

Author SHA1 Message Date
31e8c0346f 更新 @kevisual/api 版本至 0.0.15,并在 RouterViewItem 中添加 RouteViewPage 类型,重构 QueryProxy 类以支持页面路由初始化 2026-01-02 19:51:12 +08:00
xiongxiao
14a80013e1 update add cnb sync 2025-12-31 19:11:18 +08:00
67169fc0c6 更新 @kevisual/api 版本至 0.0.14,并在 QueryProxy 类中添加 getViewQuery 方法以获取视图查询 2025-12-31 17:55:33 +08:00
9532cfbe2e 更新注释,明确已初始化的query和worker实例不需要编辑配置 2025-12-31 15:34:39 +08:00
99645e6260 更新 @kevisual/api 版本至 0.0.13,提升 @kevisual/router 和 @kevisual/js-filter 版本,重构 RouterViewApi、RouterViewContext 和 RouterViewWorker 类型,合并公共属性以提高代码可读性 2025-12-31 15:06:17 +08:00
4a2d2a7958 更新 @kevisual/api 版本至 0.0.12,并在 RouterViewApi、RouterViewContext 和 RouterViewWorker 中添加 id 属性,重命名相关变量以提高代码可读性 2025-12-31 12:04:57 +08:00
1d92662650 update 2025-12-31 11:25:35 +08:00
4bd17318a4 更新 @kevisual/api 版本至 0.0.10,并将 @kevisual/js-filter 版本更新至 0.0.2 2025-12-30 01:30:18 +08:00
e8bb129abc update 2025-12-29 19:49:38 +08:00
e5c15bd70a update 2025-12-28 17:52:32 +08:00
eea07b0a8d update 2025-12-28 16:16:57 +08:00
875195f6a2 update 2025-12-28 15:45:35 +08:00
2c74755c0d init 2025-12-19 18:45:02 +08:00
45eedd397b update 2025-12-08 02:15:37 +08:00
abd17b886d update 2025-12-08 02:05:22 +08:00
7793764baa update 2025-12-08 01:56:17 +08:00
61 changed files with 1342 additions and 1961 deletions

56
.cnb.yml Normal file
View File

@@ -0,0 +1,56 @@
# .cnb.yml
$:
vscode:
- docker:
image: docker.cnb.cool/kevisual/dev-env:latest
services:
- vscode
- docker
imports: https://cnb.cool/kevisual/env/-/blob/main/env.yml
# 开发环境启动后会执行的任务
# stages:
# - name: pnpm install
# script: pnpm install
main:
web_trigger_sync_to_gitea:
- services:
- docker
imports:
- https://cnb.cool/kevisual/env/-/blob/main/env.yml
stages:
- name: 'show username'
script: echo "GITEA_USERNAME is ${GITEA_USERNAME} and GITEA_PASSWORD is ${GITEA_PASSWORD}"
- name: sync to gitea
image: tencentcom/git-sync
settings:
target_url: https://git.xiongxiao.me/kevisual/query-awesome.git
auth_type: https
username: "oauth2"
password: ${GITEA_TOKEN}
git_user: "abearxiong"
git_email: "xiongxiao@xiongxiao.me"
sync_mode: rebase
branch: main
web_trigger_sync_from_gitea:
- services:
- docker
imports:
- https://cnb.cool/kevisual/env/-/blob/main/env.yml
stages:
- name: '添加 gitea的origin'
script: |
git remote remove gitea 2>/dev/null || true
git remote add gitea https://oauth2:${GITEA_TOKEN}@git.xiongxiao.me/kevisual/query-awesome.git
- name: '同步gitea代码到当前仓库'
script: git pull gitea main
- name: '提交到原本的origin'
script: git push origin main
"**":
web_trigger_test:
- stages:
- name: 执行任务
script: echo "job"

11
.cnb/web_trigger.yml Normal file
View File

@@ -0,0 +1,11 @@
# .cnb/web_trigger.yml
branch:
# 如下按钮在分支名以 release 开头的分支详情页面显示
- reg: "^main"
buttons:
- name: 同步代码到gitea
desc: 同步代码到gitea
event: web_trigger_sync_to_gitea
- name: 同步gitea代码到当前仓库
desc: 同步gitea代码到当前仓库
event: web_trigger_sync_from_gitea

View File

@@ -1,13 +0,0 @@
// @ts-check
import { execSync } from 'node:child_process';
import glob from 'fast-glob';
import fs from 'node:fs';
const files = await glob(['packages/*/dist', 'submodules/*/dist'], { onlyDirectories: true });
console.log('files', files);
const clean = 'rimraf dist && mkdir dist';
execSync(clean);
for (let dir of files) {
const rsync = `rsync ${dir}/* ./dist`;
execSync(rsync);
}

1
mod.ts Normal file
View File

@@ -0,0 +1 @@
export * from './query/query-proxy/index.ts';

View File

@@ -1,37 +1,49 @@
{ {
"name": "@kevisual/query-awesome", "name": "@kevisual/api",
"version": "0.0.2", "version": "0.0.15",
"description": "", "description": "",
"main": "index.js", "main": "mod.ts",
"scripts": { "scripts": {
"build": "turbo run build", "build": "bun run bun.config.ts",
"postbuild": "bun bun.copy.config.mjs" "postbuild": "bun bun.copy.config.mjs"
}, },
"publishConfig": { "publishConfig": {
"access": "public" "access": "public"
}, },
"files": [ "files": [
"dist" "dist",
"query",
"mod.ts"
], ],
"keywords": [], "keywords": [],
"author": "abearxiong <xiongxiao@xiongxiao.me> (https://www.xiongxiao.me)", "author": "abearxiong <xiongxiao@xiongxiao.me> (https://www.xiongxiao.me)",
"license": "MIT", "license": "MIT",
"packageManager": "pnpm@10.24.0", "packageManager": "pnpm@10.27.0",
"type": "module", "type": "module",
"devDependencies": { "devDependencies": {
"@kevisual/query": "^0.0.31", "@kevisual/cache": "^0.0.4",
"@kevisual/router": "^0.0.36", "@kevisual/query": "^0.0.33",
"@kevisual/router": "^0.0.52",
"@kevisual/types": "^0.0.10", "@kevisual/types": "^0.0.10",
"@kevisual/use-config": "^1.0.21", "@kevisual/use-config": "^1.0.21",
"fast-glob": "^3.3.3", "@types/bun": "^1.3.5",
"tsup": "^8.5.1" "@types/node": "^25.0.3",
"dotenv": "^17.2.3",
"fast-glob": "^3.3.3"
},
"dependencies": {
"@kevisual/js-filter": "^0.0.3",
"@kevisual/load": "^0.0.6",
"es-toolkit": "^1.43.0",
"eventemitter3": "^5.0.1",
"nanoid": "^5.1.6"
}, },
"exports": { "exports": {
".": { ".": "./mod.ts",
"import": "./dist/query-login-browser.js" "./login": "./query/query-login/query-login-browser.ts",
}, "./login-node": "./query/query-login/query-login-node.ts",
"./*": { "./config": "./query/query-config/query-config.ts",
"import": "./dist/*" "./proxy": "./query/query-proxy/index.ts",
} "./query/**/*.ts": "./query/**/*.ts"
} }
} }

View File

@@ -1,3 +0,0 @@
//npm.xiongxiao.me/:_authToken=${ME_NPM_TOKEN}
//registry.npmjs.org/:_authToken=${NPM_TOKEN}
ignore-workspace-root-check=true

View File

@@ -1,22 +0,0 @@
{
"$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": {}
}

View File

@@ -1,32 +0,0 @@
{
"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"
}
}

View File

@@ -1,25 +0,0 @@
{
"$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": {}
}

View File

@@ -1,154 +0,0 @@
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);
}
}

View File

@@ -1,19 +0,0 @@
{
"extends": "@kevisual/types/json/frontend.json",
"compilerOptions": {
"baseUrl": ".",
"typeRoots": [
"./node_modules/@types",
"./node_modules/@kevisual"
],
"paths": {
"@/*": [
"src/*"
]
},
},
"include": [
"src/**/*",
"query/**/*",
],
}

View File

@@ -1,30 +0,0 @@
// @ts-check
import { resolvePath, getDevInputs } from '@kevisual/use-config/env';
import { execSync } from 'node:child_process';
import glob from 'fast-glob';
const files = await glob(['src/defines/*.ts', 'src/query/*.ts', 'src/router/*.ts']);
const inputs = getDevInputs(files);
const external = ['@kevisual/router'];
for (let input of inputs) {
const entry = input.path;
const naming = input.naming;
/**
* @type {import('bun').BuildConfig}
*/
await Bun.build({
target: 'node',
format: 'esm',
entrypoints: [resolvePath(entry, { meta: import.meta })],
outdir: resolvePath('./dist', { meta: import.meta }),
naming: {
entry: naming + '.js',
},
external: external,
env: 'KEVISUAL_*',
});
const cmd = `dts -i ${entry} -o ${naming}.d.ts`;
execSync(cmd, { stdio: 'inherit' });
}

View File

@@ -1,18 +0,0 @@
{
"name": "@kevisual/query-list",
"version": "0.0.1",
"description": "",
"main": "index.js",
"scripts": {
"build": "bun bun.config.mjs"
},
"keywords": [],
"files": [
"src",
"dist"
],
"author": "abearxiong <xiongxiao@xiongxiao.me> (https://www.xiongxiao.me)",
"license": "MIT",
"packageManager": "pnpm@10.6.2",
"type": "module"
}

View File

@@ -1,27 +0,0 @@
import { QueryUtil } from '@kevisual/router/define';
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 参数',
},
});

View File

@@ -1,19 +0,0 @@
import { shopDefine } from '../defines/query-shop-define.ts';
import { BaseQuery, DataOpts, Query } from '@kevisual/query/query';
export { shopDefine };
export class QueryApp extends BaseQuery {
appDefine = shopDefine;
constructor(opts?: { query: Query }) {
super(opts!);
this.appDefine.query = this.query;
}
get chain() {
return this.appDefine.queryChain;
}
getInstall(data: any, opts?: DataOpts) {
return this.appDefine.queryChain('install').post(data, opts);
}
}

View File

@@ -1,18 +0,0 @@
{
"extends": "@kevisual/types/json/frontend.json",
"compilerOptions": {
"baseUrl": ".",
"typeRoots": [
"./node_modules/@types",
"./node_modules/@kevisual"
],
"paths": {
"@/*": [
"src/*"
]
},
},
"include": [
"src/**/*",
],
}

View File

@@ -1,30 +0,0 @@
// @ts-check
import { resolvePath, getDevInputs } from '@kevisual/use-config/env';
import { execSync } from 'node:child_process';
import glob from 'fast-glob';
const files = await glob('src/*.ts');
const inputs = getDevInputs(files);
const external = ['@kevisual/router'];
for (let input of inputs) {
const entry = input.path;
const naming = input.naming;
/**
* @type {import('bun').BuildConfig}
*/
await Bun.build({
target: 'node',
format: 'esm',
entrypoints: [resolvePath(entry, { meta: import.meta })],
outdir: resolvePath('./dist', { meta: import.meta }),
naming: {
entry: naming + '.js',
},
external: external,
env: 'KEVISUAL_*',
});
const cmd = `dts -i ${entry} -o ${naming}.d.ts`;
execSync(cmd, { stdio: 'inherit' });
}

View File

@@ -1,18 +0,0 @@
{
"name": "@kevisual/query-app",
"version": "0.0.1",
"description": "",
"main": "index.js",
"scripts": {
"build": "bun bun.config.mjs"
},
"keywords": [],
"files": [
"src",
"dist"
],
"author": "abearxiong <xiongxiao@xiongxiao.me> (https://www.xiongxiao.me)",
"license": "MIT",
"packageManager": "pnpm@10.6.2",
"type": "module"
}

View File

@@ -1,62 +0,0 @@
import { QueryUtil } from './common.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: '获取公开应用列表',
},
});

View File

@@ -1 +0,0 @@
export { QueryUtil } from '@kevisual/router/define';

View File

@@ -1,3 +0,0 @@
import { appDefine } from './app';
import { userAppDefine } from './user-app';
export { appDefine, userAppDefine };

View File

@@ -1,33 +0,0 @@
import { QueryUtil } from './common.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 的数据进行测试,获取版本信息',
},
});

View File

@@ -1 +0,0 @@
export * from './defines/index.ts';

View File

@@ -1,18 +0,0 @@
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);
}
}

View File

@@ -1,6 +0,0 @@
import { QueryApp } from '../query-app.ts';
import { Query } from '@kevisual/query/query';
const query = new Query();
const qa = new QueryApp({ query: query });
qa.appDefine.queryChain('getApp').post({});

View File

@@ -1,18 +0,0 @@
{
"extends": "@kevisual/types/json/frontend.json",
"compilerOptions": {
"baseUrl": ".",
"typeRoots": [
"./node_modules/@types",
"./node_modules/@kevisual"
],
"paths": {
"@/*": [
"src/*"
]
},
},
"include": [
"src/**/*",
],
}

1684
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +0,0 @@
packages:
- 'submodules/*'
- 'packages/*'
- 'apps/*'
- 'libs/*'

21
query/kevisual.json Normal file
View File

@@ -0,0 +1,21 @@
{
"metadata": {
"name": "kevisual",
"share": "public"
},
"registry": "https://kevisual.cn/root/ai/kevisual/common/query/api",
"clone": {
".": {
"enabled": true
}
},
"syncd": [
{
"files": [
"**/*"
],
"registry": ""
}
],
"sync": {}
}

View File

@@ -1,4 +1,4 @@
import { QueryUtil } from '@/query/index.ts'; import { QueryUtil } from '../../../query/index.ts';
type Message = { type Message = {
role?: 'user' | 'assistant' | 'system' | 'tool'; role?: 'user' | 'assistant' | 'system' | 'tool';

View File

@@ -1,4 +1,4 @@
import { QueryUtil } from '@/query/index.ts'; import { QueryUtil } from '../../../query/index.ts';
export const appDefine = QueryUtil.create({ export const appDefine = QueryUtil.create({
getApp: { getApp: {

View File

@@ -1,4 +1,4 @@
import { QueryUtil } from '@/query/index.ts'; import { QueryUtil } from '../../../query/index.ts';
export const userAppDefine = QueryUtil.create({ export const userAppDefine = QueryUtil.create({
listUserApps: { listUserApps: {

View File

@@ -15,4 +15,18 @@ export class QueryApp extends BaseQuery {
getList(data: any, opts?: DataOpts) { getList(data: any, opts?: DataOpts) {
return this.appDefine.queryChain('listApps').post(data, opts); return this.appDefine.queryChain('listApps').post(data, opts);
} }
getPublicApp(data: any, opts?: DataOpts) {
return this.query.post({
path: 'app',
key: 'getApp',
data: data,
}, opts);
}
getApp(data: any, opts?: DataOpts) {
return this.query.post({
path: 'app',
key: 'get',
data: data,
}, opts);
}
} }

View File

@@ -0,0 +1,121 @@
/**
* 配置查询
* @updatedAt 2025-12-03 10:33:00
*/
import { Query } from '@kevisual/query';
import type { Result } from '@kevisual/query/query';
type QueryConfigOpts = {
query?: Query;
};
export type Config<T = any> = {
id?: string;
title?: string;
key?: string;
description?: string;
data?: T;
createdAt?: string;
updatedAt?: string;
};
export type UploadConfig = {
key?: string;
version?: string;
};
type PostOpts = {
token?: string;
payload?: Record<string, any>;
};
export const defaultConfigKeys = ['upload.json', 'workspace.json', 'ai.json', 'user.json', 'life.json'] as const;
type DefaultConfigKey = (typeof defaultConfigKeys)[number];
export class QueryConfig {
query: Query;
constructor(opts?: QueryConfigOpts) {
this.query = opts?.query || new Query();
}
async post<T = Config>(data: any) {
return this.query.post<T>({ path: 'config', ...data });
}
async getConfig({ id, key }: { id?: string; key?: string }, opts?: PostOpts) {
return this.post({
key: 'get',
data: {
id,
key,
},
...opts,
});
}
async updateConfig(data: Config, opts?: PostOpts) {
return this.post({
key: 'update',
data,
...opts,
});
}
async deleteConfig(data: { id?: string, key?: string }, opts?: PostOpts) {
console.log('Delete Config Params:', data);
return this.post({
key: 'delete',
data,
});
}
async listConfig(opts?: PostOpts) {
return this.post<{ list: Config[] }>({
key: 'list',
...opts,
});
}
/**
* 获取上传配置
* @returns
*/
async getUploadConfig(opts?: PostOpts) {
return this.post<Result<Config<UploadConfig>>>({
key: 'getUploadConfig',
...opts,
});
}
/**
* 更新上传配置
* @param data
* @returns
*/
async updateUploadConfig(data: Config, opts?: PostOpts) {
return this.post<Result<Config<UploadConfig>>>({
key: 'updateUploadConfig',
data,
...opts,
});
}
/**
* 检测配置是否存在
* @param id
* @returns
*/
async detectConfig(opts?: PostOpts) {
return this.post<{ updateList: Config[] }>({
key: 'detect',
...opts,
});
}
/**
* 获取配置, 获取默认的配置项
* @param key
* @returns
*/
async getConfigByKey(key: DefaultConfigKey, opts?: PostOpts) {
return this.post<Result<Config>>({
key: 'defaultConfig',
configKey: key,
...opts,
});
}
async getByKey<T = any>(key: string, opts?: PostOpts) {
return this.post<Result<Config<T>>>({
key: 'get',
...opts,
data: { key },
});
}
}

View File

@@ -3,7 +3,7 @@ import type { Result, DataOpts } from '@kevisual/query/query';
import { setBaseResponse } from '@kevisual/query/query'; import { setBaseResponse } from '@kevisual/query/query';
import { LoginCacheStore, CacheStore } from './login-cache.ts'; import { LoginCacheStore, CacheStore } from './login-cache.ts';
import { Cache } from './login-cache.ts'; import { Cache } from './login-cache.ts';
import { BaseLoad } from '@kevisual/load';
export type QueryLoginOpts = { export type QueryLoginOpts = {
query?: Query; query?: Query;
isBrowser?: boolean; isBrowser?: boolean;
@@ -418,17 +418,74 @@ export class QueryLogin extends BaseQuery {
} }
/** /**
* 使web登录,url地址, MD5和jsonwebtoken * 使web登录,url地址, MD5和jsonwebtoken
*
*
import MD5 from 'crypto-js/md5.js';
import jsonwebtoken from 'jsonwebtoken';
*/ */
loginWithWeb(baseURL: string, { MD5, jsonwebtoken }: { MD5: any; jsonwebtoken: any }) { loginWithWeb(baseURL: string, { MD5, jsonwebtoken }: { MD5?: any; jsonwebtoken?: any }) {
const randomId = Math.random().toString(36).substring(2, 15); const randomId = Math.random().toString(36).substring(2, 15);
const timestamp = Date.now(); const timestamp = Date.now();
const tokenSecret = 'xiao' + randomId; const tokenSecret = 'xiao' + randomId;
const sign = MD5(`${tokenSecret}${timestamp}`).toString(); let sign = '';
const token = jsonwebtoken.sign({ randomId, timestamp, sign }, tokenSecret, { if (MD5) {
// 10分钟过期 sign = MD5(`${tokenSecret}${timestamp}`).toString();
expiresIn: 60 * 10, // 10分钟 }
}); let token = '';
if (jsonwebtoken) {
token = jsonwebtoken.sign({ randomId, timestamp, sign }, tokenSecret, {
// 10分钟过期
expiresIn: 60 * 10, // 10分钟
});
} else {
token = tokenSecret;
}
const url = `${baseURL}/api/router?path=user&key=webLogin&p&loginToken=${token}&sign=${sign}&randomId=${randomId}`; const url = `${baseURL}/api/router?path=user&key=webLogin&p&loginToken=${token}&sign=${sign}&randomId=${randomId}`;
return { url, token, tokenSecret }; return { url, token, tokenSecret };
} }
/**
*
*
*
*
const res = queryLogin.loginWithWeb(baseURL, { MD5, jsonwebtoken });
await pollLoginStatus(res.token, { tokenSecret: res.tokenSecret });
*
*/
async pollLoginStatus(data: { token: string; tokenSecret: string }) {
const token = data.token;
const load = new BaseLoad();
load.load(
async () => {
const res = await this.checkLoginStatus(token);
if (res.code === 500) {
load.cancel('check-login-status');
}
return res;
},
{
key: 'check-login-status',
isReRun: true,
checkSuccess: (data) => {
return data?.code === 200;
},
},
);
const res = await load.hasLoaded('check-login-status', {
timeout: 60 * 3 * 1000, // 3分钟超时
});
if (res.code === 200 && res.data?.code === 200) {
try {
console.log('网页登录成功');
return true;
} catch (error) {
console.log('登录失败', error);
return false;
}
}
console.log('登录失败', res);
return false;
}
} }

405
query/query-proxy/index.ts Normal file
View File

@@ -0,0 +1,405 @@
import { Query, Result } from '@kevisual/query/query';
import { QueryRouterServer, Route } from '@kevisual/router/src/route.ts';
import { filter } from '@kevisual/js-filter'
import { EventEmitter } from 'eventemitter3';
export const RouteTypeList = ['api', 'context', 'worker', 'page'] as const;
export type RouterViewItem = RouterViewApi | RouterViewContext | RouterViewWorker | RouteViewPage;
type RouteViewBase = {
id: string;
title: string;
description: string;
enabled?: boolean;
}
export type RouterViewApi = {
type: 'api',
api: {
url: string,
// 已初始化的query实例不需要编辑配置
query?: Query
}
} & RouteViewBase;
export type RouterViewContext = {
type: 'context',
context: {
key: string,
// 从context中获取router不需要编辑配置
router?: QueryRouterServer
}
} & RouteViewBase;
export type RouterViewWorker = {
type: 'worker',
worker: {
type: 'Worker' | 'SharedWorker' | 'serviceWorker',
url: string,
// 已初始化的worker实例不需要编辑配置
worker?: Worker | SharedWorker | ServiceWorker,
/**
* worker选项
* default: { type: 'module' }
*/
workerOptions?: {
type: 'module' | 'classic'
}
}
} & RouteViewBase;
/**
* 注入 js 的url地址使用importScripts加载
*/
export type RouteViewPage = {
type: 'page',
page: {
url: string,
}
} & RouteViewBase;
export type RouterViewQuery = {
id: string,
query: string,
title: string
}
export type RouterViewData = {
data: { items: RouterViewItem[]; }
views: RouterViewQuery[];
viewId?: string;
[key: string]: any;
}
export class QueryProxy {
router: QueryRouterServer;
token?: string;
routerViewItems: RouterViewItem[];
views: RouterViewQuery[];
emitter: EventEmitter;
constructor(opts?: { router?: QueryRouterServer, token?: string, routerViewData?: RouterViewData }) {
this.router = opts?.router || new QueryRouterServer();
this.token = opts?.token || this.getDefulatToken();
this.routerViewItems = opts?.routerViewData?.data?.items || [];
this.views = opts?.routerViewData?.views || [];
this.initRouterViewQuery();
this.emitter = new EventEmitter();
}
getDefulatToken() {
try {
if (typeof window !== 'undefined' && typeof window.localStorage !== 'undefined') {
return localStorage.getItem('token') || undefined;
}
} catch (e) {
return undefined;
}
}
async initRouterViewQuery() {
this.routerViewItems = this.routerViewItems.map(item => {
if (item.type === 'api' && item.api?.url) {
const url = item.api.url;
if (item?.api?.query) return item;
item['api'] = { url: url, query: new Query({ url: url }) };
}
if (item.type === 'worker' && item.worker?.url) {
let viewItem = item as RouterViewWorker;
if (!item.worker?.workerOptions?.type) {
item.worker.workerOptions = { ...item.worker.workerOptions, type: 'module' };
}
if (item.worker.worker) {
return item;
}
let worker: Worker | SharedWorker | ServiceWorker | undefined = undefined;
if (item.worker.type === 'SharedWorker') {
worker = new SharedWorker(item.worker.url, item.worker.workerOptions);
worker.port.start();
} else if (viewItem.worker.type === 'serviceWorker') {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register(viewItem.worker.url, item.worker.workerOptions).then(function (registration) {
console.debug('注册serviceWorker成功 ', registration.scope);
}, function (err) {
console.debug('注册 serviceWorker 失败: ', err);
});
} else {
console.warn('当前浏览器不支持serviceWorker');
}
} else {
worker = new Worker(viewItem.worker.url, item.worker.workerOptions);
}
viewItem['worker']['worker'] = worker;
}
if (item.type === 'context' && item.context?.key) {
if (item.context?.router) {
return item;
}
// @ts-ignore
const context = globalThis['context'] || {}
const router = context[item.context.key] as QueryRouterServer;
if (router) {
item['context']['router'] = router;
}
}
return item;
}).filter(item => {
const enabled = item.enabled ?? true;
return enabled;
});
}
/**
* 初始化路由
* @returns
*/
async init() {
const routerViewItems = this.routerViewItems || [];
if (routerViewItems.length === 0) {
// 默认初始化api类型路由
await this.initApi();
return;
}
for (const item of routerViewItems) {
switch (item.type) {
case 'api':
await this.initApi(item);
break;
case 'context':
await this.initContext(item);
break;
case 'worker':
await this.initWorker(item);
break;
case 'page':
await this.initPage(item);
break;
}
}
this.emitter.emit('initComplete');
}
/**
* 监听初始化完成
* @returns
*/
async listenInitComplete(): Promise<boolean> {
return new Promise((resolve) => {
const timer = setTimeout(() => {
this.emitter.removeAllListeners('initComplete');
resolve(false);
}, 3 * 60000); // 3分钟超时
const func = () => {
clearTimeout(timer);
resolve(true);
}
this.emitter.once('initComplete', func);
});
}
async initApi(item?: RouterViewApi) {
const that = this;
const query = item?.api?.query || new Query({ url: item?.api?.url || '/api/router' })
const res = await query.post<{ list: RouterItem[] }>({ path: "router", key: 'list', token: this.token });
if (res.code !== 200) {
console.error('Failed to init query proxy router:', res.message);
return
}
const _list = res.data?.list || []
for (const r of _list) {
if (r.path || r.id) {
console.debug(`注册路由: [${r.path}] ${r?.key}`, 'API');
let metadata = r.metadata || {};
metadata.viewItem = item;
this.router.route({
path: r.path,
key: r.key || '',
id: r.id,
description: r.description,
metadata: metadata,
}).define(async (ctx) => {
const msg = { ...ctx.query };
if (msg.token === undefined && that.token !== undefined) {
msg.token = that.token;
}
const res = await query.post<any>({ path: r.path, key: r.key, ...msg });
ctx.forward(res)
}).addTo(that.router);
}
}
}
async initContext(item?: RouterViewContext) {
// @ts-ignore
const context = globalThis['context'] || {}
const router = item?.context?.router || context[item?.context?.key] as QueryRouterServer;
if (!router) {
console.warn(`未发现Context router ${item?.context?.key}`);
return
}
const routes = router.getList();
for (const r of routes) {
console.debug(`注册路由: [${r.path}] ${r?.key}`, 'Context');
let metadata = r.metadata || {};
metadata.viewItem = item;
this.router.route({
path: r.path,
key: r.key || '',
id: r.id,
description: r.description,
metadata: metadata,
}).define(async (ctx) => {
const res = await router.run({ path: r.path, key: r.key, ...ctx.query });
ctx.forward(res)
}).addTo(this.router);
}
}
generateId() {
return 'route_' + Math.random().toString(36).substring(2, 9);
}
async initWorker(item?: RouterViewWorker) {
const that = this;
if (!item?.worker?.url) {
console.warn('Worker URL not provided');
return;
}
const viewItem = item.worker;
const worker = viewItem?.worker;
if (!worker) {
console.warn('Worker not initialized');
return;
}
const callResponse = (e: MessageEvent) => {
const msg = e.data;
if (msg.requestId) {
const requestId = msg.requestId;
that.emitter.emit(requestId, msg);
} else {
that.router.run(msg);
}
}
if (item.worker.type === 'SharedWorker') {
const port = (worker as SharedWorker).port;
port.onmessage = callResponse;
port.start();
} else if (item.worker.type === 'serviceWorker') {
navigator.serviceWorker.addEventListener('message', callResponse);
} else {
(worker as Worker).onmessage = callResponse;
}
const callWorker = async (msg: any, viewItem: RouterViewWorker['worker']): Promise<Result> => {
const requestId = this.generateId();
const worker = viewItem?.worker;
if (!worker) {
return { code: 500, message: 'Worker未初始化' };
}
let port: MessagePort | Worker | ServiceWorker;
if (viewItem.type === 'SharedWorker') {
port = (worker as SharedWorker).port;
} else {
port = worker as Worker | ServiceWorker;
}
port.postMessage({
...msg,
requestId: requestId,
})
return new Promise((resolve) => {
const timer = setTimeout(() => {
that.emitter.removeAllListeners(requestId);
resolve({ code: 500, message: '请求超时' });
}, 3 * 60 * 1000); // 3分钟超时
that.emitter.once(requestId, (res: any) => {
clearTimeout(timer);
resolve(res);
});
});
}
const res = await callWorker({
path: "router",
key: 'list',
token: this.token,
}, viewItem);
if (res.code !== 200) {
console.error('Failed to init query proxy router:', res.message);
return;
}
const _list = res.data?.list || []
for (const r of _list) {
if (r.path || r.id) {
console.debug(`注册路由: [${r.path}] ${r?.key}`, 'API');
let metadata = r.metadata || {};
metadata.viewItem = item;
this.router.route({
path: r.path,
key: r.key || '',
id: r.id,
description: r.description,
metadata: metadata,
}).define(async (ctx) => {
const msg = { ...ctx.query };
if (msg.token === undefined && that.token !== undefined) {
msg.token = that.token;
}
const res = await callWorker({ path: r.path, key: r.key, ...msg }, viewItem);
ctx.forward(res)
}).addTo(that.router);
}
}
}
async initPage(item?: RouteViewPage) {
if (!item?.page?.url) {
console.warn('Page地址未提供');
return;
}
const url = item.page.url;
try {
if (typeof window !== 'undefined') {
await import(url).then((module) => { }).catch((err) => {
console.error('引入Page脚本失败:', url, err);
});
}
} catch (e) {
console.warn('引入Page脚本失败:', url, e);
return;
}
}
/**
* 列出路由
* @param filter
* @param query WHERE metadata.tags CONTAINS 'premium'
* @returns
*/
async listRoutes(filterFn?: (item: Route) => boolean, opts?: { viewId?: string, query?: string }) {
let query = opts?.query;
if (opts?.viewId) {
const view = this.views.find(v => v.id === opts.viewId);
if (view) {
query = view.query;
}
} if (opts?.query) {
query = opts.query;
}
const routes = this.router.routes.filter(filterFn || (() => true));
if (query) {
return filter(routes, query);
}
return routes;
}
async getViewQuery(viewId: string) {
const view = this.views.find(v => v.id === viewId);
if (view) {
return view.query;
}
return undefined;
}
/**
* 运行路由
* @param msg
* @returns
*/
async run(msg: { id?: string, path?: string, key?: string }) {
return await this.router.run(msg);
}
}
type RouterItem = {
id?: string;
path?: string;
key?: string;
description?: string;
middleware?: string[];
metadata?: Record<string, any>;
}

View File

@@ -0,0 +1,64 @@
/**
* 配置查询
* @updatedAt 2025-12-03 10:33:00
*/
import { Query } from '@kevisual/query';
type QueryConfigOpts = {
query?: Query;
};
export type Config<T = any> = {
id?: string;
title?: string;
key?: string;
description?: string;
data?: T;
createdAt?: string;
updatedAt?: string;
};
export type UploadConfig = {
key?: string;
version?: string;
};
type PostOpts = {
token?: string;
payload?: Record<string, any>;
};
export class QueryConfig {
query: Query;
constructor(opts?: QueryConfigOpts) {
this.query = opts?.query || new Query();
}
async post<T = Config>(data: any) {
return this.query.post<T>({ path: 'secret', ...data });
}
async getItem({ id, key }: { id?: string; key?: string }, opts?: PostOpts) {
return this.post({
key: 'get',
data: {
id,
key,
},
...opts,
});
}
async updateItem(data: Config, opts?: PostOpts) {
return this.post({
key: 'update',
data,
...opts,
});
}
async deleteItem(data: { id?: string, key?: string }, opts?: PostOpts) {
return this.post({
key: 'delete',
data,
});
}
async listItems(opts?: PostOpts) {
return this.post<{ list: Config[] }>({
key: 'list',
...opts,
});
}
}

View File

@@ -1,4 +1,4 @@
import { QueryUtil } from '@/query/index.ts'; import { QueryUtil } from '../../../query/index.ts';
export const shopDefine = QueryUtil.create({ export const shopDefine = QueryUtil.create({
getRegistry: { getRegistry: {

View File

@@ -4,14 +4,13 @@ import { BaseQuery, DataOpts, Query } from '@kevisual/query/query';
export { shopDefine }; export { shopDefine };
export class QueryShop<T extends Query = Query> extends BaseQuery<T, typeof shopDefine> { export class QueryShop<T extends Query = Query> extends BaseQuery<T> {
constructor(opts?: { query: T }) { constructor(opts?: { query: T }) {
super({ super({
query: opts?.query!, query: opts?.query!,
queryDefine: shopDefine,
}); });
} }
getInstall(data: any, opts?: DataOpts) { getInstall(data: any, opts?: DataOpts) {
return this.queryDefine.queryChain('install').post(data, opts); return this.query.post(data, opts);
} }
} }

View File

@@ -5,7 +5,8 @@ import { UploadProgress } from './core/upload-progress.ts';
export { uploadFiles, uploadFileChunked, UploadProgress }; export { uploadFiles, uploadFileChunked, UploadProgress };
export * from './utils/to-file.ts'; export { toTextFile, toFile, getDirectoryAndName } from './utils/to-file.ts';
export { randomId } from './utils/random-id.ts'; export { randomId } from './utils/random-id.ts';
export { filterFiles } from './utils/filter-files.ts'; export { filterFiles } from './utils/filter-files.ts';

170
readme.md
View File

@@ -1,3 +1,169 @@
# query-awesome # @kevisual/api
对 kevisual 相关的query router 的模块整理 对 kevisual 相关的query router 的模块整理
包含的模块:
## query-config - 配置管理模块
提供配置的增删改查功能,支持默认配置项管理。
**主要功能:**
- 配置的获取、更新、删除
- 上传配置管理
- 默认配置项支持upload.json, workspace.json, ai.json, user.json, life.json
- 配置检测功能
**使用示例:**
```typescript
import { QueryConfig } from './query-config/query-config';
const config = new QueryConfig();
await config.getConfig({ key: 'upload.json' });
await config.updateConfig({ key: 'config.json', data: { setting: true } });
```
## query-secret - 密钥管理模块
用于管理敏感信息和密钥的存储与检索。
**主要功能:**
- 密钥的存储和获取
- 支持按ID或key检索
- 密钥列表管理
## query-proxy - 代理路由模块
提供动态路由代理功能,可以将请求转发到不同的后端服务。
**主要功能:**
- 动态路由注册
- 请求转发代理
- 路由列表管理
- Token认证支持
**使用示例:**
```typescript
import { QueryProxy } from './query-proxy/index';
const proxy = new QueryProxy({
query: new Query(),
token: 'your-token'
});
await proxy.init(); // 初始化路由
const result = await proxy.run({ path: 'api', key: 'getData' });
```
## query-upload - 文件上传模块
功能完整的文件上传解决方案,支持多种上传方式。
**主要功能:**
- 普通文件上传
- 分片上传(大文件)
- 上传进度跟踪
- 文件过滤工具
- 文件格式转换
**核心组件:**
- `uploadFiles` - 基础文件上传
- `uploadFileChunked` - 分片上传
- `UploadProgress` - 进度管理
- `filterFiles` - 文件过滤
- `toFile` - 格式转换
**使用示例:**
```typescript
import { uploadFiles, UploadProgress } from './query-upload/query-upload';
const progress = new UploadProgress();
await uploadFiles(files, {
onProgress: (loaded, total) => {
console.log(`上传进度: ${loaded}/${total}`);
}
});
```
## query-app - 应用管理模块
管理应用的注册、获取和列表功能。
**主要功能:**
- 获取应用列表
- 获取公开应用
- 获取私有应用
- 应用定义管理
## query-shop - 应用商店模块
应用商店功能,处理应用的安装和获取。
**主要功能:**
- 应用安装
- 商店应用管理
## query-ai - AI对话模块
集成AI聊天功能支持多种模型和对话管理。
**主要功能:**
- AI对话支持GPT等模型
- 模型列表获取
- 聊天使用统计
- 缓存管理
- 使用限制管理
**使用示例:**
```typescript
import { QueryAI } from './query-ai/query-ai';
const ai = new QueryAI({ query: new Query() });
const models = await ai.getModelList();
const response = await ai.chat(
{ message: '你好' },
{ username: 'user', model: 'gpt-3.5', group: 'default' }
);
```
## query-login - 登录认证模块
完整的登录认证解决方案支持token管理和缓存。
**主要功能:**
- 用户登录认证
- Token管理access/refresh token
- 登录状态缓存
- 浏览器/Node.js环境支持
- 自动token刷新
**核心组件:**
- `QueryLogin` - 主登录类
- `LoginCacheStore` - 登录缓存
- `Cache` - 通用缓存接口
## query-resources - 资源管理模块
管理用户资源的访问和获取。
**主要功能:**
- 资源文件获取
- 资源列表管理
- 用户认证支持
- 文件预览功能
**使用示例:**
```typescript
import { QueryResources } from './query-resources/index';
const resources = new QueryResources({ username: 'user' });
const fileList = await resources.getList('images/');
const fileContent = await resources.fetchFile('document.pdf');
```
## 架构特点
- **模块化设计**:每个模块职责单一,可独立使用
- **统一接口**:基于 `@kevisual/query` 的统一查询接口
- **环境兼容**支持浏览器和Node.js环境
- **类型安全**完整的TypeScript类型定义
- **扩展性**:易于扩展和自定义

28
test/proxy.ts Normal file
View File

@@ -0,0 +1,28 @@
import { QueryProxy } from "../query/query-proxy";
import { Query } from "@kevisual/query/query";
import dotenv from "dotenv";
import util from 'node:util'
dotenv.config();
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
const token = process.env.KEVISUAL_TOKEN;
const url = process.env.KEVISUAL_URL;
export const showMore = (obj: any) => {
return util.inspect(obj, { showHidden: false, depth: null, colors: true });
}
const query = new Query({ url: url + '/api/router' });
const proxy = new QueryProxy({ query, token });
const res = await proxy.init();
console.log('Proxy Init Result:', showMore(res));
const routes = await proxy.listRoutes((item) => item.path?.startsWith('router') || false);
console.log('Filtered Routes:', showMore(routes));
await sleep(1000);
// console.log('Running route [user/me]...', proxy.router.routes.length);
const run = await proxy.run({ path: 'user', key: 'me' });
// const run = await proxy.run({ path: 'router', key: 'list' });
console.log('Run Result:', showMore(run));

View File

@@ -1,22 +0,0 @@
{
"$schema": "https://turbo.build/schema.json",
"tasks": {
"build": {
"dependsOn": [
"^build"
],
"outputs": [
"dist/**"
]
},
"dev:lib": {
"persistent": true,
"cache": true
},
"build:lib": {
"dependsOn": [
"^build:lib"
]
}
}
}