Compare commits

..

24 Commits

Author SHA1 Message Date
52d37d0679 temp 2025-07-04 00:01:22 +08:00
6a9e847ff1 功能整理 2025-06-30 02:12:24 +08:00
143cbc877c feat: add comfort user 2025-06-30 01:59:26 +08:00
42da851c37 fix: 添加总结 2025-06-29 09:05:27 +08:00
e2d0720698 feat: add summary 2025-06-28 02:46:50 +08:00
204165bf73 fix: add xiao xiao 2025-06-27 22:38:24 +08:00
bce94f52a0 feat: 添加夸一下的内容 2025-06-25 11:35:16 +08:00
b807cc9f38 temp 2025-06-24 11:52:31 +08:00
a25f7c7eb4 update time 2025-06-22 19:41:51 +08:00
e982ddb001 update time fix 2025-06-22 19:41:01 +08:00
7bc9a06b7c fix: add unread 重试 2025-06-22 10:52:31 +08:00
d4d6960a6c fix: 改成60s 恢复一下,因为中途出锅一个bug 2025-06-22 10:29:31 +08:00
1220076257 fix: 改成30s的间隔 2025-06-22 10:14:19 +08:00
530a4f21f5 feat: 自己的笔记也需要at才能评论了,优化一下 2025-06-22 09:30:03 +08:00
71359fba88 fix: fix 2025-06-21 21:47:26 +08:00
29725a8614 fix 2025-06-21 19:31:51 +08:00
32b6e04d6c temp 2025-06-21 18:59:06 +08:00
da6d4041ad perf 2025-06-21 18:21:49 +08:00
a76b506327 fix: add split and for sleep post data 2025-06-21 17:32:08 +08:00
2c6c3dd60d bump xhs-core 2025-06-21 17:08:53 +08:00
d5c2964e0e fix 2025-06-21 17:07:49 +08:00
ebe7c9afce update 2025-06-21 16:59:46 +08:00
75f1d75cae add server 2025-06-21 16:48:10 +08:00
0b6b2fe730 fix: update 2025-06-21 16:47:33 +08:00
44 changed files with 1062 additions and 83 deletions

View File

@@ -1 +1,16 @@
# router app template
# 社交路由功能模块
## 小红书
自动获取和上传
[代理浏览器](https://git.xiongxiao.me/media/social-xhs-api-server)
### 功能
#### 获取评论
#### 获取笔记信息
#### 返回笔记信息

View File

@@ -15,6 +15,7 @@
"scripts": {
"dev": "cross-env NODE_TLS_REJECT_UNAUTHORIZED=0 bun --watch src/dev.ts ",
"worker": "bun --watch src/task/worker.ts",
"worker:server": "bun src/task/worker.ts",
"build": "rimraf dist && bun run bun.config.mjs",
"test": "tsx test/**/*.ts",
"clean": "rm -rf dist",

View File

@@ -1,6 +1,6 @@
const unreadUrl = 'https://edith.xiaohongshu.com/api/sns/web/unread_count';
const cookie =
'a1=19686f83a65uiloc0y7wv79une457hsjh5lt00dpe40000147626;abRequestId=0a794332-4561-5f49-93f7-780b8b028e1f;access-token-creator.xiaohongshu.com=customer.creator.AT-68c517498636784561614544frjvxzj7yu8iewie;agora_session=6a0031373435393132333637323735343733393437313634000000000000;customerClientId=536706778174172;galaxy_creator_session_id=OhpHDDSoADhNEhnH5LLnQpletFLApu1fd91f;galaxy.creator.beaker.session.id=1745912429847011598150;gid=yjKqYfK0qDyYyj2DDSqd4ujxyW9kvxIuT62ddkMWhElyuxq8yDd6hl888q2WYy88j8i80yYD;loadts=1746020512562;sec_poison_id=441c932e-a6ac-4d8d-97ae-beb14adb1929;unread={%22ub%22:%2267eaf1fe000000001202c3ea%22%2C%22ue%22:%226803aa37000000001c0319d8%22%2C%22uc%22:35};web_session=040069b2e9c511ca302086ca253a4bde8b1cd1;webBuild=4.62.3;webId=97e5f097499594cad49aa0bd1a8ed83f;websectiga=3633fe24d49c7dd0eb923edc8205740f10fdb18b25d424d2a2322c6196d2a4ad;x-user-id-creator.xiaohongshu.com=639d86590000000026006076;xsecappid=xhs-pc-web;acw_tc=0a00df6217460205042195762e721fba339a0dbe8e4738b961a5ff15e74619;';
'a1=xxxx;abRequestId=0a794332-4561-5f49-93f7-780b8b028e1f;access-token-creator.xiaohongshu.com=customer.creator.AT-68c517498636784561614544frjvxzj7yu8iewie;agora_session=6a0031373435393132333637323735343733393437313634000000000000;customerClientId=536706778174172;galaxy_creator_session_id=OhpHDDSoADhNEhnH5LLnQpletFLApu1fd91f;galaxy.creator.beaker.session.id=1745912429847011598150;gid=yjKqYfK0qDyYyj2DDSqd4ujxyW9kvxIuT62ddkMWhElyuxq8yDd6hl888q2WYy88j8i80yYD;loadts=1746020512562;sec_poison_id=441c932e-a6ac-4d8d-97ae-beb14adb1929;unread={%22ub%22:%2267eaf1fe000000001202c3ea%22%2C%22ue%22:%226803aa37000000001c0319d8%22%2C%22uc%22:35};web_session=040069b2e9c511ca302086ca253a4bde8b1cd1;webBuild=4.62.3;webId=97e5f097499594cad49aa0bd1a8ed83f;websectiga=3633fe24d49c7dd0eb923edc8205740f10fdb18b25d424d2a2322c6196d2a4ad;x-user-id-creator.xiaohongshu.com=639d86590000000026006076;xsecappid=xhs-pc-web;acw_tc=0a00df6217460205042195762e721fba339a0dbe8e4738b961a5ff15e74619;';
const cookieObj = cookie.split('; ').reduce((acc, item) => {
const [key, value] = item.split('=');
@@ -17,7 +17,7 @@ const headers = {
const meUri = '/api/sns/web/v2/user/me';
const getSign = async (uri: string, data: any, a1: string, web_session?: string) => {
const signs = await fetch('http://light.xiongxiao.me:5006/sign', {
const signs = await fetch('http://localhost:5006/sign', {
method: 'POST',
headers: {
'Content-Type': 'application/json',

View File

@@ -7,6 +7,7 @@ import path from 'path';
const rp = (resolvePath) => {
return path.resolve(process.cwd(), resolvePath);
};
const external = ['jsdom']
// bun run src/index.ts --
await Bun.build({
target: 'node',
@@ -16,6 +17,7 @@ await Bun.build({
naming: {
entry: 'app.mjs',
},
external: external,
define: {
VERSION: JSON.stringify(pkg.version),

View File

@@ -1,6 +1,6 @@
{
"name": "@kevisual/xhs-core",
"version": "0.0.2",
"version": "0.0.4",
"description": "",
"main": "dist/app.mjs",
"types": "dist/app.d.ts",

View File

@@ -1 +1,21 @@
db-sqlite
node_modules
dist
app.config.json5
apps.config.json
deploy.tar.gz
cache-file
/apps
logs
.env*
!.env.example
.turbo

View File

@@ -19,12 +19,12 @@
"keywords": [],
"author": "abearxiong <xiongxiao@xiongxiao.me> (https://www.xiongxiao.me)",
"license": "MIT",
"packageManager": "pnpm@10.10.0",
"packageManager": "pnpm@10.12.1",
"type": "module",
"devDependencies": {
"@kevisual/use-config": "^1.0.14",
"@kevisual/xhs-core": "workspace:*",
"@types/node": "^22.15.3"
"@kevisual/use-config": "^1.0.19",
"@kevisual/xhs-core": "0.0.4",
"@types/node": "^24.0.3"
},
"exports": {
".": {
@@ -33,6 +33,10 @@
},
"./index": {
"import": "./src/index.ts"
},
"./index.ts": {
"import": "./src/index.ts",
"types": "./src/index.ts"
}
},
"dependencies": {

View File

@@ -1,16 +1,15 @@
import { QueryRouterServer } from '@kevisual/router/browser';
import { XhsServices } from '@kevisual/xhs/services/xhs-services.ts';
import { XhsServices, XhsClient } from '@kevisual/xhs/services/xhs-services.ts';
export { XhsServices };
export const app = new QueryRouterServer();
export const xhsServices = new XhsServices();// Semicolon separated Cookie File
export const xhsServices = new XhsServices(); // Semicolon separated Cookie File
const cookie =
'a1=1978d0cdcb7p5neac7cesgfm9yat0b4a7hnesexkp30000220066;abRequestId=f98f27d6-cceb-53d9-a9ef-dbd68786231b;access-token-creator.xiaohongshu.com=customer.creator.AT-68c517518075239703025713copo06lnusenqic1;customer-sso-sid=68c517518075239698553486mipyx4lwk2ac5jaj;customerClientId=716985104518687;galaxy_creator_session_id=wda55FJhoiAWZ8cekT2aGiM9fuztft3mOdRM;galaxy.creator.beaker.session.id=1750438297110093146037;gid=yjWYf8Si4SWjyjWYf8SfSxdCDWU2Id0SWSdMITUki6jv0dq898D40M888JJ88KK8fdKS8jiy;loadts=1750444792784;sec_poison_id=2f592adb-ec34-48cd-a6ce-22b90bd67c3b;unread={%22ub%22:%226852a4100000000022030063%22%2C%22ue%22:%226854e0db00000000100251f6%22%2C%22uc%22:28};web_session=040069b6528dbc23c355980a603a4b3e03bb6a;webBuild=4.68.0;webId=1dbb23b746393db622165a22357897d5;websectiga=10f9a40ba454a07755a08f27ef8194c53637eba4551cf9751c009d9afb564467;x-user-id-creator.xiaohongshu.com=6726cef4000000001c019303;xsecappid=xhs-pc-web;acw_tc=0a00d41117504447915317110e41effee63c597aa77f2c9f0dc1b0a10248f8;';
'a1=****;abRequestId=f98f27d6-cceb-53d9-a9ef-dbd68786231b;access-token-creator.xiaohongshu.com=customer.creator.AT-68c517518075239703025713copo06lnusenqic1;customer-sso-sid=68c517518075239698553486mipyx4lwk2ac5jaj;customerClientId=716985104518687;galaxy_creator_session_id=wda55FJhoiAWZ8cekT2aGiM9fuztft3mOdRM;galaxy.creator.beaker.session.id=1750438297110093146037;gid=yjWYf8Si4SWjyjWYf8SfSxdCDWU2Id0SWSdMITUki6jv0dq898D40M888JJ88KK8fdKS8jiy;loadts=1750444792784;sec_poison_id=2f592adb-ec34-48cd-a6ce-22b90bd67c3b;unread={%22ub%22:%226852a4100000000022030063%22%2C%22ue%22:%226854e0db00000000100251f6%22%2C%22uc%22:28};web_session=040069b6528dbc23c355980a603a4b3e03bb6a;webBuild=4.68.0;webId=1dbb23b746393db622165a22357897d5;websectiga=10f9a40ba454a07755a08f27ef8194c53637eba4551cf9751c009d9afb564467;x-user-id-creator.xiaohongshu.com=6726cef4000000001c019303;xsecappid=xhs-pc-web;acw_tc=0a00d41117504447915317110e41effee63c597aa77f2c9f0dc1b0a10248f8;';
xhsServices.createRoot({
export const xhsRootClient: XhsClient = xhsServices.createRoot({
cookie,
signConfig: {
signUrl: 'http://light.xiongxiao.me:5006/sign',
// signUrl: 'http://localhost:5006/sign',
signUrl: 'http://localhost:5006/sign',
},
});

View File

@@ -1,5 +1,5 @@
import { XhsClient } from './libs/xhs.ts';
import { app, xhsServices } from './app.ts';
import { app, xhsServices, xhsRootClient, XhsServices } from './app.ts';
import './routes/index.ts';
export { XhsClient, app, xhsServices };
export { XhsClient, app, xhsServices, xhsRootClient, XhsServices };

View File

@@ -38,6 +38,7 @@ export const getSign = async (signInfo: SignInfo, options?: SignOptions): Promis
// signUrl = 'http://localhost:5005/sign';
// const urlA1 = ''http://light.xiongxiao.me:5006/a1';
// const urlA1 = 'http://localhost:5005/a1';
// console.log('sign', signUrl);
const signs = await fetch(signUrl, {
method: 'POST',
headers: {
@@ -66,6 +67,9 @@ export class XhsClient extends XhsClientBase {
constructor(opts: XhsOptions) {
super(opts as any);
}
setCookie(cookie: string) {
this.cookie = cookie;
}
getApiInfo = getApiInfo;
printResult(msg: string, data: any) {
if (msg === 'response') {
@@ -212,6 +216,7 @@ export class XhsClient extends XhsClientBase {
try {
const response = await this.post(uri, data, { sign: this.sign.bind(this) });
console.log('getNoteById response', response, typeof response);
// return response['items'][0]['node_card'];
return response;
} catch (error) {

View File

@@ -1 +1,2 @@
import './mentions/index.ts'
import './notes/index.ts'

View File

@@ -4,6 +4,14 @@ import { Mention } from '@kevisual/xhs/libs/xhs-type/mention.ts';
const sleep = (ms: number) => {
return new Promise((resolve) => setTimeout(resolve, ms));
};
export const splitContent = (content: string, maxLength: number = 270) => {
const parts: string[] = [];
for (let i = 0; i < content.length; i += maxLength) {
parts.push(content.slice(i, i + maxLength));
}
return parts;
};
app
.route({
path: 'mention',
@@ -18,6 +26,7 @@ app
} else {
ctx.body = {
unread_count: 0,
likes: 0,
};
}
})
@@ -75,18 +84,10 @@ app
.define(async (ctx) => {
const { note_id, comment_id, content } = ctx.query;
const client = xhsServices.getClient();
// content 300个字内超过cai fen
const textArr: string[] = [];
if (content.length > 300) {
const num = Math.ceil(content.length / 300);
for (let i = 0; i < num; i++) {
textArr.push(content.slice(i * 300, (i + 1) * 300));
}
} else {
textArr.push(content);
}
// content 小红书最大 300个字内超过则分割, 分比300小比如270
const textArr: string[] = splitContent(content);
const resArr: any[] = [];
for (const text of textArr) {
for (let text of textArr) {
const res = await client.postComment({
note_id: note_id,
comment_id: comment_id,
@@ -98,6 +99,7 @@ app
console.log('添加评论失败', res.code);
ctx.throw(res.code, '添加评论失败');
}
await sleep(2000);
}
ctx.body = resArr;
})
@@ -123,22 +125,26 @@ app
const handleMention: any[] = [];
for (const mention of mentionList) {
const mention_id = mention.id;
const note_id = mention.item_info.id;
const note_id = mention.item_info.id; // item_info 是笔记信息
const note_userid = mention.item_info.user_info?.userid || '';
const note_username = mention.item_info.user_info?.nickname || '';
const xsec_token = mention.item_info.xsec_token;
let comment: any = Parse.getComment(mention);
// console.log('note_id', note_id, 'xsec_token', xsec_token, comment);
handleMention.push({
mention_id,
note_id,
note_userid,
note_username,
xsec_token,
comment,
mention,
});
}
console.log('获取提及列表成功', res.code, res.data?.message_list?.length);
console.log('获取提及列表成功', '[小红书code]', res.code, '提及数量', res.data?.message_list?.length);
ctx.body = handleMention;
} else {
console.log('获取提及列表失败', res.code);
console.log('获取提及列表失败', '[小红书code]', res.code);
ctx.throw(res.code, '获取提及列表失败');
}
})

View File

@@ -14,13 +14,16 @@ app
key: 'getUnread',
});
console.log('unredRes', unredRes.body, unredRes.code);
if (unredRes.code === 200) {
const unread_count = unredRes.body.unread_count;
const likes = unredRes.body.likes;
const unread = unread_count - likes;
const mentionRes = await app.call({
path: 'mention',
key: 'getMention',
payload: {
num: unread_count,
num: unread,
},
});
if (mentionRes.code === 200) {

View File

@@ -7,8 +7,8 @@ app
})
.define(async (ctx) => {
const client = xhsServices.getClient();
const res = await client.c
if (res.code === 0) {
}
// const res = await client.getNote({});
// if (res.code === 0) {
// }
})
.addTo(app);

View File

@@ -0,0 +1,18 @@
import { app, xhsServices } from '@kevisual/xhs/app.ts';
app
.route({
path: 'note',
key: 'getNote',
description: '获取笔记详情',
})
.define(async (ctx) => {
const { node_id, xsec_token } = ctx.query;
const client = xhsServices.getClient();
const res = await client.getNote(node_id, xsec_token);
if (res.code === 200) {
ctx.body = res.data || {};
} else {
ctx.throw(`获取笔记失败: ${node_id}`);
}
})
.addTo(app);

View File

@@ -1 +1,2 @@
import './create-note.ts'
import './get-note.ts'

View File

@@ -3,10 +3,12 @@ import { Sequelize } from 'sequelize';
// import { createSequelize } from '@kevisual/xhs/services/xhs-db/db.ts';
import path from 'node:path';
import fs from 'node:fs';
export { XhsClient };
type XhsClientOptions = {
key: string;
cookie: string;
userid?: string;
username?: string;
signConfig?: {
signUrl: string;
};
@@ -69,7 +71,7 @@ export class XhsServices {
createRoot(options: Partial<XhsClientOptions>) {
options.key = options.key || this.root;
return this.createClient(options as XhsClientOptions);
return this.createClient(options as XhsClientOptions) as XhsClient;
}
getKey(key?: string) {
if (!key) key = this.root;
@@ -88,4 +90,60 @@ export class XhsServices {
const xhsClient = this.map.get(this.getKey(key));
return xhsClient;
}
getXhsUserInfo(key?: string) {
const xhsClient = this.map.get(this.getKey(key));
return {
userid: xhsClient?.options?.userid || '',
username: xhsClient?.options?.username || '',
};
}
isOwner(user: { username: string; userid: string }, key?: string) {
const xhsUserInfo = this.getXhsUserInfo(key);
if (!xhsUserInfo.userid || !xhsUserInfo.username) {
return false;
}
return user.userid === xhsUserInfo.userid;
}
isReplayAi(data: any, key?: string) {
const mention = data?.mention || {};
const user_info = mention?.comment_info?.target_comment?.user_info || {};
if (user_info?.userid) {
const xhsUserInfo = this.getXhsUserInfo(key);
// 处理用户信息
return user_info.userid === xhsUserInfo.userid;
}
return false;
}
setCookie(cookie: string, key?: string) {
const xhsClient = this.map.get(this.getKey(key));
if (xhsClient) {
xhsClient.options.cookie = cookie;
xhsClient.client.setCookie(cookie);
}
}
/**
* 设置用户信息
* @param user
* @param key
*/
setUserInfo(user: { userid: string; username: string }, key?: string) {
const xhsClient = this.map.get(this.getKey(key));
if (xhsClient) {
xhsClient.options.userid = user.userid;
xhsClient.options.username = user.username;
}
}
setSignConfig(signConfig: { signUrl: string }, key?: string) {
const xhsClient = this.map.get(this.getKey(key));
if (xhsClient) {
xhsClient.options.signConfig = signConfig;
xhsClient.client.signConfig = signConfig;
}
console.log('setSignConfig', xhsClient?.options?.signConfig);
}
getSignConfig(key?: string) {
const xhsClient = this.map.get(this.getKey(key));
return xhsClient?.options?.signConfig || {};
}
}

View File

@@ -1,4 +1,7 @@
import { xhsServices, app } from '../index.ts';
import { xhsServices, app, xhsRootClient } from '../index.ts';
import { useConfig } from '@kevisual/use-config/env';
const config = useConfig();
import { program } from 'commander';
xhsRootClient.setCookie(config.XHS_ROOT_COOKIE || '');
export { program, xhsServices, app };

View File

@@ -8,17 +8,27 @@ import util from 'node:util';
// });
const getNoteById = async () => {
const client = xhsServices.getClient();
client.getNoteById('68136dab0000000007034c46', 'LByEmonX8WfJ9ebpAowVbOZX9Xh8T0Qkjil5KRFqDD6LM').then((res) => {
console.log(res);
});
// client.getNoteById('68136dab0000000007034c46', 'LByEmonX8WfJ9ebpAowVbOZX9Xh8T0Qkjil5KRFqDD6LM').then((res) => {
// console.log(res);
// });
const res = await client.getNoteById('68136dab0000000007034c46', 'LB6fmNfsd0keAQNjh3zOejDC2TVQLGY3zlTZjeRazBZdI=');
// console.log(res);
};
const getNote = async () => {
// const id = '68136dab0000000007034c46';
// const x = 'LByEmonX8WfJ9ebpAowVbOZX9Xh8T0Qkjil5KRFqDD6LM=';
const id = '68136dab0000000007034c46';
const x = 'LByEmonX8WfJ9ebpAowVbOZX9Xh8T0Qkjil5KRFqDD6LM=';
const x = 'LB6fmNfsd0keAQNjh3zOejDC2TVQLGY3zlTZjeRazBZdI=';
const client = xhsServices.getClient();
client.getNote(id, x).then((res) => {
const res = await client.getNote(id, x).then((res) => {
console.log(util.inspect(res, { depth: null }));
return res;
});
console.log('type res', typeof res);
if (res.code === 0) {
console.log('desc', res.data.desc);
}
};
program
.command('get-note')

View File

@@ -0,0 +1,8 @@
import { splitContent } from '../routes/mentions/mention.ts';
const content = `这是一个测试内容长度超过300个字符。34`.repeat(43); // 模拟一个超过300个字符的内容
const maxLength = 270; // 设置分割长度为270个字符
const parts = splitContent(content, maxLength);
console.log('分割后的内容:', parts);
// 输出分割后的内容
console.log('分割后的内容长度:', parts.map(part => part.length));

9
pnpm-lock.yaml generated
View File

@@ -146,8 +146,8 @@ importers:
specifier: ^1.0.14
version: 1.0.14(dotenv@16.5.0)
'@kevisual/xhs-core':
specifier: workspace:*
version: link:../xhs-core
specifier: 0.0.3
version: 0.0.3
'@types/node':
specifier: ^22.15.3
version: 22.15.3
@@ -265,6 +265,9 @@ packages:
peerDependencies:
dotenv: ^16.4.7
'@kevisual/xhs-core@0.0.3':
resolution: {integrity: sha512-i1prOqtRSqsj4Nkcgw9GNNJXXLMeNnA2oAYX6Xrx/uskwTLoJfSwN41Z9k1XFyZfJ4oOu4ByFKeQLxVi4UkrqA==}
'@ljharb/resumer@0.1.3':
resolution: {integrity: sha512-d+tsDgfkj9X5QTriqM4lKesCkMMJC3IrbPKHvayP00ELx2axdXvDfWkqjxrLXIzGcQzmj7VAUT1wopqARTvafw==}
engines: {node: '>= 0.4'}
@@ -2275,6 +2278,8 @@ snapshots:
'@kevisual/load': 0.0.6
dotenv: 16.5.0
'@kevisual/xhs-core@0.0.3': {}
'@ljharb/resumer@0.1.3':
dependencies:
'@ljharb/through': 2.3.14

View File

@@ -1,16 +1,24 @@
import { SiliconFlowProvider } from '@kevisual/ai';
import { useContextKey } from '@kevisual/context';
import { BailianProvider } from '@kevisual/ai';
import { config } from '../modules/config.ts';
export const ai = new SiliconFlowProvider({
model: 'Qwen/Qwen3-32B',
// model: 'Pro/deepseek-ai/DeepSeek-R1',// 只有充值能用
apiKey: config.SILICONFLOW_API_KEY,
});
ai.getUsageInfo()
.then((usage) => {
console.log('AI usage info:', usage);
})
.catch((res) => {
console.error('Error fetching AI usage info:', res.status);
const createBaiLian = () => {
return new BailianProvider({
baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
model: 'qwen3-235b-a22b',
apiKey: config.BAILIAN_API_KEY,
});
};
// export const ai = await useContextKey('ai', createBaiLian());
export const ai = createBaiLian();
console.log('Bailian AI initialized with model:', config.BAILIAN_API_KEY);
export const bailianModel = useContextKey('bailianModel', () => {
return {
turbo: 'qwen-turbo-2025-04-28',
plus: 'qwen-plus-2025-04-28',
a22b235: 'qwen3-235b-a22b',
};
});

View File

@@ -0,0 +1,80 @@
import { agent } from '@/agent/agent.ts';
import { ai } from '../ai.ts';
import { getJsonFromString } from './content.ts';
export type Category = {
category: string;
description: string;
};
export const categoryList: Category[] = [
{
category: 'daily_poetry',
description: '获取一篇今日诗词',
},
{
category: 'irony',
description: '反讽一下',
},
{
category: 'random_beautiful_text',
description: '随机生成一段优美的文字',
},
{
category: 'random_joke',
description: '随机生成一个笑话',
},
{
category: '',
description: '教教我,怎么做'
},
{
category: 'default',
description: '默认分类,无法识别的请求',
},
];
agent
.route({
path: 'analyze',
key: 'category',
description: '分析文本内容判断文本的请求分类。比如1. 获取一篇今日诗词。2. 反讽一下 3. 随机生成一段优美的文字。',
})
.define(async (ctx) => {
const text = ctx.query?.text || '';
const prompt = `
请分析以下文本内容判断文本的请求分类并返回一个JSON对象。
识别的分类包括:
${categoryList.map((item) => `- ${item.category}: ${item.description}`).join('\n')}
返回内容示例:
\`\`\`json
{
"category": "daily_poetry"
}
\`\`\`
<context>
${text}
</context>
`;
const res = await ai
.chat(
[
{
role: 'user',
content: prompt,
},
],
{
enable_thinking: false,
},
)
.catch((error) => {
ctx.throw(500, 'AI 服务错误: ' + error.status);
return error;
});
const content = res.choices?.[0]?.message?.content || '';
const jsonResponse = getJsonFromString(content);
const category = jsonResponse?.category || 'default';
ctx.body = { category };
})
.addTo(agent);

116
src/agent/analyze/cmd.ts Normal file
View File

@@ -0,0 +1,116 @@
import { agent } from '@/agent/agent.ts';
import { ai } from '../ai.ts';
import { getJsonFromString } from './content.ts';
// import { getJsonFromString } from '@kevisual/ai/src/utils/json.ts';
import { logger } from '../logger.ts';
export const cmdList: {
category: string;
description?: string;
action?: {
path?: string;
key?: string;
};
}[] = [
{
category: '指令夸人',
description: `进行夸奖`,
action: {
path: 'tools',
key: 'good-job',
},
},
{
category: '指令来了',
description: `召唤小助手过来`,
action: {
path: 'tools',
key: 'call-xiaoxiao',
},
},
{
category: '指令总结',
description: `总结当前的笔记,缩写当前笔记的内容`,
action: {
path: 'tools',
key: 'summarize-note',
},
},
{
category: '指令安慰',
description: `安慰用户`,
action: {
path: 'tools',
key: 'comfort-user',
},
},
];
agent
.route({
path: 'analyze',
key: 'cmd',
description: '分析文本内容,意图分析,判断对应的指令内容',
})
.define(async (ctx) => {
let text = ctx.query?.text || '';
if (text.length > 40) {
text = text.slice(0, 40).trim();
}
let result = {
category: 'default',
};
const prompt = `
请分析<context>包函的内容判断是否程序运行指令返回一个JSON对象。
识别的分类包括:
${cmdList.map((item) => `- ${item.category}: ${item.description}`).join('\n')}
返回内容示例:
\`\`\`json
{
"category": "daily_poetry"
}
\`\`\`
分析的内容是
<context>
${text}
</context>
`;
logger.info('Command analysis prompt:', prompt);
const res = await ai
.chat(
[
{
role: 'user',
content: prompt,
},
],
{
// @ts-ignore
enable_thinking: false,
},
)
.catch((err) => {
console.log('AI service error:', err.status);
ctx.throw(500, 'AI service error: ' + err.status);
return err;
});
const ans = res.choices[0]?.message?.content || '';
console.log('Command analysis response:', ans);
const json = getJsonFromString(ans);
if (!json) {
logger.error('Invalid JSON format in response:', ans);
ctx.throw(400, 'Invalid JSON format in response');
}
result = {
category: json.category || 'default',
};
const cmd = cmdList.find((item) => item.category === result.category);
logger.info('Command analysis result:', cmd?.category, cmd?.action);
ctx.body = { cmd, text: text };
})
.addTo(agent);

View File

@@ -1,7 +1,7 @@
import { agent } from '@/agent/agent.ts';
import { ai } from '../ai.ts';
import { logger } from '@/agent/logger.ts';
const getJsonFromString = (str: string) => {
export const getJsonFromString = (str: string) => {
// 尝试从字符串中提取JSON对象
try {
const jsonMatch = str.match(/```json\s*([\s\S]*?)\s*```/);
@@ -49,7 +49,7 @@ agent
}
\`\`\`
分析的文本的内容是:
<context>
<context>
${text}
</context>
`;

View File

@@ -22,9 +22,9 @@ agent
{
role: 'user',
content: `
你是一个提示词优化的专家,请根据用户提供的提示词进行修正和优化,其中用户的提示词返回的要求如果没有或者不明确,请你修正为要求返回的文本在500字以内内容是纯文本格式不能是markdown模式也不包含任何HTML标签或其他格式化内容。
你是一个提示词优化的专家请根据用户提供的提示词进行修正和优化其中用户的提示词返回的要求如果没有或者不明确请你修正为要求返回的文本在500字以内如果有,保持原本的要求数字的文本。与此同时,要求内容是纯文本格式不能是markdown模式也不包含任何HTML标签或其他格式化内容。
只对提示词进行优化,并且不需要对内容进行分析或总结。
只对提示词进行优化,并且不需要对内容进行分析或总结。并返回修改后的总的提示词内容。
示例1. 用户提示词
<content>总结笔记</content>
@@ -34,6 +34,10 @@ agent
<content>分析一下这个图片</content>
优化后的提示词
<content>请分析一下这个图片要求返回的内容是纯文本格式字数不超过500字。</content>
示例3. 用户提示词
<content>分析一下这个笔记300字内</content>
优化后的提示词
<content>请分析一下这个笔记要求返回的内容是纯文本格式字数不超过300字。</content>
用户的提示词是
@@ -55,10 +59,11 @@ ${text}
});
console.log('end', Date.now() - now, 'ms');
console.log('AI response:', res);
const ans = res.choices[0]?.message?.content || '';
if (!ans) {
logger.error('Empty response from AI:', res);
}
ctx.body = getTagContent(ans)
ctx.body = getTagContent(ans);
})
.addTo(agent);

View File

@@ -1,7 +1,14 @@
import { agent } from './agent.ts';
import './analyze/content.ts';
import './analyze/category.ts';
import './analyze/cmd.ts';
import './fix/prompt.ts';
import './xhs.ts';
import './tools/kuaren.ts';
import './tools/call-xiaoxiao.ts';
import './tools/summarize-note.ts';
import './tools/comfort-user.ts';
export { agent };

View File

@@ -0,0 +1,21 @@
import { agent } from '../index.ts';
const main = async () => {
const text1 = '解答一下这个笔记。';
const text2 = '分析一下这个图片';
const text3 = '这个视频介绍的是什么';
const text4 = '评价一下这个评论。';
const text5 = '关于这个评论。';
const text6 = '1+1=';
const text7 = '反讽一下';
const text8 = '随机生成一段优美的文字';
const res = await agent.call({
path: 'analyze',
key: 'category',
payload: {
text: text1,
},
});
console.log('analyze category res', res.code, 'content', res.body);
};
main();

18
src/agent/test/cmd.ts Normal file
View File

@@ -0,0 +1,18 @@
import { agent } from '../index.ts';
const main = async () => {
const text1 = '夸一下这个人。';
const text2 = '指令这个人很不错';
const text3 = '指令夸人, 这个人写代码写的非常好';
const text4 = '我想飞';
const text5 = '指令 笔记 安慰';
const res = await agent.call({
path: 'analyze',
key: 'cmd',
payload: {
text: text5
},
});
console.log('analyze category res', res.code, 'content', res.body);
};
main();

View File

@@ -6,11 +6,12 @@ const main = async () => {
const text2 = '告诉我1+1的值';
const text3 = 'html和css的大纲是什么';
const text4 = '1+1=';
const text5 = '请分析一下这个图片300字内';
const res = await agent.call({
path: 'fix',
key: 'xhs',
payload: {
text: text4,
text: text5,
},
});
console.log('fix xhs res', res.code, 'content', res.body);

View File

@@ -0,0 +1,16 @@
import { agent } from '../../index.ts';
const main = async () => {
const text1 = '你长得真好看啊';
const text2 = '你说话是撒了魔法金粉吗?听一句我灵魂都被净化了!';
const res = await agent.call({
path: 'tools',
key: 'good-job',
payload: {
text: text2,
},
});
console.log('good job res', res.code, 'content', res.body);
};
main();

View File

@@ -0,0 +1,15 @@
import { agent } from '../agent.ts';
import { ai } from '../ai.ts';
const pickGoodJobPrompt = `
`;
agent
.route({
path: 'tools',
key: 'call-xiaoxiao',
})
.define(async (ctx) => {
ctx.body = '来了,来了。';
})
.addTo(agent);

View File

@@ -0,0 +1,53 @@
import { agent } from '../agent.ts';
import { ai } from '../ai.ts';
agent
.route({
path: 'tools',
key: 'comfort-user',
})
.define(async (ctx: any) => {
const text = ctx?.query?.text || '';
const note = ctx?.query?.note || '';
if (!text) {
ctx.throw('请提供要安慰的内容');
}
const prompt = `
用户感觉到不开心,请根据以下要求生成一段安慰的话:
要求
0. 如果没有必要安慰,直接返回 "你好,你好,我来了"。
1. 内容要简洁明了,突出重点。
2. 使用口语化的表达方式,易于理解。
3. 安慰内容要有逻辑性,能够清晰地传达信息。
4. 如果有必要,可以使用一些比喻或形象的表达方式来增强安慰的效果。
5. 安慰内容要与原文内容相关,不能脱离主题。
6. 200字以内。
7. 不要包含除开安慰内容以外的其他内容。
8. 其他要求:
${text ? text : '无'}
当前的笔记内容是:
${note}
`;
const res = await ai.chat(
[
{
role: 'user',
content: prompt,
},
],
{
// @ts-ignore
enable_thinking: false,
},
);
const pickRes = res.choices[0]?.message?.content || '';
if (!pickRes) {
ctx.throw('AI 没有返回任何内容,请稍后再试');
}
// 返回总结内容
ctx.body = pickRes.trim();
})
.addTo(agent);

177
src/agent/tools/kuaren.ts Normal file
View File

@@ -0,0 +1,177 @@
import { agent } from '../agent.ts';
import { ai } from '../ai.ts';
const kuarenPrompt = `### 浮夸夸人
**核心要求**
⚠️ 用词极致夸张| ⚠️ 比喻突破天际| ⚠️ 语气充满崇拜| ⚠️ 营造“凡人 vs 神仙”对比感
---
#### **夸人维度 & 浮夸话术示例**
1. **颜值/气质类**
- ✨ **例句**"你这张脸是上帝亲手雕的吧?下凡辛苦了!"
- ✨ **关键词**:女娲毕设、建模脸、自带滤镜、呼吸都带仙气
2. **才华/能力类**
- ✨ **例句**"你这大脑是装了个量子计算机吗?!建议直接保送诺贝尔奖!"
- ✨ **关键词**:人类天花板、降维打击、天才操作、教科书成精
3. **性格/情商类**
- ✨ **例句**"你说话是撒了魔法金粉吗?听一句我灵魂都被净化了!"
- ✨ **关键词**:人间充电宝、社交天花板、灵魂按摩师、情商天花板
4. **细节/小事类**_重点把小事吹成神迹_
- ✨ **例句**"你刚刚递咖啡的姿势,直接拍成广告能救活整个咖啡行业!"
- ✨ **关键词**:随手拯救世界、文艺复兴级操作、人类文明之光
---
#### **浮夸技巧工具箱**
✅ **宇宙级比喻**
> “你这创意是偷了宙斯的闪电吧?!”
> “你的存在让地球自转加速了 0.1 秒!”
✅ **玄幻修辞法**
> “建议科学家把你列入未解之谜!”
> “你一笑,北极极光都暗淡了!”
✅ **凡尔赛对比**
> “别人 XX 叫努力,你 XX 叫刷新人类极限!”
> “你这水平还谦虚?让普通人怎么活啊?!”
✅ **动作加持**_配合文字使用效果翻倍_
> “给大佬递茶.jpg 🍵”
> “跪着听讲.gif 🙇‍♂️”
---
#### **示例输出**
💥 **场景 1**(对方随手画了张小涂鸦)
> “这线条!这配色!达芬奇转世没你画得灵!!建议卢浮宫连夜来收购!!”
💥 **场景 2**(对方讲了个冷笑话)
> “你这幽默感是黑洞做的吗?!我笑到平行宇宙都裂开了!!🌌”
💥 **场景 3**(对方帮忙解决了小问题)
> “你是雅典娜派来的救世主吧?!这波操作够我刻成碑传家!!🗿”
#### 当前的场景是
`;
const pickGoodJobPrompt = `对提供的文字,提取单个的夸奖内容,并丰富为纯口语化模式,同时在括号中添加对应的姿态语言描述,同时添加了括号中的姿态语言描述,使其更具临场感和情感色彩。
要求:
1. 只返回单个的一条夸奖内容,不能有其他内容。
2. 夸奖内容要口语化,富有情感色彩。
3. 姿态语言描述要符合夸奖内容,且要在括号中描述。
4. 夸奖内容要有夸张的比喻和形容词,突出对方的优点和成就。
5. 夸奖内容要让人感到被认可和赞赏,能够激励对方。
6. 不要返回任何其他内容或解释,只返回夸奖内容。
7. 夸奖内容要简洁明了,易于理解,篇幅不易过长。
当前文字是:
`;
agent
.route({
path: 'tools',
key: 'pick-good-job',
description: '对用户的内容进行夸奖后,提取出夸奖的内容',
})
.define(async (ctx) => {
let { text } = ctx.query;
if (!text) {
text = '真厉害啊';
}
const prompt = `${pickGoodJobPrompt} ${text}`;
const res = await ai
.chat(
[
{
role: 'user',
content: prompt,
},
],
{
enable_thinking: false,
},
)
.catch((err) => {
console.error('AI service error:', err.status);
ctx.throw(500, 'AI service error: ' + err.status);
return err;
});
const ans = res.choices[0]?.message?.content || '';
if (!ans) {
ctx.throw(500, 'AI response is empty');
}
ctx.body = ans;
})
.addTo(agent);
agent
.route({
path: 'tools',
key: 'good-job',
})
.define(async (ctx) => {
let { text } = ctx.query;
if (!text) {
text = '作者发了一篇好的文章';
}
const prompt = `${kuarenPrompt} ${text}`;
const res = await ai
.chat(
[
{
role: 'user',
content: prompt,
},
],
{
enable_thinking: false,
},
)
.catch((err) => {
console.error('AI service error:', err.status);
ctx.throw(500, 'AI service error: ' + err.status);
return err;
});
const ans = res.choices[0]?.message?.content || '';
if (!ans) {
ctx.throw(500, 'AI response is empty');
}
// ctx.body = ans;
// console.log('AI response:', ans);
const resPick = await agent.call({
path: 'tools',
key: 'pick-good-job',
payload: {
text: ans,
},
});
if (resPick.code !== 200) {
ctx.throw(500, 'AI pick good job error: ' + resPick.message);
return;
}
const pickAns = resPick.body || '';
if (!pickAns) {
ctx.throw(500, 'AI pick good job response is empty');
}
ctx.body = pickAns;
})
.addTo(agent);

View File

@@ -0,0 +1,53 @@
import { agent } from '../agent.ts';
import { ai } from '../ai.ts';
agent
.route({
path: 'tools',
key: 'summarize-note',
})
.define(async (ctx: any) => {
const text = ctx?.query?.text || '';
const note = ctx?.query?.note || '';
if (!text) {
ctx.throw('请提供要总结的笔记内容');
}
const prompt = `
请根据以下内容生成一段总结:
要求
0. 如果没有必要总结,直接返回 "当前内容不需要总结"。
1. 总结内容要简洁明了,突出重点。
2. 使用口语化的表达方式,易于理解。
3. 总结内容要有逻辑性,能够清晰地传达信息。
4. 如果有必要,可以使用一些比喻或形象的表达方式来增强总结的效果。
5. 总结内容要与原文内容相关,不能脱离主题。
6. 250字以内。
7. 不要包含除开总结内容以外的其他内容。
8. 其他要求:
${text ? text : '无'}
当前的笔记内容是:
${note}
`;
const res = await ai.chat(
[
{
role: 'user',
content: prompt,
},
],
{
// @ts-ignore
enable_thinking: false,
},
);
const pickRes = res.choices[0]?.message?.content || '';
if (!pickRes) {
ctx.throw('AI 没有返回任何内容,请稍后再试');
}
// 返回总结内容
ctx.body = pickRes.trim();
})
.addTo(agent);

View File

@@ -1,6 +1,7 @@
import { nanoid } from 'nanoid';
import { agent } from './agent.ts';
import { ai } from './ai.ts';
import { logger } from './logger.ts';
/**
* 清除文本中的@信息
* @param text
@@ -9,14 +10,65 @@ const clearAtInfo = (text: string = '') => {
const newText = text.replace(/@[\u4e00-\u9fa5\w]+/g, '').replace(/#.*?#/g, '');
return newText.trim();
};
/**
* 从文本中提取标签信息, 如 #标签
* @param text
*/
export const pickTagsInfo = (text: string = '') => {
if (!text) {
return {
tags: [],
text: '',
};
}
const _tags = text.match(/#([\u4e00-\u9fa5\w]+)/g) || [];
const validTags = _tags.map((tag) => tag.replace(/#/g, '')).filter((tag) => tag.trim() !== '');
const noTagsText = text.replace(/#([\u4e00-\u9fa5\w]+)/g, '').trim();
return {
tags: validTags,
text: noTagsText,
};
};
agent
.route({
path: 'xhs',
})
.define(async (ctx) => {
const { text = '' } = ctx.query || {};
const { text = '', note = '', hasNote } = ctx.query || {};
const id = nanoid();
const no_at_text = clearAtInfo(text);
const pickNote = pickTagsInfo(note);
const no_tags_text = pickNote.text;
const some_text = no_at_text.length > 20 ? no_at_text.slice(0, 20) : no_at_text;
const hasCmd = some_text.includes('指令');
if (hasCmd) {
const analyzeRes = await agent.call({
path: 'analyze',
key: 'cmd',
payload: {
text: no_at_text,
},
});
if (analyzeRes.code === 200) {
const cmd = analyzeRes.body?.cmd;
if (cmd) {
const res = await agent.call({
...cmd.action,
payload: {
text: no_at_text,
note: no_tags_text,
hasNote: hasNote || false,
},
});
ctx.body = res.body || '';
return;
}
} else {
logger.error('指令分析错误:', analyzeRes.message);
}
}
const resFix = await agent.call({
path: 'fix',
key: 'xhs',

View File

@@ -1,3 +1,5 @@
import { useConfig } from '@kevisual/use-config/env';
export const config = useConfig();
export const isDev = config.ENV === 'development';

View File

@@ -1,5 +1,5 @@
import { agent } from '@/agent/index.ts';
import { taskApp, queue, xhsApp } from '../task.ts';
import { taskApp, queue, xhsApp, xhsServices } from '../task.ts';
import { random, omit } from 'lodash-es';
import util from 'node:util';
@@ -21,18 +21,20 @@ taskApp
if (res.code === 200) {
const data = res.body;
const unread_count = data.unread_count;
if (unread_count > 0) {
const likes = data.likes;
const unread = unread_count - likes;
if (unread > 0) {
queue.add(
'mention',
{
path: 'task',
key: 'getMention',
payload: {
unread_count,
unread_count: unread,
},
},
{
attempts: 3,
attempts: 1,
delay: 0,
removeOnComplete: true,
removeOnFail: {
@@ -42,7 +44,7 @@ taskApp
);
}
ctx.body = {
job: unread_count,
job: unread,
};
}
})
@@ -109,18 +111,52 @@ taskApp
.define(async (ctx) => {
const data = ctx.query.data; // 为提及的相关信息
const note_id = data.note_id;
const note_userid = data.note_userid;
const note_username = data.note_username;
// 检测是这个用户的username的笔记如果是的话需要有at的用户信息才继续。
const isOwner = xhsServices.isOwner({ username: note_username, userid: note_userid });
const isReplayAi = xhsServices.isReplayAi(data);
const xsec_token = data.xsec_token;
console.log(data);
const comment_id = data.comment.comment_id;
const content = data.comment?.content || 'test';
let content: string = data.comment?.content || 'test';
const postData = {
note_id,
content,
comment_id,
};
if (isOwner) {
// 如果是自己的笔记,且笔记不包含 @信息 则不需要AI回复,
const hasAt = content.includes('@' + note_username) || content.includes('@' + note_userid);
if (!hasAt) {
// console.log('不需要AI回复自己的笔记', note_username, note_id, content);
return;
}
}
content = content.replace('@' + note_username, '');
const sliceContentCmd = content.slice(0, 20);
let note = '';
let hasNote = false;
if (sliceContentCmd.includes('笔记') || sliceContentCmd.includes('总结')) {
const res = await xhsApp.call({ path: 'note', key: 'getNote', payload: { node_id: note_id, xsec_token } });
if (res.code === 200) {
note = res.body?.desc || '';
hasNote = note ? true : false;
}
}
if (isReplayAi) {
// 如果是对AI回复的评论则不需要再回复
console.log('不需要AI回复AI的评论', note_username, note_id, content);
return;
}
const resAgent = await agent.call({
path: 'xhs',
payload: {
text: content,
note,
hasNote,
},
});
let responseText = '';

View File

@@ -3,13 +3,33 @@
import { QueryRouterServer } from '@kevisual/router';
import { redis } from '@/modules/redis.ts';
import { Queue } from 'bullmq';
import { app as xhsApp } from '@kevisual/xhs/index';
import { app as xhsApp, xhsServices as xhs, xhsRootClient, XhsServices } from '@kevisual/xhs/index.ts';
import { nanoid } from 'nanoid';
export const XHS_GET_UNREAD = 'unread_count';
export const XHS_QUEUE_NAME = 'XHS_QUEUE';
import { config, isDev } from '../modules/config.ts';
const server: XhsServices = xhs;
server.setCookie(config.XHS_ROOT_COOKIE || '');
server.setUserInfo({
userid: config.XHS_USER_ID || '',
username: config.XHS_USER_NAME || '',
});
if (isDev) {
server.setSignConfig({
signUrl: 'http://localhost:5006/sign',
});
} else {
server.setSignConfig({
signUrl: config.XHS_API_SIGN_URL,
});
}
console.log('XHS_USER_INFO', config.XHS_USER_ID, config.XHS_USER_NAME, 'XHS_ROOT_COOKIE', config.XHS_ROOT_COOKIE);
console.log('XHS_SIGN_URL', server.getSignConfig().signUrl);
export const taskApp = new QueryRouterServer();
export { xhsApp };
export const xhsServices = server;
export const queue = new Queue(XHS_QUEUE_NAME, {
connection: redis,
});
@@ -25,6 +45,7 @@ export const addUnreadTask = async (nextTime = 0) => {
{
delay: nextTime,
removeOnComplete: true,
attempts: 1,
removeOnFail: {
age: 24 * 3600, // keep up to 24 hours
},

59
src/task/utils/time.ts Normal file
View File

@@ -0,0 +1,59 @@
import dayjs from 'dayjs';
/**
* 根据当前时间返回对应的时间段
* 返回的时间段是毫秒数
* 例如03:01 - 09:00 返回 120000 (120秒)
* 如果当前时间不在任何时间段内返回0
* @returns
*/
export const getTimeDuration = () => {
// 根据时间返回,返回需要
const timeRangeList = [
{
start: '03:01',
end: '09:00',
duration: 240 * 1000, // 240s
},
{
start: '09:01',
end: '12:00',
duration: 60 * 1000, // 60s
},
{
start: '12:01',
end: '14:00',
duration: 30 * 1000, // 30s
},
{
start: '14:01',
end: '18:00',
duration: 120 * 1000, // 120s
},
{
start: '18:01',
end: '22:00',
duration: 10 * 1000, // 10s
},
{
start: '22:01',
end: '23:59',
duration: 3 * 1000, // 3s
},
{
start: '00:01',
end: '03:00',
duration: 20 * 1000, // 20s
},
];
const currentTime = Date.now();
const currentHour = dayjs(currentTime).format('HH:mm');
for (const range of timeRangeList) {
if (currentHour >= range.start && currentHour <= range.end) {
return range.duration;
}
}
// 如果没有匹配到默认返回0
return 0;
};

View File

@@ -5,6 +5,9 @@ import { nanoid } from 'nanoid';
import { queue, XHS_QUEUE_NAME, taskApp } from './index.ts';
import { addUnreadTask } from './task.ts';
import dayjs from 'dayjs';
import { getTimeDuration } from './utils/time.ts';
import { config, isDev } from '../modules/config.ts';
export const sleep = (ms: number) => {
return new Promise((resolve) => setTimeout(resolve, ms));
};
@@ -13,7 +16,7 @@ class TimeRecorder {
endTime: number;
duration: number;
updateTime: number;
maxDuration: number = 60 * 1000; // 20s;
maxDuration: number = 30 * 1000; // 30s;
constructor() {
const now = Date.now();
this.startTime = now;
@@ -34,13 +37,20 @@ class TimeRecorder {
this.updateTime = Date.now();
return this.updateTime;
}
getClampDuration() {
const duration = Date.now() - this.updateTime;
getClampDuration(random = false) {
let randomDuration = 0;
if (random) {
randomDuration = Math.floor(Math.random() * 5) * 1000; // 随机0-5秒
}
const duration = Date.now() - this.updateTime + randomDuration;
const nextTime = clamp(this.maxDuration - duration, 0, this.maxDuration);
// console.log('getClampDuration', duration, this.maxDuration, 'nextTime', nextTime);
return {
duration: duration,
maxDuration: this.maxDuration,
updateTime: this.updateTime,
nextTime: clamp(this.maxDuration - duration, 0, this.maxDuration),
nextTime: nextTime,
};
}
time() {
@@ -52,6 +62,7 @@ class TimeRecorder {
};
}
}
const timeRecorder = new TimeRecorder();
let errorCount = 0;
export const worker = new Worker(
@@ -59,8 +70,9 @@ export const worker = new Worker(
async (job) => {
const timer = new TimeRecorder();
const data = job.data;
if (data.path === 'task' && data.key === 'getUnread') {
console.log('====run time', dayjs().format('YYYY-MM-DD HH:mm:ss'));
console.log('====run time start', dayjs().format('YYYY-MM-DD HH:mm:ss'));
timeRecorder.update();
}
const res = await taskApp.call(data);
@@ -71,6 +83,9 @@ export const worker = new Worker(
if (errorCount > 3) {
queue.pause();
console.log('error count', errorCount);
if (data.path === 'task' && data.key === 'getUnread') {
process.exit(1);
}
}
throw new Error('job error' + job.name + ' ' + job.id);
}
@@ -83,15 +98,17 @@ export const worker = new Worker(
worker.on('completed', async (job) => {
const jobCounts = await queue.getJobCounts('waiting', 'wait', 'delayed');
if (job.name !== 'unread') {
console.log('job completed', job.name, job.id, job.returnvalue, jobCounts.delayed, jobCounts.wait);
console.log('job completed', job.name, 'run id', job.id, job.returnvalue, jobCounts.delayed, jobCounts.wait);
}
if (jobCounts.delayed + jobCounts.wait > 0) {
// console.log('======has jobs, no need to add new job');
} else {
const up = timeRecorder.getClampDuration();
const nextTime = up.nextTime;
const up = timeRecorder.getClampDuration(true);
const timeDuration = isDev ? 0 : getTimeDuration();
const nextTime = up.nextTime + timeDuration;
const unread = await queue.getJob('unread');
if (!unread) {
console.log('====add unread next-time', nextTime, timeDuration);
addUnreadTask(nextTime);
}
}

3
src/test/common.ts Normal file
View File

@@ -0,0 +1,3 @@
import { config } from '../modules/config.ts';
export { config };

View File

@@ -0,0 +1,3 @@
import { getTimeDuration } from '@/task/utils/time.ts';
console.log('getTimeDuration', getTimeDuration());

57
src/test/tts.ts Normal file
View File

@@ -0,0 +1,57 @@
import { config } from './common.ts';
const API_KEY = config.BAILIAN_API_KEY;
// 使用DashScope API进行TTS (文本转语音) 请求
const dashscopeTTS = async ({ text, voice = 'Chelsie', token }) => {
try {
const response = await fetch('https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation', {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
// model: 'qwen-tts',
model: 'qwen-tts-latest',
input: {
text,
voice,
},
}),
});
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
console.error('TTS 请求失败:', error);
throw error;
}
};
// 使用示例
const sampleText =
'那我来给大家推荐一款T恤这款呢真的是超级好看这个颜色呢很显气质而且呢也是搭配的绝佳单品大家可以闭眼入真的是非常好看对身材的包容性也很好不管啥身材的宝宝呢穿上去都是很好看的。推荐宝宝们下单哦。';
async function main() {
const voiceList = ['Cherry', 'Serena', 'Ethan', 'Chelsie'];
const latestVoiceList = ['Dylan', 'Jada', 'Sunny'];
try {
const result = await dashscopeTTS({ text: sampleText, voice: latestVoiceList[2], token: API_KEY });
console.log('TTS 生成成功:', result);
// 如果API返回音频数据(通常是base64格式),可以这样处理
// 例如: 保存到文件或播放音频
if (result.output?.audio) {
// 处理音频数据
console.log('获取到音频数据,长度:', result.output.audio.length);
}
} catch (error) {
console.error('处理失败:', error);
}
}
main();