temp
This commit is contained in:
@@ -43,7 +43,8 @@
|
||||
],
|
||||
"license": "UNLICENSED",
|
||||
"dependencies": {
|
||||
"@kevisual/ai": "^0.0.12",
|
||||
"@kevisual/ai": "^0.0.15",
|
||||
"@kevisual/query": "^0.0.29",
|
||||
"@types/busboy": "^1.5.4",
|
||||
"@types/send": "^1.2.1",
|
||||
"@types/ws": "^8.18.1",
|
||||
|
||||
136
pnpm-lock.yaml
generated
136
pnpm-lock.yaml
generated
@@ -13,8 +13,11 @@ importers:
|
||||
.:
|
||||
dependencies:
|
||||
'@kevisual/ai':
|
||||
specifier: ^0.0.12
|
||||
version: 0.0.12
|
||||
specifier: ^0.0.15
|
||||
version: 0.0.15
|
||||
'@kevisual/query':
|
||||
specifier: ^0.0.29
|
||||
version: 0.0.29(@kevisual/ws@8.0.0)(zod@4.1.13)
|
||||
'@types/busboy':
|
||||
specifier: ^1.5.4
|
||||
version: 1.5.4
|
||||
@@ -155,36 +158,14 @@ importers:
|
||||
specifier: ^4.1.13
|
||||
version: 4.1.13
|
||||
|
||||
wxmsg:
|
||||
dependencies:
|
||||
'@kevisual/context':
|
||||
specifier: ^0.0.4
|
||||
version: 0.0.4
|
||||
'@kevisual/router':
|
||||
specifier: 0.0.33
|
||||
version: 0.0.33
|
||||
'@types/node':
|
||||
specifier: ^24.10.1
|
||||
version: 24.10.1
|
||||
crypto-js:
|
||||
specifier: ^4.2.0
|
||||
version: 4.2.0
|
||||
xml2js:
|
||||
specifier: ^0.6.2
|
||||
version: 0.6.2
|
||||
devDependencies:
|
||||
'@types/crypto-js':
|
||||
specifier: ^4.2.2
|
||||
version: 4.2.2
|
||||
'@types/xml2js':
|
||||
specifier: ^0.4.14
|
||||
version: 0.4.14
|
||||
|
||||
wxmsg/pack-dist:
|
||||
dependencies:
|
||||
'@kevisual/context':
|
||||
specifier: ^0.0.4
|
||||
version: 0.0.4
|
||||
'@kevisual/query':
|
||||
specifier: ^0.0.29
|
||||
version: 0.0.29(@kevisual/ws@8.0.0)(zod@3.25.67)
|
||||
'@kevisual/router':
|
||||
specifier: 0.0.33
|
||||
version: 0.0.33
|
||||
@@ -198,34 +179,6 @@ importers:
|
||||
specifier: ^0.6.2
|
||||
version: 0.6.2
|
||||
|
||||
wxmsg/task/worker:
|
||||
dependencies:
|
||||
'@kevisual/context':
|
||||
specifier: ^0.0.4
|
||||
version: 0.0.4
|
||||
'@kevisual/router':
|
||||
specifier: 0.0.33
|
||||
version: 0.0.33
|
||||
'@types/node':
|
||||
specifier: ^24.10.1
|
||||
version: 24.10.1
|
||||
crypto-js:
|
||||
specifier: ^4.2.0
|
||||
version: 4.2.0
|
||||
xml2js:
|
||||
specifier: ^0.6.2
|
||||
version: 0.6.2
|
||||
devDependencies:
|
||||
'@types/bun':
|
||||
specifier: ^1.3.3
|
||||
version: 1.3.3
|
||||
'@types/crypto-js':
|
||||
specifier: ^4.2.2
|
||||
version: 4.2.2
|
||||
'@types/xml2js':
|
||||
specifier: ^0.4.14
|
||||
version: 0.4.14
|
||||
|
||||
packages:
|
||||
|
||||
'@ioredis/commands@1.4.0':
|
||||
@@ -239,8 +192,8 @@ packages:
|
||||
resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
'@kevisual/ai@0.0.12':
|
||||
resolution: {integrity: sha512-c02ozy4B+1utJsjQee4nnQZ2vDKTMDYJxbya5CIpghm+ujs7jSiB05cyIREELAHErSOzJUQXlHC3Dr8rdb5F0A==}
|
||||
'@kevisual/ai@0.0.15':
|
||||
resolution: {integrity: sha512-7oX/wHUKJCfvphFJq7fLBGpl4f6ASEJooQVvmgHZ7fZiYBEeVAEYAB28BNqk36iOItEyWlhuOCxq1oQz3wN+XQ==}
|
||||
|
||||
'@kevisual/auth@1.0.5':
|
||||
resolution: {integrity: sha512-GwsLj7unKXi7lmMiIIgdig4LwwLiDJnOy15HHZR5gMbyK6s5/uJiMY5RXPB2+onGzTNDqFo/hXjsD2wkerHPVg==}
|
||||
@@ -272,6 +225,9 @@ packages:
|
||||
'@kevisual/permission@0.0.3':
|
||||
resolution: {integrity: sha512-8JsA/5O5Ax/z+M+MYpFYdlioHE6jNmWMuFSokBWYs9CCAHNiSKMR01YLkoVDoPvncfH/Y8F5K/IEXRCbptuMNA==}
|
||||
|
||||
'@kevisual/query@0.0.29':
|
||||
resolution: {integrity: sha512-rQZk0J073UuC1QGzuyq+pb4Y0hu8/Qx/xYHs9NbsmslM+RuMnd1zpXmvhXNj7Kn1MdYTH90ng2MlFLBkkQFaIg==}
|
||||
|
||||
'@kevisual/router@0.0.21':
|
||||
resolution: {integrity: sha512-XKTxbNO924cT18UOAGplWErZ+hMze8Y53F2jYCk18v4jsdsvjRho5uXXjJb6HSVsuITMtQR4R3rG0IcM3jkDKQ==}
|
||||
|
||||
@@ -377,9 +333,6 @@ packages:
|
||||
'@types/archiver@7.0.0':
|
||||
resolution: {integrity: sha512-/3vwGwx9n+mCQdYZ2IKGGHEFL30I96UgBlk8EtRDDFQ9uxM1l4O5Ci6r00EMAkiDaTqD9DQ6nVrWRICnBPtzzg==}
|
||||
|
||||
'@types/bun@1.3.3':
|
||||
resolution: {integrity: sha512-ogrKbJ2X5N0kWLLFKeytG0eHDleBYtngtlbu9cyBKFtNL3cnpDZkNdQj8flVf6WTZUX5ulI9AY1oa7ljhSrp+g==}
|
||||
|
||||
'@types/busboy@1.5.4':
|
||||
resolution: {integrity: sha512-kG7WrUuAKK0NoyxfQHsVE6j1m01s6kMma64E+OZenQABMQyTJop1DumUWcLwAQ2JzpefU7PDYoRDKl8uZosFjw==}
|
||||
|
||||
@@ -434,9 +387,6 @@ packages:
|
||||
'@types/ws@8.18.1':
|
||||
resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==}
|
||||
|
||||
'@types/xml2js@0.4.14':
|
||||
resolution: {integrity: sha512-4YnrRemBShWRO2QjvUin8ESA41rH+9nQGLUGZV/1IDhi3SL9OhdpNC/MrulTWuptXKwhx/aDxE7toV0f/ypIXQ==}
|
||||
|
||||
'@zxing/text-encoding@0.9.0':
|
||||
resolution: {integrity: sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA==}
|
||||
|
||||
@@ -573,9 +523,6 @@ packages:
|
||||
bullmq@5.65.1:
|
||||
resolution: {integrity: sha512-QgDAzX1G9L5IRy4Orva5CfQTXZT+5K+OfO/kbPrAqN+pmL9LJekCzxijXehlm/u2eXfWPfWvIdJJIqiuz3WJSg==}
|
||||
|
||||
bun-types@1.3.3:
|
||||
resolution: {integrity: sha512-z3Xwlg7j2l9JY27x5Qn3Wlyos8YAp0kKRlrePAOjgjMGS5IG6E7Jnlx736vH9UVI4wUICwwhC9anYL++XeOgTQ==}
|
||||
|
||||
busboy@1.6.0:
|
||||
resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==}
|
||||
engines: {node: '>=10.16.0'}
|
||||
@@ -1261,6 +1208,18 @@ packages:
|
||||
once@1.4.0:
|
||||
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
|
||||
|
||||
openai@5.23.2:
|
||||
resolution: {integrity: sha512-MQBzmTulj+MM5O8SKEk/gL8a7s5mktS9zUtAkU257WjvobGc9nKcBuVwjyEEcb9SI8a8Y2G/mzn3vm9n1Jlleg==}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
ws: ^8.18.0
|
||||
zod: ^3.23.8
|
||||
peerDependenciesMeta:
|
||||
ws:
|
||||
optional: true
|
||||
zod:
|
||||
optional: true
|
||||
|
||||
p-queue@9.0.1:
|
||||
resolution: {integrity: sha512-RhBdVhSwJb7Ocn3e8ULk4NMwBEuOxe+1zcgphUy9c2e5aR/xbEsdVXxHJ3lynw6Qiqu7OINEyHlZkiblEpaq7w==}
|
||||
engines: {node: '>=20'}
|
||||
@@ -1871,9 +1830,10 @@ snapshots:
|
||||
dependencies:
|
||||
minipass: 7.1.2
|
||||
|
||||
'@kevisual/ai@0.0.12':
|
||||
'@kevisual/ai@0.0.15':
|
||||
dependencies:
|
||||
'@kevisual/logger': 0.0.4
|
||||
'@kevisual/permission': 0.0.3
|
||||
|
||||
'@kevisual/auth@1.0.5': {}
|
||||
|
||||
@@ -1977,6 +1937,20 @@ snapshots:
|
||||
|
||||
'@kevisual/permission@0.0.3': {}
|
||||
|
||||
'@kevisual/query@0.0.29(@kevisual/ws@8.0.0)(zod@3.25.67)':
|
||||
dependencies:
|
||||
openai: 5.23.2(@kevisual/ws@8.0.0)(zod@3.25.67)
|
||||
transitivePeerDependencies:
|
||||
- ws
|
||||
- zod
|
||||
|
||||
'@kevisual/query@0.0.29(@kevisual/ws@8.0.0)(zod@4.1.13)':
|
||||
dependencies:
|
||||
openai: 5.23.2(@kevisual/ws@8.0.0)(zod@4.1.13)
|
||||
transitivePeerDependencies:
|
||||
- ws
|
||||
- zod
|
||||
|
||||
'@kevisual/router@0.0.21':
|
||||
dependencies:
|
||||
path-to-regexp: 8.3.0
|
||||
@@ -2111,10 +2085,6 @@ snapshots:
|
||||
dependencies:
|
||||
'@types/readdir-glob': 1.1.5
|
||||
|
||||
'@types/bun@1.3.3':
|
||||
dependencies:
|
||||
bun-types: 1.3.3
|
||||
|
||||
'@types/busboy@1.5.4':
|
||||
dependencies:
|
||||
'@types/node': 24.10.1
|
||||
@@ -2174,10 +2144,6 @@ snapshots:
|
||||
dependencies:
|
||||
'@types/node': 24.10.1
|
||||
|
||||
'@types/xml2js@0.4.14':
|
||||
dependencies:
|
||||
'@types/node': 24.10.1
|
||||
|
||||
'@zxing/text-encoding@0.9.0':
|
||||
optional: true
|
||||
|
||||
@@ -2318,10 +2284,6 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
bun-types@1.3.3:
|
||||
dependencies:
|
||||
'@types/node': 24.10.1
|
||||
|
||||
busboy@1.6.0:
|
||||
dependencies:
|
||||
streamsearch: 1.1.0
|
||||
@@ -2435,10 +2397,6 @@ snapshots:
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
|
||||
debug@4.4.3:
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
|
||||
debug@4.4.3(supports-color@5.5.0):
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
@@ -2992,6 +2950,16 @@ snapshots:
|
||||
dependencies:
|
||||
wrappy: 1.0.2
|
||||
|
||||
openai@5.23.2(@kevisual/ws@8.0.0)(zod@3.25.67):
|
||||
optionalDependencies:
|
||||
ws: '@kevisual/ws@8.0.0'
|
||||
zod: 3.25.67
|
||||
|
||||
openai@5.23.2(@kevisual/ws@8.0.0)(zod@4.1.13):
|
||||
optionalDependencies:
|
||||
ws: '@kevisual/ws@8.0.0'
|
||||
zod: 4.1.13
|
||||
|
||||
p-queue@9.0.1:
|
||||
dependencies:
|
||||
eventemitter3: 5.0.1
|
||||
@@ -3298,7 +3266,7 @@ snapshots:
|
||||
|
||||
send@1.2.0:
|
||||
dependencies:
|
||||
debug: 4.4.3
|
||||
debug: 4.4.3(supports-color@5.5.0)
|
||||
encodeurl: 2.0.0
|
||||
escape-html: 1.0.3
|
||||
etag: 1.8.1
|
||||
|
||||
@@ -8,7 +8,14 @@ import { OauthUser } from '../oauth/oauth.ts';
|
||||
export const redis = useContextKey<Redis>('redis');
|
||||
|
||||
const UserSecretStatus = ['active', 'inactive', 'expired'] as const;
|
||||
|
||||
const randomString = (length: number) => {
|
||||
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
||||
let result = '';
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
}
|
||||
return result;
|
||||
};
|
||||
type Data = {
|
||||
[key: string]: any;
|
||||
/**
|
||||
@@ -45,10 +52,12 @@ export class UserSecret extends Model {
|
||||
if (!oauth.isSecretKey(token)) {
|
||||
return await oauth.verifyToken(token);
|
||||
}
|
||||
// const secretToken = await oauth.verifyToken(token);
|
||||
// if (secretToken) {
|
||||
// return secretToken;
|
||||
// }
|
||||
const secretToken = await oauth.verifyToken(token);
|
||||
if (secretToken) {
|
||||
console.log('verifyToken: verified as normal token');
|
||||
return secretToken;
|
||||
}
|
||||
console.log('verifyToken: try to verify as secret key');
|
||||
const userSecret = await UserSecret.findOne({
|
||||
where: { token },
|
||||
});
|
||||
@@ -66,7 +75,7 @@ export class UserSecret extends Model {
|
||||
if (!oauthUser) {
|
||||
return null; // 如果没有找到对应的oauth用户,则返回null
|
||||
}
|
||||
// await oauth.saveSecretKey(oauthUser, userSecret.token);
|
||||
await oauth.saveSecretKey(oauthUser, userSecret.token);
|
||||
// 存储到oauth中的token store中
|
||||
return oauthUser;
|
||||
}
|
||||
@@ -74,10 +83,10 @@ export class UserSecret extends Model {
|
||||
* owner 组织用户的 oauthUser
|
||||
* @returns
|
||||
*/
|
||||
async getOauthUser() {
|
||||
async getOauthUser(opts?: { wx?: boolean }) {
|
||||
const user = await User.findOne({
|
||||
where: { id: this.userId },
|
||||
attributes: ['id', 'username', 'type', 'owner'],
|
||||
attributes: ['id', 'username', 'type', 'owner', 'data'],
|
||||
});
|
||||
let org: User = null;
|
||||
if (!user) {
|
||||
@@ -117,6 +126,44 @@ export class UserSecret extends Model {
|
||||
const expiredTime = new Date(this.expiredTime);
|
||||
return now > expiredTime.getTime(); // 如果当前时间大于过期时间,则认为已过期
|
||||
}
|
||||
/**
|
||||
* 检查是否过期,如果过期则更新状态为expired
|
||||
*
|
||||
* @returns
|
||||
*/
|
||||
async checkOnUse() {
|
||||
if (!this.expiredTime) {
|
||||
return {
|
||||
code: 200
|
||||
}
|
||||
}
|
||||
try {
|
||||
|
||||
const now = Date.now();
|
||||
const expiredTime = new Date(this.expiredTime);
|
||||
const isExpired = now > expiredTime.getTime(); // 如果当前时间大于过期时间,则认为已过期
|
||||
if (isExpired) {
|
||||
this.status = 'active';
|
||||
const expireTime = UserSecret.getExpiredTime();
|
||||
this.expiredTime = expireTime;
|
||||
await this.save()
|
||||
}
|
||||
if (this.status !== 'active') {
|
||||
this.status = 'active';
|
||||
await this.save()
|
||||
}
|
||||
return {
|
||||
code: 200
|
||||
};
|
||||
}
|
||||
catch (e) {
|
||||
console.error('checkExpiredAndUpdate error', this.id, this.title);
|
||||
return {
|
||||
code: 500,
|
||||
message: 'checkExpiredAndUpdate error'
|
||||
}
|
||||
}
|
||||
}
|
||||
async createNewToken() {
|
||||
if (this.token) {
|
||||
await oauth.delToken(this.token);
|
||||
@@ -134,8 +181,21 @@ export class UserSecret extends Model {
|
||||
}
|
||||
return token;
|
||||
}
|
||||
static async createSecret(tokenUser: { id: string; uid?: string }, expireDay = 365) {
|
||||
const expireTime = expireDay * 24 * 60 * 60 * 1000; // 转换为毫秒
|
||||
/**
|
||||
* 根据 unionid 生成redis的key
|
||||
* `wxmp:unionid:token:${unionid}`
|
||||
* @param unionid
|
||||
* @returns
|
||||
*/
|
||||
static wxRedisKey(unionid: string) {
|
||||
return `wxmp:unionid:token:${unionid}`;
|
||||
}
|
||||
static getExpiredTime(expireDays?: number) {
|
||||
const defaultExpireDays = expireDays || 365;
|
||||
const expireTime = defaultExpireDays * 24 * 60 * 60 * 1000;
|
||||
return new Date(Date.now() + expireTime)
|
||||
}
|
||||
static async createSecret(tokenUser: { id: string; uid?: string, title?: string }, expireDays = 365) {
|
||||
const token = await UserSecret.createToken();
|
||||
let userId = tokenUser.id;
|
||||
let orgId: string = null;
|
||||
@@ -147,11 +207,13 @@ export class UserSecret extends Model {
|
||||
userId,
|
||||
orgId,
|
||||
token,
|
||||
expiredTime: new Date(Date.now() + expireTime),
|
||||
title: tokenUser.title || randomString(6),
|
||||
expiredTime: UserSecret.getExpiredTime(expireDays),
|
||||
});
|
||||
|
||||
return userSecret;
|
||||
}
|
||||
|
||||
async getPermission(opts: { id: string; uid?: string }) {
|
||||
const { id, uid } = opts;
|
||||
let userId: string = id;
|
||||
|
||||
@@ -2,6 +2,7 @@ import { User, UserInit, UserServices } from '../auth/models/index.ts';
|
||||
import { UserSecretInit, UserSecret } from '../auth/models/index.ts';
|
||||
import { OrgInit } from '../auth/models/index.ts';
|
||||
export { User, UserInit, UserServices, UserSecret };
|
||||
import { useContextKey } from '@kevisual/context';
|
||||
const init = async () => {
|
||||
await OrgInit(null, null, {
|
||||
alter: true,
|
||||
@@ -21,5 +22,7 @@ const init = async () => {
|
||||
}).catch((e) => {
|
||||
console.error('UserSecret sync', e);
|
||||
});
|
||||
console.log('Models synced');
|
||||
useContextKey('models-synced', true);
|
||||
};
|
||||
init();
|
||||
|
||||
@@ -68,16 +68,7 @@ export class WxServices {
|
||||
},
|
||||
});
|
||||
// @ts-ignore
|
||||
if (type === 'open' && user && user.data.wxOpenid !== token.openid) {
|
||||
user.data = {
|
||||
...user.data,
|
||||
// @ts-ignore
|
||||
wxOpenid: token.openid,
|
||||
};
|
||||
user = await user.update({ data: user.data });
|
||||
console.log('mp-user login openid update=============', token.openid, token.unionid);
|
||||
// @ts-ignore
|
||||
} else if (type === 'mp' && user && user.data.wxmpOpenid !== token.openid) {
|
||||
if (type === 'mp' && user && user.data.wxmpOpenid !== token.openid) {
|
||||
user.data = {
|
||||
...user.data,
|
||||
// @ts-ignore
|
||||
@@ -94,7 +85,7 @@ export class WxServices {
|
||||
canChangeUsername: true,
|
||||
};
|
||||
user.data = data;
|
||||
if ((type = 'mp')) {
|
||||
if (type === 'mp') {
|
||||
// @ts-ignore
|
||||
data.wxmpOpenid = token.openid;
|
||||
} else {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Op } from 'sequelize';
|
||||
import { User, UserSecret } from '@/models/user.ts';
|
||||
import { app } from '@/app.ts';
|
||||
|
||||
import { redis } from '@/app.ts';
|
||||
app
|
||||
.route({
|
||||
path: 'secret',
|
||||
@@ -10,7 +10,7 @@ app
|
||||
})
|
||||
.define(async (ctx) => {
|
||||
const tokenUser = ctx.state.tokenUser;
|
||||
const { page = 1, pageSize = 100, search, sort = 'DESC', orgId } = ctx.query;
|
||||
const { page = 1, pageSize = 100, search, sort = 'DESC', orgId, showToken = false } = ctx.query;
|
||||
const searchWhere: Record<string, any> = search
|
||||
? {
|
||||
[Op.or]: [{ title: { [Op.like]: `%${search}%` } }, { description: { [Op.like]: `%${search}%` } }],
|
||||
@@ -18,7 +18,10 @@ app
|
||||
: {};
|
||||
if (orgId) {
|
||||
searchWhere.orgId = orgId;
|
||||
} else {
|
||||
searchWhere.orgId = null;
|
||||
}
|
||||
const excludeFields = showToken ? [] : ['token'];
|
||||
const { rows: secrets, count } = await UserSecret.findAndCountAll({
|
||||
where: {
|
||||
userId: tokenUser.userId,
|
||||
@@ -27,7 +30,7 @@ app
|
||||
offset: (page - 1) * pageSize,
|
||||
limit: pageSize,
|
||||
attributes: {
|
||||
exclude: ['token'], // Exclude sensitive token field
|
||||
exclude: excludeFields, // Exclude sensitive token field
|
||||
},
|
||||
order: [['updatedAt', sort]],
|
||||
});
|
||||
@@ -166,3 +169,52 @@ app
|
||||
ctx.body = secret;
|
||||
})
|
||||
.addTo(app);
|
||||
|
||||
app.route({
|
||||
path: 'secret',
|
||||
key: 'wxnotify',
|
||||
description: '为了微信去缓存需要的数据, unionid是公众号下的用户的unionid',
|
||||
}).define(async (ctx) => {
|
||||
const { openid, unionid } = ctx.query;
|
||||
if (!openid && !unionid) {
|
||||
// ctx.throw(400, '需要提供 openid 或者 unionid 参数');
|
||||
ctx.throw(400, '需要提供 unionid 参数');
|
||||
}
|
||||
// 最少20为的openid
|
||||
if (unionid.length < 20) {
|
||||
ctx.throw(400, 'unionid 是必填的');
|
||||
}
|
||||
const redisKey = UserSecret.wxRedisKey(unionid);
|
||||
const token = await redis.get(redisKey);
|
||||
if (token) {
|
||||
ctx.body = 'success'
|
||||
return;
|
||||
}
|
||||
const user = await User.findOne({
|
||||
where: {
|
||||
data: {
|
||||
wxUnionId: unionid
|
||||
}
|
||||
}
|
||||
})
|
||||
if (!user) {
|
||||
ctx.throw(404, '请关注公众号《人生可视化助手》后再操作');
|
||||
return
|
||||
}
|
||||
let secretKey = await UserSecret.findOne({
|
||||
where: {
|
||||
userId: user.id,
|
||||
title: 'wxmp-notify-token'
|
||||
}
|
||||
});
|
||||
if (!secretKey) {
|
||||
secretKey = await UserSecret.createSecret({ id: user.id, title: 'wxmp-notify-token' });
|
||||
}
|
||||
const check = await secretKey.checkOnUse();
|
||||
if (check.code !== 200) {
|
||||
ctx.throw(check.code, check.message);
|
||||
}
|
||||
await redis.set(redisKey, secretKey.token, 'EX', 30 * 24 * 60 * 60); // 30天过期
|
||||
ctx.body = 'success'
|
||||
|
||||
}).addTo(app);
|
||||
33
src/test/common.ts
Normal file
33
src/test/common.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { app } from '@/app.ts';
|
||||
import '@/route.ts';
|
||||
import { useConfig, useContextKey } from '@kevisual/context';
|
||||
import { Query } from '@kevisual/query';
|
||||
import util from 'node:util';
|
||||
export {
|
||||
app,
|
||||
useContextKey
|
||||
}
|
||||
export const config = useConfig();
|
||||
|
||||
export const token = config.TOKEN || '';
|
||||
|
||||
export const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
export const showRes = (res, ...args) => {
|
||||
if (res.code === 200) {
|
||||
if (args.length === 0) {
|
||||
console.log(res.code, util.inspect(res.body, { depth: 6, colors: true }));
|
||||
return;
|
||||
}
|
||||
console.log(res.code, ...args);
|
||||
} else {
|
||||
console.error(res.code, res.message, ...args);
|
||||
}
|
||||
}
|
||||
|
||||
export const exit = (code = 0) => {
|
||||
process.exit(code);
|
||||
}
|
||||
|
||||
export const query = new Query({
|
||||
url: 'https://kevisual.cn/api/router'
|
||||
})
|
||||
45
src/test/secret-key.ts
Normal file
45
src/test/secret-key.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { app, token, showRes, sleep, useContextKey, exit, query } from './common.ts'
|
||||
|
||||
// await sleep(4000)
|
||||
// const token2 = 'sk_6m3gjpkpny2ma9r96ei3bzck3kpg7b7g4oajghw7gmqoqk0vlh3swgxy85e0wnpt'
|
||||
// await useContextKey('models-synced');
|
||||
|
||||
// const res = await app.call({
|
||||
// path: 'secret',
|
||||
// key: 'list',
|
||||
// payload: {
|
||||
// token,
|
||||
// showToken: true
|
||||
// }
|
||||
// })
|
||||
// showRes(res)
|
||||
|
||||
|
||||
// const userRes = await app.call({
|
||||
// path: 'user',
|
||||
// key: 'me',
|
||||
// payload: {
|
||||
// token: token2,
|
||||
// }
|
||||
// })
|
||||
// showRes(userRes)
|
||||
|
||||
// const openid = 'omcvy7AHC6bAA0QM4x9_bE0fGD1g'
|
||||
// const res = await app.call({
|
||||
// path: 'secret',
|
||||
// key: 'wxnotify',
|
||||
// payload: {
|
||||
// openid
|
||||
// }
|
||||
// });
|
||||
// showRes(res)
|
||||
|
||||
const res = await query.post({
|
||||
path: 'secret',
|
||||
key: 'wxnotify',
|
||||
payload: {
|
||||
openid: 'omcvy7M5CBAIB8TWDw6gNDHeHGeE'
|
||||
}
|
||||
})
|
||||
showRes(res)
|
||||
exit(0);
|
||||
@@ -27,6 +27,7 @@
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@kevisual/context": "^0.0.4",
|
||||
"@kevisual/query": "^0.0.29",
|
||||
"@kevisual/router": "0.0.33",
|
||||
"@types/node": "^24.10.1",
|
||||
"crypto-js": "^4.2.0",
|
||||
|
||||
3
wxmsg/readme.md
Normal file
3
wxmsg/readme.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# 根据 wx 的内容进行改动
|
||||
|
||||
如果更新了, 需要重新打包
|
||||
@@ -7,9 +7,14 @@ import http from 'node:http';
|
||||
import { Wx, WxMsgEvent, parseWxMessage } from './wx/index.ts';
|
||||
import { contextConfig as config } from './modules/config.ts';
|
||||
import { loginByTicket } from './wx/login-by-ticket.ts';
|
||||
import { Queue } from 'bullmq';
|
||||
export const simpleRouter: SimpleRouter = await useContextKey('router');
|
||||
export const redis: Redis = await useContextKey('redis');
|
||||
|
||||
export const wxmsgQueue = useContextKey<Queue>('wxmsgQueue', () => {
|
||||
return new Queue('wxmsg', {
|
||||
connection: redis
|
||||
});
|
||||
});
|
||||
simpleRouter.get('/api/wxmsg', async (req: http.IncomingMessage, res: http.ServerResponse) => {
|
||||
console.log('微信检测服务是否可用');
|
||||
const query = simpleRouter.getSearch(req);
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
import { Queue } from 'bullmq';
|
||||
|
||||
export const wxmsgQueue = new Queue('wxmsg', {
|
||||
connection: {
|
||||
host: process.env.REDIS_HOST || 'kevisual.cn',
|
||||
port: parseInt(process.env.REDIS_PORT || '6379'),
|
||||
password: process.env.REDIS_PASSWORD,
|
||||
}
|
||||
});
|
||||
38
wxmsg/src/test/get-unionid.ts
Normal file
38
wxmsg/src/test/get-unionid.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { Redis } from 'ioredis';
|
||||
import { useConfig } from '@kevisual/use-config';
|
||||
const config = useConfig();
|
||||
const redis = new Redis({
|
||||
password: config.REDIS_PASSWORD
|
||||
});
|
||||
|
||||
/**
|
||||
* 公众号获取用户信息
|
||||
* @param token
|
||||
* @param openid
|
||||
* @returns
|
||||
*/
|
||||
export const getUserInfoByMp = async (token: string, openid: string) => {
|
||||
// const phoneUrl = `https://api.weixin.qq.com/sns/userinfo?access_token=${token}&openid=${openid}`;
|
||||
const phoneUrl = `https://api.weixin.qq.com/cgi-bin/user/info?access_token=${token}&openid=${openid}&lang=zh_CN`;
|
||||
|
||||
const res = await fetch(phoneUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
const data = await res.json();
|
||||
console.log('userinfo', data);
|
||||
return data;
|
||||
};
|
||||
|
||||
// 大号的程序的服务号的openid
|
||||
const openid = 'omcvy7AHC6bAA0QM4x9_bE0fGD1g'
|
||||
const main = async () => {
|
||||
const appId = config.WX_MP_APP_ID!;
|
||||
const _accessToken = await redis.get(`wx:access_token:${appId}`);
|
||||
|
||||
const data = await getUserInfoByMp(_accessToken!, openid);
|
||||
console.log('用户信息:', data);
|
||||
}
|
||||
main();
|
||||
@@ -3,10 +3,12 @@ import { Redis } from 'ioredis';
|
||||
import { WxCustomServiceMsg, WxMsgText } from './type/custom-service.ts';
|
||||
import { Queue } from 'bullmq';
|
||||
import { useContextKey } from "@kevisual/context";
|
||||
|
||||
import { Query } from '@kevisual/query';
|
||||
import { getUserInfoByMp } from './test/get-user-info.ts';
|
||||
export * from './type/custom-service.ts';
|
||||
export * from './type/send.ts';
|
||||
|
||||
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
|
||||
/**
|
||||
* 从
|
||||
* @param str
|
||||
@@ -21,10 +23,14 @@ export class Wx {
|
||||
private appId: string;
|
||||
private appSecret: string;
|
||||
public redis: Redis | null = null;
|
||||
constructor({ appId, appSecret, redis }: { appId: string; appSecret: string; redis?: Redis }) {
|
||||
query: Query = new Query();
|
||||
constructor({ appId, appSecret, redis, url }: { appId: string; appSecret: string; redis?: Redis; url?: string }) {
|
||||
this.appId = appId;
|
||||
this.appSecret = appSecret;
|
||||
this.redis = redis! || null;
|
||||
this.query = new Query({
|
||||
url: url || 'http://localhost:3005'
|
||||
});
|
||||
}
|
||||
|
||||
public async getAccessToken(): Promise<string> {
|
||||
@@ -83,4 +89,93 @@ export class Wx {
|
||||
async get(url: string) {
|
||||
return fetch(url).then((res) => res.json());
|
||||
}
|
||||
async getUnionid(openid: string) {
|
||||
const cacheRedisKey = `wxmp:openid:unionid:${openid}`;
|
||||
const cachedUnionid = await this.redis!.get(cacheRedisKey);
|
||||
if (cachedUnionid) {
|
||||
return cachedUnionid;
|
||||
}
|
||||
const accessToken = await this.getAccessToken();
|
||||
const res = await getUserInfoByMp(accessToken, openid);
|
||||
if (!res?.unionid) {
|
||||
throw new Error('无法获取用户的unionid,用户是否未关注公众号?');
|
||||
}
|
||||
await this.redis!.set(cacheRedisKey, res.unionid, 'EX', 7 * 24 * 3600); // 缓存7天
|
||||
return res.unionid;
|
||||
}
|
||||
async getCenterCodeToken(touser: string) {
|
||||
const unionid = await this.getUnionid(touser);
|
||||
// 第一次尝试获取token
|
||||
let token = await this.redis!.get(getWxRedisKey(unionid));
|
||||
if (!token) {
|
||||
const msg = {
|
||||
path: 'secret',
|
||||
key: 'wxnotify',
|
||||
payload: { unionid: unionid }
|
||||
}
|
||||
const res = await this.query.post(msg);
|
||||
if (res.code !== 200) {
|
||||
console.error('获取不到用户配置,是否用户未关注公众号?', res, 'openid', touser, "unionid", unionid);
|
||||
throw new Error('获取不到用户配置,是否用户未关注公众号?');
|
||||
}
|
||||
|
||||
await sleep(1000);
|
||||
}
|
||||
// 尝试更新后第二次获取token
|
||||
token = await this.redis!.get(getWxRedisKey(unionid));
|
||||
if (!token) {
|
||||
console.error('这个uid获取不到用户配置', touser, "unionid", unionid);
|
||||
throw new Error('获取不到用户配置,是否用户未关注公众号?');
|
||||
}
|
||||
return token;
|
||||
}
|
||||
|
||||
async postCenter(data: any) {
|
||||
await this.getAccessToken();
|
||||
const { touser, msg } = data;
|
||||
const msgType = msg.msgtype;
|
||||
const sendUserText = async (text: string) => {
|
||||
const sendData = {
|
||||
touser,
|
||||
msgtype: 'text',
|
||||
text: {
|
||||
content: text,
|
||||
},
|
||||
};
|
||||
await this.sendUserMessage(sendData);
|
||||
}
|
||||
const userToken = await this.getCenterCodeToken(touser);
|
||||
if (!userToken) {
|
||||
await sendUserText('服务器错误:无法获取用户绑定的账号信息');
|
||||
console.error('用户未绑定账号,无法自动回复', touser);
|
||||
return;
|
||||
}
|
||||
|
||||
if (msgType !== 'text') {
|
||||
await sendUserText('暂不支持该类型消息的自动回复');
|
||||
return;
|
||||
}
|
||||
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 getWxRedisKey = (unionid: string) => {
|
||||
return `wxmp:unionid:token:${unionid}`
|
||||
}
|
||||
@@ -1,12 +1,31 @@
|
||||
|
||||
|
||||
type UserInfo = {
|
||||
subscribe: number;
|
||||
openid: string;
|
||||
nickname: string;
|
||||
sex: number;
|
||||
language: string;
|
||||
city: string;
|
||||
province: string;
|
||||
country: string;
|
||||
headimgurl: string;
|
||||
subscribe_time: number;
|
||||
unionid: string;
|
||||
remark: string;
|
||||
groupid: number;
|
||||
tagid_list: number[];
|
||||
subscribe_scene: string;
|
||||
qr_scene: number;
|
||||
qr_scene_str: string;
|
||||
};
|
||||
/**
|
||||
* 公众号获取用户信息
|
||||
* @param token
|
||||
* @param openid
|
||||
* @returns
|
||||
*/
|
||||
export const getUserInfoByMp = async (token: string, openid: string) => {
|
||||
export const getUserInfoByMp = async (token: string, openid: string): Promise<UserInfo> => {
|
||||
// const phoneUrl = `https://api.weixin.qq.com/sns/userinfo?access_token=${token}&openid=${openid}`;
|
||||
const phoneUrl = `https://api.weixin.qq.com/cgi-bin/user/info?access_token=${token}&openid=${openid}&lang=zh_CN`;
|
||||
|
||||
@@ -17,6 +36,5 @@ export const getUserInfoByMp = async (token: string, openid: string) => {
|
||||
},
|
||||
});
|
||||
const data = await res.json();
|
||||
console.log('userinfo', data);
|
||||
return data;
|
||||
};
|
||||
|
||||
@@ -1,30 +1,31 @@
|
||||
import { Worker } from "bullmq";
|
||||
import { redis } from './redis.ts';
|
||||
import { redis, config } from './redis.ts';
|
||||
|
||||
import { Wx } from "../../src/wx";
|
||||
|
||||
|
||||
const worker = new Worker('wxmsg', async job => {
|
||||
const url = 'http://localhost:3005/api/router';
|
||||
const wx = new Wx({
|
||||
appId: process.env.WX_APPID || '',
|
||||
appSecret: process.env.WX_APPSECRET || '',
|
||||
redis: redis
|
||||
appId: config.WX_MP_APP_ID, appSecret: config.WX_MP_APP_SECRET,
|
||||
redis: redis,
|
||||
url,
|
||||
});
|
||||
if (job.name === 'analyzeUserMsg') {
|
||||
const { touser, msg } = job.data;
|
||||
const accessToken = await wx.getAccessToken();
|
||||
const sendData = {
|
||||
touser,
|
||||
msgtype: 'text',
|
||||
text: {
|
||||
content: 'Hello World' + new Date().toISOString(),
|
||||
},
|
||||
};
|
||||
await wx.sendUserMessage(sendData);
|
||||
await wx.postCenter(job.data);
|
||||
} else {
|
||||
throw new Error(`Unknown job name: ${job.name}`);
|
||||
}
|
||||
}, {
|
||||
connection: redis
|
||||
connection: redis,
|
||||
removeOnComplete: {
|
||||
age: 3600, // 1 hour
|
||||
count: 5000, // keep last 5000 jobs
|
||||
},
|
||||
removeOnFail: {
|
||||
age: 7200, // 2 hours
|
||||
count: 5000, // keep last 5000 jobs
|
||||
},
|
||||
});
|
||||
|
||||
worker.on('completed', (job) => {
|
||||
|
||||
@@ -3,7 +3,6 @@ import { useConfig } from '@kevisual/use-config';
|
||||
import { useContextKey } from "@kevisual/context";
|
||||
|
||||
export const config = useConfig()
|
||||
|
||||
// 首先从 process.env 读取环境变量
|
||||
const redisConfig = {
|
||||
host: process.env.REDIS_HOST || 'kevisual.cn',
|
||||
@@ -27,14 +26,14 @@ export const createRedisClient = (options = {}) => {
|
||||
});
|
||||
// 监听连接事件
|
||||
redis.on('connect', () => {
|
||||
console.log('Redis 连接成功');
|
||||
// console.log('Redis 连接成功');
|
||||
});
|
||||
|
||||
redis.on('error', (err) => {
|
||||
console.error('Redis 连接错误', err);
|
||||
// console.error('Redis 连接错误', err);
|
||||
});
|
||||
redis.on('ready', () => {
|
||||
console.log('Redis 已准备好处理请求');
|
||||
// console.log('Redis 已准备好处理请求');
|
||||
});
|
||||
return redis;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user