Compare commits

..

10 Commits

Author SHA1 Message Date
xiongxiao
56a55ac5ae update 2026-03-21 15:11:47 +08:00
xiongxiao
8184994b2c Refactor code structure for improved readability and maintainability 2026-03-20 18:13:15 +08:00
xiongxiao
2000b474a0 feat: 更新重定向路径,将 '/root/home/' 修改为 '/root/center/' 2026-03-19 16:49:37 +08:00
xiongxiao
07efc4e468 feat: 更新 microMark schema 和 mark 路由,添加 skill 2026-03-19 01:56:47 +08:00
xiongxiao
f305a900f4 feat: 将 metadata 中的参数封装到 args 对象中,以统一结构 2026-03-19 01:17:43 +08:00
xiongxiao
6a92ee7a2d feat: 更新 mark 路由,添加 metadata 验证,移除未使用的代码 2026-03-18 22:55:08 +08:00
xiongxiao
6467e6dea8 feat: 更新 N5Proxy 以支持生成带有刷新令牌的 JWKS 令牌 2026-03-18 22:31:31 +08:00
xiongxiao
08884b7e4b feat: 添加编辑模式支持,重定向到指定文件夹路径 2026-03-18 16:54:38 +08:00
xiongxiao
9be5eb00f5 chore: update dependencies and remove unused convex package
- Removed "@kevisual/convex" dependency from package.json and pnpm-lock.yaml.
- Updated "ioredis" version from "^5.9.3" to "^5.10.0" in package.json and pnpm-lock.yaml.
- Updated "@kevisual/context", "@kevisual/query", and "@kevisual/router" versions in wxmsg/package.json.
- Updated "@types/node" version in wxmsg/package.json.
- Added resolutions for "ioredis" in wxmsg/package.json.
- Commented out the automatic reply functionality in wxmsg/src/wx/index.ts and added a placeholder message.
2026-03-18 03:24:33 +08:00
xiongxiao
2332f05cef Refactor code structure for improved readability and maintainability 2026-03-17 20:52:31 +08:00
15 changed files with 899 additions and 2067 deletions

2
.npmrc
View File

@@ -1,2 +0,0 @@
@abearxiong:registry=https://npm.pkg.github.com
ignore-workspace-root-check=true

View 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
```

1165
bun.lock

File diff suppressed because it is too large Load Diff

View File

@@ -41,16 +41,16 @@
],
"license": "UNLICENSED",
"dependencies": {
"@kevisual/ai": "^0.0.27",
"@kevisual/ai": "^0.0.28",
"@kevisual/auth": "^2.0.3",
"@kevisual/js-filter": "^0.0.6",
"@kevisual/query": "^0.0.53",
"@kevisual/query": "^0.0.55",
"@types/busboy": "^1.5.4",
"@types/send": "^1.2.1",
"@types/ws": "^8.18.1",
"bullmq": "^5.70.4",
"bullmq": "^5.71.0",
"busboy": "^1.6.0",
"drizzle-kit": "^0.31.9",
"drizzle-kit": "^0.31.10",
"drizzle-orm": "^0.45.1",
"eventemitter3": "^5.0.4",
"send": "^1.2.1",
@@ -59,38 +59,37 @@
},
"devDependencies": {
"@ai-sdk/openai-compatible": "^2.0.35",
"@aws-sdk/client-s3": "^3.1005.0",
"@kevisual/api": "^0.0.62",
"@kevisual/cnb": "^0.0.42",
"@aws-sdk/client-s3": "^3.1013.0",
"@kevisual/api": "^0.0.64",
"@kevisual/cnb": "^0.0.56",
"@kevisual/context": "^0.0.8",
"@kevisual/convex": "^0.0.6",
"@kevisual/local-app-manager": "0.1.32",
"@kevisual/logger": "^0.0.4",
"@kevisual/oss": "0.0.20",
"@kevisual/permission": "^0.0.4",
"@kevisual/router": "0.1.0",
"@kevisual/router": "0.1.6",
"@kevisual/types": "^0.0.12",
"@kevisual/use-config": "^1.0.30",
"@types/archiver": "^7.0.0",
"@types/bun": "^1.3.10",
"@types/bun": "^1.3.11",
"@types/crypto-js": "^4.2.2",
"@types/jsonwebtoken": "^9.0.10",
"@types/node": "^25.4.0",
"@types/node": "^25.5.0",
"@types/pg": "^8.18.0",
"@types/semver": "^7.7.1",
"@types/xml2js": "^0.4.14",
"ai": "^6.0.116",
"archiver": "^7.0.1",
"convex": "^1.32.0",
"convex": "^1.34.0",
"crypto-js": "^4.2.0",
"dayjs": "^1.11.19",
"dayjs": "^1.11.20",
"dotenv": "^17.3.1",
"drizzle-zod": "^0.8.3",
"es-toolkit": "^1.45.1",
"ioredis": "^5.10.0",
"ioredis": "^5.10.1",
"jsonwebtoken": "^9.0.3",
"lunar": "^2.0.0",
"nanoid": "^5.1.6",
"nanoid": "^5.1.7",
"p-queue": "^9.1.0",
"pg": "^8.20.0",
"pm2": "^6.0.14",
@@ -100,9 +99,9 @@
"resolutions": {
"inflight": "latest",
"picomatch": "^4.0.2",
"ioredis": "^5.9.3"
"ioredis": "^5.10.0"
},
"packageManager": "pnpm@10.32.0",
"packageManager": "pnpm@10.32.1",
"workspaces": [
"wxmsg"
]

1467
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -61,6 +61,8 @@ type StoreSetOpts = {
loginType?: 'default' | 'plugin' | 'month' | 'season' | 'year' | 'week' | 'day'; // 登陆类型 'default' | 'plugin' | 'month' | 'season' | 'year'
expire?: number; // 过期时间,单位为秒
hasRefreshToken?: boolean;
// refreshToken的过期时间比accessToken多多少天默认是1天
expireDay?: number;
[key: string]: any;
};
interface Store<T> {
@@ -70,7 +72,7 @@ interface Store<T> {
expire: (key: string, ttl?: number) => Promise<void>;
delObject: (value?: T) => Promise<void>;
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>;
}
@@ -138,9 +140,11 @@ export class RedisTokenStore implements Store<OauthUser> {
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;
let userPrefix = 'user:' + value?.id;
const expireDay = data?.day || 1;
if (value?.orgId) {
userPrefix = 'org:' + value?.orgId + ':user:' + value?.id;
}
@@ -156,28 +160,18 @@ export class RedisTokenStore implements Store<OauthUser> {
case 'week':
expire = 7 * 24 * 60 * 60;
break;
case 'month':
expire = 30 * 24 * 60 * 60;
break;
case 'season':
expire = 90 * 24 * 60 * 60;
break;
default:
expire = 7 * 24 * 60 * 60; // 默认过期时间为7天
}
} 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(userPrefix + ':token:' + accessToken, accessToken, expire);
// refreshToken的过期时间比accessToken多2确保在accessToken过期后refreshToken仍然有效
let refreshTokenExpiresIn = expire + 2 * day;
// refreshToken的过期时间比accessToken多expireDay确保在accessToken过期后refreshToken仍然有效
let refreshTokenExpiresIn = expire + expireDay * day;
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(userPrefix + ':refreshToken:' + refreshToken, refreshToken, refreshTokenExpiresIn);
}
@@ -239,7 +233,7 @@ export class OAuth<T extends OauthUser> {
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;
}
@@ -253,7 +247,7 @@ export class OAuth<T extends OauthUser> {
createTime: new Date().getTime(), // 创建时间
};
await this.store.setToken(
{ accessToken: secretKey, refreshToken: '', value: oauthUser },
{ accessToken: secretKey, refreshToken: '', value: oauthUser, day: opts?.day },
{
...opts,
hasRefreshToken: false,
@@ -338,6 +332,7 @@ export class OAuth<T extends OauthUser> {
{
...user.oauthExpand,
hasRefreshToken: true,
day: user.oauthExpand?.day,
},
);
console.log('resetToken token', await this.store.keys());
@@ -370,6 +365,7 @@ export class OAuth<T extends OauthUser> {
{
...user.oauthExpand,
hasRefreshToken: true,
day: user.oauthExpand?.day,
},
);
@@ -429,8 +425,8 @@ export class OAuth<T extends OauthUser> {
async setJwksToken(token: string, opts: { id: string; expire: number }) {
const expire = opts.expire ?? 2 * 3600; // 2 hours
const id = opts.id || '-';
// jwks token的过期时间比accessToken多3天,确保3天内可以用来refresh token
const addExpire = 3 * 24 * 3600;
// 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) {

View File

@@ -330,8 +330,11 @@ export const microAppsUpload = pgTable("micro_apps_upload", {
export const microMark = pgTable("micro_mark", {
id: uuid().primaryKey().defaultRandom(),
title: text().default(''),
description: text().default(''),
tags: jsonb().default([]),
link: text().default(''),
summary: text().default(''),
description: text().default(''),
data: jsonb().default({}),
uname: varchar({ length: 255 }).default(''),
uid: uuid(),
@@ -339,8 +342,7 @@ export const microMark = pgTable("micro_mark", {
updatedAt: timestamp({ withTimezone: true, mode: 'string' }).notNull().defaultNow(),
cover: text().default(''),
thumbnail: text().default(''),
link: text().default(''),
summary: text().default(''),
markType: text().default('md'),
config: jsonb().default({}),
puid: uuid(),

View File

@@ -68,12 +68,20 @@ const getAiProxy = async (req: IncomingMessage, res: ServerResponse, opts: Proxy
const password = params.get('p');
const hash = params.get('hash');
let dir = !!params.get('dir');
const edit = !!params.get('edit');
const recursive = !!params.get('recursive');
const showStat = !!params.get('stat');
const { objectName, app, owner, loginUser, isOwner } = await getObjectName(req);
if (!dir && _u.pathname.endsWith('/')) {
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);
try {
if (dir) {

View File

@@ -48,7 +48,7 @@ export const N5Proxy = async (req: IncomingMessage, res: ServerResponse, opts?:
}
try {
const user = await User.findByPk(userId);
const token = await User.createJwksTokenResponse({ id: userId, username: user?.username || '' }, { hasRefreshToken: false });
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())

View File

@@ -10,11 +10,11 @@ import { getUserConfig } from '@/modules/fm-manager/index.ts';
export const rediretHome = async (req: http.IncomingMessage, res: http.ServerResponse) => {
const user = await getLoginUser(req);
if (!user?.token) {
res.writeHead(302, { Location: '/root/home/' });
res.writeHead(302, { Location: '/root/center/' });
res.end();
return;
}
let redirectURL = '/root/home/';
let redirectURL = '/root/center/';
try {
const token = user.token;
const resConfig = await getUserConfig(token);

View File

@@ -28,7 +28,7 @@ export const defaultKeys = [
{
key: 'user.json',
description: '用户配置',
data: { key: 'user', version: '1.0.0', redirectURL: '/root/home/' },
data: { key: 'user', version: '1.0.0', redirectURL: '/root/center/' },
},
{
key: 'life.json',

View File

@@ -1,15 +1,23 @@
import { eq, desc, and, like, or, count, sql } from 'drizzle-orm';
import { app, db, schema } from '../../app.ts';
import { MarkServices } from './services/mark.ts';
import dayjs from 'dayjs';
import { nanoid } from 'nanoid';
import z from 'zod';
app
.route({
path: 'mark',
key: 'list',
description: 'mark list.',
description: '获取mark列表',
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) => {
const tokenUser = ctx.state.tokenUser;
@@ -25,7 +33,13 @@ app
.route({
path: 'mark',
key: 'getVersion',
description: '获取mark版本信息',
middleware: ['auth'],
metadata: {
args: {
id: z.string().describe('mark id'),
}
},
})
.define(async (ctx) => {
const tokenUser = ctx.state.tokenUser;
@@ -47,26 +61,6 @@ app
};
} else {
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);
@@ -76,6 +70,13 @@ app
path: 'mark',
key: 'get',
middleware: ['auth'],
description: '获取mark详情',
metadata: {
args: {
id: z.string().describe('mark id'),
}
},
})
.define(async (ctx) => {
const tokenUser = ctx.state.tokenUser;
@@ -92,24 +93,6 @@ app
ctx.body = markModel;
} else {
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);
@@ -119,7 +102,20 @@ app
path: 'mark',
key: 'update',
middleware: ['auth'],
description: '更新mark内容',
isDebug: true,
metadata: {
args: {
id: z.string().describe('mark id'),
data: z.object({
title: z.string().default(''),
tags: z.any().default([]),
link: z.string().default(''),
summary: z.string().default(''),
description: z.string().default(''),
})
}
},
})
.define(async (ctx) => {
const tokenUser = ctx.state.tokenUser;
@@ -161,11 +157,24 @@ app
ctx.body = markModel;
})
.addTo(app);
app
.route({
path: 'mark',
key: 'updateNode',
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) => {
const tokenUser = ctx.state.tokenUser;
@@ -183,7 +192,7 @@ app
const currentData = markModel.data as any || {};
const nodes = currentData.nodes || [];
const nodeIndex = nodes.findIndex((n: any) => n.id === node.id);
let updatedNodes;
if (operate === 'delete') {
updatedNodes = nodes.filter((n: any) => n.id !== node.id);
@@ -193,7 +202,7 @@ app
} else {
updatedNodes = [...nodes, node];
}
const version = Number(markModel.version) + 1;
const updated = await db.update(schema.microMark)
.set({
@@ -211,6 +220,16 @@ app
path: 'mark',
key: 'updateNodes',
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) => {
const tokenUser = ctx.state.tokenUser;
@@ -229,15 +248,15 @@ app
if (nodeOperateList.some((item: any) => !item.node)) {
ctx.throw(400, 'nodeOperateList node is required');
}
// Update multiple JSON nodes logic with Drizzle
const currentData = markModel.data as any || {};
let nodes = currentData.nodes || [];
for (const item of nodeOperateList) {
const { node, operate = 'update' } = item;
const nodeIndex = nodes.findIndex((n: any) => n.id === node.id);
if (operate === 'delete') {
nodes = nodes.filter((n: any) => n.id !== node.id);
} else if (nodeIndex >= 0) {
@@ -246,7 +265,7 @@ app
nodes.push(node);
}
}
const version = Number(markModel.version) + 1;
const updated = await db.update(schema.microMark)
.set({
@@ -265,6 +284,11 @@ app
path: 'mark',
key: 'delete',
middleware: ['auth'],
metadata: {
args: {
id: z.string().describe('mark id'),
}
},
})
.define(async (ctx) => {
const tokenUser = ctx.state.tokenUser;
@@ -283,7 +307,51 @@ app
.addTo(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) => {
const tokenUser = ctx.state.tokenUser;
const [rows, totalResult] = await Promise.all([

View File

@@ -48,7 +48,7 @@ app
<script>
const redirect = new URL('${reqUrl}', window.location.origin);
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(() => {
window.location.href = toPage.toString();
}, 1000);

View File

@@ -23,16 +23,19 @@
],
"author": "abearxiong <xiongxiao@xiongxiao.me> (https://www.xiongxiao.me)",
"license": "MIT",
"packageManager": "pnpm@10.28.2",
"packageManager": "pnpm@10.32.1",
"type": "module",
"dependencies": {
"@kevisual/context": "^0.0.4",
"@kevisual/query": "^0.0.39",
"@kevisual/router": "0.0.70",
"@types/node": "^25.2.1",
"@kevisual/context": "^0.0.8",
"@kevisual/query": "^0.0.53",
"@kevisual/router": "0.1.4",
"@types/node": "^25.5.0",
"crypto-js": "^4.2.0",
"xml2js": "^0.6.2"
},
"resolutions": {
"ioredis": "^5.10.0"
},
"devDependencies": {
"@types/crypto-js": "^4.2.2",
"@types/xml2js": "^0.4.14"

View File

@@ -157,22 +157,23 @@ export class Wx {
}
const wxMsg = msg as WxMsgText;
const question = wxMsg.content;
const nocoMsg = {
path: 'noco-life',
key: "chat",
payload: {
token: userToken,
question,
}
}
const res = await this.query.post(nocoMsg);
if (res.code !== 200) {
await sendUserText('自动回复失败,请稍后再试:' + res.message);
}
const content = res.data?.content || ''
if (content) {
await sendUserText(content);
}
// const nocoMsg = {
// path: 'noco-life',
// key: "chat",
// payload: {
// token: userToken,
// question,
// }
// }
// const res = await this.query.post(nocoMsg);
// if (res.code !== 200) {
// await sendUserText('自动回复失败,请稍后再试:' + res.message);
// }
// const content = res.data?.content || ''
// if (content) {
// await sendUserText(content);
// }
sendUserText('自动回复功能正在开发中,敬请期待!');
}
}