From 027cbecab6e1a2bdf10a003de0f0c9794384700b Mon Sep 17 00:00:00 2001 From: abearxiong Date: Tue, 10 Mar 2026 20:17:49 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=20flowme-life=20?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=EF=BC=8C=E5=8C=85=E6=8B=AC=E5=88=9B=E5=BB=BA?= =?UTF-8?q?=E3=80=81=E6=9B=B4=E6=96=B0=E3=80=81=E5=88=A0=E9=99=A4=E5=92=8C?= =?UTF-8?q?=E5=88=97=E8=A1=A8=E6=8E=A5=E5=8F=A3=EF=BC=8C=E5=AF=BC=E5=85=A5?= =?UTF-8?q?=20life=20JSON=20=E6=95=B0=E6=8D=AE=E5=88=B0=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E5=BA=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude/skills/create-routes/SKILL.md | 150 ---------------- .opencode/skills/create-routes/SKILL.md | 217 ++++++++++++++++++++++++ package.json | 2 +- pnpm-lock.yaml | 6 +- scripts/import-life.ts | 83 +++++++++ src/db/drizzle/schema.ts | 46 ----- src/db/schema.ts | 4 +- src/db/schemas/life-schema.ts | 75 ++++++++ src/routes/flowme-life/index.ts | 1 + src/routes/flowme-life/list.ts | 202 ++++++++++++++++++++++ src/routes/index.ts | 4 +- 11 files changed, 588 insertions(+), 202 deletions(-) delete mode 100644 .claude/skills/create-routes/SKILL.md create mode 100644 .opencode/skills/create-routes/SKILL.md create mode 100644 scripts/import-life.ts create mode 100644 src/db/schemas/life-schema.ts create mode 100644 src/routes/flowme-life/index.ts create mode 100644 src/routes/flowme-life/list.ts diff --git a/.claude/skills/create-routes/SKILL.md b/.claude/skills/create-routes/SKILL.md deleted file mode 100644 index fd2d7f6..0000000 --- a/.claude/skills/create-routes/SKILL.md +++ /dev/null @@ -1,150 +0,0 @@ ---- -name: create-routes -description: 创建路由例子模板代码 ---- -# 创建路由例子模板代码 - -app是自定义@kevisual/router的一个APP - -1. 一般来说,修改path,和对应的schema表,就可以快速创建对应的增删改查接口。 -2. 根据需要,每一个功能需要添加对应的描述 -3. 根据需要,对应schema表的字段进行修改代码 - -示例: -```ts -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, '没有找到对应的提示词'); - } - if (existing[0].uid !== tokenUser.id) { - ctx.throw(403, '没有权限更新该提示词'); - } - 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); - -app.route({ - path: 'prompts', - key: 'get', - 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, '没有权限查看该提示词'); - } - ctx.body = existing[0]; -}).addTo(app); -``` \ No newline at end of file diff --git a/.opencode/skills/create-routes/SKILL.md b/.opencode/skills/create-routes/SKILL.md new file mode 100644 index 0000000..4bbe5ad --- /dev/null +++ b/.opencode/skills/create-routes/SKILL.md @@ -0,0 +1,217 @@ +--- +name: create-routes +description: 创建路由例子模板代码 +--- + +# 创建路由例子模板代码 + +app是自定义@kevisual/router的一个APP + +1. 一般来说,修改path,和对应的schema表,就可以快速创建对应的增删改查接口。 +2. 根据需要,每一个功能需要添加对应的描述 +3. 根据需要,对应schema表的字段进行修改代码 + +示例: + +```ts +import { desc, eq, count, or, like, and } from 'drizzle-orm'; +import { schema, app, db } from '@/app.ts'; +import { z } from 'zod'; + +app + .route({ + path: 'prompts', + key: 'list', + middleware: ['auth'], + description: '获取提示词列表', + metadata: { + args: { + page: z.number().optional().default(1).describe('页码'), + pageSize: z.number().optional().default(20).describe('每页数量'), + search: z.string().optional().describe('搜索关键词'), + sort: z.enum(['ASC', 'DESC']).optional().default('DESC').describe('排序方式'), + }, + }, + }) + .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); +app + .route({ + path: 'prompts', + key: 'create', + middleware: ['auth'], + description: '创建提示词', + metadata: { + args: { + data: z + .object({ + title: z.string().describe('提示词标题'), + description: z.string().optional().describe('描述'), + summary: z.string().optional().describe('摘要'), + tags: z.array(z.string()).optional().describe('标签'), + link: z.string().optional().describe('链接'), + data: z.record(z.string(), z.any()).optional().describe('数据对象'), + parents: z.array(z.string()).optional().describe('父级ID数组'), + }) + .describe('提示词对象'), + }, + }, + }) + .define(async (ctx) => { + const tokenUser = ctx.state.tokenUser; + const { title, description, summary, tags, link, data, parents } = ctx.query.data || {}; + if (!title) { + ctx.throw(400, 'title 参数缺失'); + } + const newPrompt = await db + .insert(schema.prompts) + .values({ + title, + description, + summary, + tags, + link, + data, + parents, + uid: tokenUser.id, + }) + .returning(); + + ctx.body = newPrompt; + }) + .addTo(app); +app + .route({ + path: 'prompts', + key: 'update', + middleware: ['auth'], + description: '更新提示词', + metadata: { + args: { + data: z + .object({ + id: z.string().optional().describe('提示词ID, 不填表示创建'), + title: z.string().describe('提示词标题'), + description: z.string().optional().describe('描述'), + summary: z.string().optional().describe('摘要'), + tags: z.array(z.string()).optional().describe('标签'), + link: z.string().optional().describe('链接'), + data: z.record(z.string(), z.any()).optional().describe('数据对象'), + parents: z.array(z.string()).optional().describe('父级ID数组'), + }) + .describe('提示词对象'), + }, + }, + }) + .define(async (ctx) => { + const { id, uid, updatedAt, ...rest } = ctx.query.data || {}; + const tokenUser = ctx.state.tokenUser; + let prompt; + 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, '没有权限更新该提示词'); + } + 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', + metadata: { + args: { + id: z.string().describe('提示词ID'), + }, + }, + }) + .define(async (ctx) => { + const tokenUser = ctx.state.tokenUser; + const { id } = ctx.query || {}; + 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); + +app + .route({ + path: 'prompts', + key: 'get', + middleware: ['auth'], + description: '获取单个提示词, 参数: id 提示词ID', + metadata: { + args: { + id: z.string().describe('提示词ID'), + }, + }, + }) + .define(async (ctx) => { + const tokenUser = ctx.state.tokenUser; + const { id } = ctx.query || {}; + 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, '没有权限查看该提示词'); + } + ctx.body = existing[0]; + }) + .addTo(app); +``` diff --git a/package.json b/package.json index c6caec5..2cb11bf 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,6 @@ "busboy": "^1.6.0", "drizzle-kit": "^0.31.9", "drizzle-orm": "^0.45.1", - "drizzle-zod": "^0.8.3", "eventemitter3": "^5.0.4", "send": "^1.2.1", "ws": "npm:@kevisual/ws", @@ -84,6 +83,7 @@ "crypto-js": "^4.2.0", "dayjs": "^1.11.19", "dotenv": "^17.3.1", + "drizzle-zod": "^0.8.3", "es-toolkit": "^1.45.1", "ioredis": "^5.10.0", "jsonwebtoken": "^9.0.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d899dd8..8b879ee 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -46,9 +46,6 @@ importers: drizzle-orm: specifier: ^0.45.1 version: 0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.18.0)(better-sqlite3@12.6.2)(bun-types@1.3.10)(pg@8.20.0) - drizzle-zod: - specifier: ^0.8.3 - version: 0.8.3(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.18.0)(better-sqlite3@12.6.2)(bun-types@1.3.10)(pg@8.20.0))(zod@4.3.6) eventemitter3: specifier: ^5.0.4 version: 5.0.4 @@ -137,6 +134,9 @@ importers: dotenv: specifier: ^17.3.1 version: 17.3.1 + drizzle-zod: + specifier: ^0.8.3 + version: 0.8.3(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.18.0)(better-sqlite3@12.6.2)(bun-types@1.3.10)(pg@8.20.0))(zod@4.3.6) es-toolkit: specifier: ^1.45.1 version: 1.45.1 diff --git a/scripts/import-life.ts b/scripts/import-life.ts new file mode 100644 index 0000000..d89d8eb --- /dev/null +++ b/scripts/import-life.ts @@ -0,0 +1,83 @@ +/** + * 导入 life JSON 数据到数据库 + * 运行: bun run scripts/import-life.ts + */ + +import { drizzle } from 'drizzle-orm/node-postgres'; +import { life } from '@/db/schemas/life-schema.ts'; +import { useConfig } from '@kevisual/use-config'; +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; + +const config = useConfig() as any; +const DATABASE_URL = config.DATABASE_URL || process.env.DATABASE_URL || ''; +if (!DATABASE_URL) { + console.error('缺少 DATABASE_URL 配置'); + process.exit(1); +} + +const db = drizzle(DATABASE_URL); + +// 读取 JSON 数据 +const jsonPath = resolve(import.meta.dir, 'life-list.json'); +const rawData = JSON.parse(readFileSync(jsonPath, 'utf-8')) as Array<{ + data: Record; + description: string; + effectiveAt: string; + link: string; + prompt: string; + summary: string; + tags: string[]; + taskResult: Record; + taskType: string; + title: string; + type: string; + updatedAt: string; + userId: string; +}>; + +async function importData() { + console.log(`准备导入 ${rawData.length} 条 flowme-life 数据...`); + + let inserted = 0; + let skipped = 0; + + for (const item of rawData) { + const uid = item.userId; + + try { + await db + .insert(life) + .values({ + title: item.title || '', + summary: item.summary || '', + description: item.description || '', + tags: item.tags ?? [], + link: item.link || '', + data: item.data ?? {}, + effectiveAt: item.effectiveAt || '', + type: item.type || '', + prompt: item.prompt || '', + taskType: item.taskType || '', + taskResult: item.taskResult ?? {}, + uid: uid as any, + }) + .onConflictDoNothing(); + + console.log(` ✓ 导入: title=${item.title}, type=${item.type}`); + inserted++; + } catch (err: any) { + const cause = err.cause || err; + console.warn(` ✗ 跳过: title=${item.title} — ${cause.message || err.message}`); + skipped++; + } + } + + console.log(`\n完成: 成功 ${inserted} 条,跳过 ${skipped} 条`); + process.exit(0); +} + +importData().catch((err) => { + console.error('导入失败:', err); + process.exit(1); +}); diff --git a/src/db/drizzle/schema.ts b/src/db/drizzle/schema.ts index 69b5bb0..6e05bed 100644 --- a/src/db/drizzle/schema.ts +++ b/src/db/drizzle/schema.ts @@ -483,49 +483,3 @@ export const queryViews = pgTable("query_views", { index('query_title_idx').using('btree', table.title.asc().nullsLast()), ]); -export const flowme = pgTable("flowme", { - id: uuid().primaryKey().notNull().defaultRandom(), - uid: uuid(), - - title: text('title').default(''), - tags: jsonb().default([]), - summary: text('summary').default(''), - description: text('description').default(''), - link: text('link').default(''), - data: jsonb().default({}), - - channelId: uuid().references(() => flowmeChannels.id, { onDelete: 'set null' }), - type: text('type').default(''), - source: text('source').default(''), - importance: integer('importance').default(0), // 重要性等级 - isArchived: boolean('isArchived').default(false), // 是否归档 - - createdAt: timestamp('createdAt').notNull().defaultNow(), - updatedAt: timestamp('updatedAt').notNull().defaultNow().$onUpdate(() => new Date()), - -}, (table) => [ - index('flowme_uid_idx').using('btree', table.uid.asc().nullsLast()), - index('flowme_title_idx').using('btree', table.title.asc().nullsLast()), - index('flowme_channel_id_idx').using('btree', table.channelId.asc().nullsLast()), -]); - - -export const flowmeChannels = pgTable("flowme_channels", { - id: uuid().primaryKey().notNull().defaultRandom(), - uid: uuid(), - title: text('title').default(''), - tags: jsonb().default([]), - summary: text('summary').default(''), - description: text('description').default(''), - link: text('link').default(''), - data: jsonb().default({}), - - key: text('key').default(''), - color: text('color').default('#007bff'), - createdAt: timestamp('createdAt').notNull().defaultNow(), - updatedAt: timestamp('updatedAt').notNull().defaultNow().$onUpdate(() => new Date()), -}, (table) => [ - index('flowme_channels_uid_idx').using('btree', table.uid.asc().nullsLast()), - index('flowme_channels_key_idx').using('btree', table.key.asc().nullsLast()), - index('flowme_channels_title_idx').using('btree', table.title.asc().nullsLast()), -]); \ No newline at end of file diff --git a/src/db/schema.ts b/src/db/schema.ts index 5d69235..2e90063 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -2,4 +2,6 @@ import { pgTable, uuid, jsonb, timestamp, text } from "drizzle-orm/pg-core"; import { InferSelectModel, InferInsertModel, desc } from "drizzle-orm"; export * from './drizzle/schema.ts'; -export * from './schemas/n-code-schema.ts' \ No newline at end of file +export * from './schemas/n-code-schema.ts' + +export * from './schemas/life-schema.ts' \ No newline at end of file diff --git a/src/db/schemas/life-schema.ts b/src/db/schemas/life-schema.ts new file mode 100644 index 0000000..d7b7531 --- /dev/null +++ b/src/db/schemas/life-schema.ts @@ -0,0 +1,75 @@ +import { pgTable, serial, text, jsonb, varchar, timestamp, unique, uuid, doublePrecision, json, integer, boolean, index, uniqueIndex, pgEnum } from "drizzle-orm/pg-core" +import { sql, sum } from "drizzle-orm" + +export const life = pgTable("flowme_life", { + id: uuid().primaryKey().notNull().defaultRandom(), + uid: uuid(), + + title: text('title').default(''), + tags: jsonb().default([]), + summary: text('summary').default(''), + description: text('description').default(''), + link: text('link').default(''), + data: jsonb().default({}), + + effectiveAt: text('effectiveAt').default(''), + type: text('type').default(''), + prompt: text('prompt').default(''), + taskType: text('taskType').default(''), + taskResult: jsonb('taskResult').default({}), + + createdAt: timestamp('createdAt').notNull().defaultNow(), + updatedAt: timestamp('updatedAt').notNull().defaultNow().$onUpdate(() => new Date()), +}, (table) => [ + index('life_uid_idx').using('btree', table.uid.asc().nullsLast()), + index('life_title_idx').using('btree', table.title.asc().nullsLast()), + index('life_effective_at_idx').using('btree', table.effectiveAt.asc().nullsLast()), + index('life_summary_idx').using('btree', table.summary.asc().nullsLast()), +]); + +export const flowme = pgTable("flowme", { + id: uuid().primaryKey().notNull().defaultRandom(), + uid: uuid(), + + title: text('title').default(''), + tags: jsonb().default([]), + summary: text('summary').default(''), + description: text('description').default(''), + link: text('link').default(''), + data: jsonb().default({}), + + channelId: uuid().references(() => flowmeChannels.id, { onDelete: 'set null' }), + type: text('type').default(''), + source: text('source').default(''), + importance: integer('importance').default(0), // 重要性等级 + isArchived: boolean('isArchived').default(false), // 是否归档 + + createdAt: timestamp('createdAt').notNull().defaultNow(), + updatedAt: timestamp('updatedAt').notNull().defaultNow().$onUpdate(() => new Date()), + +}, (table) => [ + index('flowme_uid_idx').using('btree', table.uid.asc().nullsLast()), + index('flowme_title_idx').using('btree', table.title.asc().nullsLast()), + index('flowme_channel_id_idx').using('btree', table.channelId.asc().nullsLast()), +]); + + +export const flowmeChannels = pgTable("flowme_channels", { + id: uuid().primaryKey().notNull().defaultRandom(), + uid: uuid(), + title: text('title').default(''), + tags: jsonb().default([]), + summary: text('summary').default(''), + description: text('description').default(''), + link: text('link').default(''), + data: jsonb().default({}), + + key: text('key').default(''), + color: text('color').default('#007bff'), + createdAt: timestamp('createdAt').notNull().defaultNow(), + updatedAt: timestamp('updatedAt').notNull().defaultNow().$onUpdate(() => new Date()), +}, (table) => [ + index('flowme_channels_uid_idx').using('btree', table.uid.asc().nullsLast()), + index('flowme_channels_key_idx').using('btree', table.key.asc().nullsLast()), + index('flowme_channels_title_idx').using('btree', table.title.asc().nullsLast()), +]); \ No newline at end of file diff --git a/src/routes/flowme-life/index.ts b/src/routes/flowme-life/index.ts new file mode 100644 index 0000000..1b79037 --- /dev/null +++ b/src/routes/flowme-life/index.ts @@ -0,0 +1 @@ +import './list.ts' diff --git a/src/routes/flowme-life/list.ts b/src/routes/flowme-life/list.ts new file mode 100644 index 0000000..e8afb41 --- /dev/null +++ b/src/routes/flowme-life/list.ts @@ -0,0 +1,202 @@ +import { desc, eq, count, or, like, and } from 'drizzle-orm'; +import { schema, app, db } from '@/app.ts' +import z from 'zod'; +app.route({ + path: 'flowme-life', + key: 'list', + middleware: ['auth'], + description: '获取 flowme-life 列表', + metadata: { + args: { + page: z.number().describe('页码, 默认为 1').optional(), + pageSize: z.number().describe('每页数量, 默认为 20').optional(), + search: z.string().describe('搜索关键词').optional(), + sort: z.enum(['ASC', 'DESC']).describe('排序方式,ASC 或 DESC,默认为 DESC').optional(), + } + } +}).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.life.updatedAt : desc(schema.life.updatedAt); + + let whereCondition = eq(schema.life.uid, uid); + if (search) { + whereCondition = and( + eq(schema.life.uid, uid), + or( + like(schema.life.title, `%${search}%`), + like(schema.life.summary, `%${search}%`) + ) + ); + } + + const [list, totalCount] = await Promise.all([ + db.select() + .from(schema.life) + .where(whereCondition) + .limit(pageSize) + .offset(offset) + .orderBy(orderByField), + db.select({ count: count() }) + .from(schema.life) + .where(whereCondition) + ]); + + ctx.body = { + list, + pagination: { + page, + current: page, + pageSize, + total: totalCount[0]?.count || 0, + }, + }; + return ctx; +}).addTo(app); + +app.route({ + path: 'flowme-life', + key: 'create', + middleware: ['auth'], + description: '创建一个 flowme-life', + metadata: { + args: { + data: z.object({ + title: z.string().describe('标题').optional(), + summary: z.string().describe('摘要').optional(), + description: z.string().describe('描述').optional(), + tags: z.array(z.string()).describe('标签').optional(), + link: z.string().describe('链接').optional(), + data: z.record(z.string(), z.any()).describe('数据').optional(), + effectiveAt: z.string().describe('生效日期').optional(), + type: z.string().describe('类型').optional(), + prompt: z.string().describe('提示词').optional(), + taskType: z.string().describe('任务类型').optional(), + taskResult: z.record(z.string(), z.any()).describe('任务结果').optional(), + }) + } + } +}).define(async (ctx) => { + const { uid, updatedAt, createdAt, ...rest } = ctx.query.data || {}; + const tokenUser = ctx.state.tokenUser; + const lifeItem = await db.insert(schema.life).values({ + title: rest.title || '', + summary: rest.summary || '', + description: rest.description || '', + tags: rest.tags || [], + link: rest.link || '', + data: rest.data || {}, + effectiveAt: rest.effectiveAt || '', + type: rest.type || '', + prompt: rest.prompt || '', + taskType: rest.taskType || '', + taskResult: rest.taskResult || {}, + uid: tokenUser.id, + }).returning(); + ctx.body = lifeItem; +}).addTo(app); + +app.route({ + path: 'flowme-life', + key: 'update', + middleware: ['auth'], + description: '更新一个 flowme-life', + metadata: { + args: { + data: z.object({ + id: z.string().describe('ID'), + title: z.string().describe('标题').optional(), + summary: z.string().describe('摘要').optional(), + description: z.string().describe('描述').optional(), + tags: z.array(z.string()).describe('标签').optional(), + link: z.string().describe('链接').optional(), + data: z.record(z.string(), z.any()).describe('数据').optional(), + effectiveAt: z.string().describe('生效日期').optional(), + type: z.string().describe('类型').optional(), + prompt: z.string().describe('提示词').optional(), + taskType: z.string().describe('任务类型').optional(), + taskResult: z.record(z.string(), z.any()).describe('任务结果').optional(), + }) + } + } +}).define(async (ctx) => { + const { id, uid, updatedAt, createdAt, ...rest } = ctx.query.data || {}; + const tokenUser = ctx.state.tokenUser; + if (!id) { + ctx.throw(400, 'id 参数缺失'); + } + const existing = await db.select().from(schema.life).where(eq(schema.life.id, id)).limit(1); + if (existing.length === 0) { + ctx.throw(404, '没有找到对应的 flowme-life'); + } + if (existing[0].uid !== tokenUser.id) { + ctx.throw(403, '没有权限更新该 flowme-life'); + } + const lifeItem = await db.update(schema.life).set({ + title: rest.title, + summary: rest.summary, + description: rest.description, + tags: rest.tags, + link: rest.link, + data: rest.data, + effectiveAt: rest.effectiveAt, + type: rest.type, + prompt: rest.prompt, + taskType: rest.taskType, + taskResult: rest.taskResult, + }).where(eq(schema.life.id, id)).returning(); + ctx.body = lifeItem; +}).addTo(app); + +app.route({ + path: 'flowme-life', + key: 'delete', + middleware: ['auth'], + description: '删除 flowme-life', + metadata: { + args: { + data: z.object({ + id: z.string().describe('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.life).where(eq(schema.life.id, id)).limit(1); + if (existing.length === 0) { + ctx.throw(404, '没有找到对应的 flowme-life'); + } + if (existing[0].uid !== tokenUser.id) { + ctx.throw(403, '没有权限删除该 flowme-life'); + } + await db.delete(schema.life).where(eq(schema.life.id, id)); + ctx.body = { success: true }; +}).addTo(app); + +app.route({ + path: 'flowme-life', + key: 'get', + middleware: ['auth'], + description: '获取单个 flowme-life, 参数: data.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.life).where(eq(schema.life.id, id)).limit(1); + if (existing.length === 0) { + ctx.throw(404, '没有找到对应的 flowme-life'); + } + if (existing[0].uid !== tokenUser.id) { + ctx.throw(403, '没有权限查看该 flowme-life'); + } + ctx.body = existing[0]; +}).addTo(app); diff --git a/src/routes/index.ts b/src/routes/index.ts index f262388..1f6b8e4 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -22,4 +22,6 @@ import './query-views/index.ts'; import './flowme/index.ts' -import './n5-link/index.ts' \ No newline at end of file +import './n5-link/index.ts' + +import './flowme-life/index.ts' \ No newline at end of file