generated from tailored/router-db-template
Compare commits
27 Commits
ce7cd03cb3
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 96e6b6c0e1 | |||
| a06e12df5a | |||
| 0d42e912f5 | |||
| 52d37d0679 | |||
| 6a9e847ff1 | |||
| 143cbc877c | |||
| 42da851c37 | |||
| e2d0720698 | |||
| 204165bf73 | |||
| bce94f52a0 | |||
| b807cc9f38 | |||
| a25f7c7eb4 | |||
| e982ddb001 | |||
| 7bc9a06b7c | |||
| d4d6960a6c | |||
| 1220076257 | |||
| 530a4f21f5 | |||
| 71359fba88 | |||
| 29725a8614 | |||
| 32b6e04d6c | |||
| da6d4041ad | |||
| a76b506327 | |||
| 2c6c3dd60d | |||
| d5c2964e0e | |||
| ebe7c9afce | |||
| 75f1d75cae | |||
| 0b6b2fe730 |
@@ -5,4 +5,10 @@ POSTGRES_PORT=5432
|
|||||||
POSTGRES_DB=postgres
|
POSTGRES_DB=postgres
|
||||||
|
|
||||||
REDIS_HOST=localhost
|
REDIS_HOST=localhost
|
||||||
REDIS_PORT=6379
|
REDIS_PORT=6379
|
||||||
|
|
||||||
|
|
||||||
|
XHS_USER_ID=
|
||||||
|
XHS_USER_NAME=
|
||||||
|
XHS_API_SIGN_URL=
|
||||||
|
XHS_ROOT_COOKIE=
|
||||||
28
.github/workflows/git-sync.yml
vendored
28
.github/workflows/git-sync.yml
vendored
@@ -1,28 +0,0 @@
|
|||||||
name: Sync to CNB
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
# - main
|
|
||||||
- 'releases/**'
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
# This workflow is triggered on push events to the repository.
|
|
||||||
jobs:
|
|
||||||
sync:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Sync to CNB Repository
|
|
||||||
run: |
|
|
||||||
docker run --rm \
|
|
||||||
-v ${{ github.workspace }}:${{ github.workspace }} \
|
|
||||||
-w ${{ github.workspace }} \
|
|
||||||
-e PLUGIN_TARGET_URL="https://cnb.cool/kevisual/router-template.git" \
|
|
||||||
-e PLUGIN_AUTH_TYPE="https" \
|
|
||||||
-e PLUGIN_USERNAME="cnb" \
|
|
||||||
-e PLUGIN_PASSWORD=${{ secrets.GIT_PASSWORD }} \
|
|
||||||
-e PLUGIN_SYNC_MODE="rebase" \
|
|
||||||
tencentcom/git-sync
|
|
||||||
17
README.md
17
README.md
@@ -1 +1,16 @@
|
|||||||
# router app template
|
# 社交路由功能模块
|
||||||
|
|
||||||
|
## 小红书
|
||||||
|
|
||||||
|
自动获取和上传
|
||||||
|
|
||||||
|
[代理浏览器](https://git.xiongxiao.me/media/social-xhs-api-server)
|
||||||
|
|
||||||
|
### 功能
|
||||||
|
|
||||||
|
#### 获取评论
|
||||||
|
|
||||||
|
#### 获取笔记信息
|
||||||
|
|
||||||
|
#### 返回笔记信息
|
||||||
|
|
||||||
|
|||||||
56
package.json
56
package.json
@@ -15,12 +15,13 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "cross-env NODE_TLS_REJECT_UNAUTHORIZED=0 bun --watch src/dev.ts ",
|
"dev": "cross-env NODE_TLS_REJECT_UNAUTHORIZED=0 bun --watch src/dev.ts ",
|
||||||
"worker": "bun --watch src/task/worker.ts",
|
"worker": "bun --watch src/task/worker.ts",
|
||||||
|
"worker:server": "bun src/task/worker.ts",
|
||||||
"build": "rimraf dist && bun run bun.config.mjs",
|
"build": "rimraf dist && bun run bun.config.mjs",
|
||||||
"test": "tsx test/**/*.ts",
|
"test": "tsx test/**/*.ts",
|
||||||
"clean": "rm -rf dist",
|
"clean": "rm -rf dist",
|
||||||
"turbo:build": "turbo run build",
|
"turbo:build": "turbo run build",
|
||||||
"pub": "npm run build && envision pack -p -u",
|
"pub": "npm run build && envision pack -p -u",
|
||||||
"cmd": "tsx cmd/index.ts "
|
"cmd": "bun packages/xhs/src/test/command.ts "
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "abearxiong <xiongxiao@xiongxiao.me>",
|
"author": "abearxiong <xiongxiao@xiongxiao.me>",
|
||||||
@@ -35,43 +36,44 @@
|
|||||||
"access": "public"
|
"access": "public"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kevisual/ai": "^0.0.8",
|
"@kevisual/ai": "^0.0.19",
|
||||||
"@kevisual/code-center-module": "0.0.23",
|
"@kevisual/code-center-module": "0.0.24",
|
||||||
"@kevisual/context": "^0.0.3",
|
"@kevisual/context": "^0.0.4",
|
||||||
"@kevisual/router": "0.0.23",
|
"@kevisual/router": "0.0.49",
|
||||||
"@kevisual/use-config": "^1.0.19",
|
"@kevisual/use-config": "^1.0.21",
|
||||||
"cookie": "^1.0.2",
|
"axios": "^1.9.0",
|
||||||
"dayjs": "^1.11.13",
|
"cookie": "^1.1.1",
|
||||||
|
"dayjs": "^1.11.19",
|
||||||
"formidable": "^3.5.4",
|
"formidable": "^3.5.4",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.22",
|
||||||
"nanoid": "^5.1.5"
|
"nanoid": "^5.1.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@kevisual/app-assistant": "workspace:*",
|
|
||||||
"@kevisual/logger": "^0.0.4",
|
"@kevisual/logger": "^0.0.4",
|
||||||
"@kevisual/social-prompts": "workspace:*",
|
"@kevisual/social-prompts": "workspace:*",
|
||||||
"@kevisual/types": "^0.0.10",
|
"@kevisual/types": "^0.0.10",
|
||||||
"@kevisual/use-config": "^1.0.19",
|
"@kevisual/use-config": "^1.0.21",
|
||||||
"@kevisual/xhs": "workspace:*",
|
"@kevisual/xhs": "workspace:*",
|
||||||
"@types/bun": "^1.2.16",
|
"@types/bun": "^1.3.5",
|
||||||
"@types/crypto-js": "^4.2.2",
|
"@types/crypto-js": "^4.2.2",
|
||||||
"@types/formidable": "^3.4.5",
|
"@types/formidable": "^3.4.6",
|
||||||
"@types/lodash-es": "^4.17.12",
|
"@types/lodash-es": "^4.17.12",
|
||||||
"@types/node": "^24.0.3",
|
"@types/node": "^25.0.3",
|
||||||
"bullmq": "^5.55.0",
|
"bullmq": "^5.66.2",
|
||||||
"commander": "^14.0.0",
|
"commander": "^14.0.2",
|
||||||
"concurrently": "^9.1.2",
|
"concurrently": "^9.2.1",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^10.1.0",
|
||||||
"dotenv": "^16.5.0",
|
"dotenv": "^17.2.3",
|
||||||
"inquire": "^0.4.8",
|
"inquire": "^0.4.8",
|
||||||
"ioredis": "^5.6.1",
|
"ioredis": "^5.8.2",
|
||||||
"nodemon": "^3.1.10",
|
"nodemon": "^3.1.11",
|
||||||
"openai": "^5.6.0",
|
"openai": "^6.15.0",
|
||||||
"pg": "^8.16.2",
|
"pg": "^8.16.3",
|
||||||
"rimraf": "^6.0.1",
|
"rimraf": "^6.1.2",
|
||||||
"sequelize": "^6.37.7",
|
"sequelize": "^6.37.7",
|
||||||
"tape": "^5.9.0",
|
"tape": "^5.9.0",
|
||||||
"typescript": "^5.8.3"
|
"turbo": "^2.7.2",
|
||||||
|
"typescript": "^5.9.3"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@10.12.1"
|
"packageManager": "pnpm@10.26.2"
|
||||||
}
|
}
|
||||||
17
packages/app-assistant/.gitignore
vendored
17
packages/app-assistant/.gitignore
vendored
@@ -1,17 +0,0 @@
|
|||||||
node_modules
|
|
||||||
|
|
||||||
dist
|
|
||||||
|
|
||||||
app.config.json5
|
|
||||||
|
|
||||||
apps.config.json
|
|
||||||
|
|
||||||
deploy.tar.gz
|
|
||||||
cache-file
|
|
||||||
|
|
||||||
/apps
|
|
||||||
|
|
||||||
logs
|
|
||||||
|
|
||||||
.env*
|
|
||||||
!.env.example
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@kevisual/app-assistant",
|
|
||||||
"version": "0.0.1",
|
|
||||||
"description": "",
|
|
||||||
"main": "index.js",
|
|
||||||
"scripts": {
|
|
||||||
"dev": "echo \"Error: no test specified\" && exit 1"
|
|
||||||
},
|
|
||||||
"keywords": [],
|
|
||||||
"author": "abearxiong <xiongxiao@xiongxiao.me> (https://www.xiongxiao.me)",
|
|
||||||
"license": "MIT",
|
|
||||||
"packageManager": "pnpm@10.10.0",
|
|
||||||
"type": "module",
|
|
||||||
"devDependencies": {
|
|
||||||
"@types/node": "^22.15.3",
|
|
||||||
"bullmq": "^5.51.1"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"nanoid": "^5.1.5"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export * from './provider/comments/xhs.ts';
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import { SocialBase } from '@/social/social-base.ts';
|
|
||||||
|
|
||||||
export class XHS extends SocialBase {
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
import { nanoid } from 'nanoid';
|
|
||||||
|
|
||||||
export class SocialBase<T = any> {
|
|
||||||
id: string = '';
|
|
||||||
/**
|
|
||||||
* 是否运行中
|
|
||||||
*/
|
|
||||||
isRuning: boolean = false;
|
|
||||||
/**
|
|
||||||
* 应用的配置项
|
|
||||||
*/
|
|
||||||
config: T;
|
|
||||||
constructor(opts?: any) {
|
|
||||||
this.config = opts?.config || {};
|
|
||||||
this.id = opts?.id || nanoid();
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* 获取用户信息
|
|
||||||
*/
|
|
||||||
async getUserInfo() {
|
|
||||||
throw new Error('Method not implemented.');
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* 获取被 call 的信息
|
|
||||||
*/
|
|
||||||
async getMention() {
|
|
||||||
throw new Error('Method not implemented.');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 发送评论信息
|
|
||||||
*/
|
|
||||||
async sendComment() {
|
|
||||||
// throw new Error('Method not implemented.');
|
|
||||||
}
|
|
||||||
async getUnread() {
|
|
||||||
throw new Error('Method not implemented.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
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;';
|
|
||||||
|
|
||||||
const cookieObj = cookie.split('; ').reduce((acc, item) => {
|
|
||||||
const [key, value] = item.split('=');
|
|
||||||
acc[key] = value;
|
|
||||||
return acc;
|
|
||||||
}, {} as Record<string, string>);
|
|
||||||
const web_session = cookieObj['web_session'] || '';
|
|
||||||
const a1 = cookieObj['a1'] || '';
|
|
||||||
|
|
||||||
const headers = {
|
|
||||||
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36',
|
|
||||||
Cookie: cookie,
|
|
||||||
};
|
|
||||||
|
|
||||||
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', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
uri,
|
|
||||||
data,
|
|
||||||
a1,
|
|
||||||
web_session: web_session,
|
|
||||||
}),
|
|
||||||
}).then((res) => res.json());
|
|
||||||
return signs;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getUserMe = async () => {
|
|
||||||
const sign = await getSign(meUri, null, a1, web_session);
|
|
||||||
// const sign = {
|
|
||||||
// 'x-s':
|
|
||||||
// 'XYW_eyJzaWduU3ZuIjoiNTYiLCJzaWduVHlwZSI6IngyIiwiYXBwSWQiOiJsb2dpbiIsInNpZ25WZXJzaW9uIjoiMSIsInBheWxvYWQiOiJjYWE4NzYyMjk5NzQ0NDkzNGVhMWIxNDBjNzA0NTI2YmI0ZGZiYjcyMWJjMWQyZTEzNDhhNzZmNmM4MTI5NzljM2VkMWRmMjU0MGNkZDRkZmEyZGE0YjIzOTg0MDMyYmNmNGE4NDU1MzU4ZmZhZDQ0NjkxNzg4YWRjN2U2MmU3YjJmMzdmZGMzZTgwOWQ5NDNmOTRkM2JhMzVjNmQ3MzE4MjA1OWI3MTYyZGU0YjgxYzcyMDI2NmQyM2EzMmY1MGQ2NTQ5MmQ0ZTlhOTA3NmExY2JmMTYyZGJhMWJiMzAxNTg3MmY3MWU5MmIyNDllZGM3MmRkMjhiNGQ4Y2I4MjI2ZWY3YTdkYjI3NGQ2Y2YyMjVkZjk2ZjFmNWJlN2M2ZDkwNjY1MTE4MWJkYWNmYmQzYzVhMDk4Nzg3YjNmNThjMjc1ZWNjNDQzODFkOGNjNmU2ODAyNDFiZTRiODIwZjlkMDUyNTAyODk1ODZhYjM1NTRlMDJjYjhmZDlmNzIyNjM0NDMzZDBiZmNjMzg5NGU2ZDJmNGFiNGVlNGY0MzljMSJ9',
|
|
||||||
// 'x-t': '1746022429225',
|
|
||||||
// };
|
|
||||||
console.log('sign', sign);
|
|
||||||
const baseURL = 'https://edith.xiaohongshu.com';
|
|
||||||
const xhsMeUri = baseURL + '/api/sns/web/v2/user/me';
|
|
||||||
const res = await fetch(xhsMeUri, {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
...headers,
|
|
||||||
...sign,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const data = await res.json();
|
|
||||||
return data;
|
|
||||||
};
|
|
||||||
getUserMe().then((res) => {
|
|
||||||
console.log('res', res);
|
|
||||||
});
|
|
||||||
|
|
||||||
export async function getUnread() {
|
|
||||||
const unreadUri = '/api/sns/web/unread_count';
|
|
||||||
const sign = await getSign(unreadUri, null, a1, web_session);
|
|
||||||
console.log('sign', sign);
|
|
||||||
const res = await fetch(unreadUrl, {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
...headers,
|
|
||||||
...sign,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const data = await res.json();
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
// getUnread().then((res) => {
|
|
||||||
// console.log('res', res);
|
|
||||||
// });
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "@kevisual/types/json/backend.json",
|
|
||||||
"compilerOptions": {
|
|
||||||
"baseUrl": "./",
|
|
||||||
"paths": {
|
|
||||||
"@/*": [
|
|
||||||
"src/*"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"include": [
|
|
||||||
"src/**/*.ts",
|
|
||||||
],
|
|
||||||
"exclude": [],
|
|
||||||
}
|
|
||||||
@@ -7,6 +7,7 @@ import path from 'path';
|
|||||||
const rp = (resolvePath) => {
|
const rp = (resolvePath) => {
|
||||||
return path.resolve(process.cwd(), resolvePath);
|
return path.resolve(process.cwd(), resolvePath);
|
||||||
};
|
};
|
||||||
|
const external = ['jsdom']
|
||||||
// bun run src/index.ts --
|
// bun run src/index.ts --
|
||||||
await Bun.build({
|
await Bun.build({
|
||||||
target: 'node',
|
target: 'node',
|
||||||
@@ -16,6 +17,7 @@ await Bun.build({
|
|||||||
naming: {
|
naming: {
|
||||||
entry: 'app.mjs',
|
entry: 'app.mjs',
|
||||||
},
|
},
|
||||||
|
external: external,
|
||||||
|
|
||||||
define: {
|
define: {
|
||||||
VERSION: JSON.stringify(pkg.version),
|
VERSION: JSON.stringify(pkg.version),
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@kevisual/xhs-core",
|
"name": "@kevisual/xhs-core",
|
||||||
"version": "0.0.2",
|
"version": "0.0.4",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "dist/app.mjs",
|
"main": "dist/app.mjs",
|
||||||
"types": "dist/app.d.ts",
|
"types": "dist/app.d.ts",
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import qs from 'querystring';
|
import qs from 'querystring';
|
||||||
import { get_xs } from './jsvmp/xhs';
|
|
||||||
import fs from 'fs';
|
|
||||||
|
|
||||||
import { getXCommon, getSearchId, SearchSortType, SearchNoteType } from './helper.js';
|
import { getSearchId, SearchSortType, SearchNoteType } from './helper.js';
|
||||||
import { ErrorEnum, DataFetchError, IPBlockError, SignError, NeedVerifyError } from './exception';
|
import { ErrorEnum, DataFetchError, IPBlockError, SignError, NeedVerifyError } from './exception';
|
||||||
|
|
||||||
const camelToUnderscore = (key) => {
|
const camelToUnderscore = (key) => {
|
||||||
@@ -93,24 +91,6 @@ class XhsClient {
|
|||||||
config.headers = newHeaders;
|
config.headers = newHeaders;
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
_preHeaders(url, data = null) {
|
|
||||||
let a1 = this.cookieDict.a1;
|
|
||||||
let b1 = '';
|
|
||||||
let x_s_result = get_xs(url, data, this.cookie);
|
|
||||||
const X_S = x_s_result['X-s'];
|
|
||||||
const X_t = x_s_result['X-t'].toString();
|
|
||||||
const X_S_COMMON = getXCommon(a1, b1, X_S, X_t);
|
|
||||||
return {
|
|
||||||
headers: {
|
|
||||||
'x-s': X_S,
|
|
||||||
'x-t': X_t,
|
|
||||||
// 'x-s-common': X_S_COMMON,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
getXCommon(a1, b1, x_s, x_t) {
|
|
||||||
return getXCommon(a1, b1 || '', x_s, String(x_t));
|
|
||||||
}
|
|
||||||
getCookieMap() {
|
getCookieMap() {
|
||||||
const cookie = this.cookie;
|
const cookie = this.cookie;
|
||||||
let cookieDict = {};
|
let cookieDict = {};
|
||||||
@@ -145,16 +125,6 @@ class XhsClient {
|
|||||||
this.axiosInstance.defaults.headers.Cookie = cookieStr;
|
this.axiosInstance.defaults.headers.Cookie = cookieStr;
|
||||||
this.cookie = cookieStr;
|
this.cookie = cookieStr;
|
||||||
}
|
}
|
||||||
/**
|
|
||||||
* Get X-S and X-T
|
|
||||||
* @param {*} url
|
|
||||||
* @param {*} data
|
|
||||||
* @param {*} cookie
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
get_xs(url, data, cookie) {
|
|
||||||
return get_xs(url, data, cookie);
|
|
||||||
}
|
|
||||||
|
|
||||||
async request(method, url, config = {}) {
|
async request(method, url, config = {}) {
|
||||||
try {
|
try {
|
||||||
@@ -277,17 +247,17 @@ class XhsClient {
|
|||||||
const endpoint = this.getEndpoint(config).endpoint;
|
const endpoint = this.getEndpoint(config).endpoint;
|
||||||
config = await this.requestSign(uri, data, config);
|
config = await this.requestSign(uri, data, config);
|
||||||
this.printResult('post', { uri, data, config });
|
this.printResult('post', { uri, data, config });
|
||||||
if (data) {
|
// if (data) {
|
||||||
return this.request('POST', `${endpoint}${uri}`, {
|
// return this.request('POST', `${endpoint}${uri}`, {
|
||||||
...config,
|
// ...config,
|
||||||
data: jsonStr,
|
// data: jsonStr,
|
||||||
headers: {
|
// headers: {
|
||||||
...config.headers,
|
// ...config.headers,
|
||||||
'Content-Type': 'application/json',
|
// 'Content-Type': 'application/json',
|
||||||
},
|
// },
|
||||||
});
|
// });
|
||||||
}
|
// }
|
||||||
return this.request('POST', `${endpoint}${uri}`, { ...config, data });
|
// return this.request('POST', `${endpoint}${uri}`, { ...config, data });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
22
packages/xhs/.gitignore
vendored
22
packages/xhs/.gitignore
vendored
@@ -1 +1,21 @@
|
|||||||
db-sqlite
|
db-sqlite
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
|
||||||
|
dist
|
||||||
|
|
||||||
|
app.config.json5
|
||||||
|
|
||||||
|
apps.config.json
|
||||||
|
|
||||||
|
deploy.tar.gz
|
||||||
|
cache-file
|
||||||
|
|
||||||
|
/apps
|
||||||
|
|
||||||
|
logs
|
||||||
|
|
||||||
|
.env*
|
||||||
|
!.env.example
|
||||||
|
|
||||||
|
.turbo
|
||||||
@@ -19,12 +19,12 @@
|
|||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "abearxiong <xiongxiao@xiongxiao.me> (https://www.xiongxiao.me)",
|
"author": "abearxiong <xiongxiao@xiongxiao.me> (https://www.xiongxiao.me)",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"packageManager": "pnpm@10.10.0",
|
"packageManager": "pnpm@10.12.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@kevisual/use-config": "^1.0.14",
|
"@kevisual/use-config": "^1.0.19",
|
||||||
"@kevisual/xhs-core": "workspace:*",
|
"@kevisual/xhs-core": "0.0.4",
|
||||||
"@types/node": "^22.15.3"
|
"@types/node": "^24.0.3"
|
||||||
},
|
},
|
||||||
"exports": {
|
"exports": {
|
||||||
".": {
|
".": {
|
||||||
@@ -33,6 +33,10 @@
|
|||||||
},
|
},
|
||||||
"./index": {
|
"./index": {
|
||||||
"import": "./src/index.ts"
|
"import": "./src/index.ts"
|
||||||
|
},
|
||||||
|
"./index.ts": {
|
||||||
|
"import": "./src/index.ts",
|
||||||
|
"types": "./src/index.ts"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
import { QueryRouterServer } from '@kevisual/router/browser';
|
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 app = new QueryRouterServer();
|
||||||
export const xhsServices = new XhsServices();// Semicolon separated Cookie File
|
export const xhsServices = new XhsServices(); // Semicolon separated Cookie File
|
||||||
|
|
||||||
const cookie =
|
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,
|
cookie,
|
||||||
signConfig: {
|
signConfig: {
|
||||||
signUrl: 'http://light.xiongxiao.me:5006/sign',
|
signUrl: 'http://localhost:5006/sign',
|
||||||
// signUrl: 'http://localhost:5006/sign',
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { XhsClient } from './libs/xhs.ts';
|
import { XhsClient } from './libs/xhs.ts';
|
||||||
import { app, xhsServices } from './app.ts';
|
import { app, xhsServices, xhsRootClient, XhsServices } from './app.ts';
|
||||||
import './routes/index.ts';
|
import './routes/index.ts';
|
||||||
|
|
||||||
export { XhsClient, app, xhsServices };
|
export { XhsClient, app, xhsServices, xhsRootClient, XhsServices };
|
||||||
|
|||||||
507
packages/xhs/src/libs/client-base.ts
Normal file
507
packages/xhs/src/libs/client-base.ts
Normal file
@@ -0,0 +1,507 @@
|
|||||||
|
import qs from 'querystring';
|
||||||
|
|
||||||
|
import { getSearchId, SearchSortType, SearchNoteType } from './utils/helper.js';
|
||||||
|
import { ErrorEnum, DataFetchError, IPBlockError, NeedVerifyError } from './utils/exception.js';
|
||||||
|
|
||||||
|
const camelToUnderscore = (key) => {
|
||||||
|
return key.replace(/([A-Z])/g, '_$1').toLowerCase();
|
||||||
|
};
|
||||||
|
|
||||||
|
const transformJsonKeys = (jsonData) => {
|
||||||
|
const dataDict = typeof jsonData === 'string' ? JSON.parse(jsonData) : jsonData;
|
||||||
|
const dictNew = {};
|
||||||
|
for (const [key, value] of Object.entries(dataDict)) {
|
||||||
|
const newKey = camelToUnderscore(key);
|
||||||
|
if (!value) {
|
||||||
|
dictNew[newKey] = value;
|
||||||
|
} else if (typeof value === 'object' && !Array.isArray(value)) {
|
||||||
|
dictNew[newKey] = transformJsonKeys(value);
|
||||||
|
} else if (Array.isArray(value)) {
|
||||||
|
dictNew[newKey] = value.map((item) => (item && typeof item === 'object' ? transformJsonKeys(item) : item));
|
||||||
|
} else {
|
||||||
|
dictNew[newKey] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dictNew;
|
||||||
|
};
|
||||||
|
|
||||||
|
class XhsClient {
|
||||||
|
proxies: string | null;
|
||||||
|
timeout: number;
|
||||||
|
_host: string;
|
||||||
|
_creatorHost: string;
|
||||||
|
_customerHost: string;
|
||||||
|
home: string;
|
||||||
|
userAgent: string;
|
||||||
|
_cookie: string;
|
||||||
|
_headers: Record<string, string>;
|
||||||
|
signConfig?: Record<string, any> = {
|
||||||
|
signUrl: 'http://localhost:5005/sign',
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Constructor for XhsClient
|
||||||
|
* @param {Object} options - Configuration options
|
||||||
|
* @param {string} options.cookie - Cookie string for authentication
|
||||||
|
* @param {string} options.userAgent - User agent string for requests
|
||||||
|
* @param {number} options.timeout - Request timeout in milliseconds
|
||||||
|
* @param {string} options.proxies - Proxy settings
|
||||||
|
*/
|
||||||
|
constructor({ cookie = null, userAgent = null, timeout = 10000, proxies = null } = {}) {
|
||||||
|
this.proxies = proxies;
|
||||||
|
this.timeout = timeout;
|
||||||
|
this._host = 'https://edith.xiaohongshu.com';
|
||||||
|
this._creatorHost = 'https://creator.xiaohongshu.com';
|
||||||
|
this._customerHost = 'https://customer.xiaohongshu.com';
|
||||||
|
this.home = 'https://www.xiaohongshu.com';
|
||||||
|
this.userAgent = userAgent || 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36';
|
||||||
|
this._cookie = cookie || '';
|
||||||
|
this._headers = {
|
||||||
|
'user-agent': this.userAgent,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
};
|
||||||
|
if (cookie) {
|
||||||
|
this._headers.Cookie = cookie;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @params {*} args
|
||||||
|
*/
|
||||||
|
printResult(...args) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
// Getter for cookie
|
||||||
|
get cookie() {
|
||||||
|
return this._cookie;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setter for cookie
|
||||||
|
set cookie(cookie) {
|
||||||
|
this._cookie = cookie;
|
||||||
|
this._headers.Cookie = cookie;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getter for cookieDict
|
||||||
|
get cookieDict() {
|
||||||
|
const cookieStr = this._cookie;
|
||||||
|
return cookieStr ? qs.parse(cookieStr.replace(/; /g, '&')) : {};
|
||||||
|
}
|
||||||
|
getAgentHeader(config) {
|
||||||
|
const headers = config?.headers || {};
|
||||||
|
const newHeaders = {
|
||||||
|
'user-agent': this.userAgent,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...headers,
|
||||||
|
};
|
||||||
|
config.headers = newHeaders;
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
getCookieMap() {
|
||||||
|
const cookie = this.cookie;
|
||||||
|
let cookieDict = {};
|
||||||
|
if (cookie) {
|
||||||
|
const cookieArray = cookie.split(';');
|
||||||
|
cookieArray.forEach((item) => {
|
||||||
|
const [key, value] = item.split('=');
|
||||||
|
const trimKey = key.trim();
|
||||||
|
if (trimKey) {
|
||||||
|
const _value = value ? value.trim() : '';
|
||||||
|
cookieDict[trimKey] = _value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return cookieDict;
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {*} data
|
||||||
|
*/
|
||||||
|
setCookieMap(data = {}) {
|
||||||
|
const cookieDict = this.getCookieMap();
|
||||||
|
const newCookieDict = { ...cookieDict, ...data };
|
||||||
|
const cookieStr = Object.entries(newCookieDict)
|
||||||
|
.map(([key, value]: [string, string]) => {
|
||||||
|
const trimmedKey = key.trim();
|
||||||
|
const trimmedValue = value ? value.trim() : '';
|
||||||
|
return `${trimmedKey}=${trimmedValue}`;
|
||||||
|
})
|
||||||
|
.join('; ');
|
||||||
|
this._cookie = cookieStr;
|
||||||
|
this._headers.Cookie = cookieStr;
|
||||||
|
this.cookie = cookieStr;
|
||||||
|
}
|
||||||
|
|
||||||
|
async request(method, url, config: any = {}) {
|
||||||
|
try {
|
||||||
|
delete config.sign;
|
||||||
|
const headers = { ...this._headers, ...(config.headers || {}) };
|
||||||
|
|
||||||
|
const fetchOptions: any = {
|
||||||
|
method: method,
|
||||||
|
headers: headers,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (config.data) {
|
||||||
|
fetchOptions.body = typeof config.data === 'string' ? config.data : JSON.stringify(config.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
||||||
|
fetchOptions.signal = controller.signal;
|
||||||
|
|
||||||
|
this.printResult('request', { method, url, config });
|
||||||
|
|
||||||
|
const response = await fetch(url, fetchOptions);
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
|
||||||
|
if (response.status === 471 || response.status === 461) {
|
||||||
|
const verifyType = response.headers.get('verifytype');
|
||||||
|
const verifyUuid = response.headers.get('verifyuuid');
|
||||||
|
throw new NeedVerifyError(`出现验证码,请求失败,Verifytype: ${verifyType},Verifyuuid: ${verifyUuid}`, response, verifyType);
|
||||||
|
}
|
||||||
|
|
||||||
|
let data;
|
||||||
|
const contentType = response.headers.get('content-type');
|
||||||
|
if (contentType && contentType.includes('application/json')) {
|
||||||
|
data = await response.json();
|
||||||
|
} else {
|
||||||
|
data = await response.text();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.printResult('response', {
|
||||||
|
url: url,
|
||||||
|
response,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
return {
|
||||||
|
code: response.status,
|
||||||
|
status: response.status,
|
||||||
|
msg: '请求失败',
|
||||||
|
data: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
if (error.name === 'AbortError') {
|
||||||
|
return {
|
||||||
|
code: 408,
|
||||||
|
msg: '请求超时',
|
||||||
|
data: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (error instanceof NeedVerifyError) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
console.error('Error in request:', error);
|
||||||
|
return {
|
||||||
|
code: 500,
|
||||||
|
msg: '请求失败',
|
||||||
|
data: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* GET的请求 data必须为null
|
||||||
|
* @param {*} uri
|
||||||
|
* @param {*} data
|
||||||
|
* @param {*} config
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
async requestSign(uri, data = null, config: any = {}, method = 'GET') {
|
||||||
|
const needSign = config.needSign ?? true;
|
||||||
|
if (needSign && config.sign) {
|
||||||
|
await config.sign(uri, data, config, method);
|
||||||
|
}
|
||||||
|
delete config.sign;
|
||||||
|
delete config.needSign;
|
||||||
|
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
getEndpoint(config) {
|
||||||
|
let endpoint = this._host;
|
||||||
|
let isCreator = config?.isCreator ?? false;
|
||||||
|
let isCustomer = config?.isCustomer ?? false;
|
||||||
|
if (isCustomer) {
|
||||||
|
endpoint = this._customerHost;
|
||||||
|
} else if (isCreator) {
|
||||||
|
endpoint = this._creatorHost;
|
||||||
|
}
|
||||||
|
delete config.isCreator;
|
||||||
|
delete config.isCustomer;
|
||||||
|
|
||||||
|
return { endpoint, isCreator, isCustomer };
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {*} uri
|
||||||
|
* @param {*} params
|
||||||
|
* @param {Object} config
|
||||||
|
* @param {*} [config.sign] - Whether to sign the request
|
||||||
|
* @param {boolean} [config.isCreator] - Whether the request is for a creator
|
||||||
|
* @param {boolean} [config.isCustomer] - Whether the request is for a customer
|
||||||
|
* @param {boolean} [config.needSign] - 是否需要 sign
|
||||||
|
* @param {*} [config.headers] - XSEC token for authentication
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
async get(uri, params = null, config = {}) {
|
||||||
|
if (params) {
|
||||||
|
uri = `${uri}?${qs.stringify(params)}`;
|
||||||
|
}
|
||||||
|
this.printResult('get', { uri, params, config });
|
||||||
|
const endpoint = this.getEndpoint(config).endpoint;
|
||||||
|
config = await this.requestSign(uri, null, config, 'GET');
|
||||||
|
|
||||||
|
return this.request('GET', `${endpoint}${uri}`, config);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {*} uri
|
||||||
|
* @param {*} data
|
||||||
|
* @param {Object} config
|
||||||
|
* @param {*} [config.sign] - Whether to sign the request
|
||||||
|
* @param {boolean} [config.isCreator] - Whether the request is for a creator
|
||||||
|
* @param {boolean} [config.isCustomer] - Whether the request is for a customer
|
||||||
|
* @param {boolean} [config.needSign] - 是否需要 sign
|
||||||
|
* @param {*} [config.headers] - XSEC token for authentication
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
async post(uri, data = null, config = {}) {
|
||||||
|
const endpoint = this.getEndpoint(config).endpoint;
|
||||||
|
config = await this.requestSign(uri, data, config, 'POST');
|
||||||
|
this.printResult('post', { uri, data, config });
|
||||||
|
return this.request('POST', `${endpoint}${uri}`, {
|
||||||
|
...config,
|
||||||
|
data: data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取笔记详情
|
||||||
|
* 注意: 需要xsec_token
|
||||||
|
* @uri /api/sns/web/v1/feed
|
||||||
|
* @param {string} noteId
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
async getNoteById(noteId, xsecToken, xsecSource = 'pc_feed', config = {}) {
|
||||||
|
if (!xsecToken) {
|
||||||
|
throw new Error('xsecToken is required');
|
||||||
|
}
|
||||||
|
const data = {
|
||||||
|
source_note_id: noteId,
|
||||||
|
image_scenes: ['CRD_WM_WEBP'],
|
||||||
|
xsec_token: xsecToken,
|
||||||
|
xsec_source: xsecSource,
|
||||||
|
};
|
||||||
|
const uri = '/api/sns/web/v1/feed';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await this.post(uri, data, config);
|
||||||
|
return res.items[0].note_card;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching note:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 获取笔记详情
|
||||||
|
* @uri /api/sns/web/v1/feed
|
||||||
|
* @param {string} noteId
|
||||||
|
* @param {string} xsecToken
|
||||||
|
* @param {string} [xsecSource=pc_feed]
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
async getNoteByIdFromHtml(noteId, xsecToken, xsecSource = 'pc_feed') {
|
||||||
|
const url = `https://www.xiaohongshu.com/explore/${noteId}?xsec_token=${xsecToken}&xsec_source=${xsecSource}`;
|
||||||
|
this.printResult('html', { url, noteId, xsecToken, xsecSource });
|
||||||
|
let html = '';
|
||||||
|
try {
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
headers: {
|
||||||
|
'user-agent': this.userAgent,
|
||||||
|
referer: 'https://www.xiaohongshu.com/',
|
||||||
|
...this._headers,
|
||||||
|
},
|
||||||
|
signal: controller.signal,
|
||||||
|
});
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
|
||||||
|
html = await response.text();
|
||||||
|
const stateMatch = html.match(/window.__INITIAL_STATE__=({.*})<\/script>/);
|
||||||
|
|
||||||
|
if (stateMatch) {
|
||||||
|
const state = stateMatch[1].replace(/undefined/g, '""');
|
||||||
|
if (state !== '{}') {
|
||||||
|
const noteDict = transformJsonKeys(JSON.parse(state)) as any;
|
||||||
|
return { code: 0, data: noteDict.note.note_detail_map[noteId].note };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (html.includes(ErrorEnum.IP_BLOCK.value)) {
|
||||||
|
throw new IPBlockError(ErrorEnum.IP_BLOCK.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new DataFetchError(html);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching note:', error);
|
||||||
|
return {
|
||||||
|
code: 500,
|
||||||
|
msg: '请求失败',
|
||||||
|
error: error.message,
|
||||||
|
data: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 获取用户信息
|
||||||
|
* @uri /api/sns/web/v1/user/selfinfo
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
async getSelfInfo() {
|
||||||
|
const uri = '/api/sns/web/v1/user/selfinfo';
|
||||||
|
return this.get(uri);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @uri /api/sns/web/v2/user/me
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
async getSelfInfoV2() {
|
||||||
|
const uri = '/api/sns/web/v2/user/me';
|
||||||
|
return this.get(uri);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 获取用户信息
|
||||||
|
* @uri /api/sns/web/v1/user/otherinfo
|
||||||
|
* @param {string} userId
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
async getUserInfo(userId) {
|
||||||
|
const uri = '/api/sns/web/v1/user/otherinfo';
|
||||||
|
const params = {
|
||||||
|
target_user_id: userId,
|
||||||
|
};
|
||||||
|
return this.get(uri, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取笔记评论
|
||||||
|
* @uri /api/sns/web/v2/comment/page
|
||||||
|
* @param {string} noteId 笔记id
|
||||||
|
* @param {string} cursor 分页查询的下标,默认为""
|
||||||
|
* @param {Object} params 其他参数
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
async getNoteComments(noteId, cursor = '', otherParams = {}) {
|
||||||
|
const uri = '/api/sns/web/v2/comment/page';
|
||||||
|
const params = {
|
||||||
|
note_id: noteId,
|
||||||
|
cursor: cursor,
|
||||||
|
image_formats: 'jpg,webp,avif',
|
||||||
|
...otherParams,
|
||||||
|
};
|
||||||
|
return this.get(uri, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户笔记
|
||||||
|
* @uri /api/sns/web/v1/user_posted
|
||||||
|
* @param {*} userId
|
||||||
|
* @param {*} cursor
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
async getUserNotes(userId, cursor = '') {
|
||||||
|
const uri = '/api/sns/web/v1/user_posted';
|
||||||
|
const params = {
|
||||||
|
cursor: cursor,
|
||||||
|
num: 30,
|
||||||
|
user_id: userId,
|
||||||
|
image_scenes: 'FD_WM_WEBP',
|
||||||
|
};
|
||||||
|
return this.get(uri, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取账号@我通知
|
||||||
|
* @uri /api/sns/web/v1/you/mentions
|
||||||
|
* @param {*} num
|
||||||
|
* @param {*} cursor
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
async getMentionNotifications(num = 20, cursor = '') {
|
||||||
|
const uri = '/api/sns/web/v1/you/mentions';
|
||||||
|
const params = { num: num, cursor: cursor };
|
||||||
|
return this.get(uri, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取点赞通知
|
||||||
|
* @uri /api/sns/web/v1/you/likes
|
||||||
|
* @param {*} num
|
||||||
|
* @param {*} cursor
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
async getLikeNotifications(num = 20, cursor = '') {
|
||||||
|
const uri = '/api/sns/web/v1/you/likes';
|
||||||
|
const params = { num: num, cursor: cursor };
|
||||||
|
return this.get(uri, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取关注通知
|
||||||
|
* @uri /api/sns/web/v1/you/connections
|
||||||
|
* @param {*} num
|
||||||
|
* @param {*} cursor
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
async getFollowNotifications(num = 20, cursor = '', config = {}) {
|
||||||
|
const uri = '/api/sns/web/v1/you/connections';
|
||||||
|
const params = { num: num, cursor: cursor };
|
||||||
|
return this.get(uri, params, config);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 获取用户信息
|
||||||
|
* @uri /user/profile/{userId}
|
||||||
|
* @description 通过用户ID获取用户信息
|
||||||
|
* @param {string} userId
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
async getUserInfoFromHtml(userId) {
|
||||||
|
const url = `https://www.xiaohongshu.com/user/profile/${userId}`;
|
||||||
|
try {
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
headers: {
|
||||||
|
'user-agent': this.userAgent,
|
||||||
|
referer: 'https://www.xiaohongshu.com/',
|
||||||
|
...this._headers,
|
||||||
|
},
|
||||||
|
signal: controller.signal,
|
||||||
|
});
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
|
||||||
|
const html = await response.text();
|
||||||
|
const stateMatch = html.match(/window.__INITIAL_STATE__=({.*})<\/script>/);
|
||||||
|
if (stateMatch) {
|
||||||
|
const state = stateMatch[1].replace(/"undefined"/g, '"_"').replace(/\bundefined\b/g, '""');
|
||||||
|
if (state !== '{}') {
|
||||||
|
const parsedState = JSON.parse(state) as any;
|
||||||
|
const data = transformJsonKeys(parsedState) as any
|
||||||
|
const userBasicInfo =data.user.user_page_data.basic_info;
|
||||||
|
return userBasicInfo;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return html;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching user info:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { XhsClient };
|
||||||
46
packages/xhs/src/libs/utils/exception.js
Normal file
46
packages/xhs/src/libs/utils/exception.js
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
// ErrorTuple and ErrorEnum
|
||||||
|
const ErrorEnum = {
|
||||||
|
IP_BLOCK: { code: 300012, msg: '网络连接异常,请检查网络设置或重启试试' },
|
||||||
|
NOTE_CANT_GET: { code: 300031, msg: '当前笔记暂时无法浏览' },
|
||||||
|
NOTE_ABNORMAL: { code: -510001, msg: '笔记状态异常,请稍后查看' },
|
||||||
|
NOTE_SECRETE_FAULT: { code: -510001, msg: '当前内容无法展示' },
|
||||||
|
SIGN_FAULT: { code: 300015, msg: '浏览器异常,请尝试关闭/卸载风险插件或重启试试!' },
|
||||||
|
SESSION_EXPIRED: { code: -100, msg: '登录已过期' },
|
||||||
|
};
|
||||||
|
|
||||||
|
// Custom error classes
|
||||||
|
class DataFetchError extends Error {
|
||||||
|
constructor(message) {
|
||||||
|
super(message);
|
||||||
|
this.message = message;
|
||||||
|
this.name = 'DataFetchError';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class IPBlockError extends Error {
|
||||||
|
constructor(message) {
|
||||||
|
super(message);
|
||||||
|
this.message = message;
|
||||||
|
this.name = 'IPBlockError';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SignError extends Error {
|
||||||
|
constructor(message) {
|
||||||
|
super(message);
|
||||||
|
this.message = message;
|
||||||
|
this.name = 'SignError';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class NeedVerifyError extends Error {
|
||||||
|
constructor(message, verifyType = null, verifyUuid = null) {
|
||||||
|
super(message);
|
||||||
|
this.message = message;
|
||||||
|
this.name = 'NeedVerifyError';
|
||||||
|
this.verifyType = verifyType;
|
||||||
|
this.verifyUuid = verifyUuid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { ErrorEnum, DataFetchError, IPBlockError, SignError, NeedVerifyError };
|
||||||
32
packages/xhs/src/libs/utils/helper.js
Normal file
32
packages/xhs/src/libs/utils/helper.js
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
function getSearchId() {
|
||||||
|
const e = BigInt(Date.now()) << 64n;
|
||||||
|
const t = Math.floor(Math.random() * 2147483647);
|
||||||
|
return base36encode(e + BigInt(t));
|
||||||
|
}
|
||||||
|
|
||||||
|
function base36encode(num) {
|
||||||
|
return num.toString(36).toUpperCase();
|
||||||
|
}
|
||||||
|
const SearchSortType = Object.freeze({
|
||||||
|
// default
|
||||||
|
GENERAL: { value: 'general' },
|
||||||
|
// most popular
|
||||||
|
MOST_POPULAR: { value: 'popularity_descending' },
|
||||||
|
// Latest
|
||||||
|
LATEST: { value: 'time_descending' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const SearchNoteType = Object.freeze({
|
||||||
|
// default
|
||||||
|
ALL: { value: 0 },
|
||||||
|
// only video
|
||||||
|
VIDEO: { value: 1 },
|
||||||
|
// only image
|
||||||
|
IMAGE: { value: 2 },
|
||||||
|
});
|
||||||
|
|
||||||
|
export {
|
||||||
|
getSearchId,
|
||||||
|
SearchSortType,
|
||||||
|
SearchNoteType,
|
||||||
|
};
|
||||||
@@ -28,6 +28,12 @@ export const api: ApiInfo[] = [
|
|||||||
needSign: true,
|
needSign: true,
|
||||||
description: '获取@我的消息',
|
description: '获取@我的消息',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
uri: '/api/sns/web/v1/search/notes',
|
||||||
|
method: 'POST',
|
||||||
|
needSign: true,
|
||||||
|
description: '通过关键词搜索笔记',
|
||||||
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
type ReturnApiInfo = {
|
type ReturnApiInfo = {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { getApiInfo } from './xhs-api/api.ts';
|
import { getApiInfo } from './xhs-api/api.ts';
|
||||||
import { XhsClient as XhsClientBase } from '@kevisual/xhs-core';
|
import { XhsClient as XhsClientBase } from './client-base.js';
|
||||||
import { Mention, CommonentInfo, ResponseMession } from './xhs-type/mention.ts';
|
import { Mention, CommonentInfo, ResponseMession } from './xhs-type/mention.ts';
|
||||||
import { pick } from 'lodash-es';
|
import { pick } from 'lodash-es';
|
||||||
import { getNote } from './modules/get-note.ts';
|
import { getNote } from './modules/get-note.ts';
|
||||||
@@ -14,6 +14,7 @@ type SignInfo = {
|
|||||||
data: any;
|
data: any;
|
||||||
a1: string;
|
a1: string;
|
||||||
web_session?: string;
|
web_session?: string;
|
||||||
|
method?: 'GET' | 'POST';
|
||||||
};
|
};
|
||||||
type SignResponse = {
|
type SignResponse = {
|
||||||
a1: string;
|
a1: string;
|
||||||
@@ -27,17 +28,16 @@ type SignResponse = {
|
|||||||
};
|
};
|
||||||
type SignOptions = {
|
type SignOptions = {
|
||||||
signUrl?: string;
|
signUrl?: string;
|
||||||
|
method?: 'GET' | 'POST';
|
||||||
};
|
};
|
||||||
export const getSign = async (signInfo: SignInfo, options?: SignOptions): Promise<SignResponse> => {
|
export const getSign = async (signInfo: SignInfo, options?: SignOptions): Promise<SignResponse> => {
|
||||||
const { uri, data, a1, web_session } = signInfo;
|
const { uri, data, a1, method } = signInfo;
|
||||||
// console.log('getSign', uri, data, a1, web_session);
|
|
||||||
// let signUri = new URL(uri, 'http://light.xiongxiao.me:5006').pathname;
|
|
||||||
// signUri = '/api/sns/web/v2/user/me';
|
|
||||||
try {
|
try {
|
||||||
let signUrl = options?.signUrl || 'http://localhost:5005/sign';
|
let signUrl = options?.signUrl || 'http://localhost:5005/sign';
|
||||||
// signUrl = 'http://localhost:5005/sign';
|
// signUrl = 'http://localhost:5005/sign';
|
||||||
// const urlA1 = ''http://light.xiongxiao.me:5006/a1';
|
// const urlA1 = ''http://light.xiongxiao.me:5006/a1';
|
||||||
// const urlA1 = 'http://localhost:5005/a1';
|
// const urlA1 = 'http://localhost:5005/a1';
|
||||||
|
console.log('sign', signUrl, signInfo);
|
||||||
const signs = await fetch(signUrl, {
|
const signs = await fetch(signUrl, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
@@ -47,7 +47,7 @@ export const getSign = async (signInfo: SignInfo, options?: SignOptions): Promis
|
|||||||
uri: uri,
|
uri: uri,
|
||||||
data,
|
data,
|
||||||
a1,
|
a1,
|
||||||
web_session: web_session,
|
method: method ?? 'POST',
|
||||||
}),
|
}),
|
||||||
}).then((res) => res.json());
|
}).then((res) => res.json());
|
||||||
return signs as SignResponse;
|
return signs as SignResponse;
|
||||||
@@ -62,10 +62,13 @@ type XhsSign = {
|
|||||||
signUrl?: string;
|
signUrl?: string;
|
||||||
};
|
};
|
||||||
export class XhsClient extends XhsClientBase {
|
export class XhsClient extends XhsClientBase {
|
||||||
signConfig?: XhsSign;
|
declare signConfig?: XhsSign;
|
||||||
constructor(opts: XhsOptions) {
|
constructor(opts: XhsOptions) {
|
||||||
super(opts as any);
|
super(opts as any);
|
||||||
}
|
}
|
||||||
|
setCookie(cookie: string) {
|
||||||
|
this.cookie = cookie;
|
||||||
|
}
|
||||||
getApiInfo = getApiInfo;
|
getApiInfo = getApiInfo;
|
||||||
printResult(msg: string, data: any) {
|
printResult(msg: string, data: any) {
|
||||||
if (msg === 'response') {
|
if (msg === 'response') {
|
||||||
@@ -83,10 +86,18 @@ export class XhsClient extends XhsClientBase {
|
|||||||
}
|
}
|
||||||
switch (msg) {
|
switch (msg) {
|
||||||
case 'get':
|
case 'get':
|
||||||
// console.log('get', data);
|
console.log('get==>', data);
|
||||||
break;
|
break;
|
||||||
case 'sign':
|
case 'sign':
|
||||||
// console.log('sign', data);
|
console.log('sign==>', data);
|
||||||
|
break;
|
||||||
|
case 'post':
|
||||||
|
console.log('post==>', data);
|
||||||
|
break;
|
||||||
|
case 'error':
|
||||||
|
console.log('error==>', data);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -154,7 +165,7 @@ export class XhsClient extends XhsClientBase {
|
|||||||
const response = await this.get(url, { num, cursor }, { sign: this.sign.bind(this) });
|
const response = await this.get(url, { num, cursor }, { sign: this.sign.bind(this) });
|
||||||
return response as Result<ReturnData>;
|
return response as Result<ReturnData>;
|
||||||
}
|
}
|
||||||
async getComment(noteId: string, xsecToken?: string) {}
|
async getComment(noteId: string, xsecToken?: string) { }
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @uri /api/sns/web/v1/you/mentions
|
* @uri /api/sns/web/v1/you/mentions
|
||||||
@@ -170,32 +181,34 @@ export class XhsClient extends XhsClientBase {
|
|||||||
needSign: true,
|
needSign: true,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
return response;
|
return response as any;
|
||||||
}
|
}
|
||||||
async sign(uri: string, data: any, config: any) {
|
async sign(uri: string, data: any, config: any, method?: 'GET' | 'POST') {
|
||||||
let headers = config?.headers || {};
|
let headers = config?.headers || {};
|
||||||
const cookieDist = this.getCookieMap();
|
const cookieDist = this.getCookieMap();
|
||||||
const apiInfo = this.getApiInfo(uri);
|
const apiInfo = this.getApiInfo(uri);
|
||||||
if (apiInfo && !apiInfo?.needSign) {
|
if (apiInfo && !apiInfo?.needSign) {
|
||||||
return config || {};
|
return config || {};
|
||||||
}
|
}
|
||||||
const web_session = cookieDist['web_session'];
|
|
||||||
const a1 = cookieDist['a1'];
|
const a1 = cookieDist['a1'];
|
||||||
const res = await getSign({ uri, data, a1, web_session }, this.signConfig);
|
const res = await getSign({ uri, data, a1, method }, this.signConfig);
|
||||||
const _sign = res.sign;
|
const _sign = res.sign
|
||||||
this.printResult('sign', { uri, apiInfo, res });
|
this.printResult('sign', { uri, apiInfo, res });
|
||||||
const xs = _sign?.['x-s'];
|
const xs = _sign?.['x-s'];
|
||||||
const xt = _sign?.['x-t'];
|
const xt = _sign?.['x-t'];
|
||||||
const b1 = _sign?.['b1'];
|
|
||||||
const newA1 = _sign?.['a1'];
|
const newA1 = _sign?.['a1'];
|
||||||
|
const xsCommon = _sign?.['x-s-common'];
|
||||||
|
const xB3Traceid = _sign?.['x-b3-traceid'];
|
||||||
if (a1 !== newA1) {
|
if (a1 !== newA1) {
|
||||||
this.setCookieMap({ a1: newA1 });
|
this.setCookieMap({ a1: newA1 });
|
||||||
this.printResult('cookie change', a1);
|
this.printResult('cookie change', a1);
|
||||||
}
|
}
|
||||||
|
// throw new Error('disable sign');
|
||||||
if (res && xs) {
|
if (res && xs) {
|
||||||
headers['x-s'] = xs;
|
headers['x-s'] = xs;
|
||||||
headers['x-t'] = xt;
|
headers['x-t'] = xt;
|
||||||
// headers['x-s-common'] = this.getXCommon(a1, b1, xs, xt);
|
headers['x-s-common'] = xsCommon;
|
||||||
|
headers['x-b3-traceid'] = xB3Traceid;
|
||||||
config.headers = headers;
|
config.headers = headers;
|
||||||
} else {
|
} else {
|
||||||
console.log('get sign error', res);
|
console.log('get sign error', res);
|
||||||
@@ -212,6 +225,7 @@ export class XhsClient extends XhsClientBase {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await this.post(uri, data, { sign: this.sign.bind(this) });
|
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['items'][0]['node_card'];
|
||||||
return response;
|
return response;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -252,6 +266,21 @@ export class XhsClient extends XhsClientBase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
getNote = getNote;
|
getNote = getNote;
|
||||||
|
async getNoteByKeyword(keyword: string, page?: number, pageSize?: number, sort?: string, noteType?: number): Promise<any> {
|
||||||
|
const uri = '/api/sns/web/v1/search/notes';
|
||||||
|
const data = {
|
||||||
|
keyword,
|
||||||
|
page: page || 1,
|
||||||
|
page_size: pageSize || 20,
|
||||||
|
sort: sort || SearchSortType.GENERAL,
|
||||||
|
note_type: noteType || SearchNoteType.ALL,
|
||||||
|
search_id: getSearchId(),
|
||||||
|
image_formats: ['jpg', 'webp', 'avif'],
|
||||||
|
ext_flags: []
|
||||||
|
};
|
||||||
|
const response = await this.post(uri, data, { sign: this.sign.bind(this) });
|
||||||
|
return response;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type UnreadCount = {
|
type UnreadCount = {
|
||||||
@@ -260,3 +289,29 @@ type UnreadCount = {
|
|||||||
connections: number;
|
connections: number;
|
||||||
mentions: number;
|
mentions: number;
|
||||||
};
|
};
|
||||||
|
function getSearchId() {
|
||||||
|
const e = BigInt(Date.now()) << 64n;
|
||||||
|
const t = Math.floor(Math.random() * 2147483647);
|
||||||
|
return base36encode(e + BigInt(t));
|
||||||
|
}
|
||||||
|
function base36encode(num: bigint): string {
|
||||||
|
return num.toString(36).toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
const SearchSortType = Object.freeze({
|
||||||
|
// default
|
||||||
|
GENERAL: { value: "general" },
|
||||||
|
// most popular
|
||||||
|
MOST_POPULAR: { value: "popularity_descending" },
|
||||||
|
// Latest
|
||||||
|
LATEST: { value: "time_descending" }
|
||||||
|
});
|
||||||
|
|
||||||
|
const SearchNoteType = Object.freeze({
|
||||||
|
// default
|
||||||
|
ALL: { value: 0 },
|
||||||
|
// only video
|
||||||
|
VIDEO: { value: 1 },
|
||||||
|
// only image
|
||||||
|
IMAGE: { value: 2 }
|
||||||
|
});
|
||||||
@@ -1 +1,2 @@
|
|||||||
import './mentions/index.ts'
|
import './mentions/index.ts'
|
||||||
|
import './notes/index.ts'
|
||||||
@@ -4,6 +4,14 @@ import { Mention } from '@kevisual/xhs/libs/xhs-type/mention.ts';
|
|||||||
const sleep = (ms: number) => {
|
const sleep = (ms: number) => {
|
||||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
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
|
app
|
||||||
.route({
|
.route({
|
||||||
path: 'mention',
|
path: 'mention',
|
||||||
@@ -18,6 +26,7 @@ app
|
|||||||
} else {
|
} else {
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
unread_count: 0,
|
unread_count: 0,
|
||||||
|
likes: 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -40,16 +49,16 @@ app
|
|||||||
path: 'mention',
|
path: 'mention',
|
||||||
key: 'getNote',
|
key: 'getNote',
|
||||||
description: '获取笔记',
|
description: '获取笔记',
|
||||||
validator: {
|
// validator: {
|
||||||
node_id: {
|
// node_id: {
|
||||||
type: 'string',
|
// type: 'string',
|
||||||
required: true,
|
// required: true,
|
||||||
},
|
// },
|
||||||
xsec_token: {
|
// xsec_token: {
|
||||||
type: 'string',
|
// type: 'string',
|
||||||
required: true,
|
// required: true,
|
||||||
},
|
// },
|
||||||
},
|
// },
|
||||||
isDebug: true,
|
isDebug: true,
|
||||||
})
|
})
|
||||||
.define(async (ctx) => {
|
.define(async (ctx) => {
|
||||||
@@ -75,18 +84,10 @@ app
|
|||||||
.define(async (ctx) => {
|
.define(async (ctx) => {
|
||||||
const { note_id, comment_id, content } = ctx.query;
|
const { note_id, comment_id, content } = ctx.query;
|
||||||
const client = xhsServices.getClient();
|
const client = xhsServices.getClient();
|
||||||
// content 300个字内,超过cai fen
|
// content 小红书最大 300个字内,超过则分割, 分比300小,比如270
|
||||||
const textArr: string[] = [];
|
const textArr: string[] = splitContent(content);
|
||||||
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);
|
|
||||||
}
|
|
||||||
const resArr: any[] = [];
|
const resArr: any[] = [];
|
||||||
for (const text of textArr) {
|
for (let text of textArr) {
|
||||||
const res = await client.postComment({
|
const res = await client.postComment({
|
||||||
note_id: note_id,
|
note_id: note_id,
|
||||||
comment_id: comment_id,
|
comment_id: comment_id,
|
||||||
@@ -98,6 +99,7 @@ app
|
|||||||
console.log('添加评论失败', res.code);
|
console.log('添加评论失败', res.code);
|
||||||
ctx.throw(res.code, '添加评论失败');
|
ctx.throw(res.code, '添加评论失败');
|
||||||
}
|
}
|
||||||
|
await sleep(2000);
|
||||||
}
|
}
|
||||||
ctx.body = resArr;
|
ctx.body = resArr;
|
||||||
})
|
})
|
||||||
@@ -106,13 +108,13 @@ app
|
|||||||
.route({
|
.route({
|
||||||
path: 'mention',
|
path: 'mention',
|
||||||
key: 'getMention',
|
key: 'getMention',
|
||||||
description: '获取提及列表',
|
// description: '获取提及列表',
|
||||||
validator: {
|
// validator: {
|
||||||
num: {
|
// num: {
|
||||||
type: 'number',
|
// type: 'number',
|
||||||
required: true,
|
// required: true,
|
||||||
},
|
// },
|
||||||
},
|
// },
|
||||||
})
|
})
|
||||||
.define(async (ctx) => {
|
.define(async (ctx) => {
|
||||||
const num = ctx.query.num;
|
const num = ctx.query.num;
|
||||||
@@ -123,22 +125,26 @@ app
|
|||||||
const handleMention: any[] = [];
|
const handleMention: any[] = [];
|
||||||
for (const mention of mentionList) {
|
for (const mention of mentionList) {
|
||||||
const mention_id = mention.id;
|
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;
|
const xsec_token = mention.item_info.xsec_token;
|
||||||
let comment: any = Parse.getComment(mention);
|
let comment: any = Parse.getComment(mention);
|
||||||
// console.log('note_id', note_id, 'xsec_token', xsec_token, comment);
|
// console.log('note_id', note_id, 'xsec_token', xsec_token, comment);
|
||||||
handleMention.push({
|
handleMention.push({
|
||||||
mention_id,
|
mention_id,
|
||||||
note_id,
|
note_id,
|
||||||
|
note_userid,
|
||||||
|
note_username,
|
||||||
xsec_token,
|
xsec_token,
|
||||||
comment,
|
comment,
|
||||||
mention,
|
mention,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
console.log('获取提及列表成功', res.code, res.data?.message_list?.length);
|
console.log('获取提及列表成功', '[小红书code]', res.code, '提及数量', res.data?.message_list?.length);
|
||||||
ctx.body = handleMention;
|
ctx.body = handleMention;
|
||||||
} else {
|
} else {
|
||||||
console.log('获取提及列表失败', res.code);
|
console.log('获取提及列表失败', '[小红书code]', res.code);
|
||||||
ctx.throw(res.code, '获取提及列表失败');
|
ctx.throw(res.code, '获取提及列表失败');
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -14,13 +14,16 @@ app
|
|||||||
key: 'getUnread',
|
key: 'getUnread',
|
||||||
});
|
});
|
||||||
console.log('unredRes', unredRes.body, unredRes.code);
|
console.log('unredRes', unredRes.body, unredRes.code);
|
||||||
|
|
||||||
if (unredRes.code === 200) {
|
if (unredRes.code === 200) {
|
||||||
const unread_count = unredRes.body.unread_count;
|
const unread_count = unredRes.body.unread_count;
|
||||||
|
const likes = unredRes.body.likes;
|
||||||
|
const unread = unread_count - likes;
|
||||||
const mentionRes = await app.call({
|
const mentionRes = await app.call({
|
||||||
path: 'mention',
|
path: 'mention',
|
||||||
key: 'getMention',
|
key: 'getMention',
|
||||||
payload: {
|
payload: {
|
||||||
num: unread_count,
|
num: unread,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (mentionRes.code === 200) {
|
if (mentionRes.code === 200) {
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ app
|
|||||||
})
|
})
|
||||||
.define(async (ctx) => {
|
.define(async (ctx) => {
|
||||||
const client = xhsServices.getClient();
|
const client = xhsServices.getClient();
|
||||||
const res = await client.c
|
// const res = await client.getNote({});
|
||||||
if (res.code === 0) {
|
// if (res.code === 0) {
|
||||||
}
|
// }
|
||||||
})
|
})
|
||||||
.addTo(app);
|
.addTo(app);
|
||||||
|
|||||||
18
packages/xhs/src/routes/notes/get-note.ts
Normal file
18
packages/xhs/src/routes/notes/get-note.ts
Normal 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);
|
||||||
@@ -1 +1,2 @@
|
|||||||
import './create-note.ts'
|
import './create-note.ts'
|
||||||
|
import './get-note.ts'
|
||||||
@@ -3,10 +3,12 @@ import { Sequelize } from 'sequelize';
|
|||||||
// import { createSequelize } from '@kevisual/xhs/services/xhs-db/db.ts';
|
// import { createSequelize } from '@kevisual/xhs/services/xhs-db/db.ts';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
|
export { XhsClient };
|
||||||
type XhsClientOptions = {
|
type XhsClientOptions = {
|
||||||
key: string;
|
key: string;
|
||||||
cookie: string;
|
cookie: string;
|
||||||
|
userid?: string;
|
||||||
|
username?: string;
|
||||||
signConfig?: {
|
signConfig?: {
|
||||||
signUrl: string;
|
signUrl: string;
|
||||||
};
|
};
|
||||||
@@ -69,7 +71,7 @@ export class XhsServices {
|
|||||||
|
|
||||||
createRoot(options: Partial<XhsClientOptions>) {
|
createRoot(options: Partial<XhsClientOptions>) {
|
||||||
options.key = options.key || this.root;
|
options.key = options.key || this.root;
|
||||||
return this.createClient(options as XhsClientOptions);
|
return this.createClient(options as XhsClientOptions) as XhsClient;
|
||||||
}
|
}
|
||||||
getKey(key?: string) {
|
getKey(key?: string) {
|
||||||
if (!key) key = this.root;
|
if (!key) key = this.root;
|
||||||
@@ -88,4 +90,60 @@ export class XhsServices {
|
|||||||
const xhsClient = this.map.get(this.getKey(key));
|
const xhsClient = this.map.get(this.getKey(key));
|
||||||
return xhsClient;
|
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 || {};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ import './query/mention.ts'
|
|||||||
import './query/unread.ts';
|
import './query/unread.ts';
|
||||||
import './query/get-userinfo.ts';
|
import './query/get-userinfo.ts';
|
||||||
import './query/get-connections.ts'
|
import './query/get-connections.ts'
|
||||||
|
import './query/query-keys.ts';
|
||||||
|
|
||||||
|
|
||||||
program
|
program
|
||||||
.command('test')
|
.command('test')
|
||||||
.description('test command')
|
.description('test command')
|
||||||
|
|||||||
@@ -1,4 +1,11 @@
|
|||||||
import { xhsServices, app } from '../index.ts';
|
import { xhsServices, app, xhsRootClient } from '../index.ts';
|
||||||
|
import { useConfig } from '@kevisual/use-config/env';
|
||||||
|
export const config = useConfig();
|
||||||
import { program } from 'commander';
|
import { program } from 'commander';
|
||||||
|
|
||||||
|
xhsRootClient.setCookie(config.XHS_ROOT_COOKIE || '');
|
||||||
|
xhsServices.setSignConfig({
|
||||||
|
signUrl: config.XHS_API_SIGN_URL,
|
||||||
|
});
|
||||||
|
|
||||||
export { program, xhsServices, app };
|
export { program, xhsServices, app };
|
||||||
|
|||||||
@@ -8,17 +8,27 @@ import util from 'node:util';
|
|||||||
// });
|
// });
|
||||||
const getNoteById = async () => {
|
const getNoteById = async () => {
|
||||||
const client = xhsServices.getClient();
|
const client = xhsServices.getClient();
|
||||||
client.getNoteById('68136dab0000000007034c46', 'LByEmonX8WfJ9ebpAowVbOZX9Xh8T0Qkjil5KRFqDD6LM').then((res) => {
|
// client.getNoteById('68136dab0000000007034c46', 'LByEmonX8WfJ9ebpAowVbOZX9Xh8T0Qkjil5KRFqDD6LM').then((res) => {
|
||||||
console.log(res);
|
// console.log(res);
|
||||||
});
|
// });
|
||||||
|
|
||||||
|
const res = await client.getNoteById('68136dab0000000007034c46', 'LB6fmNfsd0keAQNjh3zOejDC2TVQLGY3zlTZjeRazBZdI=');
|
||||||
|
// console.log(res);
|
||||||
};
|
};
|
||||||
const getNote = async () => {
|
const getNote = async () => {
|
||||||
|
// const id = '68136dab0000000007034c46';
|
||||||
|
// const x = 'LByEmonX8WfJ9ebpAowVbOZX9Xh8T0Qkjil5KRFqDD6LM=';
|
||||||
const id = '68136dab0000000007034c46';
|
const id = '68136dab0000000007034c46';
|
||||||
const x = 'LByEmonX8WfJ9ebpAowVbOZX9Xh8T0Qkjil5KRFqDD6LM=';
|
const x = 'LB6fmNfsd0keAQNjh3zOejDC2TVQLGY3zlTZjeRazBZdI=';
|
||||||
const client = xhsServices.getClient();
|
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 }));
|
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
|
program
|
||||||
.command('get-note')
|
.command('get-note')
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { xhsServices, program, app } from '../common.ts';
|
import { xhsServices, program, app } from '../common.ts';
|
||||||
import util from 'node:util';
|
import util from 'node:util';
|
||||||
import { omit } from 'lodash-es';
|
|
||||||
|
|
||||||
const getMentions = async () => {
|
const getMentions = async () => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
29
packages/xhs/src/test/query/query-keys.ts
Normal file
29
packages/xhs/src/test/query/query-keys.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
// getNoteByKeyword
|
||||||
|
|
||||||
|
import { xhsServices, program, config } from '../common.ts';
|
||||||
|
import util from 'node:util';
|
||||||
|
|
||||||
|
const getNoteByKeyword = async (keyword: string) => {
|
||||||
|
const client = xhsServices.getClient();
|
||||||
|
// xhsServices.setSignConfig({
|
||||||
|
// signUrl: config.XHS_API_SIGN_URL,
|
||||||
|
// });
|
||||||
|
const res = await client.getNoteByKeyword(keyword).then((res) => {
|
||||||
|
console.log(util.inspect(res, { depth: null }));
|
||||||
|
return res;
|
||||||
|
});
|
||||||
|
console.log('type res', typeof res);
|
||||||
|
if (res.code === 0) {
|
||||||
|
console.log('total', res.data.total);
|
||||||
|
if (res.data.notes.length > 0) {
|
||||||
|
console.log('first note desc', res.data.notes[0].desc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
program
|
||||||
|
.command('get-note-by-keyword <keyword>')
|
||||||
|
.description('get note by keyword')
|
||||||
|
.action(async (keyword: string) => {
|
||||||
|
getNoteByKeyword(keyword);
|
||||||
|
});
|
||||||
8
packages/xhs/src/test/split-text.ts
Normal file
8
packages/xhs/src/test/split-text.ts
Normal 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));
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"src/**/*.ts",
|
"src/**/*.ts", "src/libs/client-base.ts",
|
||||||
],
|
],
|
||||||
"exclude": [],
|
"exclude": [],
|
||||||
}
|
}
|
||||||
880
pnpm-lock.yaml
generated
880
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -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';
|
import { config } from '../modules/config.ts';
|
||||||
|
|
||||||
export const ai = new SiliconFlowProvider({
|
const createBaiLian = () => {
|
||||||
model: 'Qwen/Qwen3-32B',
|
return new BailianProvider({
|
||||||
// model: 'Pro/deepseek-ai/DeepSeek-R1',// 只有充值能用
|
baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
|
||||||
apiKey: config.SILICONFLOW_API_KEY,
|
model: 'qwen-plus',
|
||||||
});
|
apiKey: config.BAILIAN_API_KEY,
|
||||||
|
|
||||||
ai.getUsageInfo()
|
|
||||||
.then((usage) => {
|
|
||||||
console.log('AI usage info:', usage);
|
|
||||||
})
|
|
||||||
.catch((res) => {
|
|
||||||
console.error('Error fetching AI usage info:', res.status);
|
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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',
|
||||||
|
plus: 'qwen-plus',
|
||||||
|
a22b235: 'qwen3-235b-a22b',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|||||||
80
src/agent/analyze/category.ts
Normal file
80
src/agent/analyze/category.ts
Normal 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
116
src/agent/analyze/cmd.ts
Normal 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);
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { agent } from '@/agent/agent.ts';
|
import { agent } from '@/agent/agent.ts';
|
||||||
import { ai } from '../ai.ts';
|
import { ai } from '../ai.ts';
|
||||||
import { logger } from '@/agent/logger.ts';
|
import { logger } from '@/agent/logger.ts';
|
||||||
const getJsonFromString = (str: string) => {
|
export const getJsonFromString = (str: string) => {
|
||||||
// 尝试从字符串中提取JSON对象
|
// 尝试从字符串中提取JSON对象
|
||||||
try {
|
try {
|
||||||
const jsonMatch = str.match(/```json\s*([\s\S]*?)\s*```/);
|
const jsonMatch = str.match(/```json\s*([\s\S]*?)\s*```/);
|
||||||
@@ -49,7 +49,7 @@ agent
|
|||||||
}
|
}
|
||||||
\`\`\`
|
\`\`\`
|
||||||
分析的文本的内容是:
|
分析的文本的内容是:
|
||||||
<context>
|
<context>
|
||||||
${text}
|
${text}
|
||||||
</context>
|
</context>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -22,9 +22,9 @@ agent
|
|||||||
{
|
{
|
||||||
role: 'user',
|
role: 'user',
|
||||||
content: `
|
content: `
|
||||||
你是一个提示词优化的专家,请根据用户提供的提示词进行修正和优化,其中用户的提示词返回的要求如果没有或者不明确,请你都修正为要求返回的文本在500字以内,且内容是纯文本格式,不能是markdown模式,也不包含任何HTML标签或其他格式化内容。
|
你是一个提示词优化的专家,请根据用户提供的提示词进行修正和优化,其中用户的提示词返回的要求如果没有或者不明确,请你修正为要求返回的文本在500字以内,如果有,保持原本的要求数字的文本。与此同时,要求内容是纯文本格式,不能是markdown模式,也不包含任何HTML标签或其他格式化内容。
|
||||||
|
|
||||||
只对提示词进行优化,并且不需要对内容进行分析或总结。
|
只对提示词进行优化,并且不需要对内容进行分析或总结。并返回修改后的总的提示词内容。
|
||||||
|
|
||||||
示例1. 用户提示词
|
示例1. 用户提示词
|
||||||
<content>总结笔记</content>
|
<content>总结笔记</content>
|
||||||
@@ -34,6 +34,10 @@ agent
|
|||||||
<content>分析一下这个图片</content>
|
<content>分析一下这个图片</content>
|
||||||
优化后的提示词
|
优化后的提示词
|
||||||
<content>请分析一下这个图片,要求返回的内容是纯文本格式,字数不超过500字。</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('end', Date.now() - now, 'ms');
|
||||||
|
console.log('AI response:', res);
|
||||||
const ans = res.choices[0]?.message?.content || '';
|
const ans = res.choices[0]?.message?.content || '';
|
||||||
if (!ans) {
|
if (!ans) {
|
||||||
logger.error('Empty response from AI:', res);
|
logger.error('Empty response from AI:', res);
|
||||||
}
|
}
|
||||||
ctx.body = getTagContent(ans)
|
ctx.body = getTagContent(ans);
|
||||||
})
|
})
|
||||||
.addTo(agent);
|
.addTo(agent);
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
import { agent } from './agent.ts';
|
import { agent } from './agent.ts';
|
||||||
import './analyze/content.ts';
|
import './analyze/content.ts';
|
||||||
|
import './analyze/category.ts';
|
||||||
|
import './analyze/cmd.ts';
|
||||||
|
|
||||||
import './fix/prompt.ts';
|
import './fix/prompt.ts';
|
||||||
|
|
||||||
import './xhs.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 };
|
export { agent };
|
||||||
|
|||||||
21
src/agent/test/category.ts
Normal file
21
src/agent/test/category.ts
Normal 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
18
src/agent/test/cmd.ts
Normal 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();
|
||||||
@@ -6,11 +6,12 @@ const main = async () => {
|
|||||||
const text2 = '告诉我1+1的值';
|
const text2 = '告诉我1+1的值';
|
||||||
const text3 = 'html和css的大纲是什么?';
|
const text3 = 'html和css的大纲是什么?';
|
||||||
const text4 = '1+1=';
|
const text4 = '1+1=';
|
||||||
|
const text5 = '请分析一下这个图片,300字内';
|
||||||
const res = await agent.call({
|
const res = await agent.call({
|
||||||
path: 'fix',
|
path: 'fix',
|
||||||
key: 'xhs',
|
key: 'xhs',
|
||||||
payload: {
|
payload: {
|
||||||
text: text4,
|
text: text5,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
console.log('fix xhs res', res.code, 'content', res.body);
|
console.log('fix xhs res', res.code, 'content', res.body);
|
||||||
|
|||||||
16
src/agent/test/tools/kuaren.ts
Normal file
16
src/agent/test/tools/kuaren.ts
Normal 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();
|
||||||
15
src/agent/tools/call-xiaoxiao.ts
Normal file
15
src/agent/tools/call-xiaoxiao.ts
Normal 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);
|
||||||
53
src/agent/tools/comfort-user.ts
Normal file
53
src/agent/tools/comfort-user.ts
Normal 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
177
src/agent/tools/kuaren.ts
Normal 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);
|
||||||
53
src/agent/tools/summarize-note.ts
Normal file
53
src/agent/tools/summarize-note.ts
Normal 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);
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
import { agent } from './agent.ts';
|
import { agent } from './agent.ts';
|
||||||
import { ai } from './ai.ts';
|
import { ai } from './ai.ts';
|
||||||
|
import { logger } from './logger.ts';
|
||||||
/**
|
/**
|
||||||
* 清除文本中的@信息
|
* 清除文本中的@信息
|
||||||
* @param text
|
* @param text
|
||||||
@@ -9,14 +10,65 @@ const clearAtInfo = (text: string = '') => {
|
|||||||
const newText = text.replace(/@[\u4e00-\u9fa5\w]+/g, '').replace(/#.*?#/g, '');
|
const newText = text.replace(/@[\u4e00-\u9fa5\w]+/g, '').replace(/#.*?#/g, '');
|
||||||
return newText.trim();
|
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
|
agent
|
||||||
.route({
|
.route({
|
||||||
path: 'xhs',
|
path: 'xhs',
|
||||||
})
|
})
|
||||||
.define(async (ctx) => {
|
.define(async (ctx) => {
|
||||||
const { text = '' } = ctx.query || {};
|
const { text = '', note = '', hasNote } = ctx.query || {};
|
||||||
const id = nanoid();
|
const id = nanoid();
|
||||||
const no_at_text = clearAtInfo(text);
|
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({
|
const resFix = await agent.call({
|
||||||
path: 'fix',
|
path: 'fix',
|
||||||
key: 'xhs',
|
key: 'xhs',
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
import { useConfig } from '@kevisual/use-config/env';
|
import { useConfig } from '@kevisual/use-config/env';
|
||||||
|
|
||||||
export const config = useConfig();
|
export const config = useConfig();
|
||||||
|
|
||||||
|
export const isDev = config.ENV === 'development';
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { agent } from '@/agent/index.ts';
|
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 { random, omit } from 'lodash-es';
|
||||||
import util from 'node:util';
|
import util from 'node:util';
|
||||||
|
|
||||||
@@ -21,18 +21,20 @@ taskApp
|
|||||||
if (res.code === 200) {
|
if (res.code === 200) {
|
||||||
const data = res.body;
|
const data = res.body;
|
||||||
const unread_count = data.unread_count;
|
const unread_count = data.unread_count;
|
||||||
if (unread_count > 0) {
|
const likes = data.likes;
|
||||||
|
const unread = unread_count - likes;
|
||||||
|
if (unread > 0) {
|
||||||
queue.add(
|
queue.add(
|
||||||
'mention',
|
'mention',
|
||||||
{
|
{
|
||||||
path: 'task',
|
path: 'task',
|
||||||
key: 'getMention',
|
key: 'getMention',
|
||||||
payload: {
|
payload: {
|
||||||
unread_count,
|
unread_count: unread,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
attempts: 3,
|
attempts: 1,
|
||||||
delay: 0,
|
delay: 0,
|
||||||
removeOnComplete: true,
|
removeOnComplete: true,
|
||||||
removeOnFail: {
|
removeOnFail: {
|
||||||
@@ -42,7 +44,7 @@ taskApp
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
job: unread_count,
|
job: unread,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -109,18 +111,52 @@ taskApp
|
|||||||
.define(async (ctx) => {
|
.define(async (ctx) => {
|
||||||
const data = ctx.query.data; // 为提及的相关信息
|
const data = ctx.query.data; // 为提及的相关信息
|
||||||
const note_id = data.note_id;
|
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;
|
const xsec_token = data.xsec_token;
|
||||||
|
console.log(data);
|
||||||
const comment_id = data.comment.comment_id;
|
const comment_id = data.comment.comment_id;
|
||||||
const content = data.comment?.content || 'test';
|
let content: string = data.comment?.content || 'test';
|
||||||
const postData = {
|
const postData = {
|
||||||
note_id,
|
note_id,
|
||||||
content,
|
content,
|
||||||
comment_id,
|
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({
|
const resAgent = await agent.call({
|
||||||
path: 'xhs',
|
path: 'xhs',
|
||||||
payload: {
|
payload: {
|
||||||
text: content,
|
text: content,
|
||||||
|
note,
|
||||||
|
hasNote,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
let responseText = '';
|
let responseText = '';
|
||||||
|
|||||||
@@ -3,13 +3,33 @@
|
|||||||
import { QueryRouterServer } from '@kevisual/router';
|
import { QueryRouterServer } from '@kevisual/router';
|
||||||
import { redis } from '@/modules/redis.ts';
|
import { redis } from '@/modules/redis.ts';
|
||||||
import { Queue } from 'bullmq';
|
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';
|
import { nanoid } from 'nanoid';
|
||||||
export const XHS_GET_UNREAD = 'unread_count';
|
export const XHS_GET_UNREAD = 'unread_count';
|
||||||
export const XHS_QUEUE_NAME = 'XHS_QUEUE';
|
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 const taskApp = new QueryRouterServer();
|
||||||
export { xhsApp };
|
export { xhsApp };
|
||||||
|
export const xhsServices = server;
|
||||||
|
|
||||||
export const queue = new Queue(XHS_QUEUE_NAME, {
|
export const queue = new Queue(XHS_QUEUE_NAME, {
|
||||||
connection: redis,
|
connection: redis,
|
||||||
});
|
});
|
||||||
@@ -25,6 +45,7 @@ export const addUnreadTask = async (nextTime = 0) => {
|
|||||||
{
|
{
|
||||||
delay: nextTime,
|
delay: nextTime,
|
||||||
removeOnComplete: true,
|
removeOnComplete: true,
|
||||||
|
attempts: 1,
|
||||||
removeOnFail: {
|
removeOnFail: {
|
||||||
age: 24 * 3600, // keep up to 24 hours
|
age: 24 * 3600, // keep up to 24 hours
|
||||||
},
|
},
|
||||||
|
|||||||
59
src/task/utils/time.ts
Normal file
59
src/task/utils/time.ts
Normal 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;
|
||||||
|
};
|
||||||
@@ -5,6 +5,9 @@ import { nanoid } from 'nanoid';
|
|||||||
import { queue, XHS_QUEUE_NAME, taskApp } from './index.ts';
|
import { queue, XHS_QUEUE_NAME, taskApp } from './index.ts';
|
||||||
import { addUnreadTask } from './task.ts';
|
import { addUnreadTask } from './task.ts';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
import { getTimeDuration } from './utils/time.ts';
|
||||||
|
import { config, isDev } from '../modules/config.ts';
|
||||||
|
|
||||||
export const sleep = (ms: number) => {
|
export const sleep = (ms: number) => {
|
||||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
};
|
};
|
||||||
@@ -13,7 +16,7 @@ class TimeRecorder {
|
|||||||
endTime: number;
|
endTime: number;
|
||||||
duration: number;
|
duration: number;
|
||||||
updateTime: number;
|
updateTime: number;
|
||||||
maxDuration: number = 60 * 1000; // 20s;
|
maxDuration: number = 30 * 1000; // 30s;
|
||||||
constructor() {
|
constructor() {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
this.startTime = now;
|
this.startTime = now;
|
||||||
@@ -34,13 +37,20 @@ class TimeRecorder {
|
|||||||
this.updateTime = Date.now();
|
this.updateTime = Date.now();
|
||||||
return this.updateTime;
|
return this.updateTime;
|
||||||
}
|
}
|
||||||
getClampDuration() {
|
getClampDuration(random = false) {
|
||||||
const duration = Date.now() - this.updateTime;
|
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 {
|
return {
|
||||||
duration: duration,
|
duration: duration,
|
||||||
maxDuration: this.maxDuration,
|
maxDuration: this.maxDuration,
|
||||||
updateTime: this.updateTime,
|
updateTime: this.updateTime,
|
||||||
nextTime: clamp(this.maxDuration - duration, 0, this.maxDuration),
|
nextTime: nextTime,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
time() {
|
time() {
|
||||||
@@ -52,6 +62,7 @@ class TimeRecorder {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const timeRecorder = new TimeRecorder();
|
const timeRecorder = new TimeRecorder();
|
||||||
let errorCount = 0;
|
let errorCount = 0;
|
||||||
export const worker = new Worker(
|
export const worker = new Worker(
|
||||||
@@ -59,8 +70,9 @@ export const worker = new Worker(
|
|||||||
async (job) => {
|
async (job) => {
|
||||||
const timer = new TimeRecorder();
|
const timer = new TimeRecorder();
|
||||||
const data = job.data;
|
const data = job.data;
|
||||||
|
|
||||||
if (data.path === 'task' && data.key === 'getUnread') {
|
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();
|
timeRecorder.update();
|
||||||
}
|
}
|
||||||
const res = await taskApp.call(data);
|
const res = await taskApp.call(data);
|
||||||
@@ -71,6 +83,9 @@ export const worker = new Worker(
|
|||||||
if (errorCount > 3) {
|
if (errorCount > 3) {
|
||||||
queue.pause();
|
queue.pause();
|
||||||
console.log('error count', errorCount);
|
console.log('error count', errorCount);
|
||||||
|
if (data.path === 'task' && data.key === 'getUnread') {
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
throw new Error('job error' + job.name + ' ' + job.id);
|
throw new Error('job error' + job.name + ' ' + job.id);
|
||||||
}
|
}
|
||||||
@@ -83,15 +98,17 @@ export const worker = new Worker(
|
|||||||
worker.on('completed', async (job) => {
|
worker.on('completed', async (job) => {
|
||||||
const jobCounts = await queue.getJobCounts('waiting', 'wait', 'delayed');
|
const jobCounts = await queue.getJobCounts('waiting', 'wait', 'delayed');
|
||||||
if (job.name !== 'unread') {
|
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) {
|
if (jobCounts.delayed + jobCounts.wait > 0) {
|
||||||
// console.log('======has jobs, no need to add new job');
|
// console.log('======has jobs, no need to add new job');
|
||||||
} else {
|
} else {
|
||||||
const up = timeRecorder.getClampDuration();
|
const up = timeRecorder.getClampDuration(true);
|
||||||
const nextTime = up.nextTime;
|
const timeDuration = isDev ? 0 : getTimeDuration();
|
||||||
|
const nextTime = up.nextTime + timeDuration;
|
||||||
const unread = await queue.getJob('unread');
|
const unread = await queue.getJob('unread');
|
||||||
if (!unread) {
|
if (!unread) {
|
||||||
|
console.log('====add unread next-time', nextTime, timeDuration);
|
||||||
addUnreadTask(nextTime);
|
addUnreadTask(nextTime);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
3
src/test/common.ts
Normal file
3
src/test/common.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import { config } from '../modules/config.ts';
|
||||||
|
|
||||||
|
export { config };
|
||||||
3
src/test/get-time-duration.ts
Normal file
3
src/test/get-time-duration.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import { getTimeDuration } from '@/task/utils/time.ts';
|
||||||
|
|
||||||
|
console.log('getTimeDuration', getTimeDuration());
|
||||||
57
src/test/tts.ts
Normal file
57
src/test/tts.ts
Normal 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();
|
||||||
@@ -1,15 +1,24 @@
|
|||||||
{
|
{
|
||||||
"extends": "@kevisual/types/json/backend.json",
|
"extends": "@kevisual/types/json/backend.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"baseUrl": "./",
|
"module": "NodeNext",
|
||||||
|
"target": "esnext",
|
||||||
|
"baseUrl": ".",
|
||||||
|
"typeRoots": [
|
||||||
|
"./node_modules/@types",
|
||||||
|
"./node_modules/@kevisual/types/index.d.ts"
|
||||||
|
],
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": [
|
"@/*": [
|
||||||
"src/*"
|
"src/*"
|
||||||
|
],
|
||||||
|
"@agent/*": [
|
||||||
|
"agent/*"
|
||||||
]
|
]
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"src/**/*.ts",
|
"src/**/*",
|
||||||
|
"agent/**/*",
|
||||||
],
|
],
|
||||||
"exclude": [],
|
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user