feat(container): add CRUD operations for container management
- Implemented routes for listing, retrieving, updating, and deleting containers. - Added ContainerModel with necessary fields and methods for data handling. - Created utility functions for fetching container data by ID. feat(page): enhance page management with CRUD and publish functionality - Developed routes for managing pages, including listing, updating, and deleting. - Integrated caching and zip file generation for page exports. - Added publish functionality to manage app versions and file uploads. feat(prompts): implement prompt management with CRUD operations - Created routes for listing, updating, and deleting prompts. - Added pagination and search capabilities for prompt listing. test: add common query utilities and prompt tests - Implemented common query utilities for API interactions. - Added tests for prompt listing functionality.
This commit is contained in:
17
src/app.ts
17
src/app.ts
@@ -6,11 +6,13 @@ import { useContextKey } from '@kevisual/context';
|
|||||||
import { SimpleRouter } from '@kevisual/router/simple';
|
import { SimpleRouter } from '@kevisual/router/simple';
|
||||||
import { OssBase } from '@kevisual/oss/services';
|
import { OssBase } from '@kevisual/oss/services';
|
||||||
import { BailianProvider } from '@kevisual/ai';
|
import { BailianProvider } from '@kevisual/ai';
|
||||||
|
import * as schema from './db/schema.ts';
|
||||||
|
import { drizzle } from 'drizzle-orm/node-postgres';
|
||||||
|
import { config } from './modules/config.ts'
|
||||||
export const router = useContextKey('router', () => new SimpleRouter());
|
export const router = useContextKey('router', () => new SimpleRouter());
|
||||||
export const runtime = useContextKey('runtime', () => {
|
export const runtime = useContextKey('runtime', () => {
|
||||||
return {
|
return {
|
||||||
env: process.env.NODE_ENV || 'development',
|
env: config.NODE_ENV || 'development',
|
||||||
type: 'server',
|
type: 'server',
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -27,7 +29,10 @@ export const redis = useContextKey('redis', () => redisLib.redis);
|
|||||||
export const subscriber = useContextKey('subscriber', () => redisLib.subscriber);
|
export const subscriber = useContextKey('subscriber', () => redisLib.subscriber);
|
||||||
export const minioClient = useContextKey('minioClient', () => minioLib.minioClient);
|
export const minioClient = useContextKey('minioClient', () => minioLib.minioClient);
|
||||||
export const sequelize = useContextKey('sequelize', () => sequelizeLib.sequelize);
|
export const sequelize = useContextKey('sequelize', () => sequelizeLib.sequelize);
|
||||||
|
export const db = useContextKey('db', () => {
|
||||||
|
const db = drizzle(config.DATABASE_URL || '');
|
||||||
|
return db;
|
||||||
|
})
|
||||||
const init = () => {
|
const init = () => {
|
||||||
return new App({
|
return new App({
|
||||||
serverOptions: {
|
serverOptions: {
|
||||||
@@ -42,7 +47,9 @@ export const app = useContextKey('app', init);
|
|||||||
|
|
||||||
export const ai = useContextKey('ai', () => {
|
export const ai = useContextKey('ai', () => {
|
||||||
return new BailianProvider({
|
return new BailianProvider({
|
||||||
apiKey: process.env.BAILIAN_API_KEY || '',
|
apiKey: config.BAILIAN_API_KEY || '',
|
||||||
model: 'qwen-plus',
|
model: 'qwen-plus',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export { schema };
|
||||||
42
src/db/drizzle/0001_solid_nocturne.sql
Normal file
42
src/db/drizzle/0001_solid_nocturne.sql
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
CREATE TABLE "cf_prompts" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"uid" uuid,
|
||||||
|
"parents" jsonb DEFAULT '[]'::jsonb NOT NULL,
|
||||||
|
"data" jsonb DEFAULT '{}'::jsonb NOT NULL,
|
||||||
|
"title" text DEFAULT '',
|
||||||
|
"description" text DEFAULT '',
|
||||||
|
"summary" text DEFAULT '',
|
||||||
|
"tags" jsonb DEFAULT '[]'::jsonb NOT NULL,
|
||||||
|
"link" text DEFAULT '',
|
||||||
|
"createdAt" timestamp DEFAULT now() NOT NULL,
|
||||||
|
"updatedAt" timestamp DEFAULT now() NOT NULL,
|
||||||
|
"deletedAt" timestamp
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
DROP TABLE "TestPromptTools" CASCADE;--> statement-breakpoint
|
||||||
|
DROP TABLE "ai_agent" CASCADE;--> statement-breakpoint
|
||||||
|
DROP TABLE "apps_trades" CASCADE;--> statement-breakpoint
|
||||||
|
DROP TABLE "cf_orgs" CASCADE;--> statement-breakpoint
|
||||||
|
DROP TABLE "cf_router_code" CASCADE;--> statement-breakpoint
|
||||||
|
DROP TABLE "cf_user" CASCADE;--> statement-breakpoint
|
||||||
|
DROP TABLE "cf_user_secrets" CASCADE;--> statement-breakpoint
|
||||||
|
DROP TABLE "chat_histories" CASCADE;--> statement-breakpoint
|
||||||
|
DROP TABLE "chat_prompts" CASCADE;--> statement-breakpoint
|
||||||
|
DROP TABLE "chat_sessions" CASCADE;--> statement-breakpoint
|
||||||
|
DROP TABLE "file_sync" CASCADE;--> statement-breakpoint
|
||||||
|
DROP TABLE "kv_ai_chat_history" CASCADE;--> statement-breakpoint
|
||||||
|
DROP TABLE "kv_app" CASCADE;--> statement-breakpoint
|
||||||
|
DROP TABLE "kv_app_domain" CASCADE;--> statement-breakpoint
|
||||||
|
DROP TABLE "kv_app_list" CASCADE;--> statement-breakpoint
|
||||||
|
DROP TABLE "kv_config" CASCADE;--> statement-breakpoint
|
||||||
|
DROP TABLE "kv_container" CASCADE;--> statement-breakpoint
|
||||||
|
DROP TABLE "kv_github" CASCADE;--> statement-breakpoint
|
||||||
|
DROP TABLE "kv_packages" CASCADE;--> statement-breakpoint
|
||||||
|
DROP TABLE "kv_page" CASCADE;--> statement-breakpoint
|
||||||
|
DROP TABLE "kv_resource" CASCADE;--> statement-breakpoint
|
||||||
|
DROP TABLE "kv_vip" CASCADE;--> statement-breakpoint
|
||||||
|
DROP TABLE "micro_apps_upload" CASCADE;--> statement-breakpoint
|
||||||
|
DROP TABLE "micro_mark" CASCADE;--> statement-breakpoint
|
||||||
|
DROP TABLE "prompts" CASCADE;--> statement-breakpoint
|
||||||
|
DROP TABLE "work_share_mark" CASCADE;--> statement-breakpoint
|
||||||
|
DROP TYPE "public"."enum_cf_router_code_type";
|
||||||
114
src/db/drizzle/meta/0001_snapshot.json
Normal file
114
src/db/drizzle/meta/0001_snapshot.json
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
{
|
||||||
|
"id": "f87c2440-e0ce-4caa-ab13-560d9aac9e8e",
|
||||||
|
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||||
|
"version": "7",
|
||||||
|
"dialect": "postgresql",
|
||||||
|
"tables": {
|
||||||
|
"public.cf_prompts": {
|
||||||
|
"name": "cf_prompts",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "gen_random_uuid()"
|
||||||
|
},
|
||||||
|
"uid": {
|
||||||
|
"name": "uid",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"parents": {
|
||||||
|
"name": "parents",
|
||||||
|
"type": "jsonb",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'[]'::jsonb"
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"name": "data",
|
||||||
|
"type": "jsonb",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'{}'::jsonb"
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"name": "title",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"default": "''"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"name": "description",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"default": "''"
|
||||||
|
},
|
||||||
|
"summary": {
|
||||||
|
"name": "summary",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"default": "''"
|
||||||
|
},
|
||||||
|
"tags": {
|
||||||
|
"name": "tags",
|
||||||
|
"type": "jsonb",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'[]'::jsonb"
|
||||||
|
},
|
||||||
|
"link": {
|
||||||
|
"name": "link",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"default": "''"
|
||||||
|
},
|
||||||
|
"createdAt": {
|
||||||
|
"name": "createdAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"updatedAt": {
|
||||||
|
"name": "updatedAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"deletedAt": {
|
||||||
|
"name": "deletedAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"enums": {},
|
||||||
|
"schemas": {},
|
||||||
|
"sequences": {},
|
||||||
|
"roles": {},
|
||||||
|
"policies": {},
|
||||||
|
"views": {},
|
||||||
|
"_meta": {
|
||||||
|
"columns": {},
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,13 @@
|
|||||||
"when": 1766803308366,
|
"when": 1766803308366,
|
||||||
"tag": "0000_groovy_red_skull",
|
"tag": "0000_groovy_red_skull",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 1,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1767070768620,
|
||||||
|
"tag": "0001_solid_nocturne",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -68,13 +68,13 @@ export const cfRouterCode = pgTable("cf_router_code", {
|
|||||||
key: varchar({ length: 255 }).notNull(),
|
key: varchar({ length: 255 }).notNull(),
|
||||||
active: boolean().default(false),
|
active: boolean().default(false),
|
||||||
project: varchar({ length: 255 }).default('default'),
|
project: varchar({ length: 255 }).default('default'),
|
||||||
code: text().default('),
|
code: text().default(''),
|
||||||
type: enumCfRouterCodeType().default('route'),
|
type: enumCfRouterCodeType().default('route'),
|
||||||
createdAt: timestamp({ withTimezone: true, mode: 'string' }).notNull(),
|
createdAt: timestamp({ withTimezone: true, mode: 'string' }).notNull(),
|
||||||
updatedAt: timestamp({ withTimezone: true, mode: 'string' }).notNull(),
|
updatedAt: timestamp({ withTimezone: true, mode: 'string' }).notNull(),
|
||||||
middleware: varchar({ length: 255 }).array().default(["RRAY[]::character varying[])::character varying(25"]),
|
middleware: varchar({ length: 255 }).array().default(["RRAY[]::character varying[])::character varying(25"]),
|
||||||
next: varchar({ length: 255 }).default('),
|
next: varchar({ length: 255 }).default(''),
|
||||||
exec: text().default('),
|
exec: text().default(''),
|
||||||
data: json().default({}),
|
data: json().default({}),
|
||||||
validator: json().default({}),
|
validator: json().default({}),
|
||||||
deletedAt: timestamp({ withTimezone: true, mode: 'string' }),
|
deletedAt: timestamp({ withTimezone: true, mode: 'string' }),
|
||||||
@@ -107,7 +107,7 @@ export const cfUserSecrets = pgTable("cf_user_secrets", {
|
|||||||
status: varchar({ length: 255 }).default('active'),
|
status: varchar({ length: 255 }).default('active'),
|
||||||
title: text(),
|
title: text(),
|
||||||
expiredTime: timestamp({ withTimezone: true, mode: 'string' }),
|
expiredTime: timestamp({ withTimezone: true, mode: 'string' }),
|
||||||
token: varchar({ length: 255 }).default(').notNull(),
|
token: varchar({ length: 255 }).default('').notNull(),
|
||||||
userId: uuid(),
|
userId: uuid(),
|
||||||
data: jsonb().default({}),
|
data: jsonb().default({}),
|
||||||
orgId: uuid(),
|
orgId: uuid(),
|
||||||
@@ -133,7 +133,7 @@ export const chatPrompts = pgTable("chat_prompts", {
|
|||||||
title: varchar({ length: 255 }).notNull(),
|
title: varchar({ length: 255 }).notNull(),
|
||||||
description: text(),
|
description: text(),
|
||||||
data: json(),
|
data: json(),
|
||||||
key: varchar({ length: 255 }).default(').notNull(),
|
key: varchar({ length: 255 }).default('').notNull(),
|
||||||
uid: varchar({ length: 255 }),
|
uid: varchar({ length: 255 }),
|
||||||
createdAt: timestamp({ withTimezone: true, mode: 'string' }).notNull(),
|
createdAt: timestamp({ withTimezone: true, mode: 'string' }).notNull(),
|
||||||
updatedAt: timestamp({ withTimezone: true, mode: 'string' }).notNull(),
|
updatedAt: timestamp({ withTimezone: true, mode: 'string' }).notNull(),
|
||||||
@@ -148,7 +148,7 @@ export const chatSessions = pgTable("chat_sessions", {
|
|||||||
uid: varchar({ length: 255 }),
|
uid: varchar({ length: 255 }),
|
||||||
createdAt: timestamp({ withTimezone: true, mode: 'string' }).notNull(),
|
createdAt: timestamp({ withTimezone: true, mode: 'string' }).notNull(),
|
||||||
updatedAt: timestamp({ withTimezone: true, mode: 'string' }).notNull(),
|
updatedAt: timestamp({ withTimezone: true, mode: 'string' }).notNull(),
|
||||||
title: varchar({ length: 255 }).default('),
|
title: varchar({ length: 255 }).default(''),
|
||||||
key: varchar({ length: 255 }),
|
key: varchar({ length: 255 }),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -167,10 +167,10 @@ export const fileSync = pgTable("file_sync", {
|
|||||||
|
|
||||||
export const kvAiChatHistory = pgTable("kv_ai_chat_history", {
|
export const kvAiChatHistory = pgTable("kv_ai_chat_history", {
|
||||||
id: uuid().primaryKey().notNull(),
|
id: uuid().primaryKey().notNull(),
|
||||||
username: varchar({ length: 255 }).default(').notNull(),
|
username: varchar({ length: 255 }).default('').notNull(),
|
||||||
model: varchar({ length: 255 }).default(').notNull(),
|
model: varchar({ length: 255 }).default('').notNull(),
|
||||||
group: varchar({ length: 255 }).default(').notNull(),
|
group: varchar({ length: 255 }).default('').notNull(),
|
||||||
title: varchar({ length: 255 }).default(').notNull(),
|
title: varchar({ length: 255 }).default('').notNull(),
|
||||||
messages: jsonb().default([]).notNull(),
|
messages: jsonb().default([]).notNull(),
|
||||||
promptTokens: integer("prompt_tokens").default(0),
|
promptTokens: integer("prompt_tokens").default(0),
|
||||||
totalTokens: integer("total_tokens").default(0),
|
totalTokens: integer("total_tokens").default(0),
|
||||||
@@ -186,14 +186,14 @@ export const kvAiChatHistory = pgTable("kv_ai_chat_history", {
|
|||||||
export const kvApp = pgTable("kv_app", {
|
export const kvApp = pgTable("kv_app", {
|
||||||
id: uuid().primaryKey().notNull(),
|
id: uuid().primaryKey().notNull(),
|
||||||
data: jsonb().default({}),
|
data: jsonb().default({}),
|
||||||
version: varchar({ length: 255 }).default('),
|
version: varchar({ length: 255 }).default(''),
|
||||||
key: varchar({ length: 255 }),
|
key: varchar({ length: 255 }),
|
||||||
uid: uuid(),
|
uid: uuid(),
|
||||||
createdAt: timestamp({ withTimezone: true, mode: 'string' }).notNull(),
|
createdAt: timestamp({ withTimezone: true, mode: 'string' }).notNull(),
|
||||||
updatedAt: timestamp({ withTimezone: true, mode: 'string' }).notNull(),
|
updatedAt: timestamp({ withTimezone: true, mode: 'string' }).notNull(),
|
||||||
deletedAt: timestamp({ withTimezone: true, mode: 'string' }),
|
deletedAt: timestamp({ withTimezone: true, mode: 'string' }),
|
||||||
title: varchar({ length: 255 }).default('),
|
title: varchar({ length: 255 }).default(''),
|
||||||
description: varchar({ length: 255 }).default('),
|
description: varchar({ length: 255 }).default(''),
|
||||||
user: varchar({ length: 255 }),
|
user: varchar({ length: 255 }),
|
||||||
status: varchar({ length: 255 }).default('running'),
|
status: varchar({ length: 255 }).default('running'),
|
||||||
pid: uuid(),
|
pid: uuid(),
|
||||||
@@ -220,7 +220,7 @@ export const kvAppDomain = pgTable("kv_app_domain", {
|
|||||||
export const kvAppList = pgTable("kv_app_list", {
|
export const kvAppList = pgTable("kv_app_list", {
|
||||||
id: uuid().primaryKey().notNull(),
|
id: uuid().primaryKey().notNull(),
|
||||||
data: json().default({}),
|
data: json().default({}),
|
||||||
version: varchar({ length: 255 }).default('),
|
version: varchar({ length: 255 }).default(''),
|
||||||
uid: uuid(),
|
uid: uuid(),
|
||||||
createdAt: timestamp({ withTimezone: true, mode: 'string' }).notNull(),
|
createdAt: timestamp({ withTimezone: true, mode: 'string' }).notNull(),
|
||||||
updatedAt: timestamp({ withTimezone: true, mode: 'string' }).notNull(),
|
updatedAt: timestamp({ withTimezone: true, mode: 'string' }).notNull(),
|
||||||
@@ -231,24 +231,24 @@ export const kvAppList = pgTable("kv_app_list", {
|
|||||||
|
|
||||||
export const kvConfig = pgTable("kv_config", {
|
export const kvConfig = pgTable("kv_config", {
|
||||||
id: uuid().primaryKey().notNull(),
|
id: uuid().primaryKey().notNull(),
|
||||||
title: text().default('),
|
title: text().default(''),
|
||||||
key: text().default('),
|
key: text().default(''),
|
||||||
description: text().default('),
|
description: text().default(''),
|
||||||
tags: jsonb().default([]),
|
tags: jsonb().default([]),
|
||||||
data: jsonb().default({}),
|
data: jsonb().default({}),
|
||||||
uid: uuid(),
|
uid: uuid(),
|
||||||
createdAt: timestamp({ withTimezone: true, mode: 'string' }).notNull(),
|
createdAt: timestamp({ withTimezone: true, mode: 'string' }).notNull(),
|
||||||
updatedAt: timestamp({ withTimezone: true, mode: 'string' }).notNull(),
|
updatedAt: timestamp({ withTimezone: true, mode: 'string' }).notNull(),
|
||||||
deletedAt: timestamp({ withTimezone: true, mode: 'string' }),
|
deletedAt: timestamp({ withTimezone: true, mode: 'string' }),
|
||||||
hash: text().default('),
|
hash: text().default(''),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const kvContainer = pgTable("kv_container", {
|
export const kvContainer = pgTable("kv_container", {
|
||||||
id: uuid().primaryKey().notNull(),
|
id: uuid().primaryKey().notNull(),
|
||||||
title: text().default('),
|
title: text().default(''),
|
||||||
description: text().default('),
|
description: text().default(''),
|
||||||
type: varchar({ length: 255 }).default('render-js'),
|
type: varchar({ length: 255 }).default('render-js'),
|
||||||
code: text().default('),
|
code: text().default(''),
|
||||||
data: json().default({}),
|
data: json().default({}),
|
||||||
createdAt: timestamp({ withTimezone: true, mode: 'string' }).notNull(),
|
createdAt: timestamp({ withTimezone: true, mode: 'string' }).notNull(),
|
||||||
updatedAt: timestamp({ withTimezone: true, mode: 'string' }).notNull(),
|
updatedAt: timestamp({ withTimezone: true, mode: 'string' }).notNull(),
|
||||||
@@ -256,13 +256,13 @@ export const kvContainer = pgTable("kv_container", {
|
|||||||
publish: json().default({}),
|
publish: json().default({}),
|
||||||
tags: json().default([]),
|
tags: json().default([]),
|
||||||
deletedAt: timestamp({ withTimezone: true, mode: 'string' }),
|
deletedAt: timestamp({ withTimezone: true, mode: 'string' }),
|
||||||
hash: text().default('),
|
hash: text().default(''),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const kvGithub = pgTable("kv_github", {
|
export const kvGithub = pgTable("kv_github", {
|
||||||
id: uuid().primaryKey().notNull(),
|
id: uuid().primaryKey().notNull(),
|
||||||
title: varchar({ length: 255 }).default('),
|
title: varchar({ length: 255 }).default(''),
|
||||||
githubToken: varchar({ length: 255 }).default('),
|
githubToken: varchar({ length: 255 }).default(''),
|
||||||
uid: uuid(),
|
uid: uuid(),
|
||||||
createdAt: timestamp({ withTimezone: true, mode: 'string' }).notNull(),
|
createdAt: timestamp({ withTimezone: true, mode: 'string' }).notNull(),
|
||||||
updatedAt: timestamp({ withTimezone: true, mode: 'string' }).notNull(),
|
updatedAt: timestamp({ withTimezone: true, mode: 'string' }).notNull(),
|
||||||
@@ -271,8 +271,8 @@ export const kvGithub = pgTable("kv_github", {
|
|||||||
|
|
||||||
export const kvPackages = pgTable("kv_packages", {
|
export const kvPackages = pgTable("kv_packages", {
|
||||||
id: uuid().primaryKey().notNull(),
|
id: uuid().primaryKey().notNull(),
|
||||||
title: text().default('),
|
title: text().default(''),
|
||||||
description: text().default('),
|
description: text().default(''),
|
||||||
tags: jsonb().default([]),
|
tags: jsonb().default([]),
|
||||||
data: jsonb().default({}),
|
data: jsonb().default({}),
|
||||||
publish: jsonb().default({}),
|
publish: jsonb().default({}),
|
||||||
@@ -285,9 +285,9 @@ export const kvPackages = pgTable("kv_packages", {
|
|||||||
|
|
||||||
export const kvPage = pgTable("kv_page", {
|
export const kvPage = pgTable("kv_page", {
|
||||||
id: uuid().primaryKey().notNull(),
|
id: uuid().primaryKey().notNull(),
|
||||||
title: varchar({ length: 255 }).default('),
|
title: varchar({ length: 255 }).default(''),
|
||||||
description: text().default('),
|
description: text().default(''),
|
||||||
type: varchar({ length: 255 }).default('),
|
type: varchar({ length: 255 }).default(''),
|
||||||
data: json().default({}),
|
data: json().default({}),
|
||||||
uid: uuid(),
|
uid: uuid(),
|
||||||
createdAt: timestamp({ withTimezone: true, mode: 'string' }).notNull(),
|
createdAt: timestamp({ withTimezone: true, mode: 'string' }).notNull(),
|
||||||
@@ -298,10 +298,10 @@ export const kvPage = pgTable("kv_page", {
|
|||||||
|
|
||||||
export const kvResource = pgTable("kv_resource", {
|
export const kvResource = pgTable("kv_resource", {
|
||||||
id: uuid().primaryKey().notNull(),
|
id: uuid().primaryKey().notNull(),
|
||||||
name: varchar({ length: 255 }).default('),
|
name: varchar({ length: 255 }).default(''),
|
||||||
description: text().default('),
|
description: text().default(''),
|
||||||
source: varchar({ length: 255 }).default('),
|
source: varchar({ length: 255 }).default(''),
|
||||||
sourceId: varchar({ length: 255 }).default('),
|
sourceId: varchar({ length: 255 }).default(''),
|
||||||
version: varchar({ length: 255 }).default('0.0.0'),
|
version: varchar({ length: 255 }).default('0.0.0'),
|
||||||
data: json().default({}),
|
data: json().default({}),
|
||||||
uid: uuid(),
|
uid: uuid(),
|
||||||
@@ -321,20 +321,20 @@ export const kvVip = pgTable("kv_vip", {
|
|||||||
createdAt: timestamp({ withTimezone: true, mode: 'string' }).notNull(),
|
createdAt: timestamp({ withTimezone: true, mode: 'string' }).notNull(),
|
||||||
updatedAt: timestamp({ withTimezone: true, mode: 'string' }).notNull(),
|
updatedAt: timestamp({ withTimezone: true, mode: 'string' }).notNull(),
|
||||||
deletedAt: timestamp({ withTimezone: true, mode: 'string' }),
|
deletedAt: timestamp({ withTimezone: true, mode: 'string' }),
|
||||||
title: text().default(').notNull(),
|
title: text().default('').notNull(),
|
||||||
description: text().default(').notNull(),
|
description: text().default('').notNull(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const microAppsUpload = pgTable("micro_apps_upload", {
|
export const microAppsUpload = pgTable("micro_apps_upload", {
|
||||||
id: uuid().primaryKey().notNull(),
|
id: uuid().primaryKey().notNull(),
|
||||||
title: varchar({ length: 255 }).default('),
|
title: varchar({ length: 255 }).default(''),
|
||||||
description: varchar({ length: 255 }).default('),
|
description: varchar({ length: 255 }).default(''),
|
||||||
tags: jsonb().default([]),
|
tags: jsonb().default([]),
|
||||||
type: varchar({ length: 255 }).default('),
|
type: varchar({ length: 255 }).default(''),
|
||||||
source: varchar({ length: 255 }).default('),
|
source: varchar({ length: 255 }).default(''),
|
||||||
data: jsonb().default({}),
|
data: jsonb().default({}),
|
||||||
share: boolean().default(false),
|
share: boolean().default(false),
|
||||||
uname: varchar({ length: 255 }).default('),
|
uname: varchar({ length: 255 }).default(''),
|
||||||
uid: uuid(),
|
uid: uuid(),
|
||||||
createdAt: timestamp({ withTimezone: true, mode: 'string' }).notNull(),
|
createdAt: timestamp({ withTimezone: true, mode: 'string' }).notNull(),
|
||||||
updatedAt: timestamp({ withTimezone: true, mode: 'string' }).notNull(),
|
updatedAt: timestamp({ withTimezone: true, mode: 'string' }).notNull(),
|
||||||
@@ -342,54 +342,41 @@ export const microAppsUpload = pgTable("micro_apps_upload", {
|
|||||||
|
|
||||||
export const microMark = pgTable("micro_mark", {
|
export const microMark = pgTable("micro_mark", {
|
||||||
id: uuid().primaryKey().notNull(),
|
id: uuid().primaryKey().notNull(),
|
||||||
title: text().default('),
|
title: text().default(''),
|
||||||
description: text().default('),
|
description: text().default(''),
|
||||||
tags: jsonb().default([]),
|
tags: jsonb().default([]),
|
||||||
data: jsonb().default({}),
|
data: jsonb().default({}),
|
||||||
uname: varchar({ length: 255 }).default('),
|
uname: varchar({ length: 255 }).default(''),
|
||||||
uid: uuid(),
|
uid: uuid(),
|
||||||
createdAt: timestamp({ withTimezone: true, mode: 'string' }).notNull(),
|
createdAt: timestamp({ withTimezone: true, mode: 'string' }).notNull(),
|
||||||
updatedAt: timestamp({ withTimezone: true, mode: 'string' }).notNull(),
|
updatedAt: timestamp({ withTimezone: true, mode: 'string' }).notNull(),
|
||||||
cover: text().default('),
|
cover: text().default(''),
|
||||||
thumbnail: text().default('),
|
thumbnail: text().default(''),
|
||||||
link: text().default('),
|
link: text().default(''),
|
||||||
summary: text().default('),
|
summary: text().default(''),
|
||||||
markType: text().default('md'),
|
markType: text().default('md'),
|
||||||
config: jsonb().default({}),
|
config: jsonb().default({}),
|
||||||
puid: uuid(),
|
puid: uuid(),
|
||||||
deletedAt: timestamp({ withTimezone: true, mode: 'string' }),
|
deletedAt: timestamp({ withTimezone: true, mode: 'string' }),
|
||||||
version: integer().default(1),
|
version: integer().default(1),
|
||||||
fileList: jsonb().default([]),
|
fileList: jsonb().default([]),
|
||||||
key: text().default('),
|
key: text().default(''),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const prompts = pgTable("prompts", {
|
|
||||||
id: uuid().primaryKey().notNull(),
|
|
||||||
title: varchar({ length: 255 }).notNull(),
|
|
||||||
description: text(),
|
|
||||||
presetData: json(),
|
|
||||||
key: varchar({ length: 255 }).notNull(),
|
|
||||||
createdAt: timestamp({ withTimezone: true, mode: 'string' }).notNull(),
|
|
||||||
updatedAt: timestamp({ withTimezone: true, mode: 'string' }).notNull(),
|
|
||||||
deletedAt: timestamp({ withTimezone: true, mode: 'string' }),
|
|
||||||
}, (table) => [
|
|
||||||
unique("prompts_key_key").on(table.key),
|
|
||||||
]);
|
|
||||||
|
|
||||||
export const workShareMark = pgTable("work_share_mark", {
|
export const workShareMark = pgTable("work_share_mark", {
|
||||||
id: uuid().primaryKey().notNull(),
|
id: uuid().primaryKey().notNull(),
|
||||||
title: text().default('),
|
title: text().default(''),
|
||||||
key: text().default('),
|
key: text().default(''),
|
||||||
markType: text().default('md'),
|
markType: text().default('md'),
|
||||||
description: text().default('),
|
description: text().default(''),
|
||||||
cover: text().default('),
|
cover: text().default(''),
|
||||||
link: text().default('),
|
link: text().default(''),
|
||||||
tags: jsonb().default([]),
|
tags: jsonb().default([]),
|
||||||
summary: text().default('),
|
summary: text().default(''),
|
||||||
config: jsonb().default({}),
|
config: jsonb().default({}),
|
||||||
data: jsonb().default({}),
|
data: jsonb().default({}),
|
||||||
fileList: jsonb().default([]),
|
fileList: jsonb().default([]),
|
||||||
uname: varchar({ length: 255 }).default('),
|
uname: varchar({ length: 255 }).default(''),
|
||||||
version: integer().default(1),
|
version: integer().default(1),
|
||||||
markedAt: timestamp({ withTimezone: true, mode: 'string' }),
|
markedAt: timestamp({ withTimezone: true, mode: 'string' }),
|
||||||
uid: uuid(),
|
uid: uuid(),
|
||||||
@@ -398,3 +385,21 @@ export const workShareMark = pgTable("work_share_mark", {
|
|||||||
updatedAt: timestamp({ withTimezone: true, mode: 'string' }).notNull(),
|
updatedAt: timestamp({ withTimezone: true, mode: 'string' }).notNull(),
|
||||||
deletedAt: timestamp({ withTimezone: true, mode: 'string' }),
|
deletedAt: timestamp({ withTimezone: true, mode: 'string' }),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
export const prompts = pgTable('cf_prompts', {
|
||||||
|
id: uuid('id').primaryKey().defaultRandom(),
|
||||||
|
uid: uuid('uid'),
|
||||||
|
parents: jsonb('parents').notNull().default([]),
|
||||||
|
data: jsonb('data').notNull().default({}),
|
||||||
|
|
||||||
|
title: text('title').default(''),
|
||||||
|
description: text('description').default(''),
|
||||||
|
summary: text('summary').default(''),
|
||||||
|
tags: jsonb('tags').notNull().default([]),
|
||||||
|
link: text('link').default(''),
|
||||||
|
|
||||||
|
createdAt: timestamp('createdAt').notNull().defaultNow(),
|
||||||
|
updatedAt: timestamp('updatedAt').notNull().defaultNow(),
|
||||||
|
deletedAt: timestamp('deletedAt'),
|
||||||
|
});
|
||||||
@@ -1,14 +1,3 @@
|
|||||||
import { pgTable, serial, text, varchar, uuid, boolean, jsonb, timestamp } from "drizzle-orm/pg-core";
|
import { pgTable, uuid, jsonb, timestamp, text } from "drizzle-orm/pg-core";
|
||||||
import { InferSelectModel, InferInsertModel } from "drizzle-orm";
|
import { InferSelectModel, InferInsertModel, desc } from "drizzle-orm";
|
||||||
|
export * from './drizzle/schema.ts';
|
||||||
export const aiUsages = pgTable('cf_ai_usage_cache', {
|
|
||||||
id: uuid('id').primaryKey().defaultRandom(),
|
|
||||||
owner: uuid('owner'),
|
|
||||||
data: jsonb('data').notNull().default({}),
|
|
||||||
createdAt: timestamp('createdAt').notNull().defaultNow(),
|
|
||||||
updatedAt: timestamp('updatedAt').notNull().defaultNow(),
|
|
||||||
deletedAt: timestamp('deletedAt'),
|
|
||||||
});
|
|
||||||
|
|
||||||
// 类型推断
|
|
||||||
export type AiUsage = InferSelectModel<typeof aiUsages>;
|
|
||||||
@@ -13,7 +13,6 @@ const init = async () => {
|
|||||||
await UserSecretInit(null, null).catch((e) => {
|
await UserSecretInit(null, null).catch((e) => {
|
||||||
console.error('UserSecret sync', e);
|
console.error('UserSecret sync', e);
|
||||||
});
|
});
|
||||||
console.log('Models synced');
|
|
||||||
useContextKey('models-synced', true);
|
useContextKey('models-synced', true);
|
||||||
};
|
};
|
||||||
init();
|
init();
|
||||||
|
|||||||
@@ -142,14 +142,6 @@ app
|
|||||||
path: 'page',
|
path: 'page',
|
||||||
key: 'delete',
|
key: 'delete',
|
||||||
})
|
})
|
||||||
.define({
|
|
||||||
validator: {
|
|
||||||
id: {
|
|
||||||
required: true,
|
|
||||||
type: 'string',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.define(async (ctx) => {
|
.define(async (ctx) => {
|
||||||
const id = ctx.query.id;
|
const id = ctx.query.id;
|
||||||
const page = await PageModel.findByPk(id);
|
const page = await PageModel.findByPk(id);
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useFileStore } from '@kevisual/use-config/file-store';
|
import { useFileStore } from '@kevisual/use-config/file-store';
|
||||||
import { PageModel } from '../models/index.ts';
|
import { PageModel } from '../models/index.ts';
|
||||||
import { ContainerModel } from '@/routes/container/models/index.ts';
|
import { ContainerModel } from '@/old-apps/container/models/index.ts';
|
||||||
import { Op } from 'sequelize';
|
import { Op } from 'sequelize';
|
||||||
import { getContainerData } from './get-container.ts';
|
import { getContainerData } from './get-container.ts';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { CustomError } from '@kevisual/router';
|
import { CustomError } from '@kevisual/router';
|
||||||
import { app } from '../../app.ts';
|
import { app } from '../../app.ts';
|
||||||
import { PageModel } from './models/index.ts';
|
import { PageModel } from './models/index.ts';
|
||||||
import { AppListModel, AppModel } from '../app-manager/index.ts';
|
import { AppListModel, AppModel } from '../../routes/app-manager/index.ts';
|
||||||
import { cachePage, getZip } from './module/cache-file.ts';
|
import { cachePage, getZip } from './module/cache-file.ts';
|
||||||
import { uniqBy } from 'lodash-es';
|
import { uniqBy } from 'lodash-es';
|
||||||
import semver from 'semver';
|
import semver from 'semver';
|
||||||
@@ -7,7 +7,7 @@ import { app, minioClient } from '@/app.ts';
|
|||||||
import { bucketName } from '@/modules/minio.ts';
|
import { bucketName } from '@/modules/minio.ts';
|
||||||
import { getContentType } from '@/utils/get-content-type.ts';
|
import { getContentType } from '@/utils/get-content-type.ts';
|
||||||
import { User } from '@/models/user.ts';
|
import { User } from '@/models/user.ts';
|
||||||
import { getContainerById } from '@/routes/container/module/get-container-file.ts';
|
import { getContainerById } from '@/old-apps/container/module/get-container-file.ts';
|
||||||
import { router, error, checkAuth, writeEvents } from './router.ts';
|
import { router, error, checkAuth, writeEvents } from './router.ts';
|
||||||
import './index.ts';
|
import './index.ts';
|
||||||
import { handleRequest as PageProxy } from './page-proxy.ts';
|
import { handleRequest as PageProxy } from './page-proxy.ts';
|
||||||
|
|||||||
@@ -11,4 +11,6 @@ import './config/index.ts';
|
|||||||
import './file-listener/index.ts';
|
import './file-listener/index.ts';
|
||||||
|
|
||||||
|
|
||||||
import './ai/index.ts';
|
import './ai/index.ts';
|
||||||
|
|
||||||
|
import './prompts/index.ts'
|
||||||
1
src/routes/prompts/index.ts
Normal file
1
src/routes/prompts/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
import './list.ts'
|
||||||
111
src/routes/prompts/list.ts
Normal file
111
src/routes/prompts/list.ts
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
import { desc, eq, count, or, like, and } from 'drizzle-orm';
|
||||||
|
import { schema, app, db } from '@/app.ts'
|
||||||
|
|
||||||
|
|
||||||
|
app.route({
|
||||||
|
path: 'prompts',
|
||||||
|
key: 'list',
|
||||||
|
middleware: ['auth'],
|
||||||
|
description: '获取提示词列表',
|
||||||
|
}).define(async (ctx) => {
|
||||||
|
const tokenUser = ctx.state.tokenUser;
|
||||||
|
const uid = tokenUser.id;
|
||||||
|
const { page = 1, pageSize = 20, search, sort = 'DESC' } = ctx.query || {};
|
||||||
|
|
||||||
|
const offset = (page - 1) * pageSize;
|
||||||
|
const orderByField = sort === 'ASC' ? schema.prompts.updatedAt : desc(schema.prompts.updatedAt);
|
||||||
|
|
||||||
|
let whereCondition = eq(schema.prompts.uid, uid);
|
||||||
|
if (search) {
|
||||||
|
whereCondition = and(
|
||||||
|
eq(schema.prompts.uid, uid),
|
||||||
|
or(
|
||||||
|
like(schema.prompts.title, `%${search}%`),
|
||||||
|
like(schema.prompts.summary, `%${search}%`)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [list, totalCount] = await Promise.all([
|
||||||
|
db.select()
|
||||||
|
.from(schema.prompts)
|
||||||
|
.where(whereCondition)
|
||||||
|
.limit(pageSize)
|
||||||
|
.offset(offset)
|
||||||
|
.orderBy(orderByField),
|
||||||
|
db.select({ count: count() })
|
||||||
|
.from(schema.prompts)
|
||||||
|
.where(whereCondition)
|
||||||
|
]);
|
||||||
|
|
||||||
|
ctx.body = {
|
||||||
|
list,
|
||||||
|
pagination: {
|
||||||
|
page,
|
||||||
|
current: page,
|
||||||
|
pageSize,
|
||||||
|
total: totalCount[0]?.count || 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return ctx;
|
||||||
|
}).addTo(app);
|
||||||
|
|
||||||
|
const promptUpdate = `创建或更新一个提示词, 参数定义:
|
||||||
|
title: 提示词标题, 必填
|
||||||
|
description: 描述, 选填
|
||||||
|
summary: 摘要, 选填
|
||||||
|
tags: 标签, 数组, 选填
|
||||||
|
link: 链接, 选填
|
||||||
|
data: 数据, 对象, 选填
|
||||||
|
parents: 父级ID数组, 选填
|
||||||
|
`;
|
||||||
|
app.route({
|
||||||
|
path: 'prompts',
|
||||||
|
key: 'update',
|
||||||
|
middleware: ['auth'],
|
||||||
|
description: promptUpdate,
|
||||||
|
}).define(async (ctx) => {
|
||||||
|
const { id, uid, updatedAt, ...rest } = ctx.query.data || {};
|
||||||
|
const tokenUser = ctx.state.tokenUser;
|
||||||
|
let prompt;
|
||||||
|
if (!id) {
|
||||||
|
prompt = await db.insert(schema.prompts).values({
|
||||||
|
title: rest.title,
|
||||||
|
description: rest.description,
|
||||||
|
...rest,
|
||||||
|
uid: tokenUser.id,
|
||||||
|
}).returning();
|
||||||
|
} else {
|
||||||
|
const existing = await db.select().from(schema.prompts).where(eq(schema.prompts.id, id)).limit(1);
|
||||||
|
if (existing.length === 0) {
|
||||||
|
ctx.throw(404, '没有找到对应的提示词');
|
||||||
|
}
|
||||||
|
prompt = await db.update(schema.prompts).set({
|
||||||
|
...rest,
|
||||||
|
}).where(eq(schema.prompts.id, id)).returning();
|
||||||
|
}
|
||||||
|
ctx.body = prompt;
|
||||||
|
}).addTo(app);
|
||||||
|
|
||||||
|
|
||||||
|
app.route({
|
||||||
|
path: 'prompts',
|
||||||
|
key: 'delete',
|
||||||
|
middleware: ['auth'],
|
||||||
|
description: '删除提示词, 参数: id 提示词ID',
|
||||||
|
}).define(async (ctx) => {
|
||||||
|
const tokenUser = ctx.state.tokenUser;
|
||||||
|
const { id } = ctx.query.data || {};
|
||||||
|
if (!id) {
|
||||||
|
ctx.throw(400, 'id 参数缺失');
|
||||||
|
}
|
||||||
|
const existing = await db.select().from(schema.prompts).where(eq(schema.prompts.id, id)).limit(1);
|
||||||
|
if (existing.length === 0) {
|
||||||
|
ctx.throw(404, '没有找到对应的提示词');
|
||||||
|
}
|
||||||
|
if (existing[0].uid !== tokenUser.id) {
|
||||||
|
ctx.throw(403, '没有权限删除该提示词');
|
||||||
|
}
|
||||||
|
await db.delete(schema.prompts).where(eq(schema.prompts.id, id));
|
||||||
|
ctx.body = { success: true };
|
||||||
|
}).addTo(app);
|
||||||
@@ -1 +1 @@
|
|||||||
export * from './container/type.ts'
|
export * from '../old-apps/container/type.ts'
|
||||||
27
src/test/common-query.ts
Normal file
27
src/test/common-query.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
|
||||||
|
|
||||||
|
import { useConfig, useContextKey } from '@kevisual/context';
|
||||||
|
import { Query } from '@kevisual/query';
|
||||||
|
import util from 'node:util';
|
||||||
|
const config = useConfig();
|
||||||
|
export const showMore = (res: any) => {
|
||||||
|
return util.inspect(res, { depth: 6, colors: true });
|
||||||
|
}
|
||||||
|
const token = 'st_r3u38c0jbhoc412ovzeeuaucygt6w5qg';
|
||||||
|
export const query = new Query({
|
||||||
|
url: 'http://localhost:4005/api/router',
|
||||||
|
});
|
||||||
|
// const loginRes = await query.post({
|
||||||
|
// path: 'user',
|
||||||
|
// key: 'login',
|
||||||
|
// username: 'root',
|
||||||
|
// password: config.KEVISUAL_PASSWORD ||'',
|
||||||
|
// });
|
||||||
|
// console.log('login:', showMore(loginRes));
|
||||||
|
query.beforeRequest = async (options) => {
|
||||||
|
options.headers = {
|
||||||
|
...options.headers,
|
||||||
|
'Authorization': 'Bearer ' + token,
|
||||||
|
};
|
||||||
|
return options;
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ import '@/route.ts';
|
|||||||
import { useConfig, useContextKey } from '@kevisual/context';
|
import { useConfig, useContextKey } from '@kevisual/context';
|
||||||
import { Query } from '@kevisual/query';
|
import { Query } from '@kevisual/query';
|
||||||
import util from 'node:util';
|
import util from 'node:util';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
app,
|
app,
|
||||||
useContextKey
|
useContextKey
|
||||||
@@ -23,6 +24,9 @@ export const showRes = (res, ...args) => {
|
|||||||
console.error(res.code, res.message, ...args);
|
console.error(res.code, res.message, ...args);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
export const showMore = (res: any) => {
|
||||||
|
return util.inspect(res, { depth: 6, colors: true });
|
||||||
|
}
|
||||||
|
|
||||||
export const exit = (code = 0) => {
|
export const exit = (code = 0) => {
|
||||||
process.exit(code);
|
process.exit(code);
|
||||||
@@ -30,4 +34,4 @@ export const exit = (code = 0) => {
|
|||||||
|
|
||||||
export const query = new Query({
|
export const query = new Query({
|
||||||
url: 'https://kevisual.cn/api/router'
|
url: 'https://kevisual.cn/api/router'
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ContainerModel } from '../routes/container/models/index.ts';
|
import { ContainerModel } from '../old-apps/container/models/index.ts';
|
||||||
|
|
||||||
const main = async () => {
|
const main = async () => {
|
||||||
// await ContainerModel.update(
|
// await ContainerModel.update(
|
||||||
|
|||||||
8
src/test/prompt.ts
Normal file
8
src/test/prompt.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { query, showMore } from './common-query.ts';
|
||||||
|
|
||||||
|
const res = await query.post({
|
||||||
|
path: 'prompts',
|
||||||
|
key: 'list'
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log('res:', showMore(res));
|
||||||
Reference in New Issue
Block a user