From 7c61bd3ac5e67e9e1f892c6cbbd83a368a5fe737 Mon Sep 17 00:00:00 2001 From: abearxiong Date: Sat, 31 Jan 2026 18:27:21 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=20flowme=20=E5=92=8C=20flowm?= =?UTF-8?q?e-channel=20=E8=B7=AF=E7=94=B1=EF=BC=8C=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E5=A2=9E=E5=88=A0=E6=94=B9=E6=9F=A5=E5=8A=9F=E8=83=BD=EF=BC=9B?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=20schema=20=E4=BB=A5=E5=8C=85=E5=90=AB?= =?UTF-8?q?=E7=9B=B8=E5=85=B3=E5=AD=97=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude/skills/create-routes/SKILL.md | 150 +++++++++++++++++++++ src/db/drizzle/schema.ts | 27 +++- src/routes/flowme/flowme-channel/list.ts | 144 ++++++++++++++++++++ src/routes/flowme/index.ts | 5 + src/routes/flowme/list.ts | 160 +++++++++++++++++++++++ 5 files changed, 484 insertions(+), 2 deletions(-) create mode 100644 .claude/skills/create-routes/SKILL.md create mode 100644 src/routes/flowme/flowme-channel/list.ts create mode 100644 src/routes/flowme/index.ts create mode 100644 src/routes/flowme/list.ts diff --git a/.claude/skills/create-routes/SKILL.md b/.claude/skills/create-routes/SKILL.md new file mode 100644 index 0000000..fd2d7f6 --- /dev/null +++ b/.claude/skills/create-routes/SKILL.md @@ -0,0 +1,150 @@ +--- +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/src/db/drizzle/schema.ts b/src/db/drizzle/schema.ts index af47af1..f713829 100644 --- a/src/db/drizzle/schema.ts +++ b/src/db/drizzle/schema.ts @@ -502,12 +502,35 @@ export const flowme = pgTable("flowme", { tags: jsonb().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(), + }, (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(''), + description: text('description').default(''), + tags: jsonb().default([]), + link: text('link').default(''), + data: jsonb().default({}), + color: text('color').default('#007bff'), + createdAt: timestamp('createdAt').notNull().defaultNow(), + updatedAt: timestamp('updatedAt').notNull().defaultNow(), +}, (table) => [ + index('flowme_channels_uid_idx').using('btree', table.uid.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/flowme-channel/list.ts b/src/routes/flowme/flowme-channel/list.ts new file mode 100644 index 0000000..428644f --- /dev/null +++ b/src/routes/flowme/flowme-channel/list.ts @@ -0,0 +1,144 @@ +import { desc, eq, count, or, like, and } from 'drizzle-orm'; +import { schema, app, db } from '@/app.ts' + +// 获取 flowme-channel 列表 +app.route({ + path: 'flowme-channel', + key: 'list', + middleware: ['auth'], + description: '获取 flowme-channel 列表', +}).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.flowmeChannels.updatedAt : desc(schema.flowmeChannels.updatedAt); + + let whereCondition = eq(schema.flowmeChannels.uid, uid); + if (search) { + whereCondition = and( + eq(schema.flowmeChannels.uid, uid), + or( + like(schema.flowmeChannels.title, `%${search}%`), + like(schema.flowmeChannels.description, `%${search}%`) + ) + ); + } + + const [list, totalCount] = await Promise.all([ + db.select() + .from(schema.flowmeChannels) + .where(whereCondition) + .limit(pageSize) + .offset(offset) + .orderBy(orderByField), + db.select({ count: count() }) + .from(schema.flowmeChannels) + .where(whereCondition) + ]); + + ctx.body = { + list, + pagination: { + page, + current: page, + pageSize, + total: totalCount[0]?.count || 0, + }, + }; + return ctx; +}).addTo(app); + +// 创建或更新 flowme-channel +const channelUpdate = `创建或更新一个 flowme-channel, 参数定义: +title: 标题, 必填 +description: 描述, 选填 +tags: 标签, 数组, 选填 +link: 链接, 选填 +data: 数据, 对象, 选填 +color: 颜色, 选填, 默认 #007bff +`; +app.route({ + path: 'flowme-channel', + key: 'update', + middleware: ['auth'], + description: channelUpdate, +}).define(async (ctx) => { + const { id, uid, updatedAt, createdAt, ...rest } = ctx.query.data || {}; + const tokenUser = ctx.state.tokenUser; + let channel; + if (!id) { + channel = await db.insert(schema.flowmeChannels).values({ + title: rest.title || '', + description: rest.description || '', + tags: rest.tags || [], + link: rest.link || '', + data: rest.data || {}, + color: rest.color || '#007bff', + uid: tokenUser.id, + }).returning(); + } else { + const existing = await db.select().from(schema.flowmeChannels).where(eq(schema.flowmeChannels.id, id)).limit(1); + if (existing.length === 0) { + ctx.throw(404, '没有找到对应的 channel'); + } + if (existing[0].uid !== tokenUser.id) { + ctx.throw(403, '没有权限更新该 channel'); + } + channel = await db.update(schema.flowmeChannels).set({ + title: rest.title, + description: rest.description, + tags: rest.tags, + link: rest.link, + data: rest.data, + color: rest.color, + }).where(eq(schema.flowmeChannels.id, id)).returning(); + } + ctx.body = channel; +}).addTo(app); + +// 删除 flowme-channel +app.route({ + path: 'flowme-channel', + key: 'delete', + middleware: ['auth'], + description: '删除 flowme-channel, 参数: 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.flowmeChannels).where(eq(schema.flowmeChannels.id, id)).limit(1); + if (existing.length === 0) { + ctx.throw(404, '没有找到对应的 channel'); + } + if (existing[0].uid !== tokenUser.id) { + ctx.throw(403, '没有权限删除该 channel'); + } + await db.delete(schema.flowmeChannels).where(eq(schema.flowmeChannels.id, id)); + ctx.body = { success: true }; +}).addTo(app); + +// 获取单个 flowme-channel +app.route({ + path: 'flowme-channel', + key: 'get', + middleware: ['auth'], + description: '获取单个 flowme-channel, 参数: 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.flowmeChannels).where(eq(schema.flowmeChannels.id, id)).limit(1); + if (existing.length === 0) { + ctx.throw(404, '没有找到对应的 channel'); + } + if (existing[0].uid !== tokenUser.id) { + ctx.throw(403, '没有权限查看该 channel'); + } + ctx.body = existing[0]; +}).addTo(app); diff --git a/src/routes/flowme/index.ts b/src/routes/flowme/index.ts new file mode 100644 index 0000000..4550f9f --- /dev/null +++ b/src/routes/flowme/index.ts @@ -0,0 +1,5 @@ +import './list.ts' + +// flowme channel 相关路由 + +import './flowme-channel/list.ts' \ No newline at end of file diff --git a/src/routes/flowme/list.ts b/src/routes/flowme/list.ts new file mode 100644 index 0000000..4ffb491 --- /dev/null +++ b/src/routes/flowme/list.ts @@ -0,0 +1,160 @@ +import { desc, eq, count, or, like, and } from 'drizzle-orm'; +import { schema, app, db } from '@/app.ts' + +// 获取 flowme 列表 +app.route({ + path: 'flowme', + key: 'list', + middleware: ['auth'], + description: '获取 flowme 列表', +}).define(async (ctx) => { + const tokenUser = ctx.state.tokenUser; + const uid = tokenUser.id; + const { page = 1, pageSize = 20, search, channelId, sort = 'DESC' } = ctx.query || {}; + + const offset = (page - 1) * pageSize; + const orderByField = sort === 'ASC' ? schema.flowme.updatedAt : desc(schema.flowme.updatedAt); + + let whereCondition = eq(schema.flowme.uid, uid); + if (search) { + whereCondition = and( + eq(schema.flowme.uid, uid), + or( + like(schema.flowme.title, `%${search}%`), + like(schema.flowme.description, `%${search}%`) + ) + ); + } + if (channelId) { + whereCondition = and( + whereCondition, + eq(schema.flowme.channelId, channelId) + ); + } + + const [list, totalCount] = await Promise.all([ + db.select() + .from(schema.flowme) + .where(whereCondition) + .limit(pageSize) + .offset(offset) + .orderBy(orderByField), + db.select({ count: count() }) + .from(schema.flowme) + .where(whereCondition) + ]); + + ctx.body = { + list, + pagination: { + page, + current: page, + pageSize, + total: totalCount[0]?.count || 0, + }, + }; + return ctx; +}).addTo(app); + +// 创建或更新 flowme +const flowmeUpdate = `创建或更新一个 flowme, 参数定义: +title: 标题, 必填 +description: 描述, 选填 +tags: 标签, 数组, 选填 +link: 链接, 选填 +data: 数据, 对象, 选填 +channelId: 频道ID, 选填 +type: 类型, 选填 +source: 来源, 选填 +importance: 重要性等级, 数字, 选填 +`; +app.route({ + path: 'flowme', + key: 'update', + middleware: ['auth'], + description: flowmeUpdate, +}).define(async (ctx) => { + const { id, uid, updatedAt, createdAt, ...rest } = ctx.query.data || {}; + const tokenUser = ctx.state.tokenUser; + let flowmeItem; + if (!id) { + flowmeItem = await db.insert(schema.flowme).values({ + title: rest.title || '', + description: rest.description || '', + tags: rest.tags || [], + link: rest.link || '', + data: rest.data || {}, + channelId: rest.channelId || null, + type: rest.type || '', + source: rest.source || '', + importance: rest.importance || 0, + uid: tokenUser.id, + }).returning(); + } else { + const existing = await db.select().from(schema.flowme).where(eq(schema.flowme.id, id)).limit(1); + if (existing.length === 0) { + ctx.throw(404, '没有找到对应的 flowme'); + } + if (existing[0].uid !== tokenUser.id) { + ctx.throw(403, '没有权限更新该 flowme'); + } + flowmeItem = await db.update(schema.flowme).set({ + title: rest.title, + description: rest.description, + tags: rest.tags, + link: rest.link, + data: rest.data, + channelId: rest.channelId, + type: rest.type, + source: rest.source, + importance: rest.importance, + isArchived: rest.isArchived, + }).where(eq(schema.flowme.id, id)).returning(); + } + ctx.body = flowmeItem; +}).addTo(app); + +// 删除 flowme +app.route({ + path: 'flowme', + key: 'delete', + middleware: ['auth'], + description: '删除 flowme, 参数: 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.flowme).where(eq(schema.flowme.id, id)).limit(1); + if (existing.length === 0) { + ctx.throw(404, '没有找到对应的 flowme'); + } + if (existing[0].uid !== tokenUser.id) { + ctx.throw(403, '没有权限删除该 flowme'); + } + await db.delete(schema.flowme).where(eq(schema.flowme.id, id)); + ctx.body = { success: true }; +}).addTo(app); + +// 获取单个 flowme +app.route({ + path: 'flowme', + key: 'get', + middleware: ['auth'], + description: '获取单个 flowme, 参数: 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.flowme).where(eq(schema.flowme.id, id)).limit(1); + if (existing.length === 0) { + ctx.throw(404, '没有找到对应的 flowme'); + } + if (existing[0].uid !== tokenUser.id) { + ctx.throw(403, '没有权限查看该 flowme'); + } + ctx.body = existing[0]; +}).addTo(app);