Compare commits
54 Commits
8c6d57d228
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
afa5802ef2 | ||
|
|
dfd4aacf1c | ||
|
|
56a55ac5ae | ||
|
|
8184994b2c | ||
|
|
2000b474a0 | ||
|
|
07efc4e468 | ||
|
|
f305a900f4 | ||
|
|
6a92ee7a2d | ||
|
|
6467e6dea8 | ||
|
|
08884b7e4b | ||
|
|
9be5eb00f5 | ||
|
|
2332f05cef | ||
| 10874917f2 | |||
| 99141a926e | |||
| 61a809ecd7 | |||
| 66a19139b7 | |||
| 027cbecab6 | |||
| 48425c6120 | |||
| 91eaad04d7 | |||
| 38d31a9fb5 | |||
| efb30708eb | |||
| e68b77f871 | |||
| 2b5d3250a4 | |||
| 69422f3e3f | |||
| fb58d91e50 | |||
| 53204291ce | |||
| b31f2840b9 | |||
| 42957af179 | |||
| 2518f6bba3 | |||
| bbdf9f087d | |||
| aaedcb881b | |||
| d2913dd32d | |||
| bb4096ce7e | |||
| 120303961c | |||
| 75ab160509 | |||
| a48cc48589 | |||
| 1ae4c979dc | |||
| 999a75c76b | |||
| 0084f9878b | |||
| 79e07d6689 | |||
| 4b8f47cea8 | |||
| 6b5164e845 | |||
| d50f5ed2af | |||
| 71c238f953 | |||
| 672208ab6b | |||
| 77273bcfeb | |||
| 366a21d621 | |||
| 1782a9ef19 | |||
| 0d73941127 | |||
| 7088d025c9 | |||
| 577b6bfaa4 | |||
| 9cc48821b1 | |||
| 9d39e1cd46 | |||
| 6c611dcf78 |
@@ -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);
|
|
||||||
```
|
|
||||||
2
.npmrc
2
.npmrc
@@ -1,2 +0,0 @@
|
|||||||
@abearxiong:registry=https://npm.pkg.github.com
|
|
||||||
ignore-workspace-root-check=true
|
|
||||||
217
.opencode/skills/create-routes/SKILL.md
Normal file
217
.opencode/skills/create-routes/SKILL.md
Normal file
@@ -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);
|
||||||
|
```
|
||||||
29
.opencode/skills/pnpm-deploy/SKILL.md
Normal file
29
.opencode/skills/pnpm-deploy/SKILL.md
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
---
|
||||||
|
name: pnpm-deploy
|
||||||
|
description: 使用pnpm部署应用到测试或生成环境
|
||||||
|
---
|
||||||
|
|
||||||
|
# pnpm-deploy 部署技能
|
||||||
|
|
||||||
|
部署应用到测试或生成环境。
|
||||||
|
|
||||||
|
## 部署环境
|
||||||
|
|
||||||
|
| 环境 | 命令 |
|
||||||
|
|------|------|
|
||||||
|
| 测试环境 | `pnpm pub:me` |
|
||||||
|
| 生成环境 | `pnpm pub:kevisual` |
|
||||||
|
|
||||||
|
## 使用方法
|
||||||
|
|
||||||
|
在项目目录下执行部署命令:
|
||||||
|
|
||||||
|
### 部署到测试环境
|
||||||
|
```bash
|
||||||
|
pnpm pub:me
|
||||||
|
```
|
||||||
|
|
||||||
|
### 部署到生成环境
|
||||||
|
```bash
|
||||||
|
pnpm pub:kevisual
|
||||||
|
```
|
||||||
66
AGENTS.md
Normal file
66
AGENTS.md
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
# Agents
|
||||||
|
|
||||||
|
## src/aura
|
||||||
|
AI语音识别相关模块,包含ASR功能、配置和库文件
|
||||||
|
|
||||||
|
## src/auth
|
||||||
|
认证授权模块,包含drizzle ORM集成、数据模型和OAuth认证
|
||||||
|
|
||||||
|
## src/db
|
||||||
|
数据库核心模块,包含drizzle配置和数据库schema定义
|
||||||
|
|
||||||
|
## src/models
|
||||||
|
数据模型定义,包含组织和用户相关模型
|
||||||
|
|
||||||
|
## src/modules
|
||||||
|
核心功能模块集合:
|
||||||
|
- auth.ts: 认证模块
|
||||||
|
- config.ts: 配置管理
|
||||||
|
- db.ts: 数据库连接
|
||||||
|
- domain.ts: 域管理
|
||||||
|
- fm-manager/: 文件管理器
|
||||||
|
- html/: HTML处理
|
||||||
|
- jwks/: JWT密钥集
|
||||||
|
- logger.ts: 日志记录
|
||||||
|
- off/: 离线功能
|
||||||
|
- redis.ts: Redis缓存
|
||||||
|
- s3.ts: S3存储
|
||||||
|
- self-restart.ts: 自重启
|
||||||
|
- user-app/: 用户应用
|
||||||
|
- v1-ws-proxy/: V1 WebSocket代理
|
||||||
|
- v3/: V3版本功能
|
||||||
|
|
||||||
|
## src/realtime
|
||||||
|
实时通信模块,包含flowme实时功能
|
||||||
|
|
||||||
|
## src/routes
|
||||||
|
API路由集合,按功能分类:
|
||||||
|
- ai/: AI相关路由
|
||||||
|
- app-manager/: 应用管理
|
||||||
|
- config/: 配置管理
|
||||||
|
- file/: 文件操作
|
||||||
|
- file-listener/: 文件监听
|
||||||
|
- flowme/: Flowme功能
|
||||||
|
- light-code/: 轻量级代码
|
||||||
|
- mark/: 标记功能
|
||||||
|
- micro-app/: 微应用
|
||||||
|
- prompts/: 提示词
|
||||||
|
- query-views/: 查询视图
|
||||||
|
- user/: 用户管理
|
||||||
|
- views/: 视图管理
|
||||||
|
|
||||||
|
## src/routes-simple
|
||||||
|
简化路由模块,包含页面代理和路由器
|
||||||
|
|
||||||
|
## src/scripts
|
||||||
|
实用脚本集合,包含数据库操作、用户管理、S3统计、密钥管理等维护脚本
|
||||||
|
|
||||||
|
## src/test
|
||||||
|
测试相关文件
|
||||||
|
|
||||||
|
## src/utils
|
||||||
|
通用工具函数:
|
||||||
|
- filter.ts: 过滤工具
|
||||||
|
- get-content-type.ts: 内容类型获取
|
||||||
|
- get-engine.ts: 引擎获取
|
||||||
|
- sleep.ts: 延迟工具
|
||||||
@@ -16,7 +16,23 @@ await Bun.build({
|
|||||||
entry: `${naming}.js`,
|
entry: `${naming}.js`,
|
||||||
},
|
},
|
||||||
external,
|
external,
|
||||||
env: 'KEVISUAL_*',
|
// 启用模块转换和优化
|
||||||
|
minify: false,
|
||||||
|
splitting: false,
|
||||||
|
// sourcemap: 'external',
|
||||||
|
// 处理 CommonJS 到 ESM 的转换
|
||||||
|
plugins: [{
|
||||||
|
name: 'transform-requires',
|
||||||
|
setup(build) {
|
||||||
|
// 转换内置模块为 node: 前缀
|
||||||
|
build.onResolve({ filter: /^(path|fs|module|url|util|crypto|stream|buffer|events|http|https|net|os|querystring|zlib|cluster|child_process|worker_threads|perf_hooks|inspector|dgram|dns|tls|readline|repl|process|assert|vm|timers|constants|string_decoder|punycode|v8)$/ }, args => {
|
||||||
|
return {
|
||||||
|
path: `node:${args.path}`,
|
||||||
|
external: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}]
|
||||||
});
|
});
|
||||||
|
|
||||||
// const cmd = `dts -i src/index.ts -o app.d.ts`;
|
// const cmd = `dts -i src/index.ts -o app.d.ts`;
|
||||||
|
|||||||
66
package.json
66
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@kevisual/code-center",
|
"name": "@kevisual/code-center",
|
||||||
"version": "0.0.12",
|
"version": "0.0.13",
|
||||||
"description": "code center",
|
"description": "code center",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
@@ -12,9 +12,7 @@
|
|||||||
"runtime": [
|
"runtime": [
|
||||||
"client"
|
"client"
|
||||||
],
|
],
|
||||||
"pm2Options": {
|
"engine": "bun"
|
||||||
"interpreter": "bun"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "bun run --watch --hot src/index.ts",
|
"dev": "bun run --watch --hot src/index.ts",
|
||||||
@@ -31,76 +29,80 @@
|
|||||||
"pub:kevisual": "npm run build && npm run deploy:kevisual && npm run reload:kevisual",
|
"pub:kevisual": "npm run build && npm run deploy:kevisual && npm run reload:kevisual",
|
||||||
"start": "pm2 start dist/app.js --name code-center",
|
"start": "pm2 start dist/app.js --name code-center",
|
||||||
"client:start": "pm2 start apps/code-center/dist/app.js --name code-center",
|
"client:start": "pm2 start apps/code-center/dist/app.js --name code-center",
|
||||||
"ssl": "ssh -L 5432:localhost:5432 light",
|
"import-data": "bun run scripts/import-data.ts",
|
||||||
"ssl:redis": "ssh -L 6379:localhost:6379 light",
|
|
||||||
"ssl:minio": "ssh -L 9000:localhost:9000 light",
|
|
||||||
"studio": "npx drizzle-kit studio",
|
"studio": "npx drizzle-kit studio",
|
||||||
"drizzle:migrate": "npx drizzle-kit migrate",
|
"drizzle:migrate": "npx drizzle-kit migrate",
|
||||||
"drizzle:push": "npx drizzle-kit push",
|
"drizzle:push": "npx drizzle-kit push",
|
||||||
"pub": "envision pack -p -u -c"
|
"pub": "envision pack -p -u -c"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"types": "types/index.d.ts",
|
|
||||||
"files": [
|
"files": [
|
||||||
"dist"
|
"dist"
|
||||||
],
|
],
|
||||||
"license": "UNLICENSED",
|
"license": "UNLICENSED",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kevisual/ai": "^0.0.24",
|
"@kevisual/ai": "^0.0.28",
|
||||||
"@kevisual/auth": "^2.0.3",
|
"@kevisual/auth": "^2.0.3",
|
||||||
"@kevisual/js-filter": "^0.0.5",
|
"@kevisual/js-filter": "^0.0.6",
|
||||||
"@kevisual/query": "^0.0.40",
|
"@kevisual/query": "^0.0.55",
|
||||||
"@types/busboy": "^1.5.4",
|
"@types/busboy": "^1.5.4",
|
||||||
"@types/send": "^1.2.1",
|
"@types/send": "^1.2.1",
|
||||||
"@types/ws": "^8.18.1",
|
"@types/ws": "^8.18.1",
|
||||||
"bullmq": "^5.67.3",
|
"bullmq": "^5.71.0",
|
||||||
"busboy": "^1.6.0",
|
"busboy": "^1.6.0",
|
||||||
"drizzle-kit": "^0.31.9",
|
"drizzle-kit": "^0.31.10",
|
||||||
"drizzle-orm": "^0.45.1",
|
"drizzle-orm": "^0.45.1",
|
||||||
"drizzle-zod": "^0.8.3",
|
|
||||||
"eventemitter3": "^5.0.4",
|
"eventemitter3": "^5.0.4",
|
||||||
"pg": "^8.18.0",
|
|
||||||
"pm2": "^6.0.14",
|
|
||||||
"send": "^1.2.1",
|
"send": "^1.2.1",
|
||||||
"ws": "npm:@kevisual/ws",
|
"ws": "npm:@kevisual/ws",
|
||||||
"xml2js": "^0.6.2"
|
"xml2js": "^0.6.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@aws-sdk/client-s3": "^3.986.0",
|
"@ai-sdk/openai-compatible": "^2.0.37",
|
||||||
"@kevisual/api": "^0.0.47",
|
"@aws-sdk/client-s3": "^3.1014.0",
|
||||||
"@kevisual/context": "^0.0.4",
|
"@kevisual/api": "^0.0.65",
|
||||||
|
"@kevisual/cnb": "^0.0.59",
|
||||||
|
"@kevisual/context": "^0.0.8",
|
||||||
"@kevisual/local-app-manager": "0.1.32",
|
"@kevisual/local-app-manager": "0.1.32",
|
||||||
"@kevisual/logger": "^0.0.4",
|
"@kevisual/logger": "^0.0.4",
|
||||||
"@kevisual/oss": "0.0.19",
|
"@kevisual/oss": "0.0.20",
|
||||||
"@kevisual/permission": "^0.0.4",
|
"@kevisual/permission": "^0.0.4",
|
||||||
"@kevisual/router": "0.0.70",
|
"@kevisual/router": "0.2.2",
|
||||||
"@kevisual/types": "^0.0.12",
|
"@kevisual/types": "^0.0.12",
|
||||||
"@kevisual/use-config": "^1.0.30",
|
"@kevisual/use-config": "^1.0.30",
|
||||||
"@types/archiver": "^7.0.0",
|
"@types/archiver": "^7.0.0",
|
||||||
"@types/bun": "^1.3.8",
|
"@types/bun": "^1.3.11",
|
||||||
"@types/crypto-js": "^4.2.2",
|
"@types/crypto-js": "^4.2.2",
|
||||||
"@types/jsonwebtoken": "^9.0.10",
|
"@types/jsonwebtoken": "^9.0.10",
|
||||||
"@types/node": "^25.2.3",
|
"@types/node": "^25.5.0",
|
||||||
"@types/pg": "^8.16.0",
|
"@types/pg": "^8.20.0",
|
||||||
"@types/semver": "^7.7.1",
|
"@types/semver": "^7.7.1",
|
||||||
"@types/xml2js": "^0.4.14",
|
"@types/xml2js": "^0.4.14",
|
||||||
|
"ai": "^6.0.134",
|
||||||
"archiver": "^7.0.1",
|
"archiver": "^7.0.1",
|
||||||
|
"convex": "^1.34.0",
|
||||||
"crypto-js": "^4.2.0",
|
"crypto-js": "^4.2.0",
|
||||||
"dayjs": "^1.11.19",
|
"dayjs": "^1.11.20",
|
||||||
"dotenv": "^17.2.4",
|
"dotenv": "^17.3.1",
|
||||||
"es-toolkit": "^1.44.0",
|
"drizzle-zod": "^0.8.3",
|
||||||
"ioredis": "^5.9.2",
|
"es-toolkit": "^1.45.1",
|
||||||
|
"ioredis": "^5.10.1",
|
||||||
"jsonwebtoken": "^9.0.3",
|
"jsonwebtoken": "^9.0.3",
|
||||||
"nanoid": "^5.1.6",
|
"lunar": "^2.0.0",
|
||||||
|
"nanoid": "^5.1.7",
|
||||||
"p-queue": "^9.1.0",
|
"p-queue": "^9.1.0",
|
||||||
"pg": "^8.18.0",
|
"pg": "^8.20.0",
|
||||||
"pm2": "^6.0.14",
|
"pm2": "^6.0.14",
|
||||||
"semver": "^7.7.4",
|
"semver": "^7.7.4",
|
||||||
"zod": "^4.3.6"
|
"zod": "^4.3.6"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"inflight": "latest",
|
"inflight": "latest",
|
||||||
"picomatch": "^4.0.2"
|
"picomatch": "^4.0.2",
|
||||||
|
"ioredis": "^5.10.0"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@10.29.2"
|
"packageManager": "pnpm@10.32.1",
|
||||||
|
"workspaces": [
|
||||||
|
"wxmsg"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
2503
pnpm-lock.yaml
generated
2503
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
78
scripts/import-data.ts
Normal file
78
scripts/import-data.ts
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
/**
|
||||||
|
* 导入 short-link JSON 数据到数据库
|
||||||
|
* 运行: bun run scripts/import-data.ts
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { drizzle } from 'drizzle-orm/node-postgres';
|
||||||
|
import { shortLink } from '@/db/schemas/n-code-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, 'ncode-list.json');
|
||||||
|
const rawData = JSON.parse(readFileSync(jsonPath, 'utf-8')) as Array<{
|
||||||
|
code: string;
|
||||||
|
data: Record<string, any>;
|
||||||
|
description: string;
|
||||||
|
slug: string;
|
||||||
|
tags: string[];
|
||||||
|
title: string;
|
||||||
|
type: string;
|
||||||
|
userId: string;
|
||||||
|
version: string;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
async function importData() {
|
||||||
|
console.log(`准备导入 ${rawData.length} 条 short-link 数据...`);
|
||||||
|
|
||||||
|
let inserted = 0;
|
||||||
|
let skipped = 0;
|
||||||
|
|
||||||
|
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
||||||
|
|
||||||
|
for (const item of rawData) {
|
||||||
|
const userId = item.userId && uuidRegex.test(item.userId) ? item.userId : null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await db
|
||||||
|
.insert(shortLink)
|
||||||
|
.values({
|
||||||
|
slug: item.slug,
|
||||||
|
code: item.code,
|
||||||
|
type: item.type || 'link',
|
||||||
|
version: item.version || '1.0',
|
||||||
|
title: item.title || '',
|
||||||
|
description: item.description || '',
|
||||||
|
tags: item.tags ?? [],
|
||||||
|
data: item.data ?? {},
|
||||||
|
userId: userId as any,
|
||||||
|
})
|
||||||
|
.onConflictDoNothing();
|
||||||
|
|
||||||
|
console.log(` ✓ 导入: slug=${item.slug}, code=${item.code}, title=${item.title}`);
|
||||||
|
inserted++;
|
||||||
|
} catch (err: any) {
|
||||||
|
const cause = err.cause || err;
|
||||||
|
console.warn(` ✗ 跳过: slug=${item.slug}, code=${item.code} — ${cause.message || err.message}`);
|
||||||
|
skipped++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\n完成: 成功 ${inserted} 条,跳过 ${skipped} 条`);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
importData().catch((err) => {
|
||||||
|
console.error('导入失败:', err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
83
scripts/import-life.ts
Normal file
83
scripts/import-life.ts
Normal file
@@ -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<string, any>;
|
||||||
|
description: string;
|
||||||
|
effectiveAt: string;
|
||||||
|
link: string;
|
||||||
|
prompt: string;
|
||||||
|
summary: string;
|
||||||
|
tags: string[];
|
||||||
|
taskResult: Record<string, any>;
|
||||||
|
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);
|
||||||
|
});
|
||||||
104
scripts/ncode-list.json
Normal file
104
scripts/ncode-list.json
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"code": "anwjgg",
|
||||||
|
"data": {
|
||||||
|
"link": "https://kevisual.cn/root/name-card/"
|
||||||
|
},
|
||||||
|
"description": "这是一个测试码",
|
||||||
|
"slug": "pmzsq4gp4g",
|
||||||
|
"tags": [],
|
||||||
|
"title": "测试码",
|
||||||
|
"type": "link",
|
||||||
|
"userId": "0e700dc8-90dd-41b7-91dd-336ea51de3d2",
|
||||||
|
"version": "1.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "0gmn12",
|
||||||
|
"data": {
|
||||||
|
"link": "https://kevisual.cn/root/v1/ha-api?path=ha&key=open-balcony-light",
|
||||||
|
"permission": {
|
||||||
|
"share": "public"
|
||||||
|
},
|
||||||
|
"useOwnerToken": true
|
||||||
|
},
|
||||||
|
"description": "test 阳台灯",
|
||||||
|
"slug": "3z1nbdogew",
|
||||||
|
"tags": [],
|
||||||
|
"title": "阳台灯",
|
||||||
|
"type": "link",
|
||||||
|
"userId": "0e700dc8-90dd-41b7-91dd-336ea51de3d2",
|
||||||
|
"version": "1.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "abc111",
|
||||||
|
"data": {
|
||||||
|
"link": "https://kevisual.cn/root/nfc/",
|
||||||
|
"permission": {
|
||||||
|
"share": "public"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": "nfc link",
|
||||||
|
"slug": "0000000001",
|
||||||
|
"tags": [],
|
||||||
|
"title": "nfc link",
|
||||||
|
"type": "link",
|
||||||
|
"userId": "",
|
||||||
|
"version": "1.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "ej73jm",
|
||||||
|
"data": {
|
||||||
|
"link": "https://kevisual.cn/root/nfc/",
|
||||||
|
"permission": {
|
||||||
|
"share": "public"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": "nfc link",
|
||||||
|
"slug": "001",
|
||||||
|
"tags": [],
|
||||||
|
"title": "nfc link",
|
||||||
|
"type": "link",
|
||||||
|
"userId": "",
|
||||||
|
"version": "1.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "09dd42",
|
||||||
|
"data": {
|
||||||
|
"details": [
|
||||||
|
{
|
||||||
|
"description": "算法基于七卡瓦,自建",
|
||||||
|
"title": "beads(kevisual.cn)",
|
||||||
|
"url": "https://kevisual.cn/root/beads/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "算法很不错,图片转图纸很好",
|
||||||
|
"title": "拼豆七卡瓦(zippland.com)",
|
||||||
|
"url": "https://perlerbeads.zippland.com/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "功能偏向PS,画板类,像素类",
|
||||||
|
"title": "拼豆像素格子(zwpyyds.com)",
|
||||||
|
"url": "https://www.zwpyyds.com/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "编辑不错,拼豆社区",
|
||||||
|
"title": "我嘞个豆(ohmybead.cn)",
|
||||||
|
"url": "https://ohmybead.cn/"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"link": "https://kevisual.cn/root/nfc/way/",
|
||||||
|
"permission": {
|
||||||
|
"share": "public"
|
||||||
|
},
|
||||||
|
"title": "拼豆图纸自取方案",
|
||||||
|
"type": "html-render"
|
||||||
|
},
|
||||||
|
"description": "Pindou",
|
||||||
|
"slug": "pindou",
|
||||||
|
"tags": [],
|
||||||
|
"title": "Pindou",
|
||||||
|
"type": "link",
|
||||||
|
"userId": "",
|
||||||
|
"version": "1.0"
|
||||||
|
}
|
||||||
|
]
|
||||||
25
src/app.ts
25
src/app.ts
@@ -7,6 +7,9 @@ import { BailianProvider } from '@kevisual/ai';
|
|||||||
import * as schema from './db/schema.ts';
|
import * as schema from './db/schema.ts';
|
||||||
import { config } from './modules/config.ts'
|
import { config } from './modules/config.ts'
|
||||||
import { db } from './modules/db.ts'
|
import { db } from './modules/db.ts'
|
||||||
|
import { createOpenAICompatible } from '@ai-sdk/openai-compatible';
|
||||||
|
|
||||||
|
|
||||||
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 {
|
||||||
@@ -42,3 +45,25 @@ export const ai = useContextKey('ai', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
export { schema };
|
export { schema };
|
||||||
|
|
||||||
|
export const bailian = createOpenAICompatible({
|
||||||
|
baseURL: 'https://coding.dashscope.aliyuncs.com/v1',
|
||||||
|
name: 'custom-bailian',
|
||||||
|
apiKey: process.env.BAILIAN_CODE_API_KEY!,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const cnb = createOpenAICompatible({
|
||||||
|
baseURL: 'https://api.cnb.cool/kevisual/kevisual/-/ai/',
|
||||||
|
name: 'custom-cnb',
|
||||||
|
apiKey: process.env.CNB_API_KEY!,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const models = {
|
||||||
|
'doubao-ark-code-latest': 'doubao-ark-code-latest',
|
||||||
|
'GLM-4.7': 'GLM-4.7',
|
||||||
|
'MiniMax-M2.1': 'MiniMax-M2.1',
|
||||||
|
'qwen3-coder-plus': 'qwen3-coder-plus',
|
||||||
|
'hunyuan-a13b': 'hunyuan-a13b',
|
||||||
|
'qwen-plus': 'qwen-plus',
|
||||||
|
'auto': 'auto',
|
||||||
|
}
|
||||||
@@ -1,20 +1,109 @@
|
|||||||
import { app } from '@/app.ts'
|
import { app, oss } from '@/app.ts'
|
||||||
import { asr } from './modules/index.ts'
|
import { asr } from './modules/index.ts'
|
||||||
|
import z from 'zod'
|
||||||
|
import { baseURL } from '@/modules/domain.ts'
|
||||||
|
import { getObjectByPathname } from '@/modules/fm-manager/index.ts'
|
||||||
|
export const createAsr = async (opts: { base64Data: string }) => {
|
||||||
|
const { base64Data } = opts
|
||||||
|
const result = await asr.getText({
|
||||||
|
audio: {
|
||||||
|
data: base64Data,
|
||||||
|
format: 'wav' as any,
|
||||||
|
rate: 16000,
|
||||||
|
channel: 1
|
||||||
|
},
|
||||||
|
request: {
|
||||||
|
enable_words: true,
|
||||||
|
enable_sentence_info: true,
|
||||||
|
enable_utterance_info: true,
|
||||||
|
enable_punctuation_prediction: true,
|
||||||
|
enable_inverse_text_normalization: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
text: result.result?.text || '',
|
||||||
|
result
|
||||||
|
};
|
||||||
|
}
|
||||||
app.route({
|
app.route({
|
||||||
path: 'asr',
|
path: 'asr',
|
||||||
key: 'text',
|
key: 'text',
|
||||||
middleware: ['auth'],
|
middleware: ['auth'],
|
||||||
description: '语音转文字,将base64的音频数据转换为文字, 参数: base64Audio, 为base64编码的音频数据',
|
description: '语音转文字,将base64的音频数据转换为文字, 参数: base64Data 为base64编码的音频数据',
|
||||||
}).define(async (ctx) => {
|
metadata: {
|
||||||
const base64Audio = ctx.query.base64Audio as string
|
args: {
|
||||||
if (!base64Audio) {
|
base64Data: z.string().describe('base64编码的音频数据').nonempty('base64Data参数不能为空'),
|
||||||
ctx.throw('Missing base64Audio parameter')
|
|
||||||
}
|
|
||||||
const result = await asr.getText({
|
|
||||||
audio: {
|
|
||||||
data: base64Audio
|
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
ctx.body = result
|
}).define(async (ctx) => {
|
||||||
|
const base64Data = ctx.query.base64Data as string
|
||||||
|
if (!base64Data) {
|
||||||
|
ctx.throw(400, 'base64Data参数不能为空')
|
||||||
|
}
|
||||||
|
const result = await createAsr({ base64Data })
|
||||||
|
ctx.body = {
|
||||||
|
text: result.text
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.addTo(app)
|
.addTo(app)
|
||||||
|
|
||||||
|
app.route({
|
||||||
|
path: 'asr',
|
||||||
|
key: 'link',
|
||||||
|
middleware: ['auth'],
|
||||||
|
description: '语音转文字,将音频链接的音频数据转换为文字, 参数: url 为音频链接',
|
||||||
|
metadata: {
|
||||||
|
args: {
|
||||||
|
url: z.string().describe('音频链接').nonempty('url参数不能为空'),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).define(async (ctx) => {
|
||||||
|
const tokenUser = ctx.state.tokenUser;
|
||||||
|
const url = ctx.query.url as string
|
||||||
|
if (!url) {
|
||||||
|
ctx.throw(400, 'url参数不能为空')
|
||||||
|
}
|
||||||
|
let base64Data: string = null;
|
||||||
|
if (url.startsWith(baseURL) || url.startsWith('/')) {
|
||||||
|
const pathname = new URL(url, baseURL).pathname;
|
||||||
|
const [username] = pathname.split('/').filter(Boolean)
|
||||||
|
if (username !== tokenUser.username) {
|
||||||
|
ctx.throw(403, '没有权限访问该音频链接')
|
||||||
|
}
|
||||||
|
let data: Awaited<ReturnType<typeof oss.getObject>>;
|
||||||
|
try {
|
||||||
|
console.log('fetch audio from minio with objectName', pathname.slice(1))
|
||||||
|
const objectName = getObjectByPathname({ pathname })
|
||||||
|
data = await oss.getObject(objectName.objectName)
|
||||||
|
} catch (e: any) {
|
||||||
|
if (e?.name === 'NoSuchKey' || e?.$metadata?.httpStatusCode === 404) {
|
||||||
|
ctx.throw(404, '音频文件不存在')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!data.Body) {
|
||||||
|
ctx.throw(404, '音频文件内容为空')
|
||||||
|
}
|
||||||
|
const bytes = await data.Body.transformToByteArray()
|
||||||
|
base64Data = Buffer.from(bytes).toString('base64')
|
||||||
|
} else if (url.startsWith('http')) {
|
||||||
|
base64Data = await fetchAudioAsBase64(url)
|
||||||
|
} else {
|
||||||
|
ctx.throw(400, 'url参数必须是有效的链接')
|
||||||
|
}
|
||||||
|
// 这里需要将音频链接转换为base64数据,可以使用fetch获取音频数据并转换为base64
|
||||||
|
const result = await createAsr({ base64Data })
|
||||||
|
ctx.body = {
|
||||||
|
text: result.text
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.addTo(app)
|
||||||
|
|
||||||
|
const fetchAudioAsBase64 = async (url: string): Promise<string> => {
|
||||||
|
const response = await fetch(url);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to fetch audio from URL: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
const arrayBuffer = await response.arrayBuffer();
|
||||||
|
const base64String = btoa(String.fromCharCode(...new Uint8Array(arrayBuffer)));
|
||||||
|
return base64String;
|
||||||
|
};
|
||||||
@@ -4,4 +4,5 @@ import { auraConfig } from '../../config.ts'
|
|||||||
export const asr = new Asr({
|
export const asr = new Asr({
|
||||||
appid: auraConfig.VOLCENGINE_AUC_APPID,
|
appid: auraConfig.VOLCENGINE_AUC_APPID,
|
||||||
token: auraConfig.VOLCENGINE_AUC_TOKEN,
|
token: auraConfig.VOLCENGINE_AUC_TOKEN,
|
||||||
|
type: 'flash'
|
||||||
})
|
})
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
import { drizzle } from 'drizzle-orm/node-postgres';
|
|
||||||
import { users } from './user.ts';
|
|
||||||
import dotenv from 'dotenv';
|
|
||||||
|
|
||||||
dotenv.config();
|
|
||||||
const db = drizzle(process.env.DATABASE_URL!);
|
|
||||||
|
|
||||||
const one = await db.select().from(users).limit(1);
|
|
||||||
|
|
||||||
console.log(one);
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useContextKey } from '@kevisual/context';
|
import { useContextKey } from '@kevisual/context';
|
||||||
import { Redis } from 'ioredis';
|
import { Redis } from 'ioredis';
|
||||||
import { User } from './user.ts';
|
import { User } from './user.ts';
|
||||||
import { oauth } from '../oauth/auth.ts';
|
import { oauth, jwksManager } from '../oauth/auth.ts';
|
||||||
import { OauthUser } from '../oauth/oauth.ts';
|
import { OauthUser } from '../oauth/oauth.ts';
|
||||||
import { db } from '../../modules/db.ts';
|
import { db } from '../../modules/db.ts';
|
||||||
import { cfUserSecrets, cfUser } from '../../db/drizzle/schema.ts';
|
import { cfUserSecrets, cfUser } from '../../db/drizzle/schema.ts';
|
||||||
@@ -53,6 +53,34 @@ export class UserSecret {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
static async verifyToken(token: string) {
|
static async verifyToken(token: string) {
|
||||||
|
if (oauth.getTokenType(token) === 'jwks') {
|
||||||
|
// 先尝试作为jwt token验证,如果验证成功则直接返回用户信息
|
||||||
|
console.log('[jwksManager] 验证token');
|
||||||
|
try {
|
||||||
|
|
||||||
|
const verified = await jwksManager.verify(token);
|
||||||
|
if (verified) {
|
||||||
|
const sub = verified.sub;
|
||||||
|
const userId = sub.split(':')[1];
|
||||||
|
const user = await User.findByPk(userId);
|
||||||
|
if (!user) {
|
||||||
|
console.warn(`[jwksManager] 验证token成功,但用户不存在,userId: ${userId}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const oauthUser = oauth.getOauthUser({
|
||||||
|
id: user.id,
|
||||||
|
username: user.username,
|
||||||
|
type: user.type,
|
||||||
|
});
|
||||||
|
return oauthUser;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[jwksManager] 验证token失败', e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
if (!oauth.isSecretKey(token)) {
|
if (!oauth.isSecretKey(token)) {
|
||||||
return await oauth.verifyToken(token);
|
return await oauth.verifyToken(token);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ import { nanoid, customAlphabet } from 'nanoid';
|
|||||||
import { CustomError } from '@kevisual/router';
|
import { CustomError } from '@kevisual/router';
|
||||||
import { useContextKey } from '@kevisual/context';
|
import { useContextKey } from '@kevisual/context';
|
||||||
import { Redis } from 'ioredis';
|
import { Redis } from 'ioredis';
|
||||||
import { oauth } from '../oauth/auth.ts';
|
import { oauth, jwksManager } from '../oauth/auth.ts';
|
||||||
import { cryptPwd } from '../oauth/salt.ts';
|
import { cryptPwd } from '../oauth/salt.ts';
|
||||||
import { OauthUser } from '../oauth/oauth.ts';
|
import { OauthUser } from '../oauth/oauth.ts';
|
||||||
import { db } from '../../modules/db.ts';
|
import { db } from '../../modules/db.ts';
|
||||||
import { Org } from './org.ts';
|
import { Org } from './org.ts';
|
||||||
|
import { UserSecret } from './user-secret.ts';
|
||||||
import { cfUser, cfOrgs, cfUserSecrets } from '../../db/drizzle/schema.ts';
|
import { cfUser, cfOrgs, cfUserSecrets } from '../../db/drizzle/schema.ts';
|
||||||
import { eq, sql, InferSelectModel, InferInsertModel } from 'drizzle-orm';
|
import { eq, sql, InferSelectModel, InferInsertModel } from 'drizzle-orm';
|
||||||
|
|
||||||
@@ -17,6 +17,7 @@ export type UserData = {
|
|||||||
wxUnionId?: string;
|
wxUnionId?: string;
|
||||||
phone?: string;
|
phone?: string;
|
||||||
canChangeUsername?: boolean;
|
canChangeUsername?: boolean;
|
||||||
|
cnbId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export enum UserTypes {
|
export enum UserTypes {
|
||||||
@@ -33,8 +34,20 @@ const usersTable = cfUser;
|
|||||||
const orgsTable = cfOrgs;
|
const orgsTable = cfOrgs;
|
||||||
const userSecretsTable = cfUserSecrets;
|
const userSecretsTable = cfUserSecrets;
|
||||||
|
|
||||||
|
// 常量定义
|
||||||
|
const JWKS_TOKEN_EXPIRY = 2 * 3600; // 2 hours in seconds
|
||||||
|
|
||||||
export const redis = useContextKey<Redis>('redis');
|
export const redis = useContextKey<Redis>('redis');
|
||||||
|
|
||||||
|
type TokenOptions = {
|
||||||
|
expire?: number; // 过期时间,单位秒
|
||||||
|
ip?: string; // 用户ID,默认为当前用户ID
|
||||||
|
browser?: string; // 浏览器信息
|
||||||
|
host?: string; // 主机信息
|
||||||
|
wx?: any;
|
||||||
|
loginWith?: string; // 登录方式,如 'cli', 'web', 'plugin' 等
|
||||||
|
hasRefreshToken?: boolean; // 是否需要 refresh token,默认为 false
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* 用户模型,使用 Drizzle ORM
|
* 用户模型,使用 Drizzle ORM
|
||||||
*/
|
*/
|
||||||
@@ -55,8 +68,10 @@ export class User {
|
|||||||
avatar: string;
|
avatar: string;
|
||||||
tokenUser: any;
|
tokenUser: any;
|
||||||
|
|
||||||
constructor(data: UserSelect) {
|
constructor(data?: UserSelect) {
|
||||||
Object.assign(this, data);
|
if (data) {
|
||||||
|
Object.assign(this, data);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setTokenUser(tokenUser: any) {
|
setTokenUser(tokenUser: any) {
|
||||||
@@ -68,8 +83,38 @@ export class User {
|
|||||||
* @param uid
|
* @param uid
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
async createToken(uid?: string, loginType?: 'default' | 'plugin' | 'month' | 'season' | 'year' | 'week', expand: any = {}) {
|
/**
|
||||||
|
* 创建JWKS token的通用方法
|
||||||
|
*/
|
||||||
|
static async createJwksTokenResponse(user: { id: string; username: string }, opts: { expire?: number, hasRefreshToken?: boolean } = {}) {
|
||||||
|
const expiresIn = opts?.expire ?? JWKS_TOKEN_EXPIRY;
|
||||||
|
const hasRefreshToken = opts?.hasRefreshToken ?? true;
|
||||||
|
const accessToken = await jwksManager.sign({
|
||||||
|
sub: 'user:' + user.id,
|
||||||
|
name: user.username,
|
||||||
|
exp: Math.floor(Date.now() / 1000) + expiresIn,
|
||||||
|
});
|
||||||
|
if (hasRefreshToken) {
|
||||||
|
await oauth.setJwksToken(accessToken, { id: user.id, expire: expiresIn });
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = {
|
||||||
|
accessToken,
|
||||||
|
refreshToken: accessToken,
|
||||||
|
token: accessToken,
|
||||||
|
refreshTokenExpiresIn: expiresIn,
|
||||||
|
accessTokenExpiresIn: expiresIn,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'jwks',
|
||||||
|
...token,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async createToken(uid?: string, loginType?: 'default' | 'plugin' | 'month' | 'season' | 'year' | 'week' | 'jwks', opts: TokenOptions = {}) {
|
||||||
const { id, username, type } = this;
|
const { id, username, type } = this;
|
||||||
|
const hasRefreshToken = opts.hasRefreshToken ?? true;
|
||||||
const oauthUser: OauthUser = {
|
const oauthUser: OauthUser = {
|
||||||
id,
|
id,
|
||||||
username,
|
username,
|
||||||
@@ -80,13 +125,13 @@ export class User {
|
|||||||
if (uid) {
|
if (uid) {
|
||||||
oauthUser.orgId = id;
|
oauthUser.orgId = id;
|
||||||
}
|
}
|
||||||
const token = await oauth.generateToken(oauthUser, { type: loginType, hasRefreshToken: true, ...expand });
|
if (loginType === 'jwks') {
|
||||||
|
return await User.createJwksTokenResponse(this, opts);
|
||||||
|
}
|
||||||
|
const token = await oauth.generateToken(oauthUser, { type: loginType, hasRefreshToken, ...opts });
|
||||||
return {
|
return {
|
||||||
accessToken: token.accessToken,
|
type: 'default',
|
||||||
refreshToken: token.refreshToken,
|
...token,
|
||||||
token: token.accessToken,
|
|
||||||
refreshTokenExpiresIn: token.refreshTokenExpiresIn,
|
|
||||||
accessTokenExpiresIn: token.accessTokenExpiresIn,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
@@ -95,20 +140,98 @@ export class User {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
static async verifyToken(token: string) {
|
static async verifyToken(token: string) {
|
||||||
const { UserSecret } = await import('./user-secret.ts');
|
|
||||||
return await UserSecret.verifyToken(token);
|
return await UserSecret.verifyToken(token);
|
||||||
}
|
}
|
||||||
|
static async checkJwksValid(token: string) {
|
||||||
|
const verified = await User.verifyToken(token);
|
||||||
|
let isValid = false;
|
||||||
|
if (verified) {
|
||||||
|
isValid = true;
|
||||||
|
}
|
||||||
|
const jwksToken = await oauth.getJwksToken(token);
|
||||||
|
if (!isValid && !jwksToken) {
|
||||||
|
throw new CustomError('Invalid refresh token');
|
||||||
|
}
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* 刷新token
|
* 刷新token
|
||||||
* @param refreshToken
|
* @param refreshToken
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
static async refreshToken(refreshToken: string) {
|
static async refreshToken(opts: { refreshToken?: string, accessToken?: string }) {
|
||||||
|
const { refreshToken, accessToken } = opts;
|
||||||
|
let jwsRefreshToken = accessToken || refreshToken;
|
||||||
|
if (oauth.getTokenType(jwsRefreshToken) === 'jwks') {
|
||||||
|
// 可能是 jwks token
|
||||||
|
await User.checkJwksValid(jwsRefreshToken);
|
||||||
|
const decoded = await jwksManager.decode(jwsRefreshToken);
|
||||||
|
return await User.createJwksTokenResponse({
|
||||||
|
id: decoded.sub.replace('user:', ''),
|
||||||
|
username: decoded.name
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!refreshToken && !accessToken) {
|
||||||
|
throw new CustomError('Refresh Token or Access Token 必须提供一个');
|
||||||
|
}
|
||||||
|
if (accessToken) {
|
||||||
|
try {
|
||||||
|
const token = await User.refreshTokenByAccessToken(accessToken);
|
||||||
|
return token;
|
||||||
|
} catch (e) {
|
||||||
|
// access token 无效,继续使用 refresh token 刷新
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const token = await User.refreshTokenByRefreshToken(refreshToken);
|
||||||
|
return {
|
||||||
|
type: 'default',
|
||||||
|
...token,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
static async refreshTokenByAccessToken(accessToken: string) {
|
||||||
|
const accessUser = await User.verifyToken(accessToken);
|
||||||
|
if (!accessUser) {
|
||||||
|
throw new CustomError('Invalid access token');
|
||||||
|
}
|
||||||
|
const refreshToken = accessUser.oauthExpand?.refreshToken;
|
||||||
|
if (refreshToken) {
|
||||||
|
return await User.refreshTokenByRefreshToken(refreshToken);
|
||||||
|
} else {
|
||||||
|
await User.oauth.delToken(accessToken);
|
||||||
|
const token = await User.oauth.generateToken(accessUser, {
|
||||||
|
...accessUser.oauthExpand,
|
||||||
|
hasRefreshToken: true,
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
type: 'default',
|
||||||
|
...token,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
static async refreshTokenByRefreshToken(refreshToken: string) {
|
||||||
const token = await oauth.refreshToken(refreshToken);
|
const token = await oauth.refreshToken(refreshToken);
|
||||||
return { accessToken: token.accessToken, refreshToken: token.refreshToken, token: token.accessToken };
|
return {
|
||||||
|
type: 'default',
|
||||||
|
...token
|
||||||
|
};
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 重置token,立即过期token
|
||||||
|
* @param token
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
static async resetToken(refreshToken: string, expand?: Record<string, any>) {
|
||||||
|
if (oauth.getTokenType(refreshToken) === 'jwks') {
|
||||||
|
// 可能是 jwks token
|
||||||
|
await User.checkJwksValid(refreshToken);
|
||||||
|
const decoded = await jwksManager.decode(refreshToken);
|
||||||
|
return await User.createJwksTokenResponse({
|
||||||
|
id: decoded.sub.replace('user:', ''),
|
||||||
|
username: decoded.name
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return await oauth.resetToken(refreshToken, expand);
|
||||||
}
|
}
|
||||||
static async getOauthUser(token: string) {
|
static async getOauthUser(token: string) {
|
||||||
const { UserSecret } = await import('./user-secret.ts');
|
|
||||||
return await UserSecret.verifyToken(token);
|
return await UserSecret.verifyToken(token);
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
@@ -126,7 +249,6 @@ export class User {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
static async getUserByToken(token: string) {
|
static async getUserByToken(token: string) {
|
||||||
const { UserSecret } = await import('./user-secret.ts');
|
|
||||||
const oauthUser = await UserSecret.verifyToken(token);
|
const oauthUser = await UserSecret.verifyToken(token);
|
||||||
if (!oauthUser) {
|
if (!oauthUser) {
|
||||||
throw new CustomError('Token is invalid. get UserByToken');
|
throw new CustomError('Token is invalid. get UserByToken');
|
||||||
@@ -176,6 +298,20 @@ export class User {
|
|||||||
return users.length > 0 ? new User(users[0]) : null;
|
return users.length > 0 ? new User(users[0]) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据 CNB ID 查找用户
|
||||||
|
* @param cnbId
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
static async findByCnbId(cnbId: string): Promise<User | null> {
|
||||||
|
const users = await db
|
||||||
|
.select()
|
||||||
|
.from(usersTable)
|
||||||
|
.where(sql`${usersTable.data}->>'cnbId' = ${cnbId}`)
|
||||||
|
.limit(1);
|
||||||
|
return users.length > 0 ? new User(users[0]) : null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据条件查找一个用户
|
* 根据条件查找一个用户
|
||||||
*/
|
*/
|
||||||
@@ -193,10 +329,9 @@ export class User {
|
|||||||
const users = await query.limit(1);
|
const users = await query.limit(1);
|
||||||
return users.length > 0 ? new User(users[0]) : null;
|
return users.length > 0 ? new User(users[0]) : null;
|
||||||
}
|
}
|
||||||
static findByunionid(){
|
/**
|
||||||
|
* 创建新用户
|
||||||
}
|
*/
|
||||||
|
|
||||||
static async createUser(username: string, password?: string, description?: string) {
|
static async createUser(username: string, password?: string, description?: string) {
|
||||||
const user = await User.findOne({ username });
|
const user = await User.findOne({ username });
|
||||||
if (user) {
|
if (user) {
|
||||||
@@ -345,7 +480,7 @@ export class User {
|
|||||||
if (this.tokenUser && this.tokenUser.uid) {
|
if (this.tokenUser && this.tokenUser.uid) {
|
||||||
id = this.tokenUser.uid;
|
id = this.tokenUser.uid;
|
||||||
} else {
|
} else {
|
||||||
throw new CustomError(400, 'Permission denied');
|
throw new CustomError('Permission denied', { code: 400 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const cache = await redis.get(`user:${id}:orgs`);
|
const cache = await redis.get(`user:${id}:orgs`);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { OAuth, RedisTokenStore } from './oauth.ts';
|
import { OAuth, RedisTokenStore } from './oauth.ts';
|
||||||
import { useContextKey } from '@kevisual/context';
|
import { useContextKey } from '@kevisual/context';
|
||||||
import { Redis } from 'ioredis';
|
import { Redis } from 'ioredis';
|
||||||
|
import { manager } from '../models/jwks-manager.ts';
|
||||||
|
|
||||||
export const oauth = useContextKey('oauth', () => {
|
export const oauth = useContextKey('oauth', () => {
|
||||||
const redis = useContextKey<Redis>('redis');
|
const redis = useContextKey<Redis>('redis');
|
||||||
@@ -16,3 +17,7 @@ export const oauth = useContextKey('oauth', () => {
|
|||||||
const oauth = new OAuth(store);
|
const oauth = new OAuth(store);
|
||||||
return oauth;
|
return oauth;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const jwksManager = useContextKey('jwksManager', () => manager);
|
||||||
|
|
||||||
|
await manager.init()
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
export * from './oauth.ts';
|
export * from './oauth.ts';
|
||||||
export * from './salt.ts';
|
export * from './salt.ts';
|
||||||
|
export * from './auth.ts';
|
||||||
@@ -61,6 +61,8 @@ type StoreSetOpts = {
|
|||||||
loginType?: 'default' | 'plugin' | 'month' | 'season' | 'year' | 'week' | 'day'; // 登陆类型 'default' | 'plugin' | 'month' | 'season' | 'year'
|
loginType?: 'default' | 'plugin' | 'month' | 'season' | 'year' | 'week' | 'day'; // 登陆类型 'default' | 'plugin' | 'month' | 'season' | 'year'
|
||||||
expire?: number; // 过期时间,单位为秒
|
expire?: number; // 过期时间,单位为秒
|
||||||
hasRefreshToken?: boolean;
|
hasRefreshToken?: boolean;
|
||||||
|
// refreshToken的过期时间比accessToken多多少天,默认是1天
|
||||||
|
expireDay?: number;
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
};
|
};
|
||||||
interface Store<T> {
|
interface Store<T> {
|
||||||
@@ -70,11 +72,11 @@ interface Store<T> {
|
|||||||
expire: (key: string, ttl?: number) => Promise<void>;
|
expire: (key: string, ttl?: number) => Promise<void>;
|
||||||
delObject: (value?: T) => Promise<void>;
|
delObject: (value?: T) => Promise<void>;
|
||||||
keys: (key?: string) => Promise<string[]>;
|
keys: (key?: string) => Promise<string[]>;
|
||||||
setToken: (value: { accessToken: string; refreshToken: string; value?: T }, opts?: StoreSetOpts) => Promise<TokenData>;
|
setToken: (value: { accessToken: string; refreshToken: string; value?: T, day?: number }, opts?: StoreSetOpts) => Promise<TokenData>;
|
||||||
delKeys: (keys: string[]) => Promise<number>;
|
delKeys: (keys: string[]) => Promise<number>;
|
||||||
}
|
}
|
||||||
|
|
||||||
type TokenData = {
|
export type TokenData = {
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
accessTokenExpiresIn?: number;
|
accessTokenExpiresIn?: number;
|
||||||
refreshToken?: string;
|
refreshToken?: string;
|
||||||
@@ -138,15 +140,18 @@ export class RedisTokenStore implements Store<OauthUser> {
|
|||||||
await this.del(userPrefix + ':token:' + accessToken);
|
await this.del(userPrefix + ':token:' + accessToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async setToken(data: { accessToken: string; refreshToken: string; value?: OauthUser }, opts?: StoreSetOpts): Promise<TokenData> {
|
async setToken(data: { accessToken: string; refreshToken: string; value?: OauthUser, day?: number }, opts?: StoreSetOpts): Promise<TokenData> {
|
||||||
const { accessToken, refreshToken, value } = data;
|
const { accessToken, refreshToken, value } = data;
|
||||||
let userPrefix = 'user:' + value?.id;
|
let userPrefix = 'user:' + value?.id;
|
||||||
|
const expireDay = data?.day || 1;
|
||||||
|
|
||||||
if (value?.orgId) {
|
if (value?.orgId) {
|
||||||
userPrefix = 'org:' + value?.orgId + ':user:' + value?.id;
|
userPrefix = 'org:' + value?.orgId + ':user:' + value?.id;
|
||||||
}
|
}
|
||||||
// 计算过期时间,根据opts.expire 和 opts.loginType
|
// 计算过期时间,根据opts.expire 和 opts.loginType
|
||||||
// 如果expire存在,则使用expire,否则使用opts.loginType 进行计算;
|
// 如果expire存在,则使用expire,否则使用opts.loginType 进行计算;
|
||||||
let expire = opts?.expire;
|
let expire = opts?.expire;
|
||||||
|
const day = 24 * 60 * 60; // 一天的秒数
|
||||||
if (!expire) {
|
if (!expire) {
|
||||||
switch (opts.loginType) {
|
switch (opts.loginType) {
|
||||||
case 'day':
|
case 'day':
|
||||||
@@ -155,27 +160,18 @@ export class RedisTokenStore implements Store<OauthUser> {
|
|||||||
case 'week':
|
case 'week':
|
||||||
expire = 7 * 24 * 60 * 60;
|
expire = 7 * 24 * 60 * 60;
|
||||||
break;
|
break;
|
||||||
case 'month':
|
|
||||||
expire = 30 * 24 * 60 * 60;
|
|
||||||
break;
|
|
||||||
case 'season':
|
|
||||||
expire = 90 * 24 * 60 * 60;
|
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
expire = 7 * 24 * 60 * 60; // 默认过期时间为7天
|
expire = 7 * 24 * 60 * 60; // 默认过期时间为7天
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
expire = Math.min(expire, 60 * 60 * 24 * 30, 60 * 60 * 24 * 90); // 默认的过期时间最大为90天
|
expire = Math.min(expire, 60 * 60 * 24 * 30); // 默认的过期时间最大为30天
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.set(accessToken, JSON.stringify(value), expire);
|
await this.set(accessToken, JSON.stringify(value), expire);
|
||||||
await this.set(userPrefix + ':token:' + accessToken, accessToken, expire);
|
await this.set(userPrefix + ':token:' + accessToken, accessToken, expire);
|
||||||
let refreshTokenExpiresIn = Math.min(expire * 7, 60 * 60 * 24 * 30, 60 * 60 * 24 * 365); // 最大为一年
|
// refreshToken的过期时间比accessToken多expireDay天,确保在accessToken过期后,refreshToken仍然有效
|
||||||
|
let refreshTokenExpiresIn = expire + expireDay * day;
|
||||||
if (refreshToken) {
|
if (refreshToken) {
|
||||||
// 小于7天, 则设置为7天
|
|
||||||
if (refreshTokenExpiresIn < 60 * 60 * 24 * 7) {
|
|
||||||
refreshTokenExpiresIn = 60 * 60 * 24 * 7;
|
|
||||||
}
|
|
||||||
await this.set(refreshToken, JSON.stringify(value), refreshTokenExpiresIn);
|
await this.set(refreshToken, JSON.stringify(value), refreshTokenExpiresIn);
|
||||||
await this.set(userPrefix + ':refreshToken:' + refreshToken, refreshToken, refreshTokenExpiresIn);
|
await this.set(userPrefix + ':refreshToken:' + refreshToken, refreshToken, refreshTokenExpiresIn);
|
||||||
}
|
}
|
||||||
@@ -237,7 +233,7 @@ export class OAuth<T extends OauthUser> {
|
|||||||
user.oauthExpand.refreshToken = refreshToken;
|
user.oauthExpand.refreshToken = refreshToken;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const tokenData = await this.store.setToken({ accessToken, refreshToken, value: user }, expandOpts);
|
const tokenData = await this.store.setToken({ accessToken, refreshToken, value: user, day: expandOpts?.day }, expandOpts);
|
||||||
|
|
||||||
return tokenData;
|
return tokenData;
|
||||||
}
|
}
|
||||||
@@ -251,7 +247,7 @@ export class OAuth<T extends OauthUser> {
|
|||||||
createTime: new Date().getTime(), // 创建时间
|
createTime: new Date().getTime(), // 创建时间
|
||||||
};
|
};
|
||||||
await this.store.setToken(
|
await this.store.setToken(
|
||||||
{ accessToken: secretKey, refreshToken: '', value: oauthUser },
|
{ accessToken: secretKey, refreshToken: '', value: oauthUser, day: opts?.day },
|
||||||
{
|
{
|
||||||
...opts,
|
...opts,
|
||||||
hasRefreshToken: false,
|
hasRefreshToken: false,
|
||||||
@@ -296,6 +292,28 @@ export class OAuth<T extends OauthUser> {
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* 获取token类型:jwks, secretKey, accessToken, refreshToken
|
||||||
|
* @param token 要检查的token
|
||||||
|
* @returns token类型或null
|
||||||
|
*/
|
||||||
|
getTokenType(token: string) {
|
||||||
|
if (!token) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (token.includes('.')) {
|
||||||
|
return 'jwks';
|
||||||
|
}
|
||||||
|
if (token.startsWith('sk_')) {
|
||||||
|
return 'secretKey';
|
||||||
|
}
|
||||||
|
if (token.startsWith('st_')) {
|
||||||
|
return 'accessToken';
|
||||||
|
}
|
||||||
|
if (token.startsWith('rk_')) {
|
||||||
|
return 'refreshToken';
|
||||||
|
}
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* 刷新token
|
* 刷新token
|
||||||
* @param refreshToken
|
* @param refreshToken
|
||||||
@@ -314,6 +332,7 @@ export class OAuth<T extends OauthUser> {
|
|||||||
{
|
{
|
||||||
...user.oauthExpand,
|
...user.oauthExpand,
|
||||||
hasRefreshToken: true,
|
hasRefreshToken: true,
|
||||||
|
day: user.oauthExpand?.day,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
console.log('resetToken token', await this.store.keys());
|
console.log('resetToken token', await this.store.keys());
|
||||||
@@ -346,6 +365,7 @@ export class OAuth<T extends OauthUser> {
|
|||||||
{
|
{
|
||||||
...user.oauthExpand,
|
...user.oauthExpand,
|
||||||
hasRefreshToken: true,
|
hasRefreshToken: true,
|
||||||
|
day: user.oauthExpand?.day,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -399,4 +419,51 @@ export class OAuth<T extends OauthUser> {
|
|||||||
const tokens = await this.store.keys('*');
|
const tokens = await this.store.keys('*');
|
||||||
await this.store.delKeys(tokens);
|
await this.store.delKeys(tokens);
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* 设置 jwks token, 用于jwt的验证, 过期时间为2小时
|
||||||
|
*/
|
||||||
|
async setJwksToken(token: string, opts: { id: string; expire: number }) {
|
||||||
|
const expire = opts.expire ?? 2 * 3600; // 2 hours
|
||||||
|
const id = opts.id || '-';
|
||||||
|
// jwks token的过期时间比accessToken多2天,确保2天内可以用来refresh token
|
||||||
|
const addExpire = 2 * 24 * 3600;
|
||||||
|
await this.store.redis.set('user:jwks:' + token, id, 'EX', expire + addExpire);
|
||||||
|
}
|
||||||
|
async deleteJwksToken(token: string) {
|
||||||
|
await this.store.redis.expire('user:jwks:' + token, 0);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 获取后就删除jwks token,确保token只能使用一次。
|
||||||
|
* @param token
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
async getJwksToken(token: string) {
|
||||||
|
const id = await this.store.redis.get('user:jwks:' + token);
|
||||||
|
if (id) {
|
||||||
|
this.deleteJwksToken(token);
|
||||||
|
}
|
||||||
|
return id;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 长度为8位的时间的十六进制字符串,单位为秒,使用createHexTime创建,使用parseHexTime解析
|
||||||
|
* @param date
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const createHexTime = (date: Date) => {
|
||||||
|
const timestamp = Math.floor(date.getTime() / 1000);
|
||||||
|
const hex = timestamp.toString(16);
|
||||||
|
return hex;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析createHexTime创建的十六进制时间字符串,返回Date对象
|
||||||
|
* @param hex
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const parseHexTime = (hex: string) => {
|
||||||
|
const timestamp = parseInt(hex, 16);
|
||||||
|
const date = new Date(timestamp * 1000);
|
||||||
|
return date;
|
||||||
|
};
|
||||||
505
src/db/drizzle/0002_loving_lyja.sql
Normal file
505
src/db/drizzle/0002_loving_lyja.sql
Normal file
@@ -0,0 +1,505 @@
|
|||||||
|
CREATE TYPE "public"."enum_cf_router_code_type" AS ENUM('route', 'middleware');--> statement-breakpoint
|
||||||
|
CREATE TABLE "ai_agent" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"type" varchar(255) NOT NULL,
|
||||||
|
"baseUrl" varchar(255) NOT NULL,
|
||||||
|
"apiKey" varchar(255) NOT NULL,
|
||||||
|
"temperature" double precision,
|
||||||
|
"cache" varchar(255),
|
||||||
|
"cacheName" varchar(255),
|
||||||
|
"createdAt" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updatedAt" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"model" varchar(255) NOT NULL,
|
||||||
|
"data" json DEFAULT '{}'::json,
|
||||||
|
"status" varchar(255) DEFAULT 'open',
|
||||||
|
"key" varchar(255) NOT NULL,
|
||||||
|
"description" text,
|
||||||
|
"deletedAt" timestamp with time zone,
|
||||||
|
CONSTRAINT "ai_agent_key_key" UNIQUE("key")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "apps_trades" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"out_trade_no" varchar(255) NOT NULL,
|
||||||
|
"money" integer NOT NULL,
|
||||||
|
"subject" text NOT NULL,
|
||||||
|
"status" varchar(255) DEFAULT 'WAIT_BUYER_PAY' NOT NULL,
|
||||||
|
"type" varchar(255) DEFAULT 'alipay' NOT NULL,
|
||||||
|
"data" jsonb DEFAULT '{"list":[]}'::jsonb,
|
||||||
|
"uid" uuid,
|
||||||
|
"createdAt" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updatedAt" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"deletedAt" timestamp with time zone,
|
||||||
|
CONSTRAINT "apps_trades_out_trade_no_key" UNIQUE("out_trade_no")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "cf_orgs" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"username" varchar(255) NOT NULL,
|
||||||
|
"users" jsonb DEFAULT '[]'::jsonb,
|
||||||
|
"createdAt" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updatedAt" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"deletedAt" timestamp with time zone,
|
||||||
|
"description" varchar(255),
|
||||||
|
CONSTRAINT "cf_orgs_username_key" UNIQUE("username")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "cf_router_code" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"path" varchar(255) NOT NULL,
|
||||||
|
"key" varchar(255) NOT NULL,
|
||||||
|
"active" boolean DEFAULT false,
|
||||||
|
"project" varchar(255) DEFAULT 'default',
|
||||||
|
"code" text DEFAULT '',
|
||||||
|
"type" "enum_cf_router_code_type" DEFAULT 'route',
|
||||||
|
"createdAt" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updatedAt" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"middleware" varchar(255)[] DEFAULT '{"RRAY[]::character varying[])::character varying(25"}',
|
||||||
|
"next" varchar(255) DEFAULT '',
|
||||||
|
"exec" text DEFAULT '',
|
||||||
|
"data" json DEFAULT '{}'::json,
|
||||||
|
"validator" json DEFAULT '{}'::json,
|
||||||
|
"deletedAt" timestamp with time zone
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "cf_user" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"username" varchar(255) NOT NULL,
|
||||||
|
"password" varchar(255),
|
||||||
|
"salt" varchar(255),
|
||||||
|
"needChangePassword" boolean DEFAULT false,
|
||||||
|
"createdAt" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updatedAt" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"description" text,
|
||||||
|
"data" jsonb DEFAULT '{}'::jsonb,
|
||||||
|
"deletedAt" timestamp with time zone,
|
||||||
|
"type" varchar(255) DEFAULT 'user',
|
||||||
|
"owner" uuid,
|
||||||
|
"orgId" uuid,
|
||||||
|
"email" varchar(255),
|
||||||
|
"avatar" text,
|
||||||
|
"nickname" text,
|
||||||
|
CONSTRAINT "cf_user_username_key" UNIQUE("username")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "cf_user_secrets" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"description" text,
|
||||||
|
"status" varchar(255) DEFAULT 'active',
|
||||||
|
"title" text,
|
||||||
|
"expiredTime" timestamp with time zone,
|
||||||
|
"token" varchar(255) DEFAULT '' NOT NULL,
|
||||||
|
"userId" uuid,
|
||||||
|
"data" jsonb DEFAULT '{}'::jsonb,
|
||||||
|
"orgId" uuid,
|
||||||
|
"createdAt" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updatedAt" timestamp with time zone DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "chat_histories" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"data" json,
|
||||||
|
"chatId" uuid,
|
||||||
|
"chatPromptId" uuid,
|
||||||
|
"root" boolean DEFAULT false,
|
||||||
|
"show" boolean DEFAULT true,
|
||||||
|
"uid" varchar(255),
|
||||||
|
"createdAt" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updatedAt" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"role" varchar(255) DEFAULT 'user'
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "chat_prompts" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"title" varchar(255) NOT NULL,
|
||||||
|
"description" text,
|
||||||
|
"data" json,
|
||||||
|
"key" varchar(255) DEFAULT '' NOT NULL,
|
||||||
|
"uid" varchar(255),
|
||||||
|
"createdAt" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updatedAt" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"deletedAt" timestamp with time zone
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "chat_sessions" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"data" json DEFAULT '{}'::json,
|
||||||
|
"chatPromptId" uuid,
|
||||||
|
"type" varchar(255) DEFAULT 'production',
|
||||||
|
"uid" varchar(255),
|
||||||
|
"createdAt" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updatedAt" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"title" varchar(255) DEFAULT '',
|
||||||
|
"key" varchar(255)
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "file_sync" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"name" varchar(255),
|
||||||
|
"hash" varchar(255),
|
||||||
|
"stat" jsonb DEFAULT '{}'::jsonb,
|
||||||
|
"data" jsonb DEFAULT '{}'::jsonb,
|
||||||
|
"checkedAt" timestamp with time zone,
|
||||||
|
"createdAt" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updatedAt" timestamp with time zone DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "kv_ai_chat_history" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"username" varchar(255) DEFAULT '' NOT NULL,
|
||||||
|
"model" varchar(255) DEFAULT '' NOT NULL,
|
||||||
|
"group" varchar(255) DEFAULT '' NOT NULL,
|
||||||
|
"title" varchar(255) DEFAULT '' NOT NULL,
|
||||||
|
"messages" jsonb DEFAULT '[]'::jsonb NOT NULL,
|
||||||
|
"prompt_tokens" integer DEFAULT 0,
|
||||||
|
"total_tokens" integer DEFAULT 0,
|
||||||
|
"completion_tokens" integer DEFAULT 0,
|
||||||
|
"data" jsonb DEFAULT '{}'::jsonb,
|
||||||
|
"uid" uuid,
|
||||||
|
"createdAt" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updatedAt" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"version" integer DEFAULT 0,
|
||||||
|
"type" varchar(255) DEFAULT 'keep' NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "kv_app" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"data" jsonb DEFAULT '{}'::jsonb,
|
||||||
|
"version" varchar(255) DEFAULT '',
|
||||||
|
"key" varchar(255),
|
||||||
|
"uid" uuid,
|
||||||
|
"createdAt" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updatedAt" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"deletedAt" timestamp with time zone,
|
||||||
|
"title" varchar(255) DEFAULT '',
|
||||||
|
"description" varchar(255) DEFAULT '',
|
||||||
|
"user" varchar(255),
|
||||||
|
"status" varchar(255) DEFAULT 'running',
|
||||||
|
"pid" uuid,
|
||||||
|
"proxy" boolean DEFAULT false,
|
||||||
|
CONSTRAINT "key_uid_unique" UNIQUE("key","uid")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "kv_app_domain" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"domain" varchar(255) NOT NULL,
|
||||||
|
"appId" varchar(255),
|
||||||
|
"uid" varchar(255),
|
||||||
|
"createdAt" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updatedAt" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"deletedAt" timestamp with time zone,
|
||||||
|
"data" jsonb,
|
||||||
|
"status" varchar(255) DEFAULT 'running' NOT NULL,
|
||||||
|
CONSTRAINT "kv_app_domain_domain_key" UNIQUE("domain")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "kv_app_list" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"data" json DEFAULT '{}'::json,
|
||||||
|
"version" varchar(255) DEFAULT '',
|
||||||
|
"uid" uuid,
|
||||||
|
"createdAt" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updatedAt" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"deletedAt" timestamp with time zone,
|
||||||
|
"key" varchar(255),
|
||||||
|
"status" varchar(255) DEFAULT 'running'
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "kv_config" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"title" text DEFAULT '',
|
||||||
|
"key" text DEFAULT '',
|
||||||
|
"description" text DEFAULT '',
|
||||||
|
"tags" jsonb DEFAULT '[]'::jsonb,
|
||||||
|
"data" jsonb DEFAULT '{}'::jsonb,
|
||||||
|
"uid" uuid,
|
||||||
|
"createdAt" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updatedAt" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"deletedAt" timestamp with time zone,
|
||||||
|
"hash" text DEFAULT ''
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "kv_light_code" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"title" text DEFAULT '',
|
||||||
|
"description" text DEFAULT '',
|
||||||
|
"type" text DEFAULT 'render-js',
|
||||||
|
"code" text DEFAULT '',
|
||||||
|
"data" jsonb DEFAULT '{}'::jsonb,
|
||||||
|
"createdAt" timestamp DEFAULT now() NOT NULL,
|
||||||
|
"updatedAt" timestamp DEFAULT now() NOT NULL,
|
||||||
|
"uid" uuid,
|
||||||
|
"tags" jsonb DEFAULT '[]'::jsonb,
|
||||||
|
"hash" text DEFAULT ''
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "kv_github" (
|
||||||
|
"id" uuid PRIMARY KEY NOT NULL,
|
||||||
|
"title" varchar(255) DEFAULT '',
|
||||||
|
"githubToken" varchar(255) DEFAULT '',
|
||||||
|
"uid" uuid,
|
||||||
|
"createdAt" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updatedAt" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"deletedAt" timestamp with time zone
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "kv_packages" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"title" text DEFAULT '',
|
||||||
|
"description" text DEFAULT '',
|
||||||
|
"tags" jsonb DEFAULT '[]'::jsonb,
|
||||||
|
"data" jsonb DEFAULT '{}'::jsonb,
|
||||||
|
"publish" jsonb DEFAULT '{}'::jsonb,
|
||||||
|
"expand" jsonb DEFAULT '{}'::jsonb,
|
||||||
|
"uid" uuid,
|
||||||
|
"createdAt" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updatedAt" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"deletedAt" timestamp with time zone
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "kv_page" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"title" varchar(255) DEFAULT '',
|
||||||
|
"description" text DEFAULT '',
|
||||||
|
"type" varchar(255) DEFAULT '',
|
||||||
|
"data" json DEFAULT '{}'::json,
|
||||||
|
"uid" uuid,
|
||||||
|
"createdAt" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updatedAt" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"deletedAt" timestamp with time zone,
|
||||||
|
"publish" json DEFAULT '{}'::json
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "kv_resource" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"name" varchar(255) DEFAULT '',
|
||||||
|
"description" text DEFAULT '',
|
||||||
|
"source" varchar(255) DEFAULT '',
|
||||||
|
"sourceId" varchar(255) DEFAULT '',
|
||||||
|
"version" varchar(255) DEFAULT '0.0.0',
|
||||||
|
"data" json DEFAULT '{}'::json,
|
||||||
|
"uid" uuid,
|
||||||
|
"createdAt" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updatedAt" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"deletedAt" timestamp with time zone
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "kv_vip" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"userId" uuid NOT NULL,
|
||||||
|
"level" varchar(255) DEFAULT 'free',
|
||||||
|
"category" varchar(255) NOT NULL,
|
||||||
|
"startDate" timestamp with time zone,
|
||||||
|
"endDate" timestamp with time zone,
|
||||||
|
"data" jsonb DEFAULT '{}'::jsonb,
|
||||||
|
"createdAt" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updatedAt" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"deletedAt" timestamp with time zone,
|
||||||
|
"title" text DEFAULT '' NOT NULL,
|
||||||
|
"description" text DEFAULT '' NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "micro_apps_upload" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"title" varchar(255) DEFAULT '',
|
||||||
|
"description" varchar(255) DEFAULT '',
|
||||||
|
"tags" jsonb DEFAULT '[]'::jsonb,
|
||||||
|
"type" varchar(255) DEFAULT '',
|
||||||
|
"source" varchar(255) DEFAULT '',
|
||||||
|
"data" jsonb DEFAULT '{}'::jsonb,
|
||||||
|
"share" boolean DEFAULT false,
|
||||||
|
"uname" varchar(255) DEFAULT '',
|
||||||
|
"uid" uuid,
|
||||||
|
"createdAt" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updatedAt" timestamp with time zone DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "micro_mark" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"title" text DEFAULT '',
|
||||||
|
"description" text DEFAULT '',
|
||||||
|
"tags" jsonb DEFAULT '[]'::jsonb,
|
||||||
|
"data" jsonb DEFAULT '{}'::jsonb,
|
||||||
|
"uname" varchar(255) DEFAULT '',
|
||||||
|
"uid" uuid,
|
||||||
|
"createdAt" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updatedAt" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"cover" text DEFAULT '',
|
||||||
|
"thumbnail" text DEFAULT '',
|
||||||
|
"link" text DEFAULT '',
|
||||||
|
"summary" text DEFAULT '',
|
||||||
|
"markType" text DEFAULT 'md',
|
||||||
|
"config" jsonb DEFAULT '{}'::jsonb,
|
||||||
|
"puid" uuid,
|
||||||
|
"deletedAt" timestamp with time zone,
|
||||||
|
"version" integer DEFAULT 1,
|
||||||
|
"fileList" jsonb DEFAULT '[]'::jsonb,
|
||||||
|
"key" text DEFAULT ''
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "query_views" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"uid" uuid,
|
||||||
|
"title" text DEFAULT '',
|
||||||
|
"summary" text DEFAULT '',
|
||||||
|
"description" text DEFAULT '',
|
||||||
|
"tags" jsonb DEFAULT '[]'::jsonb,
|
||||||
|
"link" text DEFAULT '',
|
||||||
|
"data" jsonb DEFAULT '{}'::jsonb,
|
||||||
|
"createdAt" timestamp DEFAULT now() NOT NULL,
|
||||||
|
"updatedAt" timestamp DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "router_views" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"uid" uuid,
|
||||||
|
"title" text DEFAULT '',
|
||||||
|
"summary" text DEFAULT '',
|
||||||
|
"description" text DEFAULT '',
|
||||||
|
"tags" jsonb DEFAULT '[]'::jsonb,
|
||||||
|
"link" text DEFAULT '',
|
||||||
|
"data" jsonb DEFAULT '{}'::jsonb,
|
||||||
|
"views" jsonb DEFAULT '[]'::jsonb,
|
||||||
|
"createdAt" timestamp DEFAULT now() NOT NULL,
|
||||||
|
"updatedAt" timestamp DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "work_share_mark" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"title" text DEFAULT '',
|
||||||
|
"key" text DEFAULT '',
|
||||||
|
"markType" text DEFAULT 'md',
|
||||||
|
"description" text DEFAULT '',
|
||||||
|
"cover" text DEFAULT '',
|
||||||
|
"link" text DEFAULT '',
|
||||||
|
"tags" jsonb DEFAULT '[]'::jsonb,
|
||||||
|
"summary" text DEFAULT '',
|
||||||
|
"config" jsonb DEFAULT '{}'::jsonb,
|
||||||
|
"data" jsonb DEFAULT '{}'::jsonb,
|
||||||
|
"fileList" jsonb DEFAULT '[]'::jsonb,
|
||||||
|
"uname" varchar(255) DEFAULT '',
|
||||||
|
"version" integer DEFAULT 1,
|
||||||
|
"markedAt" timestamp with time zone,
|
||||||
|
"uid" uuid,
|
||||||
|
"puid" uuid,
|
||||||
|
"createdAt" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updatedAt" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"deletedAt" timestamp with time zone
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "n_code_make" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"slug" text NOT NULL,
|
||||||
|
"resources" jsonb DEFAULT '[]'::jsonb,
|
||||||
|
"userId" uuid,
|
||||||
|
"createdAt" timestamp DEFAULT now() NOT NULL,
|
||||||
|
"updatedAt" timestamp DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "n_code_shop" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"slug" text NOT NULL,
|
||||||
|
"title" text NOT NULL,
|
||||||
|
"tags" jsonb,
|
||||||
|
"link" text,
|
||||||
|
"description" text DEFAULT '' NOT NULL,
|
||||||
|
"data" jsonb,
|
||||||
|
"platform" text NOT NULL,
|
||||||
|
"userinfo" text,
|
||||||
|
"orderLink" text NOT NULL,
|
||||||
|
"userId" uuid,
|
||||||
|
"createdAt" timestamp DEFAULT now() NOT NULL,
|
||||||
|
"updatedAt" timestamp DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "n_code_short_link" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"slug" text NOT NULL,
|
||||||
|
"code" text DEFAULT '' NOT NULL,
|
||||||
|
"type" text DEFAULT 'link' NOT NULL,
|
||||||
|
"version" text DEFAULT '1.0.0' NOT NULL,
|
||||||
|
"title" text DEFAULT '' NOT NULL,
|
||||||
|
"description" text DEFAULT '' NOT NULL,
|
||||||
|
"tags" jsonb DEFAULT '[]'::jsonb,
|
||||||
|
"data" jsonb DEFAULT '{}'::jsonb,
|
||||||
|
"userId" uuid,
|
||||||
|
"createdAt" timestamp DEFAULT now() NOT NULL,
|
||||||
|
"updatedAt" timestamp DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "flowme" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"uid" uuid,
|
||||||
|
"title" text DEFAULT '',
|
||||||
|
"tags" jsonb DEFAULT '[]'::jsonb,
|
||||||
|
"summary" text DEFAULT '',
|
||||||
|
"description" text DEFAULT '',
|
||||||
|
"link" text DEFAULT '',
|
||||||
|
"data" jsonb DEFAULT '{}'::jsonb,
|
||||||
|
"channelId" uuid,
|
||||||
|
"type" text DEFAULT '',
|
||||||
|
"source" text DEFAULT '',
|
||||||
|
"importance" integer DEFAULT 0,
|
||||||
|
"isArchived" boolean DEFAULT false,
|
||||||
|
"createdAt" timestamp DEFAULT now() NOT NULL,
|
||||||
|
"updatedAt" timestamp DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "flowme_channels" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"uid" uuid,
|
||||||
|
"title" text DEFAULT '',
|
||||||
|
"tags" jsonb DEFAULT '[]'::jsonb,
|
||||||
|
"summary" text DEFAULT '',
|
||||||
|
"description" text DEFAULT '',
|
||||||
|
"link" text DEFAULT '',
|
||||||
|
"data" jsonb DEFAULT '{}'::jsonb,
|
||||||
|
"key" text DEFAULT '',
|
||||||
|
"color" text DEFAULT '#007bff',
|
||||||
|
"createdAt" timestamp DEFAULT now() NOT NULL,
|
||||||
|
"updatedAt" timestamp DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "flowme_life" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"uid" uuid,
|
||||||
|
"title" text DEFAULT '',
|
||||||
|
"tags" jsonb DEFAULT '[]'::jsonb,
|
||||||
|
"summary" text DEFAULT '',
|
||||||
|
"description" text DEFAULT '',
|
||||||
|
"link" text DEFAULT '',
|
||||||
|
"data" jsonb DEFAULT '{}'::jsonb,
|
||||||
|
"effectiveAt" timestamp with time zone,
|
||||||
|
"type" text DEFAULT '',
|
||||||
|
"prompt" text DEFAULT '',
|
||||||
|
"taskType" text DEFAULT '',
|
||||||
|
"taskResult" jsonb DEFAULT '{}'::jsonb,
|
||||||
|
"createdAt" timestamp DEFAULT now() NOT NULL,
|
||||||
|
"updatedAt" timestamp DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "cf_prompts" ALTER COLUMN "parents" SET DATA TYPE text[];--> statement-breakpoint
|
||||||
|
ALTER TABLE "cf_prompts" ALTER COLUMN "parents" SET DEFAULT '{}';--> statement-breakpoint
|
||||||
|
ALTER TABLE "flowme" ADD CONSTRAINT "flowme_channelId_flowme_channels_id_fk" FOREIGN KEY ("channelId") REFERENCES "public"."flowme_channels"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
|
||||||
|
CREATE INDEX "file_sync_name_idx" ON "file_sync" USING btree ("name");--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX "kv_app_key_uid" ON "kv_app" USING btree ("key","uid");--> statement-breakpoint
|
||||||
|
CREATE INDEX "query_views_uid_idx" ON "query_views" USING btree ("uid");--> statement-breakpoint
|
||||||
|
CREATE INDEX "query_title_idx" ON "query_views" USING btree ("title");--> statement-breakpoint
|
||||||
|
CREATE INDEX "router_views_uid_idx" ON "router_views" USING btree ("uid");--> statement-breakpoint
|
||||||
|
CREATE INDEX "router_title_idx" ON "router_views" USING btree ("title");--> statement-breakpoint
|
||||||
|
CREATE INDEX "router_views_views_idx" ON "router_views" USING gin ("views");--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX "n_code_make_idx_slug" ON "n_code_make" USING btree ("slug");--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX "n_code_shop_idx_slug" ON "n_code_shop" USING btree ("slug");--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX "n_code_short_idx_slug" ON "n_code_short_link" USING btree ("slug");--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX "n_code_short_idx_code" ON "n_code_short_link" USING btree ("code");--> statement-breakpoint
|
||||||
|
CREATE INDEX "flowme_uid_idx" ON "flowme" USING btree ("uid");--> statement-breakpoint
|
||||||
|
CREATE INDEX "flowme_title_idx" ON "flowme" USING btree ("title");--> statement-breakpoint
|
||||||
|
CREATE INDEX "flowme_channel_id_idx" ON "flowme" USING btree ("channelId");--> statement-breakpoint
|
||||||
|
CREATE INDEX "flowme_channels_uid_idx" ON "flowme_channels" USING btree ("uid");--> statement-breakpoint
|
||||||
|
CREATE INDEX "flowme_channels_key_idx" ON "flowme_channels" USING btree ("key");--> statement-breakpoint
|
||||||
|
CREATE INDEX "flowme_channels_title_idx" ON "flowme_channels" USING btree ("title");--> statement-breakpoint
|
||||||
|
CREATE INDEX "life_uid_idx" ON "flowme_life" USING btree ("uid");--> statement-breakpoint
|
||||||
|
CREATE INDEX "life_title_idx" ON "flowme_life" USING btree ("title");--> statement-breakpoint
|
||||||
|
CREATE INDEX "life_effective_at_idx" ON "flowme_life" USING btree ("effectiveAt");--> statement-breakpoint
|
||||||
|
CREATE INDEX "life_summary_idx" ON "flowme_life" USING btree ("summary");--> statement-breakpoint
|
||||||
|
CREATE INDEX "prompts_parents_idx" ON "cf_prompts" USING gin ("parents");
|
||||||
3479
src/db/drizzle/meta/0002_snapshot.json
Normal file
3479
src/db/drizzle/meta/0002_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -15,6 +15,13 @@
|
|||||||
"when": 1767070768620,
|
"when": 1767070768620,
|
||||||
"tag": "0001_solid_nocturne",
|
"tag": "0001_solid_nocturne",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 2,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1773148571509,
|
||||||
|
"tag": "0002_loving_lyja",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -330,8 +330,11 @@ export const microAppsUpload = pgTable("micro_apps_upload", {
|
|||||||
export const microMark = pgTable("micro_mark", {
|
export const microMark = pgTable("micro_mark", {
|
||||||
id: uuid().primaryKey().defaultRandom(),
|
id: uuid().primaryKey().defaultRandom(),
|
||||||
title: text().default(''),
|
title: text().default(''),
|
||||||
description: text().default(''),
|
|
||||||
tags: jsonb().default([]),
|
tags: jsonb().default([]),
|
||||||
|
link: text().default(''),
|
||||||
|
summary: text().default(''),
|
||||||
|
description: text().default(''),
|
||||||
|
|
||||||
data: jsonb().default({}),
|
data: jsonb().default({}),
|
||||||
uname: varchar({ length: 255 }).default(''),
|
uname: varchar({ length: 255 }).default(''),
|
||||||
uid: uuid(),
|
uid: uuid(),
|
||||||
@@ -339,8 +342,7 @@ export const microMark = pgTable("micro_mark", {
|
|||||||
updatedAt: timestamp({ withTimezone: true, mode: 'string' }).notNull().defaultNow(),
|
updatedAt: timestamp({ withTimezone: true, mode: 'string' }).notNull().defaultNow(),
|
||||||
cover: text().default(''),
|
cover: text().default(''),
|
||||||
thumbnail: text().default(''),
|
thumbnail: text().default(''),
|
||||||
link: text().default(''),
|
|
||||||
summary: text().default(''),
|
|
||||||
markType: text().default('md'),
|
markType: text().default('md'),
|
||||||
config: jsonb().default({}),
|
config: jsonb().default({}),
|
||||||
puid: uuid(),
|
puid: uuid(),
|
||||||
@@ -457,7 +459,7 @@ export const routerViews = pgTable("router_views", {
|
|||||||
|
|
||||||
views: jsonb().default([]).$type<Array<RouterViewQuery>>(),
|
views: jsonb().default([]).$type<Array<RouterViewQuery>>(),
|
||||||
createdAt: timestamp('createdAt').notNull().defaultNow(),
|
createdAt: timestamp('createdAt').notNull().defaultNow(),
|
||||||
updatedAt: timestamp('updatedAt').notNull().defaultNow(),
|
updatedAt: timestamp('updatedAt').notNull().defaultNow().$onUpdate(() => new Date()),
|
||||||
}, (table) => [
|
}, (table) => [
|
||||||
index('router_views_uid_idx').using('btree', table.uid.asc().nullsLast()),
|
index('router_views_uid_idx').using('btree', table.uid.asc().nullsLast()),
|
||||||
index('router_title_idx').using('btree', table.title.asc().nullsLast()),
|
index('router_title_idx').using('btree', table.title.asc().nullsLast()),
|
||||||
@@ -477,51 +479,9 @@ export const queryViews = pgTable("query_views", {
|
|||||||
data: jsonb().default({}),
|
data: jsonb().default({}),
|
||||||
|
|
||||||
createdAt: timestamp('createdAt').notNull().defaultNow(),
|
createdAt: timestamp('createdAt').notNull().defaultNow(),
|
||||||
updatedAt: timestamp('updatedAt').notNull().defaultNow(),
|
updatedAt: timestamp('updatedAt').notNull().defaultNow().$onUpdate(() => new Date()),
|
||||||
}, (table) => [
|
}, (table) => [
|
||||||
index('query_views_uid_idx').using('btree', table.uid.asc().nullsLast()),
|
index('query_views_uid_idx').using('btree', table.uid.asc().nullsLast()),
|
||||||
index('query_title_idx').using('btree', table.title.asc().nullsLast()),
|
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(''),
|
|
||||||
description: text('description').default(''),
|
|
||||||
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(''),
|
|
||||||
key: text('key').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_key_idx').using('btree', table.key.asc().nullsLast()),
|
|
||||||
index('flowme_channels_title_idx').using('btree', table.title.asc().nullsLast()),
|
|
||||||
]);
|
|
||||||
@@ -1,3 +1,7 @@
|
|||||||
import { pgTable, uuid, jsonb, timestamp, text } from "drizzle-orm/pg-core";
|
import { pgTable, uuid, jsonb, timestamp, text } from "drizzle-orm/pg-core";
|
||||||
import { InferSelectModel, InferInsertModel, desc } from "drizzle-orm";
|
import { InferSelectModel, InferInsertModel, desc } from "drizzle-orm";
|
||||||
export * from './drizzle/schema.ts';
|
export * from './drizzle/schema.ts';
|
||||||
|
|
||||||
|
export * from './schemas/n-code-schema.ts'
|
||||||
|
|
||||||
|
export * from './schemas/life-schema.ts'
|
||||||
81
src/db/schemas/life-schema.ts
Normal file
81
src/db/schemas/life-schema.ts
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
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: timestamp('effectiveAt', { withTimezone: true }),
|
||||||
|
/**
|
||||||
|
* 智能,
|
||||||
|
* 每年农历
|
||||||
|
* 备忘
|
||||||
|
* 归档
|
||||||
|
*/
|
||||||
|
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(''), // muse,
|
||||||
|
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()),
|
||||||
|
]);
|
||||||
57
src/db/schemas/n-code-schema.ts
Normal file
57
src/db/schemas/n-code-schema.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import { pgTable, serial, text, jsonb, varchar, timestamp, unique, uuid, doublePrecision, json, integer, boolean, index, uniqueIndex, pgEnum } from "drizzle-orm/pg-core"
|
||||||
|
import { desc, sql, sum } from "drizzle-orm"
|
||||||
|
|
||||||
|
export const shortLink = pgTable("n_code_short_link", {
|
||||||
|
id: uuid().primaryKey().defaultRandom(),
|
||||||
|
// 对外暴露的唯一业务 ID,nanoid 生成
|
||||||
|
slug: text("slug").notNull(),
|
||||||
|
// 协作码,管理员才能编辑, 6-12 位随机字符串,唯一
|
||||||
|
code: text("code").notNull().default(''),
|
||||||
|
// 码的类型,link, agent,默认值为 link
|
||||||
|
type: text("type").notNull().default("link"),
|
||||||
|
version: text("version").notNull().default('1.0.0'),
|
||||||
|
|
||||||
|
title: text("title").notNull().default(''),
|
||||||
|
description: text("description").notNull().default(''),
|
||||||
|
tags: jsonb().default([]),
|
||||||
|
data: jsonb().default({}),
|
||||||
|
|
||||||
|
userId: uuid(),
|
||||||
|
createdAt: timestamp('createdAt').notNull().defaultNow(),
|
||||||
|
updatedAt: timestamp('updatedAt').notNull().defaultNow().$onUpdate(() => new Date()),
|
||||||
|
|
||||||
|
}, (table) => [
|
||||||
|
uniqueIndex("n_code_short_idx_slug").on(table.slug),
|
||||||
|
uniqueIndex("n_code_short_idx_code").on(table.code)
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const n5Make = pgTable("n_code_make", {
|
||||||
|
id: uuid().primaryKey().defaultRandom(),
|
||||||
|
slug: text("slug").notNull(),
|
||||||
|
resources: jsonb().default([]),
|
||||||
|
|
||||||
|
userId: uuid(),
|
||||||
|
createdAt: timestamp('createdAt').notNull().defaultNow(),
|
||||||
|
updatedAt: timestamp('updatedAt').notNull().defaultNow().$onUpdate(() => new Date()),
|
||||||
|
}, (table) => [
|
||||||
|
uniqueIndex("n_code_make_idx_slug").on(table.slug),
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const n5Shop = pgTable("n_code_shop", {
|
||||||
|
id: uuid().primaryKey().defaultRandom(),
|
||||||
|
slug: text("slug").notNull(),
|
||||||
|
title: text("title").notNull(), // 商品标题
|
||||||
|
tags: jsonb(), // 商品标签
|
||||||
|
link: text("link"), // 商品链接
|
||||||
|
description: text("description").notNull().default(''), // 商品描述
|
||||||
|
data: jsonb(), // 其他商品发货信息等
|
||||||
|
platform: text("platform").notNull(), // 商品平台,如 "淘宝", "抖音", "小红书" 等
|
||||||
|
userinfo: text("userinfo"), // 卖家信息,如 "店铺名称", "联系方式" 等
|
||||||
|
orderLink: text("orderLink").notNull(), // 订单链接
|
||||||
|
|
||||||
|
userId: uuid(),
|
||||||
|
createdAt: timestamp('createdAt').notNull().defaultNow(),
|
||||||
|
updatedAt: timestamp('updatedAt').notNull().defaultNow().$onUpdate(() => new Date()),
|
||||||
|
}, (table) => [
|
||||||
|
uniqueIndex("n_code_shop_idx_slug").on(table.slug),
|
||||||
|
]);
|
||||||
@@ -2,7 +2,7 @@ import { app } from './app.ts';
|
|||||||
import './route.ts';
|
import './route.ts';
|
||||||
import { handleRequest } from './routes-simple/index.ts';
|
import { handleRequest } from './routes-simple/index.ts';
|
||||||
import { port } from './modules/config.ts';
|
import { port } from './modules/config.ts';
|
||||||
import { wssFun } from './modules/ws-proxy/index.ts';
|
import { wssFun } from './modules/v1-ws-proxy/index.ts';
|
||||||
import { WebSocketListenerFun, HttpListenerFun } from '@kevisual/router/src/server/server-type.js';
|
import { WebSocketListenerFun, HttpListenerFun } from '@kevisual/router/src/server/server-type.js';
|
||||||
console.log('Starting server...', port);
|
console.log('Starting server...', port);
|
||||||
app.listen(port, '0.0.0.0', () => {
|
app.listen(port, '0.0.0.0', () => {
|
||||||
|
|||||||
65
src/modules/ai/agent-run.ts
Normal file
65
src/modules/ai/agent-run.ts
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import { type QueryRouterServer, type App, type RouteInfo } from '@kevisual/router'
|
||||||
|
import { generateText, tool, type ModelMessage, type LanguageModel, type GenerateTextResult } from 'ai';
|
||||||
|
import z from 'zod';
|
||||||
|
import { filter } from '@kevisual/js-filter'
|
||||||
|
export const createTool = async (app: QueryRouterServer | App, message: { path: string, key: string, token?: string }) => {
|
||||||
|
const route = app.findRoute({ path: message.path, key: message.key });
|
||||||
|
if (!route) {
|
||||||
|
console.error(`未找到路径 ${message.path} 和 key ${message.key} 的路由`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const _tool = tool({
|
||||||
|
description: route?.metadata?.summary || route?.description || '无描述',
|
||||||
|
inputSchema: z.object({
|
||||||
|
...route.metadata?.args
|
||||||
|
}), // 这里可以根据实际需要定义输入参数的 schema
|
||||||
|
execute: async (args: any) => {
|
||||||
|
const res = await app.run({ path: message.path, key: message.key, payload: args, token: message.token });
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return _tool;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createTools = async (opts: { app: QueryRouterServer | App, token?: string }) => {
|
||||||
|
const { app, token } = opts;
|
||||||
|
const tools: Record<string, any> = {};
|
||||||
|
for (const route of app.routes) {
|
||||||
|
const id = route.id!;
|
||||||
|
const _tool = await createTool(app, { path: route.path!, key: route.key!, token });
|
||||||
|
if (_tool && id) {
|
||||||
|
tools[id] = _tool;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tools;
|
||||||
|
}
|
||||||
|
type Route = Partial<RouteInfo>
|
||||||
|
type AgentResult = {
|
||||||
|
result: GenerateTextResult<Record<string, any>, any>,
|
||||||
|
messages: ModelMessage[],
|
||||||
|
}
|
||||||
|
export const reCallAgent = async (opts: { messages?: ModelMessage[], tools?: Record<string, any>, languageModel: LanguageModel }): Promise<AgentResult> => {
|
||||||
|
const { messages = [], tools = {}, languageModel } = opts;
|
||||||
|
const result = await generateText({
|
||||||
|
model: languageModel,
|
||||||
|
messages,
|
||||||
|
tools,
|
||||||
|
});
|
||||||
|
const step = result.steps[0]!;
|
||||||
|
if (step.finishReason === 'tool-calls') {
|
||||||
|
messages.push(...result.response.messages);
|
||||||
|
return reCallAgent({ messages, tools, languageModel });
|
||||||
|
}
|
||||||
|
return { result, messages };
|
||||||
|
}
|
||||||
|
export const runAgent = async (opts: { app: QueryRouterServer | App, messages?: ModelMessage[], routes?: Route[], query?: string, languageModel: LanguageModel, token: string }) => {
|
||||||
|
const { app, languageModel } = opts;
|
||||||
|
let messages = opts.messages || [];
|
||||||
|
|
||||||
|
let routes = opts?.routes || app.routes;
|
||||||
|
if (opts.query) {
|
||||||
|
routes = filter(routes, opts.query);
|
||||||
|
};
|
||||||
|
const tools = await createTools({ app, token: opts.token });
|
||||||
|
return await reCallAgent({ messages, tools, languageModel });
|
||||||
|
}
|
||||||
@@ -5,3 +5,8 @@ import { useKey } from "@kevisual/use-config";
|
|||||||
* 用来放cookie的域名
|
* 用来放cookie的域名
|
||||||
*/
|
*/
|
||||||
export const proxyDomain = useKey('PROXY_DOMAIN') || ''; // 请在这里填写你的域名
|
export const proxyDomain = useKey('PROXY_DOMAIN') || ''; // 请在这里填写你的域名
|
||||||
|
|
||||||
|
export const baseProxyUrl = proxyDomain ? `https://${proxyDomain}` : 'https://kevisual.cn';
|
||||||
|
|
||||||
|
|
||||||
|
export const baseURL = baseProxyUrl;
|
||||||
@@ -109,7 +109,7 @@ export const pipeProxyReq = async (req: http.IncomingMessage, proxyReq: http.Cli
|
|||||||
const bunRequest = req.bun.request;
|
const bunRequest = req.bun.request;
|
||||||
const contentType = req.headers['content-type'] || '';
|
const contentType = req.headers['content-type'] || '';
|
||||||
if (contentType.includes('multipart/form-data')) {
|
if (contentType.includes('multipart/form-data')) {
|
||||||
console.log('Processing multipart/form-data');
|
// console.log('Processing multipart/form-data');
|
||||||
const arrayBuffer = await bunRequest.arrayBuffer();
|
const arrayBuffer = await bunRequest.arrayBuffer();
|
||||||
|
|
||||||
// 设置请求头(在写入数据之前)
|
// 设置请求头(在写入数据之前)
|
||||||
@@ -123,7 +123,6 @@ export const pipeProxyReq = async (req: http.IncomingMessage, proxyReq: http.Cli
|
|||||||
proxyReq.end();
|
proxyReq.end();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.log('Bun pipeProxyReq content-type', contentType);
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const bodyString = req.body;
|
const bodyString = req.body;
|
||||||
bodyString && proxyReq.write(bodyString);
|
bodyString && proxyReq.write(bodyString);
|
||||||
|
|||||||
@@ -27,12 +27,10 @@ type FileList = {
|
|||||||
export const getFileList = async (list: any, opts?: { objectName: string; app: string; host?: string }) => {
|
export const getFileList = async (list: any, opts?: { objectName: string; app: string; host?: string }) => {
|
||||||
const { app, host } = opts || {};
|
const { app, host } = opts || {};
|
||||||
const objectName = opts?.objectName || '';
|
const objectName = opts?.objectName || '';
|
||||||
let newObjectName = objectName;
|
|
||||||
const [user] = objectName.split('/');
|
const [user] = objectName.split('/');
|
||||||
let replaceUser = user + '/';
|
let replaceUser = user + '/';
|
||||||
if (app === 'resources') {
|
if (app === 'resources') {
|
||||||
replaceUser = `${user}/resources/`;
|
replaceUser = `${user}/resources/`;
|
||||||
newObjectName = objectName.replace(`${user}/`, replaceUser);
|
|
||||||
}
|
}
|
||||||
return list.map((item: FileList) => {
|
return list.map((item: FileList) => {
|
||||||
if (item.name) {
|
if (item.name) {
|
||||||
@@ -70,12 +68,20 @@ const getAiProxy = async (req: IncomingMessage, res: ServerResponse, opts: Proxy
|
|||||||
const password = params.get('p');
|
const password = params.get('p');
|
||||||
const hash = params.get('hash');
|
const hash = params.get('hash');
|
||||||
let dir = !!params.get('dir');
|
let dir = !!params.get('dir');
|
||||||
|
const edit = !!params.get('edit');
|
||||||
const recursive = !!params.get('recursive');
|
const recursive = !!params.get('recursive');
|
||||||
const showStat = !!params.get('stat');
|
const showStat = !!params.get('stat');
|
||||||
const { objectName, app, owner, loginUser, isOwner } = await getObjectName(req);
|
const { objectName, app, owner, loginUser, isOwner } = await getObjectName(req);
|
||||||
if (!dir && _u.pathname.endsWith('/')) {
|
if (!dir && _u.pathname.endsWith('/')) {
|
||||||
dir = true; // 如果是目录请求,强制设置为true
|
dir = true; // 如果是目录请求,强制设置为true
|
||||||
}
|
}
|
||||||
|
if (edit) {
|
||||||
|
// 重定向root/codepod/#folder=路径
|
||||||
|
const redirectUrl = `/root/codepod/#folder=${_u.pathname}`;
|
||||||
|
res.writeHead(302, { Location: redirectUrl });
|
||||||
|
res.end();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
logger.debug(`proxy request: ${objectName}`, dir);
|
logger.debug(`proxy request: ${objectName}`, dir);
|
||||||
try {
|
try {
|
||||||
if (dir) {
|
if (dir) {
|
||||||
@@ -201,6 +207,8 @@ export const getObjectByPathname = (opts: {
|
|||||||
prefix = `${user}/`; // root/resources
|
prefix = `${user}/`; // root/resources
|
||||||
}
|
}
|
||||||
let objectName = opts.pathname.replace(replaceKey, prefix);
|
let objectName = opts.pathname.replace(replaceKey, prefix);
|
||||||
|
// 解码decodeURIComponent编码的路径
|
||||||
|
objectName = decodeURIComponent(objectName);
|
||||||
return { prefix, replaceKey, objectName, user, app };
|
return { prefix, replaceKey, objectName, user, app };
|
||||||
}
|
}
|
||||||
export const getObjectName = async (req: IncomingMessage, opts?: { checkOwner?: boolean }) => {
|
export const getObjectName = async (req: IncomingMessage, opts?: { checkOwner?: boolean }) => {
|
||||||
@@ -217,6 +225,8 @@ export const getObjectName = async (req: IncomingMessage, opts?: { checkOwner?:
|
|||||||
} else {
|
} else {
|
||||||
objectName = pathname.replace(`/${user}/${app}/`, `${user}/`); // root/resources
|
objectName = pathname.replace(`/${user}/${app}/`, `${user}/`); // root/resources
|
||||||
}
|
}
|
||||||
|
// 解码decodeURIComponent编码的路径
|
||||||
|
objectName = decodeURIComponent(objectName);
|
||||||
owner = user;
|
owner = user;
|
||||||
let isOwner = undefined;
|
let isOwner = undefined;
|
||||||
let loginUser: Awaited<ReturnType<typeof getLoginUser>> = null;
|
let loginUser: Awaited<ReturnType<typeof getLoginUser>> = null;
|
||||||
@@ -287,7 +297,7 @@ export const renameProxy = async (req: IncomingMessage, res: ServerResponse, opt
|
|||||||
}
|
}
|
||||||
const newUrl = new URL(newName, 'http://localhost');
|
const newUrl = new URL(newName, 'http://localhost');
|
||||||
const version = _u.searchParams.get('version') || '1.0.0';
|
const version = _u.searchParams.get('version') || '1.0.0';
|
||||||
const newNamePath = newUrl.pathname;
|
const newNamePath = decodeURIComponent(newUrl.pathname);
|
||||||
// 确保 newName 有正确的前缀路径
|
// 确保 newName 有正确的前缀路径
|
||||||
|
|
||||||
const newObject = getObjectByPathname({ pathname: newNamePath, version });
|
const newObject = getObjectByPathname({ pathname: newNamePath, version });
|
||||||
@@ -314,7 +324,6 @@ export const renameProxy = async (req: IncomingMessage, res: ServerResponse, opt
|
|||||||
await oss.deleteObject(obj.name);
|
await oss.deleteObject(obj.name);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 重命名文件
|
|
||||||
await oss.copyObject(objectName, newObjectName);
|
await oss.copyObject(objectName, newObjectName);
|
||||||
await oss.deleteObject(objectName);
|
await oss.deleteObject(objectName);
|
||||||
copiedCount = 1;
|
copiedCount = 1;
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ export const httpProxy = async (
|
|||||||
const params = _u.searchParams;
|
const params = _u.searchParams;
|
||||||
const isDownload = params.get('download') === 'true';
|
const isDownload = params.get('download') === 'true';
|
||||||
if (proxyUrl.startsWith(minioResources)) {
|
if (proxyUrl.startsWith(minioResources)) {
|
||||||
console.log('isMinio', proxyUrl)
|
// console.log('isMinio', proxyUrl)
|
||||||
const isOk = await minioProxy(req, res, { ...opts, isDownload });
|
const isOk = await minioProxy(req, res, { ...opts, isDownload });
|
||||||
if (!isOk) {
|
if (!isOk) {
|
||||||
userApp.clearCacheData();
|
userApp.clearCacheData();
|
||||||
|
|||||||
6
src/modules/html/render-server-html.ts
Normal file
6
src/modules/html/render-server-html.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export const renderServerHtml = (data: any, html: string) => {
|
||||||
|
if (html.includes('<body>')) {
|
||||||
|
return html.replace('<body>', `<body><script>window.__SERVER_DATA__ = ${JSON.stringify(data)}</script>`);
|
||||||
|
}
|
||||||
|
return html;
|
||||||
|
}
|
||||||
@@ -1,344 +0,0 @@
|
|||||||
type StudioOpts = { user: string, userAppKey?: string; appIds: string[] }
|
|
||||||
export const createStudioAppListHtml = (opts: StudioOpts) => {
|
|
||||||
const user = opts.user!;
|
|
||||||
const userAppKey = opts?.userAppKey;
|
|
||||||
let showUserAppKey = userAppKey;
|
|
||||||
if (showUserAppKey && showUserAppKey.startsWith(user + '--')) {
|
|
||||||
showUserAppKey = showUserAppKey.replace(user + '--', '');
|
|
||||||
}
|
|
||||||
const pathApps = opts?.appIds?.map(appId => {
|
|
||||||
const shortAppId = appId.replace(opts!.user + '--', '')
|
|
||||||
return {
|
|
||||||
appId,
|
|
||||||
shortAppId,
|
|
||||||
pathname: `/${user}/v1/${shortAppId}`
|
|
||||||
};
|
|
||||||
}) || []
|
|
||||||
|
|
||||||
// 应用列表内容
|
|
||||||
const appListContent = `
|
|
||||||
<div class="header">
|
|
||||||
<h1><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="12" x="2" y="6" rx="2"/><path d="M12 12h.01"/><path d="M17 12h.01"/><path d="M7 12h.01"/></svg> Studio 应用列表</h1>
|
|
||||||
<p class="user-info">用户: <strong>${user}</strong></p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="app-grid">
|
|
||||||
${pathApps.map((app, index) => `
|
|
||||||
<a href="${app.pathname}" class="app-card" style="animation-delay: ${index * 0.1}s">
|
|
||||||
<div class="app-icon"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="16" x="2" y="4" rx="2"/><path d="M6 16h12"/><path d="M2 8h20"/></svg></div>
|
|
||||||
<div class="app-info">
|
|
||||||
<h3>${app.shortAppId}</h3>
|
|
||||||
<p class="app-path">${app.pathname}</p>
|
|
||||||
</div>
|
|
||||||
<div class="app-arrow">→</div>
|
|
||||||
</a>
|
|
||||||
`).join('')}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
${pathApps.length === 0 ? `
|
|
||||||
<div class="empty-state">
|
|
||||||
<div class="empty-icon">📭</div>
|
|
||||||
<p>暂无应用</p>
|
|
||||||
</div>
|
|
||||||
` : ''}
|
|
||||||
`
|
|
||||||
|
|
||||||
return `
|
|
||||||
<!doctype html>
|
|
||||||
<html lang="zh-CN">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Studio - ${user} 的应用</title>
|
|
||||||
<style>
|
|
||||||
:root {
|
|
||||||
--primary-color: #000000;
|
|
||||||
--primary-hover: #333333;
|
|
||||||
--text-color: #111111;
|
|
||||||
--text-secondary: #666666;
|
|
||||||
--bg-color: #ffffff;
|
|
||||||
--card-bg: #ffffff;
|
|
||||||
--border-color: #e0e0e0;
|
|
||||||
--shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.08), 0 2px 4px -1px rgba(0, 0, 0, 0.04);
|
|
||||||
--shadow-hover: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
* {
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
|
||||||
background-color: var(--bg-color);
|
|
||||||
color: var(--text-color);
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
min-height: 100vh;
|
|
||||||
line-height: 1.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
max-width: 900px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Not Found Styles */
|
|
||||||
.not-found {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
min-height: 60vh;
|
|
||||||
text-align: center;
|
|
||||||
padding: 3rem;
|
|
||||||
background-color: var(--card-bg);
|
|
||||||
border-radius: 16px;
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
box-shadow: var(--shadow);
|
|
||||||
}
|
|
||||||
|
|
||||||
.not-found-icon {
|
|
||||||
width: 80px;
|
|
||||||
height: 80px;
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.not-found h1 {
|
|
||||||
font-size: 2.5rem;
|
|
||||||
color: #000000;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.not-found p {
|
|
||||||
color: var(--text-secondary);
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
max-width: 400px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.not-found code {
|
|
||||||
background-color: #f5f5f5;
|
|
||||||
padding: 0.25rem 0.5rem;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-family: 'Fira Code', 'Monaco', monospace;
|
|
||||||
color: #000000;
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.back-link {
|
|
||||||
display: inline-block;
|
|
||||||
margin-top: 1.5rem;
|
|
||||||
padding: 0.75rem 1.5rem;
|
|
||||||
background-color: var(--primary-color);
|
|
||||||
color: white;
|
|
||||||
text-decoration: none;
|
|
||||||
border-radius: 8px;
|
|
||||||
font-weight: 500;
|
|
||||||
transition: all 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.back-link:hover {
|
|
||||||
background-color: var(--primary-hover);
|
|
||||||
transform: translateY(-2px);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* App List Styles */
|
|
||||||
.header {
|
|
||||||
text-align: center;
|
|
||||||
margin-bottom: 3rem;
|
|
||||||
padding-bottom: 2rem;
|
|
||||||
border-bottom: 1px solid var(--border-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.header h1 {
|
|
||||||
font-size: 2rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--text-color);
|
|
||||||
margin: 0 0 0.5rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-info {
|
|
||||||
color: var(--text-secondary);
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-info strong {
|
|
||||||
color: var(--primary-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-grid {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-card {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 1.25rem 1.5rem;
|
|
||||||
background-color: var(--card-bg);
|
|
||||||
border-radius: 12px;
|
|
||||||
text-decoration: none;
|
|
||||||
color: inherit;
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
box-shadow: var(--shadow);
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
animation: slideIn 0.5s ease-out backwards;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes slideIn {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(20px);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-card:hover {
|
|
||||||
transform: translateY(-3px);
|
|
||||||
box-shadow: var(--shadow-hover);
|
|
||||||
border-color: var(--primary-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-icon {
|
|
||||||
font-size: 2rem;
|
|
||||||
margin-right: 1rem;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-info {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-info h3 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 1.1rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--text-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-path {
|
|
||||||
margin: 0.25rem 0 0 0;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
font-family: 'Fira Code', 'Monaco', monospace;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-arrow {
|
|
||||||
font-size: 1.25rem;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-card:hover .app-arrow {
|
|
||||||
color: var(--primary-color);
|
|
||||||
transform: translateX(5px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-state {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 4rem 2rem;
|
|
||||||
text-align: center;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-icon {
|
|
||||||
font-size: 4rem;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-state p {
|
|
||||||
font-size: 1.1rem;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Footer */
|
|
||||||
.footer {
|
|
||||||
margin-top: 3rem;
|
|
||||||
padding-top: 2rem;
|
|
||||||
border-top: 1px solid var(--border-color);
|
|
||||||
text-align: center;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Dark mode support */
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
:root {
|
|
||||||
--primary-color: #ffffff;
|
|
||||||
--primary-hover: #cccccc;
|
|
||||||
--text-color: #ffffff;
|
|
||||||
--text-secondary: #999999;
|
|
||||||
--bg-color: #000000;
|
|
||||||
--card-bg: #1a1a1a;
|
|
||||||
--border-color: #333333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.not-found code {
|
|
||||||
background-color: #333333;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Responsive */
|
|
||||||
@media (max-width: 600px) {
|
|
||||||
.container {
|
|
||||||
padding: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header h1 {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-card {
|
|
||||||
padding: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-icon {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
margin-right: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-info h3 {
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-path {
|
|
||||||
font-size: 0.75rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="container">
|
|
||||||
${showUserAppKey ? `
|
|
||||||
<div class="not-found">
|
|
||||||
<svg class="not-found-icon" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21 21-4.34-4.34"/><circle cx="11" cy="11" r="8"/></svg>
|
|
||||||
<h1>应用不存在</h1>
|
|
||||||
<p>抱歉,您访问的应用 <code>${showUserAppKey || ''}</code> 不存在。</p>
|
|
||||||
<p>请检查应用 Key 是否正确,或联系管理员。</p>
|
|
||||||
<a href="/${user}/v1/" class="back-link">← 返回应用列表</a>
|
|
||||||
</div>
|
|
||||||
` : ''}
|
|
||||||
${appListContent}
|
|
||||||
|
|
||||||
<div class="footer">
|
|
||||||
© ${new Date().getFullYear()} Studio - 应用管理
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
`;
|
|
||||||
};
|
|
||||||
79
src/modules/n5/index.ts
Normal file
79
src/modules/n5/index.ts
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import { User } from '@/models/user.ts';
|
||||||
|
import { omit } from 'es-toolkit';
|
||||||
|
import { IncomingMessage, ServerResponse } from 'http';
|
||||||
|
import { renderServerHtml } from '../html/render-server-html.ts';
|
||||||
|
import { baseURL } from '../domain.ts';
|
||||||
|
import { app } from '@/app.ts'
|
||||||
|
import { N5Service } from '@/routes/n5-link/modules/n5.services.ts';
|
||||||
|
type ProxyOptions = {
|
||||||
|
createNotFoundPage: (msg?: string) => any;
|
||||||
|
};
|
||||||
|
// /n5/:slug/
|
||||||
|
export const N5Proxy = async (req: IncomingMessage, res: ServerResponse, opts?: ProxyOptions) => {
|
||||||
|
const { url } = req;
|
||||||
|
const _url = new URL(url || '', baseURL);
|
||||||
|
const { pathname, searchParams } = _url;
|
||||||
|
let [user, app, userAppKey] = pathname.split('/').slice(1);
|
||||||
|
if (!app) {
|
||||||
|
opts?.createNotFoundPage?.('应用未找到');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let n5Data = null;
|
||||||
|
let data = null;
|
||||||
|
let link = '';
|
||||||
|
try {
|
||||||
|
const convexResult = await N5Service.getBySlug(app);
|
||||||
|
if (!convexResult || convexResult.length === 0) {
|
||||||
|
opts?.createNotFoundPage?.('应用未找到');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
n5Data = convexResult[0];
|
||||||
|
data = n5Data.data ?? {};
|
||||||
|
link = data.link;
|
||||||
|
if (!link) {
|
||||||
|
opts?.createNotFoundPage?.('不存在对应的跳转的链接');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error fetching N5 data:', e);
|
||||||
|
opts?.createNotFoundPage?.('应用数据异常');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data?.useOwnerToken) {
|
||||||
|
const userId = n5Data.userId;
|
||||||
|
if (!userId) {
|
||||||
|
opts?.createNotFoundPage?.('未绑定账号');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const user = await User.findByPk(userId);
|
||||||
|
const token = await User.createJwksTokenResponse({ id: userId, username: user?.username || '' }, { hasRefreshToken: true });
|
||||||
|
const urlObj = new URL(link);
|
||||||
|
urlObj.searchParams.set('token', token.accessToken);
|
||||||
|
const resultLink = await fetch(urlObj.toString(), { method: 'GET' }).then(res => res.json())
|
||||||
|
res.end(JSON.stringify(resultLink));
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error fetching the link:', e);
|
||||||
|
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({ error: '请求服务区失败' }));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const type = data?.type;
|
||||||
|
if (type === 'html-render') {
|
||||||
|
const createJson = omit(data, ['useOwnerToken', 'permission']);
|
||||||
|
const html = await fetch(link, { method: 'GET' }).then(res => res.text());
|
||||||
|
const renderedHtml = await renderServerHtml({
|
||||||
|
data: createJson,
|
||||||
|
pathname: _url.toString(),
|
||||||
|
}, html);
|
||||||
|
res.writeHead(200, { 'Content-Type': 'text/html' });
|
||||||
|
res.end(renderedHtml);
|
||||||
|
} else {
|
||||||
|
res.writeHead(302, {
|
||||||
|
Location: link,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
res.end();
|
||||||
|
};
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import childProcess from 'child_process';
|
import childProcess from 'child_process';
|
||||||
|
|
||||||
export const selfRestart = async () => {
|
export const selfRestart = async () => {
|
||||||
const appName = 'code-center';
|
const appName = 'root/code-center';
|
||||||
// 检测 pm2 是否安装和是否有 appName 这个应用
|
// 检测 pm2 是否安装和是否有 appName 这个应用
|
||||||
try {
|
try {
|
||||||
const res = childProcess.execSync(`pm2 list`);
|
const res = childProcess.execSync(`pm2 list`);
|
||||||
|
|||||||
@@ -110,7 +110,6 @@ export class UserApp {
|
|||||||
const key = 'user:app:set:' + app + ':' + user;
|
const key = 'user:app:set:' + app + ':' + user;
|
||||||
const value = await redis.hget(key, appFileUrl);
|
const value = await redis.hget(key, appFileUrl);
|
||||||
// const values = await redis.hgetall(key);
|
// const values = await redis.hgetall(key);
|
||||||
// console.log('getFile', values);
|
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
static async getDomainApp(domain: string) {
|
static async getDomainApp(domain: string) {
|
||||||
|
|||||||
@@ -10,11 +10,11 @@ import { getUserConfig } from '@/modules/fm-manager/index.ts';
|
|||||||
export const rediretHome = async (req: http.IncomingMessage, res: http.ServerResponse) => {
|
export const rediretHome = async (req: http.IncomingMessage, res: http.ServerResponse) => {
|
||||||
const user = await getLoginUser(req);
|
const user = await getLoginUser(req);
|
||||||
if (!user?.token) {
|
if (!user?.token) {
|
||||||
res.writeHead(302, { Location: '/root/home/' });
|
res.writeHead(302, { Location: '/root/center/' });
|
||||||
res.end();
|
res.end();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let redirectURL = '/root/home/';
|
let redirectURL = '/root/center/';
|
||||||
try {
|
try {
|
||||||
const token = user.token;
|
const token = user.token;
|
||||||
const resConfig = await getUserConfig(token);
|
const resConfig = await getUserConfig(token);
|
||||||
|
|||||||
52
src/modules/v1-ws-proxy/index.ts
Normal file
52
src/modules/v1-ws-proxy/index.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import { WsProxyManager } from './manager.ts';
|
||||||
|
import { getLoginUserByToken } from '@/modules/auth.ts';
|
||||||
|
import { logger } from '../logger.ts';
|
||||||
|
export const wsProxyManager = new WsProxyManager();
|
||||||
|
import { WebSocketListenerFun } from '@kevisual/router/src/server/server-type.ts'
|
||||||
|
// 生成一个随机六位字符串作为注册 ID
|
||||||
|
const generateRegistryId = () => {
|
||||||
|
return Math.random().toString(36).substring(2, 8);
|
||||||
|
}
|
||||||
|
export const wssFun: WebSocketListenerFun = async (req, res) => {
|
||||||
|
// do nothing, just to enable ws upgrade event
|
||||||
|
const { id, ws, token, data, emitter } = req;
|
||||||
|
// console.log('req', req)
|
||||||
|
const { type } = data || {};
|
||||||
|
|
||||||
|
if (type === 'registryClient') {
|
||||||
|
const loginUser = await getLoginUserByToken(token);
|
||||||
|
let isLogin = false;
|
||||||
|
let user = '';
|
||||||
|
if (loginUser?.tokenUser) {
|
||||||
|
isLogin = true;
|
||||||
|
user = loginUser?.tokenUser?.username;
|
||||||
|
} else {
|
||||||
|
logger.debug('未登录,请求等待用户验证', data);
|
||||||
|
user = data?.username || '';
|
||||||
|
}
|
||||||
|
if (!user) {
|
||||||
|
logger.debug('未提供用户名,无法注册 ws 连接');
|
||||||
|
ws.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let userApp = user + '--' + id;
|
||||||
|
// TODO: 如果存在, 而且之前的那个关闭了,不需要验证,直接覆盖和复用.
|
||||||
|
let wsConnect = await wsProxyManager.createNewConnection({ ws, user, userApp, isLogin });
|
||||||
|
if (wsConnect.isNew) {
|
||||||
|
logger.debug('新连接注册成功', userApp);
|
||||||
|
}
|
||||||
|
// @ts-ignore
|
||||||
|
ws.data.userApp = wsConnect.id;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// @ts-ignore
|
||||||
|
const userApp = ws.data.userApp;
|
||||||
|
logger.debug('message', data, ' userApp=', userApp);
|
||||||
|
const wsMessage = wsProxyManager.get(userApp);
|
||||||
|
if (wsMessage) {
|
||||||
|
wsMessage.sendResponse(data);
|
||||||
|
} else {
|
||||||
|
// @ts-ignore
|
||||||
|
logger.debug('账号应用未注册,无法处理消息。未授权?', ws.data);
|
||||||
|
}
|
||||||
|
}
|
||||||
263
src/modules/v1-ws-proxy/manager.ts
Normal file
263
src/modules/v1-ws-proxy/manager.ts
Normal file
@@ -0,0 +1,263 @@
|
|||||||
|
import { customAlphabet } from 'nanoid';
|
||||||
|
import { WebSocket } from 'ws';
|
||||||
|
import { logger } from '../logger.ts';
|
||||||
|
import { EventEmitter } from 'eventemitter3';
|
||||||
|
const letters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||||
|
const nanoid = customAlphabet(letters, 10);
|
||||||
|
|
||||||
|
class WsMessage {
|
||||||
|
ws: WebSocket;
|
||||||
|
user?: string;
|
||||||
|
emitter: EventEmitter;
|
||||||
|
private pingTimer?: NodeJS.Timeout;
|
||||||
|
private readonly PING_INTERVAL = 30000; // 30 秒发送一次 ping
|
||||||
|
id?: string;
|
||||||
|
status?: 'waiting' | 'connected' | 'closed';
|
||||||
|
manager: WsProxyManager;
|
||||||
|
constructor({ ws, user, id, isLogin, manager }: WssMessageOptions) {
|
||||||
|
this.ws = ws;
|
||||||
|
this.user = user;
|
||||||
|
this.id = id;
|
||||||
|
this.emitter = new EventEmitter();
|
||||||
|
this.manager = manager;
|
||||||
|
this.status = isLogin ? 'connected' : 'waiting';
|
||||||
|
this.startPing();
|
||||||
|
}
|
||||||
|
|
||||||
|
private startPing() {
|
||||||
|
this.stopPing();
|
||||||
|
this.pingTimer = setInterval(() => {
|
||||||
|
if (this.ws.readyState === WebSocket.OPEN) {
|
||||||
|
this.ws.ping();
|
||||||
|
} else {
|
||||||
|
this.stopPing();
|
||||||
|
}
|
||||||
|
}, this.PING_INTERVAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
private stopPing() {
|
||||||
|
if (this.pingTimer) {
|
||||||
|
clearInterval(this.pingTimer);
|
||||||
|
this.pingTimer = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
this.stopPing();
|
||||||
|
this.emitter.removeAllListeners();
|
||||||
|
}
|
||||||
|
isClosed() {
|
||||||
|
return this.ws.readyState === WebSocket.CLOSED;
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendResponse(data: any) {
|
||||||
|
if (data.id) {
|
||||||
|
this.emitter.emit(data.id, data?.data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async sendConnected() {
|
||||||
|
const id = this.id;
|
||||||
|
const user = this.user;
|
||||||
|
const data = { type: 'verified', user, id };
|
||||||
|
if (this.ws.readyState === WebSocket.OPEN) {
|
||||||
|
this.ws.send(JSON.stringify(data));
|
||||||
|
this.status = 'connected';
|
||||||
|
}
|
||||||
|
if (id.includes('-registry-')) {
|
||||||
|
const newId = id.split('-registry-')[0];
|
||||||
|
this.manager.changeId(id, newId);
|
||||||
|
const ws = this.ws;
|
||||||
|
// @ts-ignore
|
||||||
|
if (this.ws?.data) {
|
||||||
|
// @ts-ignore
|
||||||
|
this.ws.data.userApp = newId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
getInfo() {
|
||||||
|
const shortAppId = this.id ? this.id.split('--')[1] : '';
|
||||||
|
return {
|
||||||
|
user: this.user,
|
||||||
|
id: this.id,
|
||||||
|
status: this.status,
|
||||||
|
shortAppId,
|
||||||
|
pathname: this.id ? `/${this.user}/v1/${shortAppId}` : '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
async sendData(data: any, context?: any, opts?: { timeout?: number }) {
|
||||||
|
if (this.ws.readyState !== WebSocket.OPEN) {
|
||||||
|
return { code: 500, message: 'WebSocket is not open' };
|
||||||
|
}
|
||||||
|
const timeout = opts?.timeout || 10 * 6 * 1000; // 10 minutes
|
||||||
|
const id = nanoid();
|
||||||
|
const message = JSON.stringify({
|
||||||
|
id,
|
||||||
|
type: 'proxy',
|
||||||
|
data: {
|
||||||
|
message: data,
|
||||||
|
context: context || {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
logger.info('ws-proxy sendData', message);
|
||||||
|
this.ws.send(message);
|
||||||
|
const msg = { path: data?.path, key: data?.key, id: data?.id };
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
console.log('ws-proxy sendData timeout', msg);
|
||||||
|
resolve({
|
||||||
|
code: 500,
|
||||||
|
message: `运行超时,执行的id: ${id},参数是${JSON.stringify(msg)}`,
|
||||||
|
});
|
||||||
|
}, timeout);
|
||||||
|
this.emitter.once(id, (data: any) => {
|
||||||
|
resolve(data);
|
||||||
|
clearTimeout(timer);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
type WssMessageOptions = {
|
||||||
|
ws: WebSocket;
|
||||||
|
user?: string;
|
||||||
|
id?: string;
|
||||||
|
realId?: string;
|
||||||
|
isLogin?: boolean;
|
||||||
|
manager: WsProxyManager;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class WsProxyManager {
|
||||||
|
wssMap: Map<string, WsMessage> = new Map();
|
||||||
|
PING_INTERVAL = 30000; // 30 秒检查一次连接状态
|
||||||
|
constructor(opts?: { pingInterval?: number }) {
|
||||||
|
if (opts?.pingInterval) {
|
||||||
|
this.PING_INTERVAL = opts.pingInterval;
|
||||||
|
}
|
||||||
|
this.checkConnceted();
|
||||||
|
}
|
||||||
|
register(id: string, opts?: { ws: WebSocket; user: string, id?: string, isLogin: boolean }) {
|
||||||
|
if (this.wssMap.has(id)) {
|
||||||
|
const value = this.wssMap.get(id);
|
||||||
|
if (value) {
|
||||||
|
value.ws.close();
|
||||||
|
value.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const [username, appId] = id.split('--');
|
||||||
|
const url = new URL(`/${username}/v1/${appId}`, 'https://kevisual.cn/');
|
||||||
|
console.log('WsProxyManager register', id, '访问地址', url.toString());
|
||||||
|
const value = new WsMessage({ ...opts, manager: this } as WssMessageOptions);
|
||||||
|
this.wssMap.set(id, value);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
changeId(oldId: string, newId: string) {
|
||||||
|
const value = this.wssMap.get(oldId);
|
||||||
|
const originalValue = this.wssMap.get(newId);
|
||||||
|
if (originalValue) {
|
||||||
|
logger.debug(`WsProxyManager changeId: ${newId} already exists, close old connection`);
|
||||||
|
originalValue.ws.close();
|
||||||
|
originalValue.destroy();
|
||||||
|
}
|
||||||
|
if (value) {
|
||||||
|
this.wssMap.delete(oldId);
|
||||||
|
this.wssMap.set(newId, value);
|
||||||
|
value.id = newId;
|
||||||
|
// @ts-ignore
|
||||||
|
if (value.ws?.data) {
|
||||||
|
// @ts-ignore
|
||||||
|
value.ws.data.userApp = newId;
|
||||||
|
}
|
||||||
|
logger.debug(`WsProxyManager changeId: ${oldId} -> ${newId}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
unregister(id: string) {
|
||||||
|
const value = this.wssMap.get(id);
|
||||||
|
if (value) {
|
||||||
|
value.ws.close();
|
||||||
|
value.destroy();
|
||||||
|
}
|
||||||
|
this.wssMap.delete(id);
|
||||||
|
}
|
||||||
|
getIds(beginWith?: string) {
|
||||||
|
if (beginWith) {
|
||||||
|
return Array.from(this.wssMap.keys()).filter(key => key.startsWith(beginWith));
|
||||||
|
}
|
||||||
|
return Array.from(this.wssMap.keys());
|
||||||
|
}
|
||||||
|
getIdsInfo(beginWith?: string) {
|
||||||
|
const ids = this.getIds(beginWith);
|
||||||
|
const infoList = this.getInfoList(ids);
|
||||||
|
return {
|
||||||
|
ids,
|
||||||
|
infoList,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
getInfoList(ids: string[]) {
|
||||||
|
return ids.map(id => {
|
||||||
|
const value = this.wssMap.get(id);
|
||||||
|
if (value) {
|
||||||
|
return value.getInfo();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}).filter(Boolean);
|
||||||
|
}
|
||||||
|
get(id: string) {
|
||||||
|
return this.wssMap.get(id);
|
||||||
|
}
|
||||||
|
createId(id: string) {
|
||||||
|
if (!this.wssMap.has(id)) {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
const newId = id + '-' + nanoid(6);
|
||||||
|
return newId;
|
||||||
|
}
|
||||||
|
checkConnceted() {
|
||||||
|
const that = this;
|
||||||
|
setTimeout(() => {
|
||||||
|
that.wssMap.forEach((value, key) => {
|
||||||
|
if (value.ws.readyState !== WebSocket.OPEN) {
|
||||||
|
logger.debug('ws not connected, unregister', key);
|
||||||
|
that.unregister(key);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
that.checkConnceted();
|
||||||
|
}, this.PING_INTERVAL);
|
||||||
|
}
|
||||||
|
async createNewConnection(opts: { ws: any; user: string, userApp: string, isLogin?: boolean }) {
|
||||||
|
let id = opts.userApp;
|
||||||
|
let realId: string = id;
|
||||||
|
const isLogin = opts.isLogin || false;
|
||||||
|
const has = this.wssMap.has(id);
|
||||||
|
let registryId = '-registry-' + generateRegistryId(); // 生成一个随机六位字符串作为注册 ID
|
||||||
|
let isNeedVerify = !isLogin;
|
||||||
|
if (has) {
|
||||||
|
const value = this.wssMap.get(id);
|
||||||
|
if (value) {
|
||||||
|
if (value.isClosed()) {
|
||||||
|
// 短时间内还在, 等于简单重启了一下应用,不需要重新注册.
|
||||||
|
logger.debug('之前的连接已关闭,复用注册 ID 连接 ws', id);
|
||||||
|
this.unregister(id);
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||||||
|
const wsMessage = this.register(id, { ws: opts.ws, user: opts.user, id, isLogin });
|
||||||
|
wsMessage.sendConnected();
|
||||||
|
return { wsMessage, isNew: false, id: id };
|
||||||
|
} else {
|
||||||
|
// 没有关闭 生成新的 id 连接.
|
||||||
|
id = id + '-mult-' + generateRegistryId(4);
|
||||||
|
realId = id;
|
||||||
|
logger.debug('之前的连接未关闭,使用新的 ID 连接 ws', id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 没有连接, 直接注册新的连接.
|
||||||
|
if (isNeedVerify) {
|
||||||
|
realId = id + registryId;
|
||||||
|
logger.debug('未登录用户,使用临时注册 ID 连接 ws', realId);
|
||||||
|
}
|
||||||
|
const wsMessage = this.register(realId, { ws: opts.ws, user: opts.user, id: realId, isLogin });
|
||||||
|
return { wsMessage, isNew: true, id: realId };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 生成一个随机六位字符串作为注册 ID
|
||||||
|
const generateRegistryId = (len = 6) => {
|
||||||
|
return Math.random().toString(36).substring(2, 2 + len);
|
||||||
|
}
|
||||||
158
src/modules/v1-ws-proxy/proxy.ts
Normal file
158
src/modules/v1-ws-proxy/proxy.ts
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
import { IncomingMessage, ServerResponse } from 'http';
|
||||||
|
import { wsProxyManager } from './index.ts';
|
||||||
|
|
||||||
|
import { App } from '@kevisual/router';
|
||||||
|
import { logger } from '../logger.ts';
|
||||||
|
import { getLoginUser } from '@/modules/auth.ts';
|
||||||
|
import { omit } from 'es-toolkit';
|
||||||
|
import { baseProxyUrl, proxyDomain } from '../domain.ts';
|
||||||
|
import { renderServerHtml } from '../html/render-server-html.ts';
|
||||||
|
|
||||||
|
type ProxyOptions = {
|
||||||
|
createNotFoundPage: (msg?: string) => any;
|
||||||
|
};
|
||||||
|
export const UserV1Proxy = async (req: IncomingMessage, res: ServerResponse, opts?: ProxyOptions) => {
|
||||||
|
const { url, method } = req;
|
||||||
|
const _url = new URL(url || '', `http://localhost`);
|
||||||
|
const { pathname, searchParams } = _url;
|
||||||
|
const isGet = method === 'GET';
|
||||||
|
|
||||||
|
let [user, app, userAppKey] = pathname.split('/').slice(1);
|
||||||
|
if (!user || !app) {
|
||||||
|
opts?.createNotFoundPage?.('应用未找到');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await App.handleRequest(req, res);
|
||||||
|
const loginUser = await getLoginUser(req);
|
||||||
|
if (!loginUser) {
|
||||||
|
opts?.createNotFoundPage?.('没有登录');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isAdmin = loginUser.tokenUser?.username === user
|
||||||
|
|
||||||
|
if (!userAppKey) {
|
||||||
|
if (isAdmin) {
|
||||||
|
return handleRequest(req, res, { user, app, userAppKey, isAdmin });
|
||||||
|
} else {
|
||||||
|
opts?.createNotFoundPage?.('应用访问失败');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!userAppKey.includes('--')) {
|
||||||
|
userAppKey = user + '--' + userAppKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: 如果不是管理员,是否需要添加其他人可以访问的逻辑?
|
||||||
|
if (!isAdmin) {
|
||||||
|
opts?.createNotFoundPage?.('没有访问应用权限');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!userAppKey.startsWith(user + '--')) {
|
||||||
|
userAppKey = user + '--' + userAppKey;
|
||||||
|
}
|
||||||
|
logger.debug('data', data);
|
||||||
|
const client = wsProxyManager.get(userAppKey);
|
||||||
|
const { ids, infoList } = wsProxyManager.getIdsInfo(user + '--');
|
||||||
|
if (!client) {
|
||||||
|
if (isGet) {
|
||||||
|
if (isAdmin) {
|
||||||
|
// const html = createStudioAppListHtml({ user, appIds: ids, userAppKey, infoList });
|
||||||
|
// res.writeHead(200, { 'Content-Type': 'text/html' });
|
||||||
|
// res.end(html);
|
||||||
|
// https://kevisual.cn/root/v1-manager/
|
||||||
|
const html = await fetch(`${baseProxyUrl}/root/v1-manager/index.html`).then(res => res.text());
|
||||||
|
res.writeHead(200, { 'Content-Type': 'text/html' });
|
||||||
|
res.end(renderServerHtml({ user, appIds: ids, userAppKey, infoList }, html));
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
opts?.createNotFoundPage?.('应用访问失败');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
res.writeHead(404, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({ error: '应用访问失败' }));
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const path = searchParams.get('path');
|
||||||
|
if (!path) {
|
||||||
|
// 显示前端页面
|
||||||
|
const html = await fetch(`${baseProxyUrl}/root/router-studio/index.html`).then(res => res.text());
|
||||||
|
res.writeHead(200, { 'Content-Type': 'text/html' });
|
||||||
|
res.end(html);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
let message: any = data;
|
||||||
|
if (!isAdmin) {
|
||||||
|
message = omit(data, ['token', 'cookies']);
|
||||||
|
}
|
||||||
|
if (client.status === 'waiting') {
|
||||||
|
res.writeHead(603, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({ message: '应用没有鉴权' }));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const value = await client.sendData(message, {
|
||||||
|
state: { tokenUser: omit(loginUser.tokenUser, ['oauthExpand']) },
|
||||||
|
});
|
||||||
|
if (value) {
|
||||||
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify(value));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
opts?.createNotFoundPage?.('应用未启动');
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRequest = async (req: IncomingMessage, res: ServerResponse, opts?: { user?: string, app?: string, userAppKey?: string, isAdmin?: boolean }) => {
|
||||||
|
const { user, userAppKey } = opts || {};
|
||||||
|
const isGet = req.method === 'GET';
|
||||||
|
// 获取所有的管理员的应用列表
|
||||||
|
const { ids, infoList } = wsProxyManager.getIdsInfo(user + '--');
|
||||||
|
if (isGet) {
|
||||||
|
const html = await fetch(`${baseProxyUrl}/root/v1-manager/index.html`).then(res => res.text());
|
||||||
|
res.writeHead(200, { 'Content-Type': 'text/html' });
|
||||||
|
res.end(html);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
const url = new URL(req.url || '', 'http://localhost');
|
||||||
|
const path = url.searchParams.get('path');
|
||||||
|
if (path) {
|
||||||
|
const appId = url.searchParams.get('appId') || '';
|
||||||
|
const client = wsProxyManager.get(appId!)!;
|
||||||
|
if (!client) {
|
||||||
|
res.writeHead(404, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({ code: 404, message: '应用未找到' }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (path === 'connected') {
|
||||||
|
client.sendConnected();
|
||||||
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({ code: 200, message: '应用已连接' }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (path === 'rename') {
|
||||||
|
const newId = url.searchParams.get('newId') || '';
|
||||||
|
if (!newId) {
|
||||||
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({ code: 400, message: 'newId 参数缺失' }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const realNewId = user + '--' + newId;
|
||||||
|
const wsMessage = wsProxyManager.get(realNewId!)!;
|
||||||
|
if (wsMessage) {
|
||||||
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({ code: 400, message: 'newId 已存在' }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
wsProxyManager.changeId(appId, realNewId);
|
||||||
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({ code: 200, message: '应用重命名成功' }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({ code: 200, data: { ids, infoList } }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
import { WsProxyManager } from './manager.ts';
|
|
||||||
import { getLoginUserByToken } from '@/modules/auth.ts';
|
|
||||||
import { logger } from '../logger.ts';
|
|
||||||
export const wsProxyManager = new WsProxyManager();
|
|
||||||
import { WebSocketListenerFun } from '@kevisual/router/src/server/server-type.ts'
|
|
||||||
export const wssFun: WebSocketListenerFun = async (req, res) => {
|
|
||||||
// do nothing, just to enable ws upgrade event
|
|
||||||
const { id, ws, token, data, emitter } = req;
|
|
||||||
logger.debug('ws proxy connected, id=', id, ' token=', token, ' data=', data);
|
|
||||||
// console.log('req', req)
|
|
||||||
const { type } = data || {};
|
|
||||||
if (type === 'registryClient') {
|
|
||||||
const loginUser = await getLoginUserByToken(token);
|
|
||||||
if (!loginUser?.tokenUser) {
|
|
||||||
logger.debug('未登录,断开连接');
|
|
||||||
ws.send(JSON.stringify({ code: 401, message: '未登录' }));
|
|
||||||
setTimeout(() => {
|
|
||||||
ws.close(401, 'Unauthorized');
|
|
||||||
}, 1000);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const user = loginUser?.tokenUser?.username;
|
|
||||||
const userApp = user + '--' + id;
|
|
||||||
logger.debug('注册 ws 连接', userApp);
|
|
||||||
const wsMessage = wsProxyManager.get(userApp);
|
|
||||||
if (wsMessage) {
|
|
||||||
logger.debug('ws 连接已存在,关闭旧连接', userApp);
|
|
||||||
wsMessage.ws.close();
|
|
||||||
wsProxyManager.unregister(userApp);
|
|
||||||
}
|
|
||||||
// @ts-ignore
|
|
||||||
wsProxyManager.register(userApp, { user, ws });
|
|
||||||
ws.send(
|
|
||||||
JSON.stringify({
|
|
||||||
type: 'connected',
|
|
||||||
user: user,
|
|
||||||
id,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
emitter.once('close--' + id, () => {
|
|
||||||
logger.debug('ws emitter closed');
|
|
||||||
wsProxyManager.unregister(userApp);
|
|
||||||
});
|
|
||||||
// @ts-ignore
|
|
||||||
ws.data.userApp = userApp;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// @ts-ignore
|
|
||||||
const userApp = ws.data.userApp;
|
|
||||||
logger.debug('message', data, ' userApp=', userApp);
|
|
||||||
const wsMessage = wsProxyManager.get(userApp);
|
|
||||||
if (wsMessage) {
|
|
||||||
wsMessage.sendResponse(data);
|
|
||||||
} else {
|
|
||||||
// @ts-ignore
|
|
||||||
logger.debug('账号应用未注册,无法处理消息。未授权?', ws.data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,136 +0,0 @@
|
|||||||
import { nanoid } from 'nanoid';
|
|
||||||
import { WebSocket } from 'ws';
|
|
||||||
import { logger } from '../logger.ts';
|
|
||||||
import { EventEmitter } from 'eventemitter3';
|
|
||||||
import { set } from 'zod';
|
|
||||||
|
|
||||||
class WsMessage {
|
|
||||||
ws: WebSocket;
|
|
||||||
user?: string;
|
|
||||||
emitter: EventEmitter;
|
|
||||||
private pingTimer?: NodeJS.Timeout;
|
|
||||||
private readonly PING_INTERVAL = 30000; // 30 秒发送一次 ping
|
|
||||||
|
|
||||||
constructor({ ws, user }: WssMessageOptions) {
|
|
||||||
this.ws = ws;
|
|
||||||
this.user = user;
|
|
||||||
this.emitter = new EventEmitter();
|
|
||||||
this.startPing();
|
|
||||||
}
|
|
||||||
|
|
||||||
private startPing() {
|
|
||||||
this.stopPing();
|
|
||||||
this.pingTimer = setInterval(() => {
|
|
||||||
if (this.ws.readyState === WebSocket.OPEN) {
|
|
||||||
this.ws.ping();
|
|
||||||
} else {
|
|
||||||
this.stopPing();
|
|
||||||
}
|
|
||||||
}, this.PING_INTERVAL);
|
|
||||||
}
|
|
||||||
|
|
||||||
private stopPing() {
|
|
||||||
if (this.pingTimer) {
|
|
||||||
clearInterval(this.pingTimer);
|
|
||||||
this.pingTimer = undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
destroy() {
|
|
||||||
this.stopPing();
|
|
||||||
this.emitter.removeAllListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
async sendResponse(data: any) {
|
|
||||||
if (data.id) {
|
|
||||||
this.emitter.emit(data.id, data?.data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
async sendData(data: any, context?: any, opts?: { timeout?: number }) {
|
|
||||||
if (this.ws.readyState !== WebSocket.OPEN) {
|
|
||||||
return { code: 500, message: 'WebSocket is not open' };
|
|
||||||
}
|
|
||||||
const timeout = opts?.timeout || 10 * 6 * 1000; // 10 minutes
|
|
||||||
const id = nanoid();
|
|
||||||
const message = JSON.stringify({
|
|
||||||
id,
|
|
||||||
type: 'proxy',
|
|
||||||
data: {
|
|
||||||
message: data,
|
|
||||||
context: context || {},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
logger.info('ws-proxy sendData', message);
|
|
||||||
this.ws.send(message);
|
|
||||||
const msg = { path: data?.path, key: data?.key, id: data?.id };
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
const timer = setTimeout(() => {
|
|
||||||
resolve({
|
|
||||||
code: 500,
|
|
||||||
message: `运行超时,执行的id: ${id},参数是${JSON.stringify(msg)}`,
|
|
||||||
});
|
|
||||||
}, timeout);
|
|
||||||
this.emitter.once(id, (data: any) => {
|
|
||||||
resolve(data);
|
|
||||||
clearTimeout(timer);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
type WssMessageOptions = {
|
|
||||||
ws: WebSocket;
|
|
||||||
user?: string;
|
|
||||||
};
|
|
||||||
export class WsProxyManager {
|
|
||||||
wssMap: Map<string, WsMessage> = new Map();
|
|
||||||
PING_INTERVAL = 30000; // 30 秒检查一次连接状态
|
|
||||||
constructor(opts?: { pingInterval?: number }) {
|
|
||||||
if (opts?.pingInterval) {
|
|
||||||
this.PING_INTERVAL = opts.pingInterval;
|
|
||||||
}
|
|
||||||
this.checkConnceted();
|
|
||||||
}
|
|
||||||
register(id: string, opts?: { ws: WebSocket; user: string }) {
|
|
||||||
if (this.wssMap.has(id)) {
|
|
||||||
const value = this.wssMap.get(id);
|
|
||||||
if (value) {
|
|
||||||
value.ws.close();
|
|
||||||
value.destroy();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const [username, appId] = id.split('--');
|
|
||||||
const url = new URL(`/${username}/v1/${appId}`, 'https://kevisual.cn/');
|
|
||||||
console.log('WsProxyManager register', id, '访问地址', url.toString());
|
|
||||||
const value = new WsMessage({ ws: opts?.ws, user: opts?.user });
|
|
||||||
this.wssMap.set(id, value);
|
|
||||||
}
|
|
||||||
unregister(id: string) {
|
|
||||||
const value = this.wssMap.get(id);
|
|
||||||
if (value) {
|
|
||||||
value.ws.close();
|
|
||||||
value.destroy();
|
|
||||||
}
|
|
||||||
this.wssMap.delete(id);
|
|
||||||
}
|
|
||||||
getIds(beginWith?: string) {
|
|
||||||
if (beginWith) {
|
|
||||||
return Array.from(this.wssMap.keys()).filter(key => key.startsWith(beginWith));
|
|
||||||
}
|
|
||||||
return Array.from(this.wssMap.keys());
|
|
||||||
}
|
|
||||||
get(id: string) {
|
|
||||||
return this.wssMap.get(id);
|
|
||||||
}
|
|
||||||
checkConnceted() {
|
|
||||||
const that = this;
|
|
||||||
setTimeout(() => {
|
|
||||||
that.wssMap.forEach((value, key) => {
|
|
||||||
if (value.ws.readyState !== WebSocket.OPEN) {
|
|
||||||
logger.debug('ws not connected, unregister', key);
|
|
||||||
that.unregister(key);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
that.checkConnceted();
|
|
||||||
}, this.PING_INTERVAL);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
import { IncomingMessage, ServerResponse } from 'http';
|
|
||||||
import { wsProxyManager } from './index.ts';
|
|
||||||
|
|
||||||
import { App } from '@kevisual/router';
|
|
||||||
import { logger } from '../logger.ts';
|
|
||||||
import { getLoginUser } from '@/modules/auth.ts';
|
|
||||||
import { createStudioAppListHtml } from '../html/studio-app-list/index.ts';
|
|
||||||
import { omit } from 'es-toolkit';
|
|
||||||
|
|
||||||
type ProxyOptions = {
|
|
||||||
createNotFoundPage: (msg?: string) => any;
|
|
||||||
};
|
|
||||||
export const UserV1Proxy = async (req: IncomingMessage, res: ServerResponse, opts?: ProxyOptions) => {
|
|
||||||
const { url } = req;
|
|
||||||
const _url = new URL(url || '', `http://localhost`);
|
|
||||||
const { pathname, searchParams } = _url;
|
|
||||||
let [user, app, userAppKey] = pathname.split('/').slice(1);
|
|
||||||
if (!user || !app) {
|
|
||||||
opts?.createNotFoundPage?.('应用未找到');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await App.handleRequest(req, res);
|
|
||||||
const loginUser = await getLoginUser(req);
|
|
||||||
if (!loginUser) {
|
|
||||||
opts?.createNotFoundPage?.('没有登录');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isAdmin = loginUser.tokenUser?.username === user
|
|
||||||
|
|
||||||
if (!userAppKey) {
|
|
||||||
if (isAdmin) {
|
|
||||||
// 获取所有的管理员的应用列表
|
|
||||||
const ids = wsProxyManager.getIds(user + '--');
|
|
||||||
const html = createStudioAppListHtml({ user, appIds: ids, userAppKey });
|
|
||||||
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
||||||
res.end(html);
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
opts?.createNotFoundPage?.('没有访问应用权限');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!userAppKey.includes('--')) {
|
|
||||||
userAppKey = user + '--' + userAppKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: 如果不是管理员,是否需要添加其他人可以访问的逻辑?
|
|
||||||
if (!isAdmin) {
|
|
||||||
opts?.createNotFoundPage?.('没有访问应用权限');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!userAppKey.startsWith(user + '--')) {
|
|
||||||
userAppKey = user + '--' + userAppKey;
|
|
||||||
}
|
|
||||||
logger.debug('data', data);
|
|
||||||
const client = wsProxyManager.get(userAppKey);
|
|
||||||
const ids = wsProxyManager.getIds(user + '--');
|
|
||||||
if (!client) {
|
|
||||||
if (isAdmin) {
|
|
||||||
const html = createStudioAppListHtml({ user, appIds: ids, userAppKey });
|
|
||||||
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
||||||
res.end(html);
|
|
||||||
} else {
|
|
||||||
opts?.createNotFoundPage?.('应用访问失败');
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const path = searchParams.get('path');
|
|
||||||
if (!path) {
|
|
||||||
// 显示前端页面
|
|
||||||
const html = fetch('https://kevisual.cn/root/router-studio/index.html').then(res => res.text());
|
|
||||||
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
||||||
res.end(await html);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
let message: any = data;
|
|
||||||
if (!isAdmin) {
|
|
||||||
message = omit(data, ['token', 'cookies']);
|
|
||||||
}
|
|
||||||
const value = await client.sendData(message, {
|
|
||||||
state: { tokenUser: omit(loginUser.tokenUser, ['oauthExpand']) },
|
|
||||||
});
|
|
||||||
if (value) {
|
|
||||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
||||||
res.end(JSON.stringify(value));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
opts?.createNotFoundPage?.('应用未启动');
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
79
src/route.ts
79
src/route.ts
@@ -4,6 +4,25 @@ import { app } from './app.ts';
|
|||||||
import type { App } from '@kevisual/router';
|
import type { App } from '@kevisual/router';
|
||||||
import { User } from './models/user.ts';
|
import { User } from './models/user.ts';
|
||||||
import { createCookie, getSomeInfoFromReq } from './routes/user/me.ts';
|
import { createCookie, getSomeInfoFromReq } from './routes/user/me.ts';
|
||||||
|
import { toJSONSchema } from '@kevisual/router';
|
||||||
|
import { pick } from 'es-toolkit';
|
||||||
|
/**
|
||||||
|
* 验证上下文中的 App ID 是否与指定的 App ID 匹配
|
||||||
|
* @param {any} ctx - 上下文对象,可能包含 appId 属性
|
||||||
|
* @param {string} appId - 需要验证的目标 App ID
|
||||||
|
* @returns {boolean} 如果 ctx 中包含 appId 且匹配则返回 true,否则返回 false
|
||||||
|
* @throws {Error} 如果 ctx 中包含 appId 但不匹配,则抛出 403 错误
|
||||||
|
*/
|
||||||
|
const checkAppId = (ctx: any, appId: string) => {
|
||||||
|
const _appId = ctx?.app?.appId;
|
||||||
|
if (_appId) {
|
||||||
|
if (_appId !== appId) {
|
||||||
|
ctx.throw(403, 'Invalid App ID');
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 添加auth中间件, 用于验证token
|
* 添加auth中间件, 用于验证token
|
||||||
@@ -16,11 +35,17 @@ export const addAuth = (app: App) => {
|
|||||||
app
|
app
|
||||||
.route({
|
.route({
|
||||||
path: 'auth',
|
path: 'auth',
|
||||||
id: 'auth',
|
rid: 'auth',
|
||||||
description: '验证token,必须成功, 错误返回401,正确赋值到ctx.state.tokenUser',
|
description: '验证token,必须成功, 错误返回401,正确赋值到ctx.state.tokenUser',
|
||||||
})
|
})
|
||||||
.define(async (ctx) => {
|
.define(async (ctx) => {
|
||||||
const token = ctx.query.token;
|
const token = ctx.query.token;
|
||||||
|
// if (checkAppId(ctx, app.appId)) {
|
||||||
|
// ctx.state.tokenUser = {
|
||||||
|
// username: 'default',
|
||||||
|
// }
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
// 已经有用户信息则直接返回,不需要重复验证
|
// 已经有用户信息则直接返回,不需要重复验证
|
||||||
if (ctx.state.tokenUser) {
|
if (ctx.state.tokenUser) {
|
||||||
return;
|
return;
|
||||||
@@ -46,10 +71,16 @@ export const addAuth = (app: App) => {
|
|||||||
.route({
|
.route({
|
||||||
path: 'auth',
|
path: 'auth',
|
||||||
key: 'can',
|
key: 'can',
|
||||||
id: 'auth-can',
|
rid: 'auth-can',
|
||||||
description: '验证token,可以不成功,错误不返回401,正确赋值到ctx.state.tokenUser,失败赋值null',
|
description: '验证token,可以不成功,错误不返回401,正确赋值到ctx.state.tokenUser,失败赋值null',
|
||||||
})
|
})
|
||||||
.define(async (ctx) => {
|
.define(async (ctx) => {
|
||||||
|
// if (checkAppId(ctx, app.appId)) {
|
||||||
|
// ctx.state.tokenUser = {
|
||||||
|
// username: 'default',
|
||||||
|
// }
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
// 已经有用户信息则直接返回,不需要重复验证
|
// 已经有用户信息则直接返回,不需要重复验证
|
||||||
if (ctx.state.tokenUser) {
|
if (ctx.state.tokenUser) {
|
||||||
return;
|
return;
|
||||||
@@ -76,12 +107,18 @@ app
|
|||||||
.route({
|
.route({
|
||||||
path: 'auth',
|
path: 'auth',
|
||||||
key: 'admin',
|
key: 'admin',
|
||||||
id: 'auth-admin',
|
rid: 'auth-admin',
|
||||||
isDebug: true,
|
isDebug: true,
|
||||||
middleware: ['auth'],
|
middleware: ['auth'],
|
||||||
description: '验证token,必须是admin用户, 错误返回403,正确赋值到ctx.state.tokenAdmin',
|
description: '验证token,必须是admin用户, 错误返回403,正确赋值到ctx.state.tokenAdmin',
|
||||||
})
|
})
|
||||||
.define(async (ctx) => {
|
.define(async (ctx) => {
|
||||||
|
// if (checkAppId(ctx, app.appId)) {
|
||||||
|
// ctx.state.tokenUser = {
|
||||||
|
// username: 'default',
|
||||||
|
// }
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
const tokenUser = ctx.state.tokenUser;
|
const tokenUser = ctx.state.tokenUser;
|
||||||
if (!tokenUser) {
|
if (!tokenUser) {
|
||||||
ctx.throw(401, 'No User For authorized');
|
ctx.throw(401, 'No User For authorized');
|
||||||
@@ -117,7 +154,7 @@ app
|
|||||||
.route({
|
.route({
|
||||||
path: 'auth-check',
|
path: 'auth-check',
|
||||||
key: 'admin',
|
key: 'admin',
|
||||||
id: 'check-auth-admin',
|
rid: 'check-auth-admin',
|
||||||
middleware: ['auth'],
|
middleware: ['auth'],
|
||||||
})
|
})
|
||||||
.define(async (ctx) => {
|
.define(async (ctx) => {
|
||||||
@@ -159,37 +196,9 @@ app
|
|||||||
})
|
})
|
||||||
.addTo(app);
|
.addTo(app);
|
||||||
|
|
||||||
app
|
app.createRouteList({
|
||||||
.route({
|
middleware: ['auth-can']
|
||||||
path: 'router',
|
})
|
||||||
key: 'list',
|
|
||||||
description: '列出所有的当前的可请求的路由信息',
|
|
||||||
middleware: ['auth-can']
|
|
||||||
})
|
|
||||||
.define(async (ctx) => {
|
|
||||||
const tokenUser = ctx.state.tokenUser;
|
|
||||||
let isUser = !!tokenUser;
|
|
||||||
ctx.body = {
|
|
||||||
list: app.router.routes.filter(item => {
|
|
||||||
if (item.id === 'auth' || item.id === 'auth-can' || item.id === 'check-auth-admin' || item.id === 'auth-admin') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}).map((item) => {
|
|
||||||
return {
|
|
||||||
id: item.id,
|
|
||||||
path: item.path,
|
|
||||||
key: item.key,
|
|
||||||
description: item.description,
|
|
||||||
middeleware: item.middleware,
|
|
||||||
metadata: item.metadata,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
isUser
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.addTo(app);
|
|
||||||
|
|
||||||
|
|
||||||
app.route({
|
app.route({
|
||||||
path: 'system',
|
path: 'system',
|
||||||
|
|||||||
@@ -3,10 +3,12 @@ import { router } from './router.ts';
|
|||||||
import { handleRequest as PageProxy } from './page-proxy.ts';
|
import { handleRequest as PageProxy } from './page-proxy.ts';
|
||||||
|
|
||||||
import './routes/jwks.ts'
|
import './routes/jwks.ts'
|
||||||
|
import './routes/ai/openai.ts'
|
||||||
|
|
||||||
const simpleAppsPrefixs = [
|
const simpleAppsPrefixs = [
|
||||||
"/api/wxmsg",
|
"/api/wxmsg",
|
||||||
"/api/convex/"
|
"/api/convex/",
|
||||||
|
"/api/chat/completions"
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -12,11 +12,12 @@ import { UserPermission } from '@kevisual/permission';
|
|||||||
import { getLoginUser } from '../modules/auth.ts';
|
import { getLoginUser } from '../modules/auth.ts';
|
||||||
import { rediretHome } from '../modules/user-app/index.ts';
|
import { rediretHome } from '../modules/user-app/index.ts';
|
||||||
import { logger } from '../modules/logger.ts';
|
import { logger } from '../modules/logger.ts';
|
||||||
import { UserV1Proxy } from '../modules/ws-proxy/proxy.ts';
|
import { UserV1Proxy } from '../modules/v1-ws-proxy/proxy.ts';
|
||||||
import { UserV3Proxy } from '@/modules/v3/index.ts';
|
import { UserV3Proxy } from '@/modules/v3/index.ts';
|
||||||
import { hasBadUser, userIsBanned, appIsBanned, userPathIsBanned } from '@/modules/off/index.ts';
|
import { hasBadUser, userIsBanned, appIsBanned, userPathIsBanned } from '@/modules/off/index.ts';
|
||||||
import { robotsTxt } from '@/modules/html/index.ts';
|
import { robotsTxt } from '@/modules/html/index.ts';
|
||||||
import { isBun } from '@/utils/get-engine.ts';
|
import { isBun } from '@/utils/get-engine.ts';
|
||||||
|
import { N5Proxy } from '@/modules/n5/index.ts';
|
||||||
const domain = config?.proxy?.domain;
|
const domain = config?.proxy?.domain;
|
||||||
const allowedOrigins = config?.proxy?.allowedOrigin || [];
|
const allowedOrigins = config?.proxy?.allowedOrigin || [];
|
||||||
|
|
||||||
@@ -149,6 +150,11 @@ export const handleRequest = async (req: http.IncomingMessage, res: http.ServerR
|
|||||||
const isDev = isLocalhost(dns?.hostName);
|
const isDev = isLocalhost(dns?.hostName);
|
||||||
if (isDev) {
|
if (isDev) {
|
||||||
console.debug('开发环境访问:', req.url, 'Host:', dns.hostName);
|
console.debug('开发环境访问:', req.url, 'Host:', dns.hostName);
|
||||||
|
if (req.url === '/') {
|
||||||
|
res.writeHead(302, { Location: '/root/router-studio/' });
|
||||||
|
res.end();
|
||||||
|
return;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
if (isIpv4OrIpv6(dns.hostName)) {
|
if (isIpv4OrIpv6(dns.hostName)) {
|
||||||
// 打印出 req.url 和错误信息
|
// 打印出 req.url 和错误信息
|
||||||
@@ -183,7 +189,7 @@ export const handleRequest = async (req: http.IncomingMessage, res: http.ServerR
|
|||||||
/**
|
/**
|
||||||
* url是pathname的路径
|
* url是pathname的路径
|
||||||
*/
|
*/
|
||||||
const url = pathname;
|
const url = pathname || '';
|
||||||
if (!domainApp && noProxyUrl.includes(url)) {
|
if (!domainApp && noProxyUrl.includes(url)) {
|
||||||
if (url === '/') {
|
if (url === '/') {
|
||||||
rediretHome(req, res);
|
rediretHome(req, res);
|
||||||
@@ -255,6 +261,11 @@ export const handleRequest = async (req: http.IncomingMessage, res: http.ServerR
|
|||||||
createNotFoundPage,
|
createNotFoundPage,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (user === 'n5') {
|
||||||
|
return N5Proxy(req, res, {
|
||||||
|
createNotFoundPage,
|
||||||
|
});
|
||||||
|
}
|
||||||
if (user !== 'api' && app === 'v3') {
|
if (user !== 'api' && app === 'v3') {
|
||||||
return UserV3Proxy(req, res, {
|
return UserV3Proxy(req, res, {
|
||||||
createNotFoundPage,
|
createNotFoundPage,
|
||||||
@@ -306,13 +317,14 @@ export const handleRequest = async (req: http.IncomingMessage, res: http.ServerR
|
|||||||
const indexFile = isExist.indexFilePath; // 已经必定存在了
|
const indexFile = isExist.indexFilePath; // 已经必定存在了
|
||||||
try {
|
try {
|
||||||
let appFileUrl: string;
|
let appFileUrl: string;
|
||||||
if (domainApp) {
|
|
||||||
appFileUrl = (url + '').replace(`/`, '');
|
appFileUrl = url.replace(`/${user}/${app}/`, '');
|
||||||
} else {
|
|
||||||
appFileUrl = (url + '').replace(`/${user}/${app}/`, '');
|
|
||||||
}
|
|
||||||
appFileUrl = decodeURIComponent(appFileUrl); // Decode URL components
|
appFileUrl = decodeURIComponent(appFileUrl); // Decode URL components
|
||||||
let appFile = await userApp.getFile(appFileUrl);
|
let appFile = await userApp.getFile(appFileUrl);
|
||||||
|
if (!appFile && domainApp) {
|
||||||
|
const domainAppFileUrl = url.replace(`/`, '');
|
||||||
|
appFile = await userApp.getFile(domainAppFileUrl);
|
||||||
|
}
|
||||||
if (!appFile && url.endsWith('/')) {
|
if (!appFile && url.endsWith('/')) {
|
||||||
appFile = await userApp.getFile(appFileUrl + 'index.html');
|
appFile = await userApp.getFile(appFileUrl + 'index.html');
|
||||||
} else if (!appFile && !url.endsWith('/')) {
|
} else if (!appFile && !url.endsWith('/')) {
|
||||||
|
|||||||
51
src/routes-simple/routes/ai/anthropic.ts
Normal file
51
src/routes-simple/routes/ai/anthropic.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import { router } from '@/app.ts';
|
||||||
|
import http from 'node:http';
|
||||||
|
import https from 'node:https';
|
||||||
|
import { pipeProxyReq, pipeProxyRes } from '@/modules/fm-manager/index.ts';
|
||||||
|
import { useKey } from '@kevisual/context';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO: 由于目前没有找到合适的开源 Anthropic 兼容实现,暂时先把 /v1/messages 请求代理到配置的 OpenAI 兼容目标地址,等后续有了合适的 Anthropic 兼容实现再改回来
|
||||||
|
* 代理 /v1/messages 请求到配置的 OpenAI 兼容目标地址
|
||||||
|
* 配置项: config.OPENAI_BASE_URL,例如 http://localhost:11434/v1
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
router.all("/v1/messages", async (req, res) => {
|
||||||
|
const targetUrl = new URL('https://api.cnb.cool/kevisual/kevisual/-/ai/chat/messages');
|
||||||
|
const token = useKey('CNB_API_KEY');
|
||||||
|
// 收集并转发请求头(排除 host 和 authorization,用自己的 token 替换)
|
||||||
|
const headers: Record<string, any> = {};
|
||||||
|
for (const [key, value] of Object.entries(req.headers)) {
|
||||||
|
if (key.toLowerCase() !== 'host' && key.toLowerCase() !== 'authorization') {
|
||||||
|
headers[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (token) {
|
||||||
|
headers['authorization'] = `Bearer ${token}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isHttps = targetUrl.protocol === 'https:';
|
||||||
|
const protocol = isHttps ? https : http;
|
||||||
|
|
||||||
|
const options: http.RequestOptions = {
|
||||||
|
hostname: targetUrl.hostname,
|
||||||
|
port: targetUrl.port || (isHttps ? 443 : 80),
|
||||||
|
path: targetUrl.pathname + (targetUrl.search || ''),
|
||||||
|
method: req.method,
|
||||||
|
headers,
|
||||||
|
...(isHttps ? { rejectUnauthorized: false } : {}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const proxyReq = protocol.request(options, (proxyRes) => {
|
||||||
|
res.writeHead(proxyRes.statusCode || 200, proxyRes.headers);
|
||||||
|
pipeProxyRes(proxyRes, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
proxyReq.on('error', (err) => {
|
||||||
|
console.error('[anthropic proxy] error:', err.message);
|
||||||
|
res.writeHead(502, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({ error: err.message }));
|
||||||
|
});
|
||||||
|
|
||||||
|
pipeProxyReq(req, proxyReq, res);
|
||||||
|
});
|
||||||
50
src/routes-simple/routes/ai/openai.ts
Normal file
50
src/routes-simple/routes/ai/openai.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import { router } from '@/app.ts';
|
||||||
|
import http from 'node:http';
|
||||||
|
import https from 'node:https';
|
||||||
|
import { pipeProxyReq, pipeProxyRes } from '@/modules/fm-manager/index.ts';
|
||||||
|
import { useKey } from '@kevisual/context';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 代理 /api/chat/completions 请求到配置的 OpenAI 兼容目标地址
|
||||||
|
* 配置项: config.OPENAI_BASE_URL,例如 http://localhost:11434/v1
|
||||||
|
*/
|
||||||
|
router.all("/api/chat/completions", async (req, res) => {
|
||||||
|
const targetUrl = new URL('https://api.cnb.cool/kevisual/kevisual/-/ai/chat/completions');
|
||||||
|
|
||||||
|
const token = useKey('CNB_API_KEY');
|
||||||
|
// 收集并转发请求头(排除 host 和 authorization,用自己的 token 替换)
|
||||||
|
const headers: Record<string, any> = {};
|
||||||
|
for (const [key, value] of Object.entries(req.headers)) {
|
||||||
|
if (key.toLowerCase() !== 'host' && key.toLowerCase() !== 'authorization') {
|
||||||
|
headers[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (token) {
|
||||||
|
headers['authorization'] = `Bearer ${token}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isHttps = targetUrl.protocol === 'https:';
|
||||||
|
const protocol = isHttps ? https : http;
|
||||||
|
|
||||||
|
const options: http.RequestOptions = {
|
||||||
|
hostname: targetUrl.hostname,
|
||||||
|
port: targetUrl.port || (isHttps ? 443 : 80),
|
||||||
|
path: targetUrl.pathname + (targetUrl.search || ''),
|
||||||
|
method: req.method,
|
||||||
|
headers,
|
||||||
|
...(isHttps ? { rejectUnauthorized: false } : {}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const proxyReq = protocol.request(options, (proxyRes) => {
|
||||||
|
res.writeHead(proxyRes.statusCode || 200, proxyRes.headers);
|
||||||
|
pipeProxyRes(proxyRes, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
proxyReq.on('error', (err) => {
|
||||||
|
console.error('[openai proxy] error:', err.message);
|
||||||
|
res.writeHead(502, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({ error: err.message }));
|
||||||
|
});
|
||||||
|
|
||||||
|
pipeProxyReq(req, proxyReq, res);
|
||||||
|
});
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { router } from '@/app.ts'
|
import { router } from '@/app.ts'
|
||||||
import { manager } from '@/modules/jwks/index.ts'
|
import { manager } from '@/auth/models/jwks-manager.ts'
|
||||||
router.all('/api/convex/jwks.json', async (req, res) => {
|
router.all('/api/convex/jwks.json', async (req, res) => {
|
||||||
const jwks = await manager.getJWKS()
|
const jwks = await manager.getJWKS()
|
||||||
res.setHeader('Content-Type', 'application/json');
|
res.setHeader('Content-Type', 'application/json');
|
||||||
|
|||||||
@@ -1,16 +1,23 @@
|
|||||||
import { app, db, schema } from '@/app.ts';
|
import { app, db, schema } from '@/app.ts';
|
||||||
import { App, AppData } from '../module/app-drizzle.ts';
|
|
||||||
import { AppDomain, AppDomainHelper } from '../module/app-domain-drizzle.ts';
|
import { AppDomain, AppDomainHelper } from '../module/app-domain-drizzle.ts';
|
||||||
import { eq, and } from 'drizzle-orm';
|
import { eq, and } from 'drizzle-orm';
|
||||||
import { randomUUID } from 'crypto';
|
import z from 'zod';
|
||||||
|
|
||||||
app
|
app
|
||||||
.route({
|
.route({
|
||||||
path: 'app',
|
path: 'app',
|
||||||
key: 'getDomainApp',
|
key: 'getDomainApp',
|
||||||
|
description: '根据域名获取应用信息',
|
||||||
|
metadata: {
|
||||||
|
args: {
|
||||||
|
data: z.object({
|
||||||
|
domain: z.string().describe('域名'),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.define(async (ctx) => {
|
.define(async (ctx) => {
|
||||||
const { domain } = ctx.query.data;
|
const { domain } = ctx.args.data;
|
||||||
const domainInfos = await db.select().from(schema.kvAppDomain).where(eq(schema.kvAppDomain.domain, domain)).limit(1);
|
const domainInfos = await db.select().from(schema.kvAppDomain).where(eq(schema.kvAppDomain.domain, domain)).limit(1);
|
||||||
const domainInfo = domainInfos[0];
|
const domainInfo = domainInfos[0];
|
||||||
if (!domainInfo || !domainInfo.appId) {
|
if (!domainInfo || !domainInfo.appId) {
|
||||||
@@ -31,15 +38,24 @@ app
|
|||||||
path: 'app-domain',
|
path: 'app-domain',
|
||||||
key: 'create',
|
key: 'create',
|
||||||
middleware: ['auth'],
|
middleware: ['auth'],
|
||||||
|
description: '创建应用域名绑定',
|
||||||
|
metadata: {
|
||||||
|
args: {
|
||||||
|
data: z.object({
|
||||||
|
domain: z.string().describe('域名'),
|
||||||
|
appId: z.string().describe('应用ID'),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.define(async (ctx) => {
|
.define(async (ctx) => {
|
||||||
const tokenUser = ctx.state.tokenUser;
|
const tokenUser = ctx.state.tokenUser;
|
||||||
const uid = tokenUser.uid;
|
const uid = tokenUser.uid;
|
||||||
const { domain, appId } = ctx.query.data || {};
|
const { domain, appId } = ctx.args.data || {};
|
||||||
if (!domain || !appId) {
|
if (!domain || !appId) {
|
||||||
ctx.throw(400, 'domain and appId are required');
|
ctx.throw(400, 'domain and appId are required');
|
||||||
}
|
}
|
||||||
const newDomains = await db.insert(schema.kvAppDomain).values({ id: randomUUID(), domain, appId, uid }).returning();
|
const newDomains = await db.insert(schema.kvAppDomain).values({ domain, appId, uid }).returning();
|
||||||
const domainInfo = newDomains[0];
|
const domainInfo = newDomains[0];
|
||||||
ctx.body = domainInfo;
|
ctx.body = domainInfo;
|
||||||
return ctx;
|
return ctx;
|
||||||
@@ -51,11 +67,21 @@ app
|
|||||||
path: 'app-domain',
|
path: 'app-domain',
|
||||||
key: 'update',
|
key: 'update',
|
||||||
middleware: ['auth'],
|
middleware: ['auth'],
|
||||||
|
metadata: {
|
||||||
|
args: {
|
||||||
|
data: z.object({
|
||||||
|
id: z.string().optional().describe('域名ID'),
|
||||||
|
domain: z.string().optional().describe('域名'),
|
||||||
|
appId: z.string().optional().describe('应用ID'),
|
||||||
|
status: z.string().describe('状态'),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.define(async (ctx) => {
|
.define(async (ctx) => {
|
||||||
const tokenUser = ctx.state.tokenUser;
|
const tokenUser = ctx.state.tokenUser;
|
||||||
const uid = tokenUser.uid;
|
const uid = tokenUser.uid;
|
||||||
const { id, domain, appId, status } = ctx.query.data || {};
|
const { id, domain, appId, status } = ctx.args.data || {};
|
||||||
if (!domain && !id) {
|
if (!domain && !id) {
|
||||||
ctx.throw(400, 'domain and id are required at least one');
|
ctx.throw(400, 'domain and id are required at least one');
|
||||||
}
|
}
|
||||||
@@ -80,7 +106,7 @@ app
|
|||||||
if (domainInfo.uid !== uid) {
|
if (domainInfo.uid !== uid) {
|
||||||
ctx.throw(403, 'domain must be owned by the user');
|
ctx.throw(403, 'domain must be owned by the user');
|
||||||
}
|
}
|
||||||
if (!AppDomainHelper.checkCanUpdateStatus(domainInfo.status!, status)) {
|
if (!AppDomainHelper.checkCanUpdateStatus(domainInfo.status!, status as any)) {
|
||||||
ctx.throw(400, 'domain status can not be updated');
|
ctx.throw(400, 'domain status can not be updated');
|
||||||
}
|
}
|
||||||
const updateData: any = {};
|
const updateData: any = {};
|
||||||
|
|||||||
@@ -1,15 +1,22 @@
|
|||||||
import { app, db, schema } from '@/app.ts';
|
import { app, db, schema } from '@/app.ts';
|
||||||
import { AppDomain, AppDomainHelper } from '../module/app-domain-drizzle.ts';
|
import { AppDomain, AppDomainHelper } from '../module/app-domain-drizzle.ts';
|
||||||
import { App } from '../module/app-drizzle.ts';
|
import { eq } from 'drizzle-orm';
|
||||||
import { CustomError } from '@kevisual/router';
|
import z from 'zod';
|
||||||
import { eq, or } from 'drizzle-orm';
|
|
||||||
import { randomUUID } from 'crypto';
|
|
||||||
|
|
||||||
app
|
app
|
||||||
.route({
|
.route({
|
||||||
path: 'app.domain.manager',
|
path: 'app_domain_manager',
|
||||||
key: 'list',
|
key: 'list',
|
||||||
|
description: '获取域名列表,支持分页',
|
||||||
middleware: ['auth-admin'],
|
middleware: ['auth-admin'],
|
||||||
|
metadata: {
|
||||||
|
args: {
|
||||||
|
data: z.object({
|
||||||
|
page: z.number().optional(),
|
||||||
|
pageSize: z.number().optional(),
|
||||||
|
}).optional()
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.define(async (ctx) => {
|
.define(async (ctx) => {
|
||||||
const { page = 1, pageSize = 999 } = ctx.query.data || {};
|
const { page = 1, pageSize = 999 } = ctx.query.data || {};
|
||||||
@@ -26,9 +33,21 @@ app
|
|||||||
|
|
||||||
app
|
app
|
||||||
.route({
|
.route({
|
||||||
path: 'app.domain.manager',
|
path: 'app_domain_manager',
|
||||||
key: 'update',
|
key: 'update',
|
||||||
|
description: '更新一个域名的信息',
|
||||||
middleware: ['auth-admin'],
|
middleware: ['auth-admin'],
|
||||||
|
metadata: {
|
||||||
|
args: {
|
||||||
|
data: z.object({
|
||||||
|
id: z.string().optional(),
|
||||||
|
domain: z.string(),
|
||||||
|
appId: z.string().optional(),
|
||||||
|
status: z.enum(['active', 'inactive']).optional(),
|
||||||
|
data: z.record(z.string(), z.any()).optional(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.define(async (ctx) => {
|
.define(async (ctx) => {
|
||||||
const { domain, data, id, ...rest } = ctx.query.data || {};
|
const { domain, data, id, ...rest } = ctx.query.data || {};
|
||||||
@@ -58,7 +77,7 @@ app
|
|||||||
try {
|
try {
|
||||||
if (!domainInfo) {
|
if (!domainInfo) {
|
||||||
await checkAppId();
|
await checkAppId();
|
||||||
const newDomains = await db.insert(schema.kvAppDomain).values({ id: randomUUID(), domain, data: {}, ...rest }).returning();
|
const newDomains = await db.insert(schema.kvAppDomain).values({ domain, data: {}, ...rest }).returning();
|
||||||
domainInfo = newDomains[0];
|
domainInfo = newDomains[0];
|
||||||
} else {
|
} else {
|
||||||
if (rest.status && domainInfo.status !== rest.status) {
|
if (rest.status && domainInfo.status !== rest.status) {
|
||||||
@@ -95,9 +114,18 @@ app
|
|||||||
|
|
||||||
app
|
app
|
||||||
.route({
|
.route({
|
||||||
path: 'app.domain.manager',
|
path: 'app_domain_manager',
|
||||||
key: 'delete',
|
key: 'delete',
|
||||||
|
description: '删除一个域名',
|
||||||
middleware: ['auth-admin'],
|
middleware: ['auth-admin'],
|
||||||
|
metadata: {
|
||||||
|
args: {
|
||||||
|
data: z.object({
|
||||||
|
id: z.string().optional(),
|
||||||
|
domain: z.string().optional(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.define(async (ctx) => {
|
.define(async (ctx) => {
|
||||||
const { id, domain } = ctx.query.data || {};
|
const { id, domain } = ctx.query.data || {};
|
||||||
@@ -117,9 +145,18 @@ app
|
|||||||
|
|
||||||
app
|
app
|
||||||
.route({
|
.route({
|
||||||
path: 'app.domain.manager',
|
path: 'app_domain_manager',
|
||||||
key: 'get',
|
key: 'get',
|
||||||
|
description: '获取域名信息,可以通过id或者domain进行查询',
|
||||||
middleware: ['auth-admin'],
|
middleware: ['auth-admin'],
|
||||||
|
metadata: {
|
||||||
|
args: {
|
||||||
|
data: z.object({
|
||||||
|
id: z.string().optional(),
|
||||||
|
domain: z.string().optional(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.define(async (ctx) => {
|
.define(async (ctx) => {
|
||||||
const { id, domain } = ctx.query.data || {};
|
const { id, domain } = ctx.query.data || {};
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import { App as AppType, AppList, AppData } from './module/app-drizzle.ts';
|
import { App as AppType, AppList, AppData } from './module/app-drizzle.ts';
|
||||||
import { app, db, schema } from '@/app.ts';
|
import { app, db, oss, schema } from '@/app.ts';
|
||||||
import { uniqBy } from 'es-toolkit';
|
import { uniqBy } from 'es-toolkit';
|
||||||
import { getUidByUsername, prefixFix } from './util.ts';
|
import { getUidByUsername, prefixFix } from './util.ts';
|
||||||
import { deleteFiles, getMinioListAndSetToAppList } from '../file/index.ts';
|
import { deleteFiles, getMinioList, getMinioListAndSetToAppList } from '../file/index.ts';
|
||||||
import { setExpire } from './revoke.ts';
|
import { setExpire } from './revoke.ts';
|
||||||
import { User } from '@/models/user.ts';
|
import { User } from '@/models/user.ts';
|
||||||
import { callDetectAppVersion } from './export.ts';
|
import { callDetectAppVersion } from './export.ts';
|
||||||
import { eq, and, desc } from 'drizzle-orm';
|
import { eq, and, desc } from 'drizzle-orm';
|
||||||
import { randomUUID } from 'crypto';
|
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
import { logger } from '@/modules/logger.ts';
|
||||||
app
|
app
|
||||||
.route({
|
.route({
|
||||||
path: 'app',
|
path: 'app',
|
||||||
@@ -47,6 +47,16 @@ app
|
|||||||
key: 'get',
|
key: 'get',
|
||||||
middleware: ['auth'],
|
middleware: ['auth'],
|
||||||
description: '获取应用详情,可以通过id,或者key+version来获取',
|
description: '获取应用详情,可以通过id,或者key+version来获取',
|
||||||
|
metadata: {
|
||||||
|
args: {
|
||||||
|
data: z.object({
|
||||||
|
id: z.string().optional(),
|
||||||
|
version: z.string().optional(),
|
||||||
|
key: z.string().optional(),
|
||||||
|
create: z.boolean().optional().describe('如果应用版本不存在,是否创建应用版本记录,默认false'),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.define(async (ctx) => {
|
.define(async (ctx) => {
|
||||||
console.log('get app manager called');
|
console.log('get app manager called');
|
||||||
@@ -70,7 +80,6 @@ app
|
|||||||
}
|
}
|
||||||
if (!appListModel && create) {
|
if (!appListModel && create) {
|
||||||
const newApps = await db.insert(schema.kvAppList).values({
|
const newApps = await db.insert(schema.kvAppList).values({
|
||||||
id: randomUUID(),
|
|
||||||
key,
|
key,
|
||||||
version,
|
version,
|
||||||
uid: tokenUser.id,
|
uid: tokenUser.id,
|
||||||
@@ -84,7 +93,6 @@ app
|
|||||||
const appModel = appModels[0];
|
const appModel = appModels[0];
|
||||||
if (!appModel) {
|
if (!appModel) {
|
||||||
await db.insert(schema.kvApp).values({
|
await db.insert(schema.kvApp).values({
|
||||||
id: randomUUID(),
|
|
||||||
key,
|
key,
|
||||||
uid: tokenUser.id,
|
uid: tokenUser.id,
|
||||||
user: tokenUser.username,
|
user: tokenUser.username,
|
||||||
@@ -108,7 +116,7 @@ app
|
|||||||
if (!appListModel) {
|
if (!appListModel) {
|
||||||
ctx.throw('app not found');
|
ctx.throw('app not found');
|
||||||
}
|
}
|
||||||
console.log('get app', appListModel.id, appListModel.key, appListModel.version);
|
logger.debug('get app', appListModel.id, appListModel.key, appListModel.version);
|
||||||
ctx.body = prefixFix(appListModel, tokenUser.username);
|
ctx.body = prefixFix(appListModel, tokenUser.username);
|
||||||
})
|
})
|
||||||
.addTo(app);
|
.addTo(app);
|
||||||
@@ -145,7 +153,7 @@ app
|
|||||||
if (!rest.key) {
|
if (!rest.key) {
|
||||||
ctx.throw('key is required');
|
ctx.throw('key is required');
|
||||||
}
|
}
|
||||||
const newApps = await db.insert(schema.kvAppList).values({ id: randomUUID(), data, ...rest, uid: tokenUser.id }).returning();
|
const newApps = await db.insert(schema.kvAppList).values({ data, ...rest, uid: tokenUser.id }).returning();
|
||||||
ctx.body = newApps[0];
|
ctx.body = newApps[0];
|
||||||
return ctx;
|
return ctx;
|
||||||
})
|
})
|
||||||
@@ -233,7 +241,6 @@ app
|
|||||||
if (!am) {
|
if (!am) {
|
||||||
appIsNew = true;
|
appIsNew = true;
|
||||||
const newAms = await db.insert(schema.kvApp).values({
|
const newAms = await db.insert(schema.kvApp).values({
|
||||||
id: randomUUID(),
|
|
||||||
user: userPrefix,
|
user: userPrefix,
|
||||||
key: appKey,
|
key: appKey,
|
||||||
uid,
|
uid,
|
||||||
@@ -255,7 +262,6 @@ app
|
|||||||
let app = apps[0];
|
let app = apps[0];
|
||||||
if (!app) {
|
if (!app) {
|
||||||
const newApps = await db.insert(schema.kvAppList).values({
|
const newApps = await db.insert(schema.kvAppList).values({
|
||||||
id: randomUUID(),
|
|
||||||
key: appKey,
|
key: appKey,
|
||||||
version,
|
version,
|
||||||
uid: uid,
|
uid: uid,
|
||||||
@@ -294,6 +300,17 @@ app
|
|||||||
key: 'publish',
|
key: 'publish',
|
||||||
middleware: ['auth'],
|
middleware: ['auth'],
|
||||||
description: '发布应用,将某个版本的应用设置为当前应用的版本',
|
description: '发布应用,将某个版本的应用设置为当前应用的版本',
|
||||||
|
metadata: {
|
||||||
|
args: {
|
||||||
|
data: z.object({
|
||||||
|
id: z.string().optional().describe('应用版本记录id'),
|
||||||
|
username: z.string().optional().describe('用户名,默认为当前用户'),
|
||||||
|
appKey: z.string().optional().describe('应用的唯一标识'),
|
||||||
|
version: z.string().describe('应用版本'),
|
||||||
|
detect: z.boolean().optional().describe('是否自动检测版本列表,默认false'),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.define(async (ctx) => {
|
.define(async (ctx) => {
|
||||||
const tokenUser = ctx.state.tokenUser;
|
const tokenUser = ctx.state.tokenUser;
|
||||||
@@ -351,8 +368,9 @@ app
|
|||||||
if (!am) {
|
if (!am) {
|
||||||
ctx.throw('app 未发现');
|
ctx.throw('app 未发现');
|
||||||
}
|
}
|
||||||
if (!isDetect) {
|
const amData = am.data as AppData;
|
||||||
const amData = am.data as AppData;
|
if (version !== am.version) {
|
||||||
|
// 发布版本和当前版本不一致
|
||||||
await db.update(schema.kvApp)
|
await db.update(schema.kvApp)
|
||||||
.set({ data: { ...amData, files }, version: appList.version, updatedAt: new Date().toISOString() })
|
.set({ data: { ...amData, files }, version: appList.version, updatedAt: new Date().toISOString() })
|
||||||
.where(eq(schema.kvApp.id, am.id));
|
.where(eq(schema.kvApp.id, am.id));
|
||||||
@@ -368,11 +386,88 @@ app
|
|||||||
})
|
})
|
||||||
.addTo(app);
|
.addTo(app);
|
||||||
|
|
||||||
|
app.route({
|
||||||
|
path: 'app',
|
||||||
|
key: 'publishDirectory',
|
||||||
|
middleware: ['auth'],
|
||||||
|
description: '发布应用目录,将某个版本的应用目录设置为当前应用的版本',
|
||||||
|
metadata: {
|
||||||
|
args: {
|
||||||
|
data: z.object({
|
||||||
|
key: z.string().describe('应用的唯一标识'),
|
||||||
|
version: z.string().describe('应用版本'),
|
||||||
|
directory: z.string().describe('应用目录'),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).define(async (ctx) => {
|
||||||
|
const tokenUser = ctx.state.tokenUser;
|
||||||
|
const { key, version, directory } = ctx.query.data;
|
||||||
|
if (!key || !version || !directory) {
|
||||||
|
ctx.throw('key, version and directory are required');
|
||||||
|
}
|
||||||
|
const username = tokenUser.username;
|
||||||
|
const chunks = directory.split('/').filter((item) => item);
|
||||||
|
const [_username, appWay, ...rest] = chunks;
|
||||||
|
if (username !== _username) {
|
||||||
|
ctx.throw('没有权限');
|
||||||
|
}
|
||||||
|
let newChunks = [];
|
||||||
|
if (appWay === 'resources') {
|
||||||
|
newChunks = [username, ...rest];
|
||||||
|
} else if (appWay === 'ai') {
|
||||||
|
newChunks = [username, 'ai', '1.0.0', ...rest];
|
||||||
|
}
|
||||||
|
const [_, originAppKey, originVersion] = newChunks.filter((item) => item);
|
||||||
|
if (!originAppKey || !originVersion) {
|
||||||
|
ctx.throw('目录不合法');
|
||||||
|
}
|
||||||
|
const pub = async () => {
|
||||||
|
return await app.run({
|
||||||
|
path: 'app',
|
||||||
|
key: 'publish',
|
||||||
|
payload: {
|
||||||
|
data: {
|
||||||
|
appKey: key,
|
||||||
|
version,
|
||||||
|
detect: true,
|
||||||
|
},
|
||||||
|
token: ctx.query.token,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// 如果发布的版本和当前版本不一致,则将目录下的文件复制到新的目录下
|
||||||
|
if (originAppKey !== key || originVersion !== version) {
|
||||||
|
const oldPrefix = newChunks.join('/') + '/';
|
||||||
|
const newPrefix = `${username}/${key}/${version}/`;
|
||||||
|
const listSource = await getMinioList<true>({ prefix: oldPrefix, recursive: true });
|
||||||
|
for (const item of listSource) {
|
||||||
|
const newName = item.name.slice(oldPrefix.length);
|
||||||
|
await oss.copyObject(item.name, `${newPrefix}${newName}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const appRes = await app.run({ path: 'app', key: 'get', payload: { data: { key, version, create: true }, token: ctx.query.token } });
|
||||||
|
if (appRes.code !== 200) {
|
||||||
|
ctx.throw(appRes.message || '获取应用信息失败');
|
||||||
|
}
|
||||||
|
const res = await pub();
|
||||||
|
ctx.forward(res);
|
||||||
|
|
||||||
|
}).addTo(app);
|
||||||
app
|
app
|
||||||
.route({
|
.route({
|
||||||
path: 'app',
|
path: 'app',
|
||||||
key: 'getApp',
|
key: 'getApp',
|
||||||
description: '获取应用信息,可以通过id,或者key+version来获取, 参数在data中传入',
|
description: '获取应用信息,可以通过id,或者key+version来获取, 参数在data中传入',
|
||||||
|
metadata: {
|
||||||
|
args: {
|
||||||
|
data: z.object({
|
||||||
|
id: z.string().optional(),
|
||||||
|
key: z.string().optional(),
|
||||||
|
version: z.string().optional(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.define(async (ctx) => {
|
.define(async (ctx) => {
|
||||||
const { user, key, id } = ctx.query.data;
|
const { user, key, id } = ctx.query.data;
|
||||||
@@ -418,8 +513,17 @@ app
|
|||||||
.route({
|
.route({
|
||||||
path: 'app',
|
path: 'app',
|
||||||
key: 'detectVersionList',
|
key: 'detectVersionList',
|
||||||
description: '检测版本列表,minio中的数据自己上传后,根据版本信息,进行替换',
|
description: '检测版本列表, 对存储内容的网关暴露对应的的模块',
|
||||||
middleware: ['auth'],
|
middleware: ['auth'],
|
||||||
|
metadata: {
|
||||||
|
args: {
|
||||||
|
data: z.object({
|
||||||
|
appKey: z.string().describe('应用的唯一标识'),
|
||||||
|
version: z.string().describe('应用版本'),
|
||||||
|
username: z.string().optional().describe('用户名,默认为当前用户'),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.define(async (ctx) => {
|
.define(async (ctx) => {
|
||||||
const tokenUser = ctx.state.tokenUser;
|
const tokenUser = ctx.state.tokenUser;
|
||||||
@@ -436,7 +540,6 @@ app
|
|||||||
let appList = appLists[0];
|
let appList = appLists[0];
|
||||||
if (!appList) {
|
if (!appList) {
|
||||||
const newAppLists = await db.insert(schema.kvAppList).values({
|
const newAppLists = await db.insert(schema.kvAppList).values({
|
||||||
id: randomUUID(),
|
|
||||||
key: appKey,
|
key: appKey,
|
||||||
version,
|
version,
|
||||||
uid,
|
uid,
|
||||||
@@ -475,6 +578,7 @@ app
|
|||||||
)).limit(1);
|
)).limit(1);
|
||||||
let am = ams[0];
|
let am = ams[0];
|
||||||
if (!am) {
|
if (!am) {
|
||||||
|
// 如果应用不存在,则创建应用记录,版本为0.0.1
|
||||||
const newAms = await db.insert(schema.kvApp).values({
|
const newAms = await db.insert(schema.kvApp).values({
|
||||||
title: appKey,
|
title: appKey,
|
||||||
key: appKey,
|
key: appKey,
|
||||||
@@ -486,6 +590,7 @@ app
|
|||||||
}).returning();
|
}).returning();
|
||||||
am = newAms[0];
|
am = newAms[0];
|
||||||
} else {
|
} else {
|
||||||
|
// 如果应用存在,并且版本相同,则更新应用记录的文件列表
|
||||||
const appModels = await db.select().from(schema.kvApp).where(and(
|
const appModels = await db.select().from(schema.kvApp).where(and(
|
||||||
eq(schema.kvApp.key, appKey),
|
eq(schema.kvApp.key, appKey),
|
||||||
eq(schema.kvApp.version, version),
|
eq(schema.kvApp.version, version),
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { app, db, schema } from '@/app.ts';
|
import { app, db, schema } from '@/app.ts';
|
||||||
import { randomUUID } from 'crypto';
|
|
||||||
import { oss } from '@/app.ts';
|
import { oss } from '@/app.ts';
|
||||||
import { User } from '@/models/user.ts';
|
import { User } from '@/models/user.ts';
|
||||||
import { customAlphabet } from 'nanoid';
|
import { customAlphabet } from 'nanoid';
|
||||||
@@ -10,9 +9,7 @@ const number = '0123456789';
|
|||||||
const randomId = customAlphabet(letter + number, 16);
|
const randomId = customAlphabet(letter + number, 16);
|
||||||
const getShareUser = async () => {
|
const getShareUser = async () => {
|
||||||
const shareUser = await User.findOne({
|
const shareUser = await User.findOne({
|
||||||
where: {
|
username: 'share',
|
||||||
username: 'share',
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
return shareUser?.id || '';
|
return shareUser?.id || '';
|
||||||
};
|
};
|
||||||
@@ -64,7 +61,6 @@ app
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
const appModels = await db.insert(schema.kvApp).values({
|
const appModels = await db.insert(schema.kvApp).values({
|
||||||
id: randomUUID(),
|
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
version,
|
version,
|
||||||
@@ -82,7 +78,6 @@ app
|
|||||||
}).returning();
|
}).returning();
|
||||||
const appModel = appModels[0];
|
const appModel = appModels[0];
|
||||||
const appVersionModels = await db.insert(schema.kvAppList).values({
|
const appVersionModels = await db.insert(schema.kvAppList).values({
|
||||||
id: randomUUID(),
|
|
||||||
data: {
|
data: {
|
||||||
files: files,
|
files: files,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { App, AppList, AppData, AppHelper } from './module/app-drizzle.ts';
|
import { App, AppData, AppHelper } from './module/app-drizzle.ts';
|
||||||
import { app, db, schema } from '@/app.ts';
|
import { app, db, schema } from '@/app.ts';
|
||||||
import { setExpire } from './revoke.ts';
|
import { setExpire } from './revoke.ts';
|
||||||
import { deleteFileByPrefix } from '../file/index.ts';
|
import { deleteFileByPrefix } from '../file/index.ts';
|
||||||
import { eq, and, desc } from 'drizzle-orm';
|
import { eq, and, desc, sql } from 'drizzle-orm';
|
||||||
|
import z from 'zod';
|
||||||
|
|
||||||
app
|
app
|
||||||
.route({
|
.route({
|
||||||
@@ -10,6 +11,9 @@ app
|
|||||||
key: 'list',
|
key: 'list',
|
||||||
middleware: ['auth'],
|
middleware: ['auth'],
|
||||||
description: '获取用户应用列表',
|
description: '获取用户应用列表',
|
||||||
|
metadata: {
|
||||||
|
args: {}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.define(async (ctx) => {
|
.define(async (ctx) => {
|
||||||
const tokenUser = ctx.state.tokenUser;
|
const tokenUser = ctx.state.tokenUser;
|
||||||
@@ -21,17 +25,16 @@ app
|
|||||||
key: schema.kvApp.key,
|
key: schema.kvApp.key,
|
||||||
uid: schema.kvApp.uid,
|
uid: schema.kvApp.uid,
|
||||||
pid: schema.kvApp.pid,
|
pid: schema.kvApp.pid,
|
||||||
proxy: schema.kvApp.proxy,
|
|
||||||
user: schema.kvApp.user,
|
user: schema.kvApp.user,
|
||||||
status: schema.kvApp.status,
|
status: schema.kvApp.status,
|
||||||
createdAt: schema.kvApp.createdAt,
|
createdAt: schema.kvApp.createdAt,
|
||||||
updatedAt: schema.kvApp.updatedAt,
|
updatedAt: schema.kvApp.updatedAt,
|
||||||
deletedAt: schema.kvApp.deletedAt,
|
permission: sql<AppData['permission']>`${schema.kvApp.data}->'permission'`
|
||||||
})
|
})
|
||||||
.from(schema.kvApp)
|
.from(schema.kvApp)
|
||||||
.where(eq(schema.kvApp.uid, tokenUser.id))
|
.where(eq(schema.kvApp.uid, tokenUser.id))
|
||||||
.orderBy(desc(schema.kvApp.updatedAt));
|
.orderBy(desc(schema.kvApp.updatedAt));
|
||||||
ctx.body = list;
|
ctx.body = { list };
|
||||||
return ctx;
|
return ctx;
|
||||||
})
|
})
|
||||||
.addTo(app);
|
.addTo(app);
|
||||||
@@ -42,6 +45,14 @@ app
|
|||||||
key: 'get',
|
key: 'get',
|
||||||
middleware: ['auth'],
|
middleware: ['auth'],
|
||||||
description: '获取用户应用,可以指定id或者key',
|
description: '获取用户应用,可以指定id或者key',
|
||||||
|
metadata: {
|
||||||
|
args: {
|
||||||
|
id: z.string().optional(),
|
||||||
|
data: z.object({
|
||||||
|
key: z.string().optional(),
|
||||||
|
}).optional(),
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.define(async (ctx) => {
|
.define(async (ctx) => {
|
||||||
const tokenUser = ctx.state.tokenUser;
|
const tokenUser = ctx.state.tokenUser;
|
||||||
@@ -79,6 +90,20 @@ app
|
|||||||
key: 'update',
|
key: 'update',
|
||||||
middleware: ['auth'],
|
middleware: ['auth'],
|
||||||
description: '创建或更新用户应用,参数在data中传入',
|
description: '创建或更新用户应用,参数在data中传入',
|
||||||
|
metadata: {
|
||||||
|
args: {
|
||||||
|
data: z.object({
|
||||||
|
id: z.string().optional(),
|
||||||
|
key: z.string().optional(),
|
||||||
|
title: z.string().optional(),
|
||||||
|
description: z.string().optional(),
|
||||||
|
version: z.string().optional(),
|
||||||
|
proxy: z.boolean().optional(),
|
||||||
|
share: z.boolean().optional(),
|
||||||
|
status: z.enum(['running', 'stopped']).optional(),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.define(async (ctx) => {
|
.define(async (ctx) => {
|
||||||
const tokenUser = ctx.state.tokenUser;
|
const tokenUser = ctx.state.tokenUser;
|
||||||
@@ -138,6 +163,12 @@ app
|
|||||||
key: 'delete',
|
key: 'delete',
|
||||||
middleware: ['auth'],
|
middleware: ['auth'],
|
||||||
description: '删除用户应用,可以指定id,参数:deleteFile表示是否删除文件,默认不删除',
|
description: '删除用户应用,可以指定id,参数:deleteFile表示是否删除文件,默认不删除',
|
||||||
|
metadata: {
|
||||||
|
args: {
|
||||||
|
id: z.string().optional().describe('应用id'),
|
||||||
|
deleteFile: z.boolean().optional().describe('是否删除文件, 默认不删除'),
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.define(async (ctx) => {
|
.define(async (ctx) => {
|
||||||
const tokenUser = ctx.state.tokenUser;
|
const tokenUser = ctx.state.tokenUser;
|
||||||
@@ -172,6 +203,11 @@ app
|
|||||||
path: 'user-app',
|
path: 'user-app',
|
||||||
key: 'test',
|
key: 'test',
|
||||||
description: '对user-app的数据进行测试, 获取版本的信息',
|
description: '对user-app的数据进行测试, 获取版本的信息',
|
||||||
|
metadata: {
|
||||||
|
args: {
|
||||||
|
id: z.string().describe('应用id'),
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.define(async (ctx) => {
|
.define(async (ctx) => {
|
||||||
const id = ctx.query.id;
|
const id = ctx.query.id;
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export const defaultKeys = [
|
|||||||
{
|
{
|
||||||
key: 'user.json',
|
key: 'user.json',
|
||||||
description: '用户配置',
|
description: '用户配置',
|
||||||
data: { key: 'user', version: '1.0.0', redirectURL: '/root/home/' },
|
data: { key: 'user', version: '1.0.0', redirectURL: '/root/center/' },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'life.json',
|
key: 'life.json',
|
||||||
|
|||||||
@@ -18,14 +18,14 @@ export class ShareConfigService {
|
|||||||
shareCacheConfig = JSON.parse(shareCacheConfigString);
|
shareCacheConfig = JSON.parse(shareCacheConfigString);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
await redis.set(`config:share:${username}:${key}`, '', 'EX', 0); // 删除缓存
|
await redis.set(`config:share:${username}:${key}`, '', 'EX', 0); // 删除缓存
|
||||||
throw new CustomError(400, 'config parse error');
|
throw new CustomError(400, { message: 'config parse error' });
|
||||||
}
|
}
|
||||||
const owner = username;
|
const owner = username;
|
||||||
if (shareCacheConfig) {
|
if (shareCacheConfig) {
|
||||||
const permission = new UserPermission({ permission: (shareCacheConfig?.data as any)?.permission, owner });
|
const permission = new UserPermission({ permission: (shareCacheConfig?.data as any)?.permission, owner });
|
||||||
const result = permission.checkPermissionSuccess(options);
|
const result = permission.checkPermissionSuccess(options);
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
throw new CustomError(403, 'no permission');
|
throw new CustomError(403, { message: 'no permission' });
|
||||||
}
|
}
|
||||||
return shareCacheConfig;
|
return shareCacheConfig;
|
||||||
}
|
}
|
||||||
@@ -35,7 +35,7 @@ export class ShareConfigService {
|
|||||||
.limit(1);
|
.limit(1);
|
||||||
const user = users[0];
|
const user = users[0];
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new CustomError(404, 'user not found');
|
throw new CustomError(404, { message: 'user not found' });
|
||||||
}
|
}
|
||||||
const configs = await db.select()
|
const configs = await db.select()
|
||||||
.from(schema.kvConfig)
|
.from(schema.kvConfig)
|
||||||
@@ -43,12 +43,12 @@ export class ShareConfigService {
|
|||||||
.limit(1);
|
.limit(1);
|
||||||
const config = configs[0];
|
const config = configs[0];
|
||||||
if (!config) {
|
if (!config) {
|
||||||
throw new CustomError(404, 'config not found');
|
throw new CustomError(404, { message: 'config not found' });
|
||||||
}
|
}
|
||||||
const permission = new UserPermission({ permission: (config?.data as any)?.permission, owner });
|
const permission = new UserPermission({ permission: (config?.data as any)?.permission, owner });
|
||||||
const result = permission.checkPermissionSuccess(options);
|
const result = permission.checkPermissionSuccess(options);
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
throw new CustomError(403, 'no permission');
|
throw new CustomError(403, { message: 'no permission' });
|
||||||
}
|
}
|
||||||
await redis.set(`config:share:${username}:${key}`, JSON.stringify(config), 'EX', 60 * 60 * 24 * 7); // 7天
|
await redis.set(`config:share:${username}:${key}`, JSON.stringify(config), 'EX', 60 * 60 * 24 * 7); // 7天
|
||||||
return config;
|
return config;
|
||||||
|
|||||||
46
src/routes/flowme-life/chat.ts
Normal file
46
src/routes/flowme-life/chat.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
|
||||||
|
import { schema, app, cnb, models } from '@/app.ts'
|
||||||
|
import z from 'zod';
|
||||||
|
import { runAgent } from '@kevisual/ai/agent'
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
|
app.route({
|
||||||
|
path: 'flowme-life',
|
||||||
|
key: 'chat',
|
||||||
|
description: `聊天接口, 对自己的数据进行操作,参数是 question或messages,question是用户的提问,messages是对话消息列表,优先级高于 question`,
|
||||||
|
middleware: ['auth']
|
||||||
|
, metadata: {
|
||||||
|
args: {
|
||||||
|
question: z.string().describe('用户的提问'),
|
||||||
|
messages: z.any().optional().describe('对话消息列表,优先级高于 question'),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).define(async (ctx) => {
|
||||||
|
const question = ctx.query.question || '';
|
||||||
|
const _messages = ctx.query.messages;
|
||||||
|
const token = ctx.query.token || '';
|
||||||
|
if (!question && !_messages) {
|
||||||
|
ctx.throw(400, '缺少参数 question 或 messages');
|
||||||
|
}
|
||||||
|
const routes = ctx.app.getList().filter(r => r.path.startsWith('flowme-life') && r.key !== 'chat');
|
||||||
|
const currentTime = dayjs().toISOString();
|
||||||
|
const messages = _messages || [
|
||||||
|
{
|
||||||
|
"role": "system" as const,
|
||||||
|
"content": `你是我的智能助手,协助我操作我的数据, 请根据我的提问选择合适的接口进行调用。当前时间是 ${currentTime}。`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "user" as const,
|
||||||
|
"content": question
|
||||||
|
}
|
||||||
|
]
|
||||||
|
const res = await runAgent({
|
||||||
|
app: app,
|
||||||
|
messages: messages,
|
||||||
|
languageModel: cnb(models['auto']),
|
||||||
|
// query: 'WHERE path LIKE 'flowme-life%',
|
||||||
|
routes,
|
||||||
|
token,
|
||||||
|
});
|
||||||
|
ctx.body = res
|
||||||
|
}).addTo(app);
|
||||||
3
src/routes/flowme-life/index.ts
Normal file
3
src/routes/flowme-life/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import './list.ts'
|
||||||
|
import './today.ts'
|
||||||
|
import './chat.ts'
|
||||||
38
src/routes/flowme-life/life.services.ts
Normal file
38
src/routes/flowme-life/life.services.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { eq } from 'drizzle-orm';
|
||||||
|
import { schema, db } from '@/app.ts';
|
||||||
|
|
||||||
|
export type LifeItem = typeof schema.life.$inferSelect;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据 id 获取 life 记录
|
||||||
|
*/
|
||||||
|
export async function getLifeItem(id: string): Promise<{ code: number; data?: LifeItem; message?: string }> {
|
||||||
|
try {
|
||||||
|
const result = await db.select().from(schema.life).where(eq(schema.life.id, id)).limit(1);
|
||||||
|
if (result.length === 0) {
|
||||||
|
return { code: 404, message: `记录 ${id} 不存在` };
|
||||||
|
}
|
||||||
|
return { code: 200, data: result[0] };
|
||||||
|
} catch (e) {
|
||||||
|
return { code: 500, message: `获取记录 ${id} 失败: ${e?.message || e}` };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新 life 记录的 effectiveAt(下次执行时间)
|
||||||
|
*/
|
||||||
|
export async function updateLifeEffectiveAt(id: string, effectiveAt: string | Date): Promise<{ code: number; data?: LifeItem; message?: string }> {
|
||||||
|
try {
|
||||||
|
const result = await db
|
||||||
|
.update(schema.life)
|
||||||
|
.set({ effectiveAt: new Date(effectiveAt) })
|
||||||
|
.where(eq(schema.life.id, id))
|
||||||
|
.returning();
|
||||||
|
if (result.length === 0) {
|
||||||
|
return { code: 404, message: `记录 ${id} 不存在` };
|
||||||
|
}
|
||||||
|
return { code: 200, data: result[0] };
|
||||||
|
} catch (e) {
|
||||||
|
return { code: 500, message: `更新记录 ${id} 失败: ${e?.message || e}` };
|
||||||
|
}
|
||||||
|
}
|
||||||
216
src/routes/flowme-life/list.ts
Normal file
216
src/routes/flowme-life/list.ts
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
import { desc, eq, count, or, like, and } from 'drizzle-orm';
|
||||||
|
import { schema, app, db } from '@/app.ts'
|
||||||
|
import z from 'zod';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
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('生效日期, 格式为 YYYY-MM-DD HH:mm:ss').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;
|
||||||
|
if (rest.effectiveAt && isNaN(Date.parse(rest.effectiveAt))) {
|
||||||
|
rest.effectiveAt = null;
|
||||||
|
} else if (rest.effectiveAt) {
|
||||||
|
rest.effectiveAt = dayjs(rest.effectiveAt)
|
||||||
|
}
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
if (rest.effectiveAt && isNaN(Date.parse(rest.effectiveAt))) {
|
||||||
|
rest.effectiveAt = null;
|
||||||
|
} else if (rest.effectiveAt) {
|
||||||
|
rest.effectiveAt = dayjs(rest.effectiveAt)
|
||||||
|
}
|
||||||
|
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, 参数: 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.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, 参数: 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.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);
|
||||||
174
src/routes/flowme-life/today.ts
Normal file
174
src/routes/flowme-life/today.ts
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
import { desc, eq, count, like, and, lt } from 'drizzle-orm';
|
||||||
|
import { schema, app, db } from '@/app.ts'
|
||||||
|
import z from 'zod';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import { useContextKey } from '@kevisual/context';
|
||||||
|
import { createLunarDate, toGregorian } from 'lunar';
|
||||||
|
import { getLifeItem, updateLifeEffectiveAt } from './life.services.ts';
|
||||||
|
app.route({
|
||||||
|
path: 'flowme-life',
|
||||||
|
key: 'today',
|
||||||
|
description: `获取今天需要做的事情列表`,
|
||||||
|
middleware: ['auth'],
|
||||||
|
}).define(async (ctx) => {
|
||||||
|
const tokenUser = ctx.state.tokenUser;
|
||||||
|
const uid = tokenUser.id;
|
||||||
|
|
||||||
|
const tomorrow = dayjs().add(1, 'day').startOf('day').toDate();
|
||||||
|
|
||||||
|
let whereCondition = eq(schema.life.uid, uid);
|
||||||
|
whereCondition = and(
|
||||||
|
eq(schema.life.uid, uid),
|
||||||
|
eq(schema.life.taskType, '运行中'),
|
||||||
|
lt(schema.life.effectiveAt, tomorrow)
|
||||||
|
);
|
||||||
|
|
||||||
|
const list = await db.select()
|
||||||
|
.from(schema.life)
|
||||||
|
.where(whereCondition)
|
||||||
|
.orderBy(desc(schema.life.effectiveAt));
|
||||||
|
|
||||||
|
console.log('today res', list.map(i => i['title']));
|
||||||
|
if (list.length > 0) {
|
||||||
|
ctx.body = {
|
||||||
|
list,
|
||||||
|
content: list.map(item => {
|
||||||
|
return `任务id:[${item['id']}]\n标题: ${item['title']}。\n启动时间: ${dayjs(item['effectiveAt']).format('YYYY-MM-DD HH:mm:ss')}。标签: ${item['tags'] || '无'} \n总结: ${item['summary'] || '无'}`;
|
||||||
|
}).join('\n')
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
ctx.body = {
|
||||||
|
list,
|
||||||
|
content: '今天没有需要做的事情了,休息一下吧'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).addTo(app);
|
||||||
|
|
||||||
|
|
||||||
|
app.route({
|
||||||
|
path: 'flowme-life',
|
||||||
|
key: 'done',
|
||||||
|
description: `完成某件事情,然后判断下一次运行时间。参数是id(string),数据类型是string。如果多个存在,则是ids的string数组`,
|
||||||
|
middleware: ['auth'],
|
||||||
|
metadata: {
|
||||||
|
args: {
|
||||||
|
id: z.string().optional().describe('记录id'),
|
||||||
|
ids: z.array(z.string()).optional().describe('记录id数组'),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).define(async (ctx) => {
|
||||||
|
const id = ctx.query.id;
|
||||||
|
const ids: string[] = ctx.query.ids || [];
|
||||||
|
if (!id && ids.length === 0) {
|
||||||
|
ctx.throw(400, '缺少参数 id');
|
||||||
|
}
|
||||||
|
if (ids.length === 0 && id) {
|
||||||
|
ids.push(String(id));
|
||||||
|
}
|
||||||
|
console.log('id', id, ids);
|
||||||
|
const messages = [];
|
||||||
|
const changeItem = async (id: string) => {
|
||||||
|
// 获取记录详情
|
||||||
|
const recordRes = await getLifeItem(id);
|
||||||
|
if (recordRes.code !== 200) {
|
||||||
|
messages.push({
|
||||||
|
id,
|
||||||
|
content: `获取记录 ${id} 详情失败`,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const record = recordRes.data;
|
||||||
|
|
||||||
|
// 检查启动时间是否大于今天
|
||||||
|
const startTime = record.effectiveAt;
|
||||||
|
const today = dayjs().startOf('day');
|
||||||
|
const startDate = dayjs(startTime).startOf('day');
|
||||||
|
|
||||||
|
if (startDate.isAfter(today)) {
|
||||||
|
messages.push({
|
||||||
|
id,
|
||||||
|
content: `记录 ${id} 的启动时间是 ${dayjs(startTime).format('YYYY-MM-DD HH:mm:ss')},还没到今天呢,到时候再做吧`,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 计算下一次运行时间
|
||||||
|
// 1. 知道当前时间
|
||||||
|
// 2. 知道任务类型,如果是每日,则加一天;如果是每周,则加七天;如果是每月,则加一个月,如果是每年农历,需要转为新的,如果是其他,需要智能判断
|
||||||
|
// 3. 更新记录
|
||||||
|
const strTime = (time: string | Date) => {
|
||||||
|
return dayjs(time).format('YYYY-MM-DD HH:mm:ss');
|
||||||
|
}
|
||||||
|
const currentTime = strTime(new Date().toISOString());
|
||||||
|
const title = record.title || '无标题';
|
||||||
|
const isLuar = record.type?.includes?.('农历') || title.includes('农历');
|
||||||
|
let summay = record.summary || '无';
|
||||||
|
if (summay.length > 200) {
|
||||||
|
summay = summay.substring(0, 200) + '...';
|
||||||
|
}
|
||||||
|
const prompt = record.prompt || '';
|
||||||
|
const type = record.type || '';
|
||||||
|
const content = `上一次执行的时间是${strTime(startTime)},当前时间是${currentTime},请帮我计算下一次的运行时间,如果时间不存在,默认在8点启动。
|
||||||
|
${prompt ? `这是我给你的提示词,帮你更好地理解我的需求:${prompt}` : ''}
|
||||||
|
|
||||||
|
相关资料是
|
||||||
|
任务:${record.title}
|
||||||
|
总结:${summay}
|
||||||
|
类型: ${type}
|
||||||
|
`
|
||||||
|
const ai = useContextKey('ai');
|
||||||
|
await ai.chat([
|
||||||
|
{ role: 'system', content: `你是一个时间计算专家,擅长根据任务类型和时间计算下一次运行时间。只返回我对应的日期的结果,格式是:YYYY-MM-DD HH:mm:ss。如果类型是每日,则加一天;如果是每周,则加七天;如果是每月,则加一个月,如果是每年农历,需要转为新的,如果是其他,需要智能判断` },
|
||||||
|
{ role: 'user', content }
|
||||||
|
])
|
||||||
|
let nextTime = ai.responseText?.trim();
|
||||||
|
try {
|
||||||
|
// 判断返回的时间是否可以格式化
|
||||||
|
if (nextTime && dayjs(nextTime).isValid()) {
|
||||||
|
const time = dayjs(nextTime);
|
||||||
|
if (isLuar) {
|
||||||
|
const festival = createLunarDate({ year: time.year(), month: time.month() + 1, day: time.date() });
|
||||||
|
const { date } = toGregorian(festival);
|
||||||
|
nextTime = dayjs(date).toISOString();
|
||||||
|
} else {
|
||||||
|
nextTime = time.toISOString();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
messages.push({
|
||||||
|
id,
|
||||||
|
content: `记录 ${id} 的任务 "${record.title}",AI 返回的时间格式无效,无法格式化,返回内容是:${ai.responseText}`,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
messages.push({
|
||||||
|
id,
|
||||||
|
content: `记录 ${id} 的任务 "${record.title}",AI 返回结果解析失败,返回内容是:${ai.responseText}`,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const update = await updateLifeEffectiveAt(id, nextTime);
|
||||||
|
if (update.code !== 200) {
|
||||||
|
messages.push({
|
||||||
|
id,
|
||||||
|
content: `记录 ${id} 的任务 "${record.title}",更新记录失败`,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const msg = {
|
||||||
|
id,
|
||||||
|
nextTime,
|
||||||
|
showCNTime: dayjs(nextTime).format('YYYY-MM-DD HH:mm:ss'),
|
||||||
|
content: `任务 "${record.title}" 已标记为完成。下一次运行时间是 ${dayjs(nextTime).format('YYYY-MM-DD HH:mm:ss')}`
|
||||||
|
};
|
||||||
|
messages.push(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const _id of ids) {
|
||||||
|
await changeItem(String(_id));
|
||||||
|
}
|
||||||
|
ctx.body = {
|
||||||
|
content: messages.map(m => m.content).join('\n'),
|
||||||
|
list: messages
|
||||||
|
};
|
||||||
|
|
||||||
|
}).addTo(app);
|
||||||
@@ -2,4 +2,4 @@ import './list.ts'
|
|||||||
|
|
||||||
// flowme channel 相关路由
|
// flowme channel 相关路由
|
||||||
|
|
||||||
import './flowme-channel/list.ts'
|
import '../flowme-channel/list.ts'
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { desc, eq, count, or, like, and } from 'drizzle-orm';
|
import { desc, eq, or, like, and, gte, lte } from 'drizzle-orm';
|
||||||
import { schema, app, db } from '@/app.ts'
|
import { schema, app, db } from '@/app.ts'
|
||||||
|
import z from 'zod';
|
||||||
|
|
||||||
// 获取 flowme 列表
|
// 获取 flowme 列表
|
||||||
app.route({
|
app.route({
|
||||||
@@ -7,10 +8,24 @@ app.route({
|
|||||||
key: 'list',
|
key: 'list',
|
||||||
middleware: ['auth'],
|
middleware: ['auth'],
|
||||||
description: '获取 flowme 列表',
|
description: '获取 flowme 列表',
|
||||||
|
metadata: {
|
||||||
|
args: {
|
||||||
|
page: z.number().describe('页码, 默认为 1').optional(),
|
||||||
|
pageSize: z.number().describe('每页数量, 默认为 100').optional(),
|
||||||
|
search: z.string().describe('搜索关键词').optional(),
|
||||||
|
channelId: z.string().describe('频道ID').optional(),
|
||||||
|
type: z.string().describe('类型').optional(),
|
||||||
|
sort: z.enum(['ASC', 'DESC']).describe('排序方式,ASC 或 DESC,默认为 DESC').optional(),
|
||||||
|
timeRange: z.object({
|
||||||
|
from: z.string().describe('开始时间,ISO 格式').optional(),
|
||||||
|
to: z.string().describe('结束时间,ISO 格式').optional(),
|
||||||
|
}).describe('时间范围过滤').optional(),
|
||||||
|
}
|
||||||
|
}
|
||||||
}).define(async (ctx) => {
|
}).define(async (ctx) => {
|
||||||
const tokenUser = ctx.state.tokenUser;
|
const tokenUser = ctx.state.tokenUser;
|
||||||
const uid = tokenUser.id;
|
const uid = tokenUser.id;
|
||||||
const { page = 1, pageSize = 20, search, channelId, sort = 'DESC' } = ctx.query || {};
|
const { page = 1, pageSize = 100, search, channelId, type, sort = 'DESC', timeRange } = ctx.query || {};
|
||||||
|
|
||||||
const offset = (page - 1) * pageSize;
|
const offset = (page - 1) * pageSize;
|
||||||
const orderByField = sort === 'ASC' ? schema.flowme.updatedAt : desc(schema.flowme.updatedAt);
|
const orderByField = sort === 'ASC' ? schema.flowme.updatedAt : desc(schema.flowme.updatedAt);
|
||||||
@@ -31,18 +46,37 @@ app.route({
|
|||||||
eq(schema.flowme.channelId, channelId)
|
eq(schema.flowme.channelId, channelId)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (type) {
|
||||||
|
whereCondition = and(
|
||||||
|
whereCondition,
|
||||||
|
eq(schema.flowme.type, type)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (timeRange) {
|
||||||
|
const { from, to } = timeRange;
|
||||||
|
if (from) {
|
||||||
|
whereCondition = and(
|
||||||
|
whereCondition,
|
||||||
|
gte(schema.flowme.updatedAt, new Date(from))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (to) {
|
||||||
|
whereCondition = and(
|
||||||
|
whereCondition,
|
||||||
|
lte(schema.flowme.updatedAt, new Date(to))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const [list, totalCount] = await Promise.all([
|
const rows = await db.select()
|
||||||
db.select()
|
.from(schema.flowme)
|
||||||
.from(schema.flowme)
|
.where(whereCondition)
|
||||||
.where(whereCondition)
|
.limit(pageSize + 1)
|
||||||
.limit(pageSize)
|
.offset(offset)
|
||||||
.offset(offset)
|
.orderBy(orderByField);
|
||||||
.orderBy(orderByField),
|
|
||||||
db.select({ count: count() })
|
const hasMore = rows.length > pageSize;
|
||||||
.from(schema.flowme)
|
const list = hasMore ? rows.slice(0, pageSize) : rows;
|
||||||
.where(whereCondition)
|
|
||||||
]);
|
|
||||||
|
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
list,
|
list,
|
||||||
@@ -50,67 +84,99 @@ app.route({
|
|||||||
page,
|
page,
|
||||||
current: page,
|
current: page,
|
||||||
pageSize,
|
pageSize,
|
||||||
total: totalCount[0]?.count || 0,
|
hasMore,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
return ctx;
|
return ctx;
|
||||||
}).addTo(app);
|
}).addTo(app);
|
||||||
|
|
||||||
// 创建或更新 flowme
|
// 创建 flowme
|
||||||
const flowmeUpdate = `创建或更新一个 flowme, 参数定义:
|
app.route({
|
||||||
title: 标题, 必填
|
path: 'flowme',
|
||||||
description: 描述, 选填
|
key: 'create',
|
||||||
tags: 标签, 数组, 选填
|
middleware: ['auth'],
|
||||||
link: 链接, 选填
|
description: '创建一个 flowme',
|
||||||
data: 数据, 对象, 选填
|
metadata: {
|
||||||
channelId: 频道ID, 选填
|
args: {
|
||||||
type: 类型, 选填
|
data: z.object({
|
||||||
source: 来源, 选填
|
title: z.string().describe('标题').optional(),
|
||||||
importance: 重要性等级, 数字, 选填
|
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(),
|
||||||
|
channelId: z.string().describe('频道ID').optional(),
|
||||||
|
type: z.string().describe('类型').optional(),
|
||||||
|
source: z.string().describe('来源').optional(),
|
||||||
|
importance: z.number().describe('重要性等级').optional(),
|
||||||
|
isArchived: z.boolean().describe('是否归档').optional(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).define(async (ctx) => {
|
||||||
|
const { uid, updatedAt, createdAt, ...rest } = ctx.query.data || {};
|
||||||
|
const tokenUser = ctx.state.tokenUser;
|
||||||
|
const 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();
|
||||||
|
ctx.body = flowmeItem;
|
||||||
|
}).addTo(app);
|
||||||
|
|
||||||
app.route({
|
app.route({
|
||||||
path: 'flowme',
|
path: 'flowme',
|
||||||
key: 'update',
|
key: 'update',
|
||||||
middleware: ['auth'],
|
middleware: ['auth'],
|
||||||
description: flowmeUpdate,
|
description: '更新一个 flowme',
|
||||||
|
metadata: {
|
||||||
|
args: {
|
||||||
|
data: z.object({
|
||||||
|
id: z.string().describe('ID'),
|
||||||
|
title: 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(),
|
||||||
|
channelId: z.string().describe('频道ID').optional(),
|
||||||
|
type: z.string().describe('类型').optional(),
|
||||||
|
source: z.string().describe('来源').optional(),
|
||||||
|
importance: z.number().describe('重要性等级').optional(),
|
||||||
|
isArchived: z.boolean().describe('是否归档').optional(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
}).define(async (ctx) => {
|
}).define(async (ctx) => {
|
||||||
const { id, uid, updatedAt, createdAt, ...rest } = ctx.query.data || {};
|
const { id, uid, updatedAt, createdAt, ...rest } = ctx.query.data || {};
|
||||||
const tokenUser = ctx.state.tokenUser;
|
const tokenUser = ctx.state.tokenUser;
|
||||||
let flowmeItem;
|
|
||||||
if (!id) {
|
if (!id) {
|
||||||
flowmeItem = await db.insert(schema.flowme).values({
|
ctx.throw(400, 'id 参数缺失');
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
const 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;
|
ctx.body = flowmeItem;
|
||||||
}).addTo(app);
|
}).addTo(app);
|
||||||
|
|
||||||
@@ -119,7 +185,14 @@ app.route({
|
|||||||
path: 'flowme',
|
path: 'flowme',
|
||||||
key: 'delete',
|
key: 'delete',
|
||||||
middleware: ['auth'],
|
middleware: ['auth'],
|
||||||
description: '删除 flowme, 参数: data.id 必填',
|
description: '删除 flowme ',
|
||||||
|
metadata: {
|
||||||
|
args: {
|
||||||
|
data: z.object({
|
||||||
|
id: z.string().describe('ID'),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
}).define(async (ctx) => {
|
}).define(async (ctx) => {
|
||||||
const tokenUser = ctx.state.tokenUser;
|
const tokenUser = ctx.state.tokenUser;
|
||||||
const { id } = ctx.query.data || {};
|
const { id } = ctx.query.data || {};
|
||||||
|
|||||||
@@ -21,3 +21,7 @@ import './views/index.ts';
|
|||||||
import './query-views/index.ts';
|
import './query-views/index.ts';
|
||||||
|
|
||||||
import './flowme/index.ts'
|
import './flowme/index.ts'
|
||||||
|
|
||||||
|
import './n5-link/index.ts'
|
||||||
|
|
||||||
|
import './flowme-life/index.ts'
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import { eq, desc, and, like, or } from 'drizzle-orm';
|
import { eq, desc, and, like, or } from 'drizzle-orm';
|
||||||
import { app, db, schema } from '../../app.ts';
|
import { app, db, schema } from '../../app.ts';
|
||||||
import { CustomError } from '@kevisual/router';
|
|
||||||
import { filter } from '@kevisual/js-filter'
|
import { filter } from '@kevisual/js-filter'
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
app
|
app
|
||||||
@@ -77,7 +76,7 @@ app
|
|||||||
const tokenUser = ctx.state.tokenUser;
|
const tokenUser = ctx.state.tokenUser;
|
||||||
const id = ctx.query.id;
|
const id = ctx.query.id;
|
||||||
if (!id) {
|
if (!id) {
|
||||||
throw new CustomError('id is required');
|
ctx.throw(400, 'id is required');
|
||||||
}
|
}
|
||||||
const result = await db
|
const result = await db
|
||||||
.select()
|
.select()
|
||||||
|
|||||||
@@ -1,15 +1,23 @@
|
|||||||
import { eq, desc, and, like, or, count, sql } from 'drizzle-orm';
|
import { eq, desc, and, like, or, count, sql } from 'drizzle-orm';
|
||||||
import { app, db, schema } from '../../app.ts';
|
import { app, db, schema } from '../../app.ts';
|
||||||
import { MarkServices } from './services/mark.ts';
|
import { MarkServices } from './services/mark.ts';
|
||||||
import dayjs from 'dayjs';
|
import z from 'zod';
|
||||||
import { nanoid } from 'nanoid';
|
|
||||||
|
|
||||||
app
|
app
|
||||||
.route({
|
.route({
|
||||||
path: 'mark',
|
path: 'mark',
|
||||||
key: 'list',
|
key: 'list',
|
||||||
description: 'mark list.',
|
description: '获取mark列表',
|
||||||
middleware: ['auth'],
|
middleware: ['auth'],
|
||||||
|
metadata: {
|
||||||
|
args: {
|
||||||
|
page: z.number().optional().describe('页码'),
|
||||||
|
pageSize: z.number().optional().describe('每页数量'),
|
||||||
|
search: z.string().optional().describe('搜索关键词'),
|
||||||
|
markType: z.string().optional().describe('mark类型,simple,wallnote,md,draw等'),
|
||||||
|
sort: z.enum(['DESC', 'ASC']).default('DESC').describe('排序字段'),
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.define(async (ctx) => {
|
.define(async (ctx) => {
|
||||||
const tokenUser = ctx.state.tokenUser;
|
const tokenUser = ctx.state.tokenUser;
|
||||||
@@ -25,7 +33,13 @@ app
|
|||||||
.route({
|
.route({
|
||||||
path: 'mark',
|
path: 'mark',
|
||||||
key: 'getVersion',
|
key: 'getVersion',
|
||||||
|
description: '获取mark版本信息',
|
||||||
middleware: ['auth'],
|
middleware: ['auth'],
|
||||||
|
metadata: {
|
||||||
|
args: {
|
||||||
|
id: z.string().describe('mark id'),
|
||||||
|
}
|
||||||
|
},
|
||||||
})
|
})
|
||||||
.define(async (ctx) => {
|
.define(async (ctx) => {
|
||||||
const tokenUser = ctx.state.tokenUser;
|
const tokenUser = ctx.state.tokenUser;
|
||||||
@@ -47,26 +61,6 @@ app
|
|||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
ctx.throw(400, 'id is required');
|
ctx.throw(400, 'id is required');
|
||||||
// const [markModel, created] = await MarkModel.findOrCreate({
|
|
||||||
// where: {
|
|
||||||
// uid: tokenUser.id,
|
|
||||||
// puid: tokenUser.uid,
|
|
||||||
// title: dayjs().format('YYYY-MM-DD'),
|
|
||||||
// },
|
|
||||||
// defaults: {
|
|
||||||
// title: dayjs().format('YYYY-MM-DD'),
|
|
||||||
// uid: tokenUser.id,
|
|
||||||
// markType: 'wallnote',
|
|
||||||
// tags: ['daily'],
|
|
||||||
// },
|
|
||||||
// });
|
|
||||||
// ctx.body = {
|
|
||||||
// version: Number(markModel.version),
|
|
||||||
// updatedAt: markModel.updatedAt,
|
|
||||||
// createdAt: markModel.createdAt,
|
|
||||||
// id: markModel.id,
|
|
||||||
// created: created,
|
|
||||||
// };
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.addTo(app);
|
.addTo(app);
|
||||||
@@ -76,6 +70,12 @@ app
|
|||||||
path: 'mark',
|
path: 'mark',
|
||||||
key: 'get',
|
key: 'get',
|
||||||
middleware: ['auth'],
|
middleware: ['auth'],
|
||||||
|
description: '获取mark详情',
|
||||||
|
metadata: {
|
||||||
|
args: {
|
||||||
|
id: z.string().describe('mark id'),
|
||||||
|
}
|
||||||
|
},
|
||||||
})
|
})
|
||||||
.define(async (ctx) => {
|
.define(async (ctx) => {
|
||||||
const tokenUser = ctx.state.tokenUser;
|
const tokenUser = ctx.state.tokenUser;
|
||||||
@@ -92,24 +92,6 @@ app
|
|||||||
ctx.body = markModel;
|
ctx.body = markModel;
|
||||||
} else {
|
} else {
|
||||||
ctx.throw(400, 'id is required');
|
ctx.throw(400, 'id is required');
|
||||||
// id 不存在,获取当天的title为 日期的一条数据
|
|
||||||
// const [markModel, created] = await MarkModel.findOrCreate({
|
|
||||||
// where: {
|
|
||||||
// uid: tokenUser.id,
|
|
||||||
// puid: tokenUser.uid,
|
|
||||||
// title: dayjs().format('YYYY-MM-DD'),
|
|
||||||
// },
|
|
||||||
// defaults: {
|
|
||||||
// title: dayjs().format('YYYY-MM-DD'),
|
|
||||||
// uid: tokenUser.id,
|
|
||||||
// markType: 'wallnote',
|
|
||||||
// tags: ['daily'],
|
|
||||||
// uname: tokenUser.username,
|
|
||||||
// puid: tokenUser.uid,
|
|
||||||
// version: 1,
|
|
||||||
// },
|
|
||||||
// });
|
|
||||||
// ctx.body = markModel;
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.addTo(app);
|
.addTo(app);
|
||||||
@@ -119,7 +101,20 @@ app
|
|||||||
path: 'mark',
|
path: 'mark',
|
||||||
key: 'update',
|
key: 'update',
|
||||||
middleware: ['auth'],
|
middleware: ['auth'],
|
||||||
|
description: '更新mark内容',
|
||||||
isDebug: true,
|
isDebug: true,
|
||||||
|
metadata: {
|
||||||
|
args: {
|
||||||
|
data: z.object({
|
||||||
|
id: z.string().describe('mark id'),
|
||||||
|
title: z.string().default(''),
|
||||||
|
tags: z.any().default([]),
|
||||||
|
link: z.string().default(''),
|
||||||
|
summary: z.string().default(''),
|
||||||
|
description: z.string().default(''),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
})
|
})
|
||||||
.define(async (ctx) => {
|
.define(async (ctx) => {
|
||||||
const tokenUser = ctx.state.tokenUser;
|
const tokenUser = ctx.state.tokenUser;
|
||||||
@@ -161,17 +156,31 @@ app
|
|||||||
ctx.body = markModel;
|
ctx.body = markModel;
|
||||||
})
|
})
|
||||||
.addTo(app);
|
.addTo(app);
|
||||||
|
|
||||||
|
|
||||||
app
|
app
|
||||||
.route({
|
.route({
|
||||||
path: 'mark',
|
path: 'mark',
|
||||||
key: 'updateNode',
|
key: 'updateNode',
|
||||||
middleware: ['auth'],
|
middleware: ['auth'],
|
||||||
|
description: '更新mark节点,支持更新和删除操作',
|
||||||
|
metadata: {
|
||||||
|
args: {
|
||||||
|
id: z.string().describe('mark id'),
|
||||||
|
operate: z.enum(['update', 'delete']).default('update').describe('节点操作类型,update或delete'),
|
||||||
|
data: z.object({
|
||||||
|
id: z.string().describe('节点id'),
|
||||||
|
node: z.any().describe('要更新的节点数据'),
|
||||||
|
}).describe('要更新的节点数据'),
|
||||||
|
}
|
||||||
|
},
|
||||||
})
|
})
|
||||||
.define(async (ctx) => {
|
.define(async (ctx) => {
|
||||||
const tokenUser = ctx.state.tokenUser;
|
const tokenUser = ctx.state.tokenUser;
|
||||||
const operate = ctx.query.operate || 'update';
|
const operate = ctx.query.operate || 'update';
|
||||||
|
const markId = ctx.query.id;
|
||||||
const { id, node } = ctx.query.data || {};
|
const { id, node } = ctx.query.data || {};
|
||||||
const marks = await db.select().from(schema.microMark).where(eq(schema.microMark.id, id)).limit(1);
|
const marks = await db.select().from(schema.microMark).where(eq(schema.microMark.id, markId)).limit(1);
|
||||||
const markModel = marks[0];
|
const markModel = marks[0];
|
||||||
if (!markModel) {
|
if (!markModel) {
|
||||||
ctx.throw(404, 'mark not found');
|
ctx.throw(404, 'mark not found');
|
||||||
@@ -201,7 +210,7 @@ app
|
|||||||
version,
|
version,
|
||||||
updatedAt: new Date().toISOString(),
|
updatedAt: new Date().toISOString(),
|
||||||
})
|
})
|
||||||
.where(eq(schema.microMark.id, id))
|
.where(eq(schema.microMark.id, markId))
|
||||||
.returning();
|
.returning();
|
||||||
ctx.body = updated[0];
|
ctx.body = updated[0];
|
||||||
})
|
})
|
||||||
@@ -211,10 +220,20 @@ app
|
|||||||
path: 'mark',
|
path: 'mark',
|
||||||
key: 'updateNodes',
|
key: 'updateNodes',
|
||||||
middleware: ['auth'],
|
middleware: ['auth'],
|
||||||
|
description: '批量更新mark节点,支持更新和删除操作',
|
||||||
|
metadata: {
|
||||||
|
args: {
|
||||||
|
id: z.string().describe('mark id'),
|
||||||
|
nodeOperateList: z.array(z.object({
|
||||||
|
operate: z.enum(['update', 'delete']).default('update').describe('节点操作类型,update或delete'),
|
||||||
|
node: z.any().describe('要更新的节点数据'),
|
||||||
|
})).describe('要更新的节点列表'),
|
||||||
|
}
|
||||||
|
},
|
||||||
})
|
})
|
||||||
.define(async (ctx) => {
|
.define(async (ctx) => {
|
||||||
const tokenUser = ctx.state.tokenUser;
|
const tokenUser = ctx.state.tokenUser;
|
||||||
const { id, nodeOperateList } = ctx.query.data || {};
|
const { id, nodeOperateList } = ctx.query || {};
|
||||||
const marks = await db.select().from(schema.microMark).where(eq(schema.microMark.id, id)).limit(1);
|
const marks = await db.select().from(schema.microMark).where(eq(schema.microMark.id, id)).limit(1);
|
||||||
const markModel = marks[0];
|
const markModel = marks[0];
|
||||||
if (!markModel) {
|
if (!markModel) {
|
||||||
@@ -265,6 +284,11 @@ app
|
|||||||
path: 'mark',
|
path: 'mark',
|
||||||
key: 'delete',
|
key: 'delete',
|
||||||
middleware: ['auth'],
|
middleware: ['auth'],
|
||||||
|
metadata: {
|
||||||
|
args: {
|
||||||
|
id: z.string().describe('mark id'),
|
||||||
|
}
|
||||||
|
},
|
||||||
})
|
})
|
||||||
.define(async (ctx) => {
|
.define(async (ctx) => {
|
||||||
const tokenUser = ctx.state.tokenUser;
|
const tokenUser = ctx.state.tokenUser;
|
||||||
@@ -283,7 +307,51 @@ app
|
|||||||
.addTo(app);
|
.addTo(app);
|
||||||
|
|
||||||
app
|
app
|
||||||
.route({ path: 'mark', key: 'getMenu', description: '获取菜单', middleware: ['auth'] })
|
.route({
|
||||||
|
path: 'mark',
|
||||||
|
key: 'create',
|
||||||
|
description: '创建一个新的mark.',
|
||||||
|
middleware: ['auth'],
|
||||||
|
metadata: {
|
||||||
|
args: {
|
||||||
|
title: z.string().default('').describe('标题'),
|
||||||
|
tags: z.any().default([]).describe('标签'),
|
||||||
|
link: z.string().default('').describe('链接'),
|
||||||
|
summary: z.string().default('').describe('摘要'),
|
||||||
|
description: z.string().default('').describe('描述'),
|
||||||
|
markType: z.string().default('md').describe('mark类型'),
|
||||||
|
config: z.any().default({}).describe('配置'),
|
||||||
|
data: z.any().default({}).describe('数据')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.define(async (ctx) => {
|
||||||
|
const tokenUser = ctx.state.tokenUser;
|
||||||
|
const { title, tags, link, summary, description, markType, config, data } = ctx.query;
|
||||||
|
const inserted = await db.insert(schema.microMark).values({
|
||||||
|
title,
|
||||||
|
tags: tags || [],
|
||||||
|
link: link || '',
|
||||||
|
summary: summary || '',
|
||||||
|
description: description || '',
|
||||||
|
markType: markType || 'md',
|
||||||
|
config: config || {},
|
||||||
|
data: data || {},
|
||||||
|
uname: tokenUser.username,
|
||||||
|
uid: tokenUser.id,
|
||||||
|
puid: tokenUser.uid,
|
||||||
|
}).returning();
|
||||||
|
ctx.body = inserted[0];
|
||||||
|
})
|
||||||
|
.addTo(app);
|
||||||
|
|
||||||
|
app
|
||||||
|
.route({
|
||||||
|
path: 'mark',
|
||||||
|
key: 'getMenu',
|
||||||
|
description: '获取mark菜单',
|
||||||
|
middleware: ['auth']
|
||||||
|
})
|
||||||
.define(async (ctx) => {
|
.define(async (ctx) => {
|
||||||
const tokenUser = ctx.state.tokenUser;
|
const tokenUser = ctx.state.tokenUser;
|
||||||
const [rows, totalResult] = await Promise.all([
|
const [rows, totalResult] = await Promise.all([
|
||||||
|
|||||||
1
src/routes/n5-link/index.ts
Normal file
1
src/routes/n5-link/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
import './list.ts';
|
||||||
3
src/routes/n5-link/list.ts
Normal file
3
src/routes/n5-link/list.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import './short-link.ts';
|
||||||
|
import './n5-make/index.ts';
|
||||||
|
import './n5-shop/index.ts';
|
||||||
7
src/routes/n5-link/modules/n5.services.ts
Normal file
7
src/routes/n5-link/modules/n5.services.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { desc, eq, count, or, like, and } from 'drizzle-orm';
|
||||||
|
import { app, db, schema } from '@/app.ts';
|
||||||
|
export class N5Service {
|
||||||
|
static async getBySlug(slug: string) {
|
||||||
|
return db.select().from(schema.shortLink).where(eq(schema.shortLink.slug, slug)).limit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
24
src/routes/n5-link/n5-make/create.ts
Normal file
24
src/routes/n5-link/n5-make/create.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { eq } from 'drizzle-orm';
|
||||||
|
import { app, db, schema } from '@/app.ts';
|
||||||
|
|
||||||
|
app.route({
|
||||||
|
path: 'n5-make',
|
||||||
|
key: 'create',
|
||||||
|
middleware: ['auth'],
|
||||||
|
description: `创建 Make, 参数:
|
||||||
|
slug: 唯一业务ID, 必填
|
||||||
|
resources: 资源列表(数组), 选填
|
||||||
|
`,
|
||||||
|
}).define(async (ctx) => {
|
||||||
|
const tokenUser = ctx.state.tokenUser;
|
||||||
|
const { userId, createdAt, updatedAt, id: _id, ...rest } = ctx.query.data || {};
|
||||||
|
if (!rest.slug) {
|
||||||
|
ctx.throw(400, 'slug 参数缺失');
|
||||||
|
}
|
||||||
|
const result = await db.insert(schema.n5Make).values({
|
||||||
|
...rest,
|
||||||
|
userId: tokenUser.id,
|
||||||
|
}).returning();
|
||||||
|
ctx.body = result[0];
|
||||||
|
return ctx;
|
||||||
|
}).addTo(app);
|
||||||
25
src/routes/n5-link/n5-make/delete.ts
Normal file
25
src/routes/n5-link/n5-make/delete.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { eq } from 'drizzle-orm';
|
||||||
|
import { app, db, schema } from '@/app.ts';
|
||||||
|
|
||||||
|
app.route({
|
||||||
|
path: 'n5-make',
|
||||||
|
key: 'delete',
|
||||||
|
middleware: ['auth'],
|
||||||
|
description: '删除 Make, 参数: id Make 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.n5Make).where(eq(schema.n5Make.id, id)).limit(1);
|
||||||
|
if (existing.length === 0) {
|
||||||
|
ctx.throw(404, 'Make 不存在');
|
||||||
|
}
|
||||||
|
if (existing[0].userId !== tokenUser.id) {
|
||||||
|
ctx.throw(403, '没有权限删除该 Make');
|
||||||
|
}
|
||||||
|
await db.delete(schema.n5Make).where(eq(schema.n5Make.id, id));
|
||||||
|
ctx.body = { success: true };
|
||||||
|
return ctx;
|
||||||
|
}).addTo(app);
|
||||||
4
src/routes/n5-link/n5-make/index.ts
Normal file
4
src/routes/n5-link/n5-make/index.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import './list.ts';
|
||||||
|
import './create.ts';
|
||||||
|
import './update.ts';
|
||||||
|
import './delete.ts';
|
||||||
48
src/routes/n5-link/n5-make/list.ts
Normal file
48
src/routes/n5-link/n5-make/list.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { desc, eq, count } from 'drizzle-orm';
|
||||||
|
import { app, db, schema } from '@/app.ts';
|
||||||
|
|
||||||
|
// 列表
|
||||||
|
app.route({
|
||||||
|
path: 'n5-make',
|
||||||
|
key: 'list',
|
||||||
|
middleware: ['auth'],
|
||||||
|
description: '获取 Make 列表, 参数: page, pageSize, sort',
|
||||||
|
}).define(async (ctx) => {
|
||||||
|
const tokenUser = ctx.state.tokenUser;
|
||||||
|
const uid = tokenUser.id;
|
||||||
|
const { page = 1, pageSize = 20, sort = 'DESC' } = ctx.query || {};
|
||||||
|
|
||||||
|
const offset = (page - 1) * pageSize;
|
||||||
|
const orderByField = sort === 'ASC' ? schema.n5Make.updatedAt : desc(schema.n5Make.updatedAt);
|
||||||
|
|
||||||
|
const whereCondition = eq(schema.n5Make.userId, uid);
|
||||||
|
const [list, totalCount] = await Promise.all([
|
||||||
|
db.select().from(schema.n5Make).where(whereCondition).limit(pageSize).offset(offset).orderBy(orderByField),
|
||||||
|
db.select({ count: count() }).from(schema.n5Make).where(whereCondition),
|
||||||
|
]);
|
||||||
|
|
||||||
|
ctx.body = {
|
||||||
|
list,
|
||||||
|
pagination: { page, current: page, pageSize, total: totalCount[0]?.count || 0 },
|
||||||
|
};
|
||||||
|
return ctx;
|
||||||
|
}).addTo(app);
|
||||||
|
|
||||||
|
// 获取单个
|
||||||
|
app.route({
|
||||||
|
path: 'n5-make',
|
||||||
|
key: 'get',
|
||||||
|
description: '获取单个 Make, 参数: id 或 slug',
|
||||||
|
}).define(async (ctx) => {
|
||||||
|
const { id, slug } = ctx.query || {};
|
||||||
|
if (!id && !slug) {
|
||||||
|
ctx.throw(400, 'id 或 slug 参数缺失');
|
||||||
|
}
|
||||||
|
const condition = id ? eq(schema.n5Make.id, id) : eq(schema.n5Make.slug, slug);
|
||||||
|
const existing = await db.select().from(schema.n5Make).where(condition).limit(1);
|
||||||
|
if (existing.length === 0) {
|
||||||
|
ctx.throw(404, 'Make 不存在');
|
||||||
|
}
|
||||||
|
ctx.body = existing[0];
|
||||||
|
return ctx;
|
||||||
|
}).addTo(app);
|
||||||
29
src/routes/n5-link/n5-make/update.ts
Normal file
29
src/routes/n5-link/n5-make/update.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { eq } from 'drizzle-orm';
|
||||||
|
import { app, db, schema } from '@/app.ts';
|
||||||
|
|
||||||
|
app.route({
|
||||||
|
path: 'n5-make',
|
||||||
|
key: 'update',
|
||||||
|
middleware: ['auth'],
|
||||||
|
description: `更新 Make, 参数:
|
||||||
|
id: Make ID, 必填
|
||||||
|
slug: 唯一业务ID, 选填
|
||||||
|
resources: 资源列表(数组), 选填
|
||||||
|
`,
|
||||||
|
}).define(async (ctx) => {
|
||||||
|
const tokenUser = ctx.state.tokenUser;
|
||||||
|
const { id, userId, createdAt, updatedAt, ...rest } = ctx.query.data || {};
|
||||||
|
if (!id) {
|
||||||
|
ctx.throw(400, 'id 参数缺失');
|
||||||
|
}
|
||||||
|
const existing = await db.select().from(schema.n5Make).where(eq(schema.n5Make.id, id)).limit(1);
|
||||||
|
if (existing.length === 0) {
|
||||||
|
ctx.throw(404, 'Make 不存在');
|
||||||
|
}
|
||||||
|
if (existing[0].userId !== tokenUser.id) {
|
||||||
|
ctx.throw(403, '没有权限更新该 Make');
|
||||||
|
}
|
||||||
|
const result = await db.update(schema.n5Make).set({ ...rest }).where(eq(schema.n5Make.id, id)).returning();
|
||||||
|
ctx.body = result[0];
|
||||||
|
return ctx;
|
||||||
|
}).addTo(app);
|
||||||
32
src/routes/n5-link/n5-shop/create.ts
Normal file
32
src/routes/n5-link/n5-shop/create.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { eq } from 'drizzle-orm';
|
||||||
|
import { app, db, schema } from '@/app.ts';
|
||||||
|
|
||||||
|
app.route({
|
||||||
|
path: 'n5-shop',
|
||||||
|
key: 'create',
|
||||||
|
middleware: ['auth'],
|
||||||
|
description: `创建商品, 参数:
|
||||||
|
slug: 唯一业务ID, 必填
|
||||||
|
title: 商品标题, 必填
|
||||||
|
platform: 商品平台, 必填
|
||||||
|
orderLink: 订单链接, 必填
|
||||||
|
description: 商品描述, 选填
|
||||||
|
link: 商品链接, 选填
|
||||||
|
tags: 标签数组, 选填
|
||||||
|
data: 其他商品信息对象, 选填
|
||||||
|
userinfo: 卖家信息, 选填
|
||||||
|
`,
|
||||||
|
}).define(async (ctx) => {
|
||||||
|
const tokenUser = ctx.state.tokenUser;
|
||||||
|
const { userId, createdAt, updatedAt, id: _id, ...rest } = ctx.query.data || {};
|
||||||
|
if (!rest.slug) ctx.throw(400, 'slug 参数缺失');
|
||||||
|
if (!rest.title) ctx.throw(400, 'title 参数缺失');
|
||||||
|
if (!rest.platform) ctx.throw(400, 'platform 参数缺失');
|
||||||
|
if (!rest.orderLink) ctx.throw(400, 'orderLink 参数缺失');
|
||||||
|
const result = await db.insert(schema.n5Shop).values({
|
||||||
|
...rest,
|
||||||
|
userId: tokenUser.id,
|
||||||
|
}).returning();
|
||||||
|
ctx.body = result[0];
|
||||||
|
return ctx;
|
||||||
|
}).addTo(app);
|
||||||
25
src/routes/n5-link/n5-shop/delete.ts
Normal file
25
src/routes/n5-link/n5-shop/delete.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { eq } from 'drizzle-orm';
|
||||||
|
import { app, db, schema } from '@/app.ts';
|
||||||
|
|
||||||
|
app.route({
|
||||||
|
path: 'n5-shop',
|
||||||
|
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.n5Shop).where(eq(schema.n5Shop.id, id)).limit(1);
|
||||||
|
if (existing.length === 0) {
|
||||||
|
ctx.throw(404, '商品不存在');
|
||||||
|
}
|
||||||
|
if (existing[0].userId !== tokenUser.id) {
|
||||||
|
ctx.throw(403, '没有权限删除该商品');
|
||||||
|
}
|
||||||
|
await db.delete(schema.n5Shop).where(eq(schema.n5Shop.id, id));
|
||||||
|
ctx.body = { success: true };
|
||||||
|
return ctx;
|
||||||
|
}).addTo(app);
|
||||||
4
src/routes/n5-link/n5-shop/index.ts
Normal file
4
src/routes/n5-link/n5-shop/index.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import './list.ts';
|
||||||
|
import './create.ts';
|
||||||
|
import './update.ts';
|
||||||
|
import './delete.ts';
|
||||||
58
src/routes/n5-link/n5-shop/list.ts
Normal file
58
src/routes/n5-link/n5-shop/list.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import { desc, eq, count, or, like, and } from 'drizzle-orm';
|
||||||
|
import { app, db, schema } from '@/app.ts';
|
||||||
|
|
||||||
|
// 列表
|
||||||
|
app.route({
|
||||||
|
path: 'n5-shop',
|
||||||
|
key: 'list',
|
||||||
|
middleware: ['auth'],
|
||||||
|
description: '获取商品列表, 参数: page, pageSize, search, sort',
|
||||||
|
}).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.n5Shop.updatedAt : desc(schema.n5Shop.updatedAt);
|
||||||
|
|
||||||
|
let whereCondition: any = eq(schema.n5Shop.userId, uid);
|
||||||
|
if (search) {
|
||||||
|
whereCondition = and(
|
||||||
|
eq(schema.n5Shop.userId, uid),
|
||||||
|
or(
|
||||||
|
like(schema.n5Shop.title, `%${search}%`),
|
||||||
|
like(schema.n5Shop.slug, `%${search}%`),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [list, totalCount] = await Promise.all([
|
||||||
|
db.select().from(schema.n5Shop).where(whereCondition).limit(pageSize).offset(offset).orderBy(orderByField),
|
||||||
|
db.select({ count: count() }).from(schema.n5Shop).where(whereCondition),
|
||||||
|
]);
|
||||||
|
|
||||||
|
ctx.body = {
|
||||||
|
list,
|
||||||
|
pagination: { page, current: page, pageSize, total: totalCount[0]?.count || 0 },
|
||||||
|
};
|
||||||
|
return ctx;
|
||||||
|
}).addTo(app);
|
||||||
|
|
||||||
|
// 获取单个
|
||||||
|
app.route({
|
||||||
|
path: 'n5-shop',
|
||||||
|
key: 'get',
|
||||||
|
description: '获取单个商品, 参数: id 或 slug',
|
||||||
|
}).define(async (ctx) => {
|
||||||
|
const { id, slug } = ctx.query || {};
|
||||||
|
if (!id && !slug) {
|
||||||
|
ctx.throw(400, 'id 或 slug 参数缺失');
|
||||||
|
}
|
||||||
|
const condition = id ? eq(schema.n5Shop.id, id) : eq(schema.n5Shop.slug, slug);
|
||||||
|
const existing = await db.select().from(schema.n5Shop).where(condition).limit(1);
|
||||||
|
if (existing.length === 0) {
|
||||||
|
ctx.throw(404, '商品不存在');
|
||||||
|
}
|
||||||
|
ctx.body = existing[0];
|
||||||
|
return ctx;
|
||||||
|
}).addTo(app);
|
||||||
35
src/routes/n5-link/n5-shop/update.ts
Normal file
35
src/routes/n5-link/n5-shop/update.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { eq } from 'drizzle-orm';
|
||||||
|
import { app, db, schema } from '@/app.ts';
|
||||||
|
|
||||||
|
app.route({
|
||||||
|
path: 'n5-shop',
|
||||||
|
key: 'update',
|
||||||
|
middleware: ['auth'],
|
||||||
|
description: `更新商品, 参数:
|
||||||
|
id: 商品ID, 必填
|
||||||
|
title: 商品标题, 选填
|
||||||
|
platform: 商品平台, 选填
|
||||||
|
orderLink: 订单链接, 选填
|
||||||
|
description: 商品描述, 选填
|
||||||
|
link: 商品链接, 选填
|
||||||
|
tags: 标签数组, 选填
|
||||||
|
data: 其他商品信息对象, 选填
|
||||||
|
userinfo: 卖家信息, 选填
|
||||||
|
`,
|
||||||
|
}).define(async (ctx) => {
|
||||||
|
const tokenUser = ctx.state.tokenUser;
|
||||||
|
const { id, userId, createdAt, updatedAt, ...rest } = ctx.query.data || {};
|
||||||
|
if (!id) {
|
||||||
|
ctx.throw(400, 'id 参数缺失');
|
||||||
|
}
|
||||||
|
const existing = await db.select().from(schema.n5Shop).where(eq(schema.n5Shop.id, id)).limit(1);
|
||||||
|
if (existing.length === 0) {
|
||||||
|
ctx.throw(404, '商品不存在');
|
||||||
|
}
|
||||||
|
if (existing[0].userId !== tokenUser.id) {
|
||||||
|
ctx.throw(403, '没有权限更新该商品');
|
||||||
|
}
|
||||||
|
const result = await db.update(schema.n5Shop).set({ ...rest }).where(eq(schema.n5Shop.id, id)).returning();
|
||||||
|
ctx.body = result[0];
|
||||||
|
return ctx;
|
||||||
|
}).addTo(app);
|
||||||
157
src/routes/n5-link/short-link.ts
Normal file
157
src/routes/n5-link/short-link.ts
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
import { desc, eq, count, or, like, and } from 'drizzle-orm';
|
||||||
|
import { app, db, schema } from '@/app.ts';
|
||||||
|
import { nanoid } from 'nanoid';
|
||||||
|
import z from 'zod';
|
||||||
|
|
||||||
|
// 列表
|
||||||
|
app.route({
|
||||||
|
path: 'n5-short-link',
|
||||||
|
key: 'list',
|
||||||
|
middleware: ['auth'],
|
||||||
|
description: '获取短链列表, 参数: page, pageSize, search, sort',
|
||||||
|
}).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.shortLink.updatedAt : desc(schema.shortLink.updatedAt);
|
||||||
|
|
||||||
|
let whereCondition: any = eq(schema.shortLink.userId, uid);
|
||||||
|
if (search) {
|
||||||
|
whereCondition = and(
|
||||||
|
eq(schema.shortLink.userId, uid),
|
||||||
|
or(
|
||||||
|
like(schema.shortLink.title, `%${search}%`),
|
||||||
|
like(schema.shortLink.slug, `%${search}%`),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [list, totalCount] = await Promise.all([
|
||||||
|
db.select().from(schema.shortLink).where(whereCondition).limit(pageSize).offset(offset).orderBy(orderByField),
|
||||||
|
db.select({ count: count() }).from(schema.shortLink).where(whereCondition),
|
||||||
|
]);
|
||||||
|
|
||||||
|
ctx.body = {
|
||||||
|
list,
|
||||||
|
pagination: { page, current: page, pageSize, total: totalCount[0]?.count || 0 },
|
||||||
|
};
|
||||||
|
return ctx;
|
||||||
|
}).addTo(app);
|
||||||
|
|
||||||
|
// 获取单个
|
||||||
|
app.route({
|
||||||
|
path: 'n5-short-link',
|
||||||
|
key: 'get',
|
||||||
|
description: '获取单个短链, 参数: id 或 slug',
|
||||||
|
middleware: ['auth'],
|
||||||
|
}).define(async (ctx) => {
|
||||||
|
const { id, slug } = ctx.query || {};
|
||||||
|
if (!id && !slug) {
|
||||||
|
ctx.throw(400, 'id 或 slug 参数缺失');
|
||||||
|
}
|
||||||
|
const condition = id ? eq(schema.shortLink.id, id) : eq(schema.shortLink.slug, slug);
|
||||||
|
const existing = await db.select().from(schema.shortLink).where(condition).limit(1);
|
||||||
|
if (existing.length === 0) {
|
||||||
|
ctx.throw(404, '短链不存在');
|
||||||
|
}
|
||||||
|
ctx.body = existing[0];
|
||||||
|
return ctx;
|
||||||
|
}).addTo(app);
|
||||||
|
|
||||||
|
// 创建
|
||||||
|
app.route({
|
||||||
|
path: 'n5-short-link',
|
||||||
|
key: 'create',
|
||||||
|
middleware: ['auth'],
|
||||||
|
description: `创建短链`,
|
||||||
|
metadata: {
|
||||||
|
args: {
|
||||||
|
data: z.object({
|
||||||
|
slug: z.string().optional().describe('对外唯一业务ID'),
|
||||||
|
title: z.string().optional().describe('标题'),
|
||||||
|
description: z.string().optional().describe('描述'),
|
||||||
|
tags: z.array(z.string()).optional().describe('标签数组'),
|
||||||
|
data: z.record(z.string(), z.any()).optional().describe('附加数据对象'),
|
||||||
|
type: z.enum(['link', 'agent']).optional().describe('类型'),
|
||||||
|
version: z.string().optional().describe('版本号'),
|
||||||
|
}).describe('创建短链参数对象'),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).define(async (ctx) => {
|
||||||
|
const tokenUser = ctx.state.tokenUser;
|
||||||
|
const { userId, createdAt, updatedAt, id: _id, ...rest } = ctx.query.data || {};
|
||||||
|
if (!rest.slug) {
|
||||||
|
ctx.throw(400, 'slug 参数缺失');
|
||||||
|
}
|
||||||
|
const code = rest.code || nanoid(8);
|
||||||
|
const result = await db.insert(schema.shortLink).values({
|
||||||
|
...rest,
|
||||||
|
code,
|
||||||
|
userId: tokenUser.id,
|
||||||
|
}).returning();
|
||||||
|
ctx.body = result[0];
|
||||||
|
return ctx;
|
||||||
|
}).addTo(app);
|
||||||
|
|
||||||
|
// 更新
|
||||||
|
app.route({
|
||||||
|
path: 'n5-short-link',
|
||||||
|
key: 'update',
|
||||||
|
middleware: ['auth'],
|
||||||
|
description: `更新短链`,
|
||||||
|
metadata: {
|
||||||
|
args: {
|
||||||
|
data: z.object({
|
||||||
|
id: z.string().optional().describe('短链ID'),
|
||||||
|
title: z.string().optional().describe('标题'),
|
||||||
|
description: z.string().optional().describe('描述'),
|
||||||
|
tags: z.array(z.string()).optional().describe('标签数组'),
|
||||||
|
data: z.record(z.string(), z.any()).optional().describe('附加数据对象'),
|
||||||
|
type: z.enum(['link', 'agent']).optional().describe('类型'),
|
||||||
|
version: z.string().optional().describe('版本号'),
|
||||||
|
}).describe('更新短链参数对象'),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).define(async (ctx) => {
|
||||||
|
const tokenUser = ctx.state.tokenUser;
|
||||||
|
const { id, userId, createdAt, updatedAt, ...rest } = ctx.query.data || {};
|
||||||
|
if (!id) {
|
||||||
|
ctx.throw(400, 'id 参数缺失');
|
||||||
|
}
|
||||||
|
const existing = await db.select().from(schema.shortLink).where(eq(schema.shortLink.id, id)).limit(1);
|
||||||
|
if (existing.length === 0) {
|
||||||
|
ctx.throw(404, '短链不存在');
|
||||||
|
}
|
||||||
|
if (existing[0].userId !== tokenUser.id) {
|
||||||
|
ctx.throw(403, '没有权限更新该短链');
|
||||||
|
}
|
||||||
|
const result = await db.update(schema.shortLink).set({ ...rest }).where(eq(schema.shortLink.id, id)).returning();
|
||||||
|
ctx.body = result[0];
|
||||||
|
return ctx;
|
||||||
|
}).addTo(app);
|
||||||
|
|
||||||
|
// 删除
|
||||||
|
app.route({
|
||||||
|
path: 'n5-short-link',
|
||||||
|
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.shortLink).where(eq(schema.shortLink.id, id)).limit(1);
|
||||||
|
if (existing.length === 0) {
|
||||||
|
ctx.throw(404, '短链不存在');
|
||||||
|
}
|
||||||
|
if (existing[0].userId !== tokenUser.id) {
|
||||||
|
ctx.throw(403, '没有权限删除该短链');
|
||||||
|
}
|
||||||
|
await db.delete(schema.shortLink).where(eq(schema.shortLink.id, id));
|
||||||
|
ctx.body = { success: true };
|
||||||
|
return ctx;
|
||||||
|
}).addTo(app);
|
||||||
@@ -88,6 +88,7 @@ app.route({
|
|||||||
tags: rest.tags,
|
tags: rest.tags,
|
||||||
link: rest.link,
|
link: rest.link,
|
||||||
data: rest.data,
|
data: rest.data,
|
||||||
|
updatedAt: new Date()
|
||||||
}).where(eq(schema.queryViews.id, id)).returning();
|
}).where(eq(schema.queryViews.id, id)).returning();
|
||||||
}
|
}
|
||||||
ctx.body = view;
|
ctx.body = view;
|
||||||
|
|||||||
@@ -9,18 +9,18 @@ import { eq } from 'drizzle-orm';
|
|||||||
|
|
||||||
export const checkUsername = (username: string) => {
|
export const checkUsername = (username: string) => {
|
||||||
if (username.length > 30) {
|
if (username.length > 30) {
|
||||||
throw new CustomError(400, '用户名不能过长');
|
throw new CustomError(400, { message: '用户名不能过长' });
|
||||||
}
|
}
|
||||||
if (!/^[a-zA-Z0-9_@]+$/.test(username)) {
|
if (!/^[a-zA-Z0-9_@]+$/.test(username)) {
|
||||||
throw new CustomError(400, '用户名包含非法字符');
|
throw new CustomError(400, { message: '用户名包含非法字符' });
|
||||||
}
|
}
|
||||||
if (username.includes(' ')) {
|
if (username.includes(' ')) {
|
||||||
throw new CustomError(400, '用户名不能包含空格');
|
throw new CustomError(400, { message: '用户名不能包含空格' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
export const checkUsernameShort = (username: string) => {
|
export const checkUsernameShort = (username: string) => {
|
||||||
if (username.length <= 3) {
|
if (username.length <= 3) {
|
||||||
throw new CustomError(400, '用户名不能过短');
|
throw new CustomError(400, { message: '用户名不能过短' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -31,13 +31,13 @@ export const toChangeName = async (opts: { id: string; newName: string; admin?:
|
|||||||
}
|
}
|
||||||
const user = await User.findByPk(id);
|
const user = await User.findByPk(id);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
ctx.throw(404, 'User not found');
|
ctx.throw(404, { message: 'User not found' });
|
||||||
}
|
}
|
||||||
const oldName = user.username;
|
const oldName = user.username;
|
||||||
checkUsername(newName);
|
checkUsername(newName);
|
||||||
const findUserByUsername = await User.findOne({ username: newName });
|
const findUserByUsername = await User.findOne({ username: newName });
|
||||||
if (findUserByUsername) {
|
if (findUserByUsername) {
|
||||||
ctx.throw(400, 'Username already exists');
|
ctx.throw(400, { message: 'Username already exists' });
|
||||||
}
|
}
|
||||||
user.username = newName;
|
user.username = newName;
|
||||||
const data = user.data || {};
|
const data = user.data || {};
|
||||||
@@ -65,7 +65,7 @@ export const toChangeName = async (opts: { id: string; newName: string; admin?:
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('迁移文件数据失败', error);
|
console.error('迁移文件数据失败', error);
|
||||||
ctx.throw(500, 'Failed to change username');
|
ctx.throw(500, { message: 'Failed to change username' });
|
||||||
}
|
}
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
@@ -79,13 +79,13 @@ app
|
|||||||
const { id, newName } = ctx.query.data || {};
|
const { id, newName } = ctx.query.data || {};
|
||||||
try {
|
try {
|
||||||
if (!id || !newName) {
|
if (!id || !newName) {
|
||||||
ctx.throw(400, '参数错误');
|
ctx.throw(400, { message: '参数错误' });
|
||||||
}
|
}
|
||||||
const user = await toChangeName({ id, newName, admin: true, ctx });
|
const user = await toChangeName({ id, newName, admin: true, ctx });
|
||||||
ctx.body = await user?.getInfo?.();
|
ctx.body = await user?.getInfo?.();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('changeName error', error);
|
console.error('changeName error', error);
|
||||||
ctx.throw(500, 'Failed to change username');
|
ctx.throw(500, { message: 'Failed to change username' });
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.addTo(app);
|
.addTo(app);
|
||||||
@@ -99,7 +99,7 @@ app
|
|||||||
.define(async (ctx) => {
|
.define(async (ctx) => {
|
||||||
const { username } = ctx.query.data || {};
|
const { username } = ctx.query.data || {};
|
||||||
if (!username) {
|
if (!username) {
|
||||||
ctx.throw(400, 'Username is required');
|
ctx.throw(400, { message: 'Username is required' });
|
||||||
}
|
}
|
||||||
checkUsername(username);
|
checkUsername(username);
|
||||||
const user = await User.findOne({ username });
|
const user = await User.findOne({ username });
|
||||||
@@ -121,7 +121,7 @@ app
|
|||||||
const { id, password } = ctx.query.data || {};
|
const { id, password } = ctx.query.data || {};
|
||||||
const user = await User.findByPk(id);
|
const user = await User.findByPk(id);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
ctx.throw(404, 'User not found');
|
ctx.throw(404, { message: 'User not found' });
|
||||||
}
|
}
|
||||||
let pwd = password || nanoid(6);
|
let pwd = password || nanoid(6);
|
||||||
user.createPassword(pwd);
|
user.createPassword(pwd);
|
||||||
@@ -149,7 +149,7 @@ app
|
|||||||
checkUsername(username);
|
checkUsername(username);
|
||||||
const findUserByUsername = await User.findOne({ username });
|
const findUserByUsername = await User.findOne({ username });
|
||||||
if (findUserByUsername) {
|
if (findUserByUsername) {
|
||||||
ctx.throw(400, 'Username already exists');
|
ctx.throw(400, { message: 'Username already exists' });
|
||||||
}
|
}
|
||||||
let pwd = password || nanoid(6);
|
let pwd = password || nanoid(6);
|
||||||
const user = await User.createUser(username, pwd, description);
|
const user = await User.createUser(username, pwd, description);
|
||||||
@@ -172,7 +172,7 @@ app
|
|||||||
const { id } = ctx.query.data || {};
|
const { id } = ctx.query.data || {};
|
||||||
const user = await User.findByPk(id);
|
const user = await User.findByPk(id);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
ctx.throw(404, 'User not found');
|
ctx.throw(404, { message: 'User not found' });
|
||||||
}
|
}
|
||||||
await db.delete(schema.cfUser).where(eq(schema.cfUser.id, user.id));
|
await db.delete(schema.cfUser).where(eq(schema.cfUser.id, user.id));
|
||||||
backupUserA(user.username, user.id);
|
backupUserA(user.username, user.id);
|
||||||
|
|||||||
30
src/routes/user/cnb-login.ts
Normal file
30
src/routes/user/cnb-login.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { app, redis } from "@/app.ts";
|
||||||
|
import z from "zod";
|
||||||
|
import { CnbServices } from "./modules/cnb-services.ts";
|
||||||
|
import { createCookie } from "./me.ts";
|
||||||
|
app
|
||||||
|
.route({
|
||||||
|
path: 'user',
|
||||||
|
key: 'cnb-login',
|
||||||
|
description: 'cnb登陆, 根据 CNB_TOKEN 获取用户信息',
|
||||||
|
metadata: {
|
||||||
|
args: {
|
||||||
|
data: z.object({
|
||||||
|
cnbToken: z.string().describe('cnb token'),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.define(async (ctx) => {
|
||||||
|
const { cnbToken } = ctx.query.data || {};
|
||||||
|
if (!cnbToken) {
|
||||||
|
ctx.throw(400, 'CNB Token is required');
|
||||||
|
}
|
||||||
|
const cnb = new CnbServices(cnbToken);
|
||||||
|
const token = await cnb.login();
|
||||||
|
if (!token) {
|
||||||
|
ctx.throw(500, '登陆失败');
|
||||||
|
}
|
||||||
|
createCookie(token, ctx);
|
||||||
|
ctx.body = token;
|
||||||
|
}).addTo(app);
|
||||||
@@ -16,3 +16,7 @@ import './admin/user.ts';
|
|||||||
import './secret-key/list.ts';
|
import './secret-key/list.ts';
|
||||||
|
|
||||||
import './wx-login.ts'
|
import './wx-login.ts'
|
||||||
|
|
||||||
|
import './cnb-login.ts';
|
||||||
|
|
||||||
|
import './jwks.ts';
|
||||||
36
src/routes/user/jwks.ts
Normal file
36
src/routes/user/jwks.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { app } from '@/app.ts'
|
||||||
|
import { UserModel } from '@/auth/index.ts';
|
||||||
|
import z from 'zod';
|
||||||
|
|
||||||
|
app.route({
|
||||||
|
path: 'user',
|
||||||
|
key: 'token-create',
|
||||||
|
description: '根据用户token创建一个新的token,主要用于临时访问',
|
||||||
|
middleware: ['auth'],
|
||||||
|
metadata: {
|
||||||
|
args: {
|
||||||
|
loginType: z.enum(['jwks']).optional().describe('登录类型,默认为jwks'),
|
||||||
|
hasRefreshToken: z.boolean().optional().describe('是否需要refresh token,默认为false'),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).define(async (ctx) => {
|
||||||
|
const user = await UserModel.getUserByToken(ctx.query.token);
|
||||||
|
const loginType = ctx.query?.loginType ?? 'jwks';
|
||||||
|
const hasRefreshToken = ctx.query?.hasRefreshToken ?? false;
|
||||||
|
if (!user) {
|
||||||
|
ctx.throw(404, 'user not found');
|
||||||
|
}
|
||||||
|
if (loginType !== 'jwks') {
|
||||||
|
ctx.throw(400, 'unsupported login type');
|
||||||
|
}
|
||||||
|
let expire = ctx.query.expire ?? 24 * 3600;
|
||||||
|
// 大于24小时的过期时间需要管理员权限
|
||||||
|
if (expire > 24 * 3600) {
|
||||||
|
expire = 2 * 3600;
|
||||||
|
}
|
||||||
|
const value = await user.createToken(null, loginType, {
|
||||||
|
expire: expire, // 24小时过期
|
||||||
|
hasRefreshToken: hasRefreshToken,
|
||||||
|
})
|
||||||
|
ctx.body = value
|
||||||
|
}).addTo(app)
|
||||||
@@ -35,15 +35,15 @@ app
|
|||||||
const tokenUser = ctx.state.tokenUser;
|
const tokenUser = ctx.state.tokenUser;
|
||||||
const { id, username, password, description } = ctx.query.data || {};
|
const { id, username, password, description } = ctx.query.data || {};
|
||||||
if (!id) {
|
if (!id) {
|
||||||
throw new CustomError(400, 'id is required');
|
throw new CustomError(400, { message: 'id is required' });
|
||||||
}
|
}
|
||||||
const user = await User.findByPk(id);
|
const user = await User.findByPk(id);
|
||||||
if (user.id !== tokenUser.id) {
|
if (user.id !== tokenUser.id) {
|
||||||
throw new CustomError(403, 'Permission denied');
|
throw new CustomError(403, { message: 'Permission denied' });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new CustomError(500, 'user not found');
|
throw new CustomError(500, { message: 'user not found' });
|
||||||
}
|
}
|
||||||
if (username) {
|
if (username) {
|
||||||
user.username = username;
|
user.username = username;
|
||||||
@@ -73,12 +73,12 @@ app
|
|||||||
.define(async (ctx) => {
|
.define(async (ctx) => {
|
||||||
const { username, password, description } = ctx.query.data || {};
|
const { username, password, description } = ctx.query.data || {};
|
||||||
if (!username) {
|
if (!username) {
|
||||||
throw new CustomError(400, 'username is required');
|
throw new CustomError(400, { message: 'username is required' });
|
||||||
}
|
}
|
||||||
checkUsername(username);
|
checkUsername(username);
|
||||||
const findUserByUsername = await User.findOne({ username });
|
const findUserByUsername = await User.findOne({ username });
|
||||||
if (findUserByUsername) {
|
if (findUserByUsername) {
|
||||||
throw new CustomError(400, 'username already exists');
|
throw new CustomError(400, { message: 'username already exists' });
|
||||||
}
|
}
|
||||||
const pwd = password || nanoid(6);
|
const pwd = password || nanoid(6);
|
||||||
const user = await User.createUser(username, pwd, description);
|
const user = await User.createUser(username, pwd, description);
|
||||||
|
|||||||
@@ -1,19 +1,29 @@
|
|||||||
import { app } from '@/app.ts';
|
import { app } from '@/app.ts';
|
||||||
import { Org } from '@/models/org.ts';
|
|
||||||
import { User } from '@/models/user.ts';
|
import { User } from '@/models/user.ts';
|
||||||
import { proxyDomain as domain } from '@/modules/domain.ts';
|
import { proxyDomain as domain } from '@/modules/domain.ts';
|
||||||
|
import { logger } from '@/modules/logger.ts';
|
||||||
|
import z from 'zod';
|
||||||
/**
|
/**
|
||||||
* 当配置了domain后,创建cookie,当get请求地址的时候,会自动带上cookie
|
* 当配置了domain后,创建cookie,当get请求地址的时候,会自动带上cookie
|
||||||
* @param token
|
* @param token
|
||||||
* @param ctx
|
* @param ctx
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const createCookie = (token: any, ctx: any) => {
|
export const createCookie = (token: { accessToken?: string; token?: string, type?: string; }, ctx: any) => {
|
||||||
if (!domain) {
|
if (!domain) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!ctx?.req) {
|
||||||
|
logger.debug('登陆用户没有请求对象,不需要创建cookie');
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// if (!token.type || token.type === 'jwks') {
|
||||||
|
// // 如果是jwks类型的token,不创建cookie,
|
||||||
|
// // 因为jwks类型的token自己就能检测是否过期了,不需要依赖cookie了
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
//TODO, 获取访问的 hostname, 如果访问的和 domain 的不一致,也创建cookie
|
//TODO, 获取访问的 hostname, 如果访问的和 domain 的不一致,也创建cookie
|
||||||
const browser = ctx.req.headers['user-agent'];
|
const browser = ctx?.req?.headers['user-agent'];
|
||||||
const isBrowser = browser.includes('Mozilla'); // 浏览器
|
const isBrowser = browser.includes('Mozilla'); // 浏览器
|
||||||
if (isBrowser && ctx.res.cookie) {
|
if (isBrowser && ctx.res.cookie) {
|
||||||
ctx.res.cookie('token', token.accessToken || token?.token, {
|
ctx.res.cookie('token', token.accessToken || token?.token, {
|
||||||
@@ -134,7 +144,7 @@ app
|
|||||||
}
|
}
|
||||||
if (tokenUser.id === user.id) {
|
if (tokenUser.id === user.id) {
|
||||||
// 自己刷新自己的token
|
// 自己刷新自己的token
|
||||||
const token = await User.oauth.resetToken(oldToken, {
|
const token = await User.resetToken(oldToken, {
|
||||||
...tokenUser.oauthExpand,
|
...tokenUser.oauthExpand,
|
||||||
});
|
});
|
||||||
createCookie(token, ctx);
|
createCookie(token, ctx);
|
||||||
@@ -151,9 +161,7 @@ app
|
|||||||
browser: someInfo['user-agent'],
|
browser: someInfo['user-agent'],
|
||||||
host: someInfo.host,
|
host: someInfo.host,
|
||||||
});
|
});
|
||||||
createCookie({
|
createCookie(token, ctx);
|
||||||
token: token.accessToken
|
|
||||||
}, ctx);
|
|
||||||
ctx.body = token;
|
ctx.body = token;
|
||||||
})
|
})
|
||||||
.addTo(app);
|
.addTo(app);
|
||||||
@@ -248,6 +256,7 @@ app
|
|||||||
.route({
|
.route({
|
||||||
path: 'user',
|
path: 'user',
|
||||||
key: 'switchCheck',
|
key: 'switchCheck',
|
||||||
|
description: '切换用户或切换为用户组,获取切换后的token',
|
||||||
middleware: ['auth'],
|
middleware: ['auth'],
|
||||||
})
|
})
|
||||||
.define(async (ctx) => {
|
.define(async (ctx) => {
|
||||||
@@ -255,27 +264,13 @@ app
|
|||||||
const { username, accessToken } = ctx.query.data || {};
|
const { username, accessToken } = ctx.query.data || {};
|
||||||
|
|
||||||
if (accessToken && username) {
|
if (accessToken && username) {
|
||||||
const accessUser = await User.verifyToken(accessToken);
|
const result = await User.refreshToken({ accessToken });
|
||||||
const refreshToken = accessUser.oauthExpand?.refreshToken;
|
if (result.accessToken) {
|
||||||
if (refreshToken) {
|
console.log('refreshToken result', result);
|
||||||
const result = await User.oauth.refreshToken(refreshToken);
|
createCookie(result, ctx);
|
||||||
createCookie({
|
|
||||||
token: result.accessToken
|
|
||||||
}, ctx);
|
|
||||||
|
|
||||||
ctx.body = result;
|
ctx.body = result;
|
||||||
return;
|
} else {
|
||||||
} else if (accessUser) {
|
ctx.throw(500, 'Refresh Token Failed, please login again');
|
||||||
await User.oauth.delToken(accessToken);
|
|
||||||
const result = await User.oauth.generateToken(accessUser, {
|
|
||||||
...accessUser.oauthExpand,
|
|
||||||
hasRefreshToken: true,
|
|
||||||
});
|
|
||||||
createCookie({
|
|
||||||
token: result.accessToken
|
|
||||||
}, ctx);
|
|
||||||
ctx.body = result;
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const result = await ctx.call(
|
const result = await ctx.call(
|
||||||
@@ -327,18 +322,13 @@ app
|
|||||||
const orgsList = [tokenUser.username, user.username, , ...orgs];
|
const orgsList = [tokenUser.username, user.username, , ...orgs];
|
||||||
if (orgsList.includes(username)) {
|
if (orgsList.includes(username)) {
|
||||||
if (tokenUsername === username) {
|
if (tokenUsername === username) {
|
||||||
const result = await User.oauth.resetToken(token);
|
const result = await User.resetToken(token);
|
||||||
createCookie({
|
createCookie(result, ctx);
|
||||||
token: result.accessToken,
|
|
||||||
}, ctx);
|
|
||||||
await User.oauth.delToken(token);
|
|
||||||
ctx.body = result;
|
ctx.body = result;
|
||||||
} else {
|
} else {
|
||||||
const user = await User.findOne({ username });
|
const user = await User.findOne({ username });
|
||||||
const result = await user.createToken(userId, 'default');
|
const result = await user.createToken(userId, 'default');
|
||||||
createCookie({
|
createCookie(result, ctx);
|
||||||
token: result.accessToken,
|
|
||||||
}, ctx);
|
|
||||||
ctx.body = result;
|
ctx.body = result;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -351,19 +341,26 @@ app
|
|||||||
.route({
|
.route({
|
||||||
path: 'user',
|
path: 'user',
|
||||||
key: 'refreshToken',
|
key: 'refreshToken',
|
||||||
|
description: '根据refreshToken刷新token',
|
||||||
|
metadata: {
|
||||||
|
args: {
|
||||||
|
data: z.object({
|
||||||
|
refreshToken: z.string().describe('刷新token'),
|
||||||
|
accessToken: z.string().optional().describe('使用访问token去刷新token,如果提供了访问token,优先使用访问token去刷新token,刷新失败才会使用refreshToken去刷新'),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.define(async (ctx) => {
|
.define(async (ctx) => {
|
||||||
const { refreshToken } = ctx.query.data || {};
|
const { refreshToken, accessToken } = ctx.query.data || {};
|
||||||
try {
|
try {
|
||||||
if (!refreshToken) {
|
if (!refreshToken && !accessToken) {
|
||||||
ctx.throw(400, 'Refresh Token is required');
|
ctx.throw(400, 'Refresh Token or Access Token 必须提供一个');
|
||||||
}
|
}
|
||||||
const result = await User.oauth.refreshToken(refreshToken);
|
const result = await User.refreshToken({ accessToken, refreshToken });
|
||||||
if (result) {
|
if (result.accessToken) {
|
||||||
console.log('refreshToken result', result);
|
console.log('refreshToken result', result);
|
||||||
createCookie({
|
createCookie(result, ctx);
|
||||||
token: result.accessToken,
|
|
||||||
}, ctx);
|
|
||||||
ctx.body = result;
|
ctx.body = result;
|
||||||
} else {
|
} else {
|
||||||
ctx.throw(500, 'Refresh Token Failed, please login again');
|
ctx.throw(500, 'Refresh Token Failed, please login again');
|
||||||
|
|||||||
37
src/routes/user/modules/cnb-services.ts
Normal file
37
src/routes/user/modules/cnb-services.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { CNB } from '@kevisual/cnb/src/index.ts'
|
||||||
|
import { UserModel } from '../../../auth/index.ts';
|
||||||
|
import { CustomError } from '@kevisual/router';
|
||||||
|
|
||||||
|
export class CnbServices {
|
||||||
|
cnb: CNB;
|
||||||
|
constructor(token?: string) {
|
||||||
|
this.cnb = new CNB({
|
||||||
|
token,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async login(): Promise<ReturnType<typeof UserModel.prototype.createToken>> {
|
||||||
|
const cnbUserRes = await this.cnb.user.getUser();
|
||||||
|
if (cnbUserRes.code !== 200) {
|
||||||
|
throw new CustomError('CNB Token is invalid');
|
||||||
|
}
|
||||||
|
const cnbUser = cnbUserRes?.data;
|
||||||
|
const cnbUserId = cnbUser?.id
|
||||||
|
if (!cnbUserId) {
|
||||||
|
throw new CustomError('CNB User ID is missing');
|
||||||
|
}
|
||||||
|
let user = await UserModel.findByCnbId(cnbUserId);
|
||||||
|
if (!user) {
|
||||||
|
const username = '@cnb-' + cnbUser.username;
|
||||||
|
// 如果用户不存在,创建一个新用户
|
||||||
|
user = await UserModel.createUser(username, cnbUserId);
|
||||||
|
user.data = {
|
||||||
|
...user.data,
|
||||||
|
cnbId: cnbUserId,
|
||||||
|
}
|
||||||
|
await user.save();
|
||||||
|
}
|
||||||
|
const token = await user.createToken();
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -54,7 +54,7 @@ export class WxServices {
|
|||||||
const token = await fetchToken(code, type);
|
const token = await fetchToken(code, type);
|
||||||
console.log('login token', token);
|
console.log('login token', token);
|
||||||
if (!token.unionid) {
|
if (!token.unionid) {
|
||||||
throw new CustomError(400, 'code is invalid, wxdata can not be found');
|
throw new CustomError(400, { message: 'code is invalid, wxdata can not be found' });
|
||||||
}
|
}
|
||||||
this.wxToken = token;
|
this.wxToken = token;
|
||||||
const unionid = token.unionid;
|
const unionid = token.unionid;
|
||||||
@@ -180,7 +180,7 @@ export class WxServices {
|
|||||||
async getUserInfo() {
|
async getUserInfo() {
|
||||||
try {
|
try {
|
||||||
if (!this.wxToken) {
|
if (!this.wxToken) {
|
||||||
throw new CustomError(400, 'wxToken is not set');
|
throw new CustomError(400, { message: 'wxToken is not set' });
|
||||||
}
|
}
|
||||||
const openid = this.wxToken.openid;
|
const openid = this.wxToken.openid;
|
||||||
const access_token = this.wxToken.access_token;
|
const access_token = this.wxToken.access_token;
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ export const fetchToken = async (code: string, type: 'open' | 'mp' = 'open'): Pr
|
|||||||
appSecret = wx.appSecret;
|
appSecret = wx.appSecret;
|
||||||
}
|
}
|
||||||
if (!appId || !appSecret) {
|
if (!appId || !appSecret) {
|
||||||
throw new CustomError(500, 'appId or appSecret is not set');
|
throw new CustomError(500, { message: 'appId or appSecret is not set' });
|
||||||
}
|
}
|
||||||
console.log('fetchToken===', appId, appSecret, code);
|
console.log('fetchToken===', appId, appSecret, code);
|
||||||
const wxUrl = `https://api.weixin.qq.com/sns/oauth2/access_token?appid=${appId}&secret=${appSecret}&code=${code}&grant_type=authorization_code`;
|
const wxUrl = `https://api.weixin.qq.com/sns/oauth2/access_token?appid=${appId}&secret=${appSecret}&code=${code}&grant_type=authorization_code`;
|
||||||
|
|||||||
@@ -6,12 +6,21 @@ import jsonwebtoken from 'jsonwebtoken';
|
|||||||
|
|
||||||
import { redis } from '@/app.ts';
|
import { redis } from '@/app.ts';
|
||||||
import { createCookie, clearCookie } from './me.ts';
|
import { createCookie, clearCookie } from './me.ts';
|
||||||
|
import z from 'zod';
|
||||||
|
|
||||||
app
|
app
|
||||||
.route({
|
.route({
|
||||||
path: 'user',
|
path: 'user',
|
||||||
key: 'webLogin',
|
key: 'webLogin',
|
||||||
|
description: 'web登录接口,配合插件使用',
|
||||||
middleware: [authCan],
|
middleware: [authCan],
|
||||||
|
metadata: {
|
||||||
|
args: {
|
||||||
|
loginToken: z.string().describe('web登录令牌,服务端生成,客户端保持一致'),
|
||||||
|
sign: z.string().describe('签名,服务端生成,客户端保持一致'),
|
||||||
|
randomId: z.string().describe('随机字符串,服务端和客户端保持一致'),
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.define(async (ctx) => {
|
.define(async (ctx) => {
|
||||||
const tokenUser = ctx.state.tokenUser;
|
const tokenUser = ctx.state.tokenUser;
|
||||||
@@ -39,7 +48,7 @@ app
|
|||||||
<script>
|
<script>
|
||||||
const redirect = new URL('${reqUrl}', window.location.origin);
|
const redirect = new URL('${reqUrl}', window.location.origin);
|
||||||
const encodeRedirect = encodeURIComponent(redirect.toString());
|
const encodeRedirect = encodeURIComponent(redirect.toString());
|
||||||
const toPage = new URL('/root/home/?user-check=true&redirect='+encodeRedirect, window.location.origin);
|
const toPage = new URL('/root/center/login/?user-check=true&redirect='+encodeRedirect, window.location.origin);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
window.location.href = toPage.toString();
|
window.location.href = toPage.toString();
|
||||||
}, 1000);
|
}, 1000);
|
||||||
@@ -87,7 +96,7 @@ app
|
|||||||
await setErrorLoginTokenRedis(loginToken);
|
await setErrorLoginTokenRedis(loginToken);
|
||||||
ctx.throw(400, 'user not found');
|
ctx.throw(400, 'user not found');
|
||||||
}
|
}
|
||||||
const data = await user.createToken(null, 'plugin', { loginWith: 'cli' });
|
const data = await user.createToken(null, 'jwks', { loginWith: 'cli' });
|
||||||
await redis.set(loginToken, JSON.stringify(data), 'EX', 10 * 60); // 10分钟
|
await redis.set(loginToken, JSON.stringify(data), 'EX', 10 * 60); // 10分钟
|
||||||
ctx.body = 'success';
|
ctx.body = 'success';
|
||||||
})
|
})
|
||||||
@@ -97,6 +106,7 @@ app
|
|||||||
.route({
|
.route({
|
||||||
path: 'user',
|
path: 'user',
|
||||||
key: 'checkLoginStatus',
|
key: 'checkLoginStatus',
|
||||||
|
description: '循环检查登陆状态',
|
||||||
})
|
})
|
||||||
.define(async (ctx) => {
|
.define(async (ctx) => {
|
||||||
const { loginToken } = ctx.query;
|
const { loginToken } = ctx.query;
|
||||||
@@ -109,9 +119,7 @@ app
|
|||||||
const token = JSON.parse(data);
|
const token = JSON.parse(data);
|
||||||
if (token.accessToken) {
|
if (token.accessToken) {
|
||||||
ctx.body = token;
|
ctx.body = token;
|
||||||
createCookie({
|
createCookie(token, ctx);
|
||||||
token: token.accessToken,
|
|
||||||
}, ctx);
|
|
||||||
} else {
|
} else {
|
||||||
ctx.throw(500, 'Checked error Failed, login failed, please login again');
|
ctx.throw(500, 'Checked error Failed, login failed, please login again');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ app.route({
|
|||||||
link: rest.link,
|
link: rest.link,
|
||||||
data: rest.data,
|
data: rest.data,
|
||||||
views: rest.views,
|
views: rest.views,
|
||||||
|
updatedAt: new Date()
|
||||||
}).where(eq(schema.routerViews.id, id)).returning();
|
}).where(eq(schema.routerViews.id, id)).returning();
|
||||||
}
|
}
|
||||||
ctx.body = view;
|
ctx.body = view;
|
||||||
|
|||||||
22
src/test/cnb-login.ts
Normal file
22
src/test/cnb-login.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { app, showMore, cnbToken } from './common.ts';
|
||||||
|
|
||||||
|
const res = await app.run({
|
||||||
|
path: 'user',
|
||||||
|
key: 'cnb-login',
|
||||||
|
payload: {
|
||||||
|
data: {
|
||||||
|
cnbToken
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
console.log(showMore(res));
|
||||||
|
|
||||||
|
const token = res.data.token;
|
||||||
|
const me = await app.run({
|
||||||
|
path: 'user',
|
||||||
|
key: 'me',
|
||||||
|
payload: {
|
||||||
|
token
|
||||||
|
}
|
||||||
|
})
|
||||||
|
console.log(showMore(me));
|
||||||
@@ -4,7 +4,7 @@ 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';
|
||||||
import dotenv from 'dotenv';
|
import dotenv from 'dotenv';
|
||||||
|
import { QueryLoginNode } from '@kevisual/api/login-node'
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
export {
|
export {
|
||||||
app,
|
app,
|
||||||
@@ -12,7 +12,7 @@ export {
|
|||||||
}
|
}
|
||||||
export const config = useConfig();
|
export const config = useConfig();
|
||||||
|
|
||||||
export const token = config.KEVISUAL_TOKEN || '';
|
export const cnbToken = config.CNB_TOKEN || '';
|
||||||
|
|
||||||
export const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
export const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
export const showRes = (res, ...args) => {
|
export const showRes = (res, ...args) => {
|
||||||
@@ -36,4 +36,10 @@ 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'
|
||||||
|
// url: 'https://kevisual.xiongxiao.me/api/router'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const queryLogin = new QueryLoginNode({ query });
|
||||||
|
|
||||||
|
export const token = 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImtpZC1rZXktMSJ9.eyJzdWIiOiJ1c2VyOjBlNzAwZGM4LTkwZGQtNDFiNy05MWRkLTMzNmVhNTFkZTNkMiIsIm5hbWUiOiJyb290IiwiZXhwIjoxNzczMjM1NzM0LCJpc3MiOiJodHRwczovL2NvbnZleC5rZXZpc3VhbC5jbiIsImlhdCI6MTc3MzE0OTMzNCwiYXVkIjoiY29udmV4LWFwcCJ9.Zj3bepCCKnVGgXoOnmmdkM-2u0qiT2V-bLhI-0C1a-YX9-ZlcQP2W_1rYN_D2kaaL5BPduvKhoY1hJzM5UwxRYLc-tYr2oBU4fwEyHc3bn-M8p0spX2-Tbie7CN_WbBszZ9KGePNKCveWmx5rCc14YhfUiIvczviU7WP728yFsaHJ29sVu3FJqd3ezMSkdwwPtlwCBtOhuE3nyqPdWP6nRZHkSSbAZDu5jUb_-3TqGjI2cHVZwChfcIVNwdjTeQrj2KMMQ2NdXBim01PZcolr3wqNwpSsm4bN4IVyB5RmwCw7gzHyYSOSZ1bnE8kc53M0KANDSLBFynKUXzNQJ-Wmg'
|
||||||
|
// console.log('test config', token);
|
||||||
36
src/test/flowme.ts
Normal file
36
src/test/flowme.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { queryLogin, app, token, showMore } from './common.ts'
|
||||||
|
|
||||||
|
|
||||||
|
// const rest = await app.run({
|
||||||
|
// path: 'flowme-life',
|
||||||
|
// key: 'today',
|
||||||
|
// // @ts-ignore
|
||||||
|
// token: token,
|
||||||
|
// })
|
||||||
|
|
||||||
|
// console.log('flowme-life today', rest)
|
||||||
|
|
||||||
|
const updateId = '8c63cb7a-ff6d-463b-b210-6311ee12ed46'
|
||||||
|
|
||||||
|
// const updateRest = await app.run({
|
||||||
|
// path: 'flowme-life',
|
||||||
|
// key: 'done',
|
||||||
|
// // @ts-ignore
|
||||||
|
// token: token,
|
||||||
|
// payload: {
|
||||||
|
// id: updateId,
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// console.log('flowme-life done', updateRest)
|
||||||
|
|
||||||
|
const chatRes = await app.run({
|
||||||
|
path: 'flowme-life',
|
||||||
|
key: 'chat',
|
||||||
|
// @ts-ignore
|
||||||
|
token: token,
|
||||||
|
payload: {
|
||||||
|
// question: '帮我查询一下今天的待办事项'
|
||||||
|
// question: '帮我查询一下今天的待办事项, 然后帮我把键盘充电的待办标记为完成',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
console.log('flowme-life chat', showMore(chatRes))
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { manager } from '@/modules/jwks/index.ts'
|
import { manager } from '@/auth/models/jwks-manager.ts'
|
||||||
|
|
||||||
await manager.init()
|
await manager.init()
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user