generated from tailored/router-template
temp
This commit is contained in:
parent
26ca4c21c8
commit
e58adbc46b
3
.npmrc
3
.npmrc
@ -1,2 +1,3 @@
|
|||||||
//npm.xiongxiao.me/:_authToken=${ME_NPM_TOKEN}
|
//npm.xiongxiao.me/:_authToken=${ME_NPM_TOKEN}
|
||||||
//registry.npmjs.org/:_authToken=${NPM_TOKEN}
|
//registry.npmjs.org/:_authToken=${NPM_TOKEN}
|
||||||
|
ignore-workspace-root-check=true
|
@ -14,6 +14,7 @@
|
|||||||
},
|
},
|
||||||
"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",
|
||||||
"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",
|
||||||
@ -39,9 +40,12 @@
|
|||||||
"cookie": "^1.0.2",
|
"cookie": "^1.0.2",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"formidable": "^3.5.4",
|
"formidable": "^3.5.4",
|
||||||
"lodash-es": "^4.17.21"
|
"lodash-es": "^4.17.21",
|
||||||
|
"nanoid": "^5.1.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@kevisual/ai-center": "^0.0.3",
|
||||||
|
"@kevisual/app-assistant": "workspace:*",
|
||||||
"@kevisual/types": "^0.0.9",
|
"@kevisual/types": "^0.0.9",
|
||||||
"@kevisual/use-config": "^1.0.12",
|
"@kevisual/use-config": "^1.0.12",
|
||||||
"@types/bun": "^1.2.11",
|
"@types/bun": "^1.2.11",
|
||||||
@ -49,6 +53,7 @@
|
|||||||
"@types/formidable": "^3.4.5",
|
"@types/formidable": "^3.4.5",
|
||||||
"@types/lodash-es": "^4.17.12",
|
"@types/lodash-es": "^4.17.12",
|
||||||
"@types/node": "^22.15.3",
|
"@types/node": "^22.15.3",
|
||||||
|
"bullmq": "^5.51.1",
|
||||||
"commander": "^13.1.0",
|
"commander": "^13.1.0",
|
||||||
"concurrently": "^9.1.2",
|
"concurrently": "^9.1.2",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
|
17
packages/app-assistant/.gitignore
vendored
Normal file
17
packages/app-assistant/.gitignore
vendored
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
node_modules
|
||||||
|
|
||||||
|
dist
|
||||||
|
|
||||||
|
app.config.json5
|
||||||
|
|
||||||
|
apps.config.json
|
||||||
|
|
||||||
|
deploy.tar.gz
|
||||||
|
cache-file
|
||||||
|
|
||||||
|
/apps
|
||||||
|
|
||||||
|
logs
|
||||||
|
|
||||||
|
.env*
|
||||||
|
!.env.example
|
21
packages/app-assistant/package.json
Normal file
21
packages/app-assistant/package.json
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"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
packages/app-assistant/src/index.ts
Normal file
1
packages/app-assistant/src/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './provider/comments/xhs.ts';
|
7
packages/app-assistant/src/provider/comments/xhs.ts
Normal file
7
packages/app-assistant/src/provider/comments/xhs.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { SocialBase } from '@/social/social-base.ts';
|
||||||
|
|
||||||
|
export class XHS extends SocialBase {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
}
|
39
packages/app-assistant/src/social/social-base.ts
Normal file
39
packages/app-assistant/src/social/social-base.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
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.');
|
||||||
|
}
|
||||||
|
}
|
75
packages/app-assistant/src/test/get-unread.ts
Normal file
75
packages/app-assistant/src/test/get-unread.ts
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
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);
|
||||||
|
// });
|
15
packages/app-assistant/tsconfig.json
Normal file
15
packages/app-assistant/tsconfig.json
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"extends": "@kevisual/types/json/backend.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": "./",
|
||||||
|
"paths": {
|
||||||
|
"@/*": [
|
||||||
|
"src/*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src/**/*.ts",
|
||||||
|
],
|
||||||
|
"exclude": [],
|
||||||
|
}
|
17
packages/xhs-core/.gitignore
vendored
Normal file
17
packages/xhs-core/.gitignore
vendored
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
node_modules
|
||||||
|
|
||||||
|
dist
|
||||||
|
|
||||||
|
app.config.json5
|
||||||
|
|
||||||
|
apps.config.json
|
||||||
|
|
||||||
|
deploy.tar.gz
|
||||||
|
cache-file
|
||||||
|
|
||||||
|
/apps
|
||||||
|
|
||||||
|
logs
|
||||||
|
|
||||||
|
.env*
|
||||||
|
!.env.example
|
2
packages/xhs-core/.npmrc
Normal file
2
packages/xhs-core/.npmrc
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
//npm.xiongxiao.me/:_authToken=${ME_NPM_TOKEN}
|
||||||
|
//registry.npmjs.org/:_authToken=${NPM_TOKEN}
|
24
packages/xhs-core/bun.config.mjs
Normal file
24
packages/xhs-core/bun.config.mjs
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
// @ts-check
|
||||||
|
// https://bun.sh/docs/bundler
|
||||||
|
// @ts-ignore
|
||||||
|
import pkg from './package.json';
|
||||||
|
// import { resolvePath as rp } from '@kevisual/use-config/env';
|
||||||
|
import path from 'path';
|
||||||
|
const rp = (resolvePath) => {
|
||||||
|
return path.resolve(process.cwd(), resolvePath);
|
||||||
|
};
|
||||||
|
// bun run src/index.ts --
|
||||||
|
await Bun.build({
|
||||||
|
target: 'node',
|
||||||
|
format: 'esm',
|
||||||
|
entrypoints: [rp('src/index.js')],
|
||||||
|
outdir: rp('./dist'),
|
||||||
|
naming: {
|
||||||
|
entry: 'app.mjs',
|
||||||
|
},
|
||||||
|
|
||||||
|
define: {
|
||||||
|
VERSION: JSON.stringify(pkg.version),
|
||||||
|
},
|
||||||
|
env: 'KEVISUAL_*',
|
||||||
|
});
|
33
packages/xhs-core/package.json
Normal file
33
packages/xhs-core/package.json
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"name": "@kevisual/xhs-core",
|
||||||
|
"version": "0.0.2",
|
||||||
|
"description": "",
|
||||||
|
"main": "dist/app.mjs",
|
||||||
|
"types": "dist/app.d.ts",
|
||||||
|
"scripts": {
|
||||||
|
"build": "bun run bun.config.mjs",
|
||||||
|
"postbuild": "dts -i src/index.js -o app.d.ts",
|
||||||
|
"dts": "dts -i src/index.js -o app.d.ts"
|
||||||
|
},
|
||||||
|
"file": [
|
||||||
|
"src",
|
||||||
|
"python",
|
||||||
|
"test",
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "abearxiong <xiongxiao@xiongxiao.me> (https://www.xiongxiao.me)",
|
||||||
|
"license": "MIT",
|
||||||
|
"packageManager": "pnpm@10.10.0",
|
||||||
|
"type": "module",
|
||||||
|
"dependencies": {},
|
||||||
|
"devDependencies": {
|
||||||
|
"axios": "^1.9.0",
|
||||||
|
"crypto-js": "^4.2.0",
|
||||||
|
"jsdom": "^26.1.0",
|
||||||
|
"@kevisual/dts": "^0.0.2"
|
||||||
|
}
|
||||||
|
}
|
1016
packages/xhs-core/python/core.py
Normal file
1016
packages/xhs-core/python/core.py
Normal file
File diff suppressed because it is too large
Load Diff
38
packages/xhs-core/python/exception.py
Normal file
38
packages/xhs-core/python/exception.py
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
from enum import Enum
|
||||||
|
from typing import NamedTuple
|
||||||
|
|
||||||
|
from requests import RequestException
|
||||||
|
|
||||||
|
|
||||||
|
class ErrorTuple(NamedTuple):
|
||||||
|
code: int
|
||||||
|
msg: str
|
||||||
|
|
||||||
|
|
||||||
|
class ErrorEnum(Enum):
|
||||||
|
IP_BLOCK = ErrorTuple(300012, "网络连接异常,请检查网络设置或重启试试")
|
||||||
|
NOTE_ABNORMAL = ErrorTuple(-510001, "笔记状态异常,请稍后查看")
|
||||||
|
NOTE_SECRETE_FAULT = ErrorTuple(-510001, "当前内容无法展示")
|
||||||
|
SIGN_FAULT = ErrorTuple(300015, "浏览器异常,请尝试关闭/卸载风险插件或重启试试!")
|
||||||
|
SESSION_EXPIRED = ErrorTuple(-100, "登录已过期")
|
||||||
|
|
||||||
|
|
||||||
|
class DataFetchError(RequestException):
|
||||||
|
"""something error when fetch"""
|
||||||
|
|
||||||
|
|
||||||
|
class IPBlockError(RequestException):
|
||||||
|
"""fetch so fast that the server block us ip"""
|
||||||
|
|
||||||
|
|
||||||
|
class SignError(RequestException):
|
||||||
|
"""fetch error because x-s sign verror"""
|
||||||
|
|
||||||
|
|
||||||
|
class NeedVerifyError(RequestException):
|
||||||
|
"""fetch error because need captcha"""
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.verify_type = kwargs.pop("verify_type", None)
|
||||||
|
self.verify_uuid = kwargs.pop("verify_uuid", None)
|
||||||
|
super().__init__(*args, **kwargs)
|
409
packages/xhs-core/python/help.py
Normal file
409
packages/xhs-core/python/help.py
Normal file
@ -0,0 +1,409 @@
|
|||||||
|
import binascii
|
||||||
|
import ctypes
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import random
|
||||||
|
import re
|
||||||
|
import string
|
||||||
|
import time
|
||||||
|
import urllib.parse
|
||||||
|
from xml.etree import ElementTree
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
|
||||||
|
def sign(uri, data=None, ctime=None, a1="", b1=""):
|
||||||
|
"""
|
||||||
|
takes in a URI (uniform resource identifier), an optional data dictionary, and an optional ctime parameter. It returns a dictionary containing two keys: "x-s" and "x-t".
|
||||||
|
"""
|
||||||
|
|
||||||
|
def h(n):
|
||||||
|
m = ""
|
||||||
|
d = "A4NjFqYu5wPHsO0XTdDgMa2r1ZQocVte9UJBvk6/7=yRnhISGKblCWi+LpfE8xzm3"
|
||||||
|
for i in range(0, 32, 3):
|
||||||
|
o = ord(n[i])
|
||||||
|
g = ord(n[i + 1]) if i + 1 < 32 else 0
|
||||||
|
h = ord(n[i + 2]) if i + 2 < 32 else 0
|
||||||
|
x = ((o & 3) << 4) | (g >> 4)
|
||||||
|
p = ((15 & g) << 2) | (h >> 6)
|
||||||
|
v = o >> 2
|
||||||
|
b = h & 63 if h else 64
|
||||||
|
if not g:
|
||||||
|
p = b = 64
|
||||||
|
m += d[v] + d[x] + d[p] + d[b]
|
||||||
|
return m
|
||||||
|
|
||||||
|
v = int(round(time.time() * 1000) if not ctime else ctime)
|
||||||
|
raw_str = f"{v}test{uri}{json.dumps(data, separators=(',', ':'), ensure_ascii=False) if isinstance(data, dict) else ''}"
|
||||||
|
md5_str = hashlib.md5(raw_str.encode('utf-8')).hexdigest()
|
||||||
|
x_s = h(md5_str)
|
||||||
|
x_t = str(v)
|
||||||
|
|
||||||
|
common = {
|
||||||
|
"s0": 5, # getPlatformCode
|
||||||
|
"s1": "",
|
||||||
|
"x0": "1", # localStorage.getItem("b1b1")
|
||||||
|
"x1": "3.2.0", # version
|
||||||
|
"x2": "Windows",
|
||||||
|
"x3": "xhs-pc-web",
|
||||||
|
"x4": "2.3.1",
|
||||||
|
"x5": a1, # cookie of a1
|
||||||
|
"x6": x_t,
|
||||||
|
"x7": x_s,
|
||||||
|
"x8": b1, # localStorage.getItem("b1")
|
||||||
|
"x9": mrc(x_t + x_s),
|
||||||
|
"x10": 1, # getSigCount
|
||||||
|
}
|
||||||
|
encodeStr = encodeUtf8(json.dumps(common, separators=(',', ':')))
|
||||||
|
x_s_common = b64Encode(encodeStr)
|
||||||
|
return {
|
||||||
|
"x-s": x_s,
|
||||||
|
"x-t": x_t,
|
||||||
|
"x-s-common": x_s_common,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_a1_and_web_id():
|
||||||
|
"""generate a1 and webid cookie str, the first return value is a1, second is webId
|
||||||
|
|
||||||
|
for example: a1, web_id = get_a1_and_web_id()
|
||||||
|
"""
|
||||||
|
|
||||||
|
def random_str(length):
|
||||||
|
alphabet = string.ascii_letters + string.digits
|
||||||
|
return ''.join(random.choice(alphabet) for _ in range(length))
|
||||||
|
|
||||||
|
d = hex(int(time.time() * 1000))[2:] + random_str(30) + "5" + "0" + "000"
|
||||||
|
g = (d + str(binascii.crc32(str(d).encode('utf-8'))))[:52]
|
||||||
|
return g, hashlib.md5(g.encode('utf-8')).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
img_cdns = [
|
||||||
|
"https://sns-img-qc.xhscdn.com",
|
||||||
|
"https://sns-img-hw.xhscdn.com",
|
||||||
|
"https://sns-img-bd.xhscdn.com",
|
||||||
|
"https://sns-img-qn.xhscdn.com",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def get_img_url_by_trace_id(trace_id: str, format: str = "png"):
|
||||||
|
return f"{random.choice(img_cdns)}/{trace_id}?imageView2/format/{format}"
|
||||||
|
|
||||||
|
|
||||||
|
def get_img_urls_by_trace_id(trace_id: str, format: str = "png"):
|
||||||
|
return [f"{cdn}/{trace_id}?imageView2/format/{format}" for cdn in img_cdns]
|
||||||
|
|
||||||
|
|
||||||
|
def get_trace_id(img_url: str):
|
||||||
|
trace_id = img_url.split("/")[-1].split("!")[0]
|
||||||
|
if "spectrum" in img_url:
|
||||||
|
return "spectrum/" + trace_id
|
||||||
|
return trace_id
|
||||||
|
|
||||||
|
|
||||||
|
def get_imgs_url_from_note(note) -> list:
|
||||||
|
"""the return type is [img1_url, img2_url, ...]"""
|
||||||
|
imgs = note["image_list"]
|
||||||
|
if not len(imgs):
|
||||||
|
return []
|
||||||
|
return [get_img_url_by_trace_id(get_trace_id(img["info_list"][0]["url"])) for img in imgs]
|
||||||
|
|
||||||
|
|
||||||
|
def get_imgs_urls_from_note(note) -> list:
|
||||||
|
"""the return type is [[img1_url1, img1_url2, img1_url3], [img2_url, img2_url2, img2_url3], ...]"""
|
||||||
|
imgs = note["image_list"]
|
||||||
|
if not len(imgs):
|
||||||
|
return []
|
||||||
|
return [get_img_urls_by_trace_id(img["trace_id"]) for img in imgs]
|
||||||
|
|
||||||
|
|
||||||
|
video_cdns = [
|
||||||
|
"https://sns-video-qc.xhscdn.com",
|
||||||
|
"https://sns-video-hw.xhscdn.com",
|
||||||
|
"https://sns-video-bd.xhscdn.com",
|
||||||
|
"https://sns-video-qn.xhscdn.com",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def get_video_url_from_note(note) -> str:
|
||||||
|
if not note.get("video"):
|
||||||
|
return ""
|
||||||
|
origin_video_key = note['video']['consumer']['origin_video_key']
|
||||||
|
return f"{random.choice(video_cdns)}/{origin_video_key}"
|
||||||
|
|
||||||
|
|
||||||
|
def get_video_urls_from_note(note) -> list:
|
||||||
|
if not note.get("video"):
|
||||||
|
return []
|
||||||
|
origin_video_key = note['video']['consumer']['origin_video_key']
|
||||||
|
return [f"{cdn}/{origin_video_key}" for cdn in video_cdns]
|
||||||
|
|
||||||
|
|
||||||
|
def download_file(url: str, filename: str):
|
||||||
|
with requests.get(url, stream=True) as r:
|
||||||
|
r.raise_for_status()
|
||||||
|
with open(filename, 'wb') as f:
|
||||||
|
for chunk in r.iter_content(chunk_size=8192):
|
||||||
|
f.write(chunk)
|
||||||
|
|
||||||
|
|
||||||
|
def get_valid_path_name(text):
|
||||||
|
invalid_chars = '<>:"/\\|?*'
|
||||||
|
return re.sub('[{}]'.format(re.escape(invalid_chars)), '_', text)
|
||||||
|
|
||||||
|
|
||||||
|
def mrc(e):
|
||||||
|
ie = [
|
||||||
|
0, 1996959894, 3993919788, 2567524794, 124634137, 1886057615, 3915621685,
|
||||||
|
2657392035, 249268274, 2044508324, 3772115230, 2547177864, 162941995,
|
||||||
|
2125561021, 3887607047, 2428444049, 498536548, 1789927666, 4089016648,
|
||||||
|
2227061214, 450548861, 1843258603, 4107580753, 2211677639, 325883990,
|
||||||
|
1684777152, 4251122042, 2321926636, 335633487, 1661365465, 4195302755,
|
||||||
|
2366115317, 997073096, 1281953886, 3579855332, 2724688242, 1006888145,
|
||||||
|
1258607687, 3524101629, 2768942443, 901097722, 1119000684, 3686517206,
|
||||||
|
2898065728, 853044451, 1172266101, 3705015759, 2882616665, 651767980,
|
||||||
|
1373503546, 3369554304, 3218104598, 565507253, 1454621731, 3485111705,
|
||||||
|
3099436303, 671266974, 1594198024, 3322730930, 2970347812, 795835527,
|
||||||
|
1483230225, 3244367275, 3060149565, 1994146192, 31158534, 2563907772,
|
||||||
|
4023717930, 1907459465, 112637215, 2680153253, 3904427059, 2013776290,
|
||||||
|
251722036, 2517215374, 3775830040, 2137656763, 141376813, 2439277719,
|
||||||
|
3865271297, 1802195444, 476864866, 2238001368, 4066508878, 1812370925,
|
||||||
|
453092731, 2181625025, 4111451223, 1706088902, 314042704, 2344532202,
|
||||||
|
4240017532, 1658658271, 366619977, 2362670323, 4224994405, 1303535960,
|
||||||
|
984961486, 2747007092, 3569037538, 1256170817, 1037604311, 2765210733,
|
||||||
|
3554079995, 1131014506, 879679996, 2909243462, 3663771856, 1141124467,
|
||||||
|
855842277, 2852801631, 3708648649, 1342533948, 654459306, 3188396048,
|
||||||
|
3373015174, 1466479909, 544179635, 3110523913, 3462522015, 1591671054,
|
||||||
|
702138776, 2966460450, 3352799412, 1504918807, 783551873, 3082640443,
|
||||||
|
3233442989, 3988292384, 2596254646, 62317068, 1957810842, 3939845945,
|
||||||
|
2647816111, 81470997, 1943803523, 3814918930, 2489596804, 225274430,
|
||||||
|
2053790376, 3826175755, 2466906013, 167816743, 2097651377, 4027552580,
|
||||||
|
2265490386, 503444072, 1762050814, 4150417245, 2154129355, 426522225,
|
||||||
|
1852507879, 4275313526, 2312317920, 282753626, 1742555852, 4189708143,
|
||||||
|
2394877945, 397917763, 1622183637, 3604390888, 2714866558, 953729732,
|
||||||
|
1340076626, 3518719985, 2797360999, 1068828381, 1219638859, 3624741850,
|
||||||
|
2936675148, 906185462, 1090812512, 3747672003, 2825379669, 829329135,
|
||||||
|
1181335161, 3412177804, 3160834842, 628085408, 1382605366, 3423369109,
|
||||||
|
3138078467, 570562233, 1426400815, 3317316542, 2998733608, 733239954,
|
||||||
|
1555261956, 3268935591, 3050360625, 752459403, 1541320221, 2607071920,
|
||||||
|
3965973030, 1969922972, 40735498, 2617837225, 3943577151, 1913087877,
|
||||||
|
83908371, 2512341634, 3803740692, 2075208622, 213261112, 2463272603,
|
||||||
|
3855990285, 2094854071, 198958881, 2262029012, 4057260610, 1759359992,
|
||||||
|
534414190, 2176718541, 4139329115, 1873836001, 414664567, 2282248934,
|
||||||
|
4279200368, 1711684554, 285281116, 2405801727, 4167216745, 1634467795,
|
||||||
|
376229701, 2685067896, 3608007406, 1308918612, 956543938, 2808555105,
|
||||||
|
3495958263, 1231636301, 1047427035, 2932959818, 3654703836, 1088359270,
|
||||||
|
936918000, 2847714899, 3736837829, 1202900863, 817233897, 3183342108,
|
||||||
|
3401237130, 1404277552, 615818150, 3134207493, 3453421203, 1423857449,
|
||||||
|
601450431, 3009837614, 3294710456, 1567103746, 711928724, 3020668471,
|
||||||
|
3272380065, 1510334235, 755167117,
|
||||||
|
]
|
||||||
|
o = -1
|
||||||
|
|
||||||
|
def right_without_sign(num, bit=0) -> int:
|
||||||
|
val = ctypes.c_uint32(num).value >> bit
|
||||||
|
MAX32INT = 4294967295
|
||||||
|
return (val + (MAX32INT + 1)) % (2 * (MAX32INT + 1)) - MAX32INT - 1
|
||||||
|
|
||||||
|
for n in range(57):
|
||||||
|
o = ie[(o & 255) ^ ord(e[n])] ^ right_without_sign(o, 8)
|
||||||
|
return o ^ -1 ^ 3988292384
|
||||||
|
|
||||||
|
|
||||||
|
lookup = [
|
||||||
|
"Z",
|
||||||
|
"m",
|
||||||
|
"s",
|
||||||
|
"e",
|
||||||
|
"r",
|
||||||
|
"b",
|
||||||
|
"B",
|
||||||
|
"o",
|
||||||
|
"H",
|
||||||
|
"Q",
|
||||||
|
"t",
|
||||||
|
"N",
|
||||||
|
"P",
|
||||||
|
"+",
|
||||||
|
"w",
|
||||||
|
"O",
|
||||||
|
"c",
|
||||||
|
"z",
|
||||||
|
"a",
|
||||||
|
"/",
|
||||||
|
"L",
|
||||||
|
"p",
|
||||||
|
"n",
|
||||||
|
"g",
|
||||||
|
"G",
|
||||||
|
"8",
|
||||||
|
"y",
|
||||||
|
"J",
|
||||||
|
"q",
|
||||||
|
"4",
|
||||||
|
"2",
|
||||||
|
"K",
|
||||||
|
"W",
|
||||||
|
"Y",
|
||||||
|
"j",
|
||||||
|
"0",
|
||||||
|
"D",
|
||||||
|
"S",
|
||||||
|
"f",
|
||||||
|
"d",
|
||||||
|
"i",
|
||||||
|
"k",
|
||||||
|
"x",
|
||||||
|
"3",
|
||||||
|
"V",
|
||||||
|
"T",
|
||||||
|
"1",
|
||||||
|
"6",
|
||||||
|
"I",
|
||||||
|
"l",
|
||||||
|
"U",
|
||||||
|
"A",
|
||||||
|
"F",
|
||||||
|
"M",
|
||||||
|
"9",
|
||||||
|
"7",
|
||||||
|
"h",
|
||||||
|
"E",
|
||||||
|
"C",
|
||||||
|
"v",
|
||||||
|
"u",
|
||||||
|
"R",
|
||||||
|
"X",
|
||||||
|
"5",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def tripletToBase64(e):
|
||||||
|
return (
|
||||||
|
lookup[63 & (e >> 18)] + lookup[63 & (e >> 12)] + lookup[(e >> 6) & 63] + lookup[e & 63]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def encodeChunk(e, t, r):
|
||||||
|
m = []
|
||||||
|
for b in range(t, r, 3):
|
||||||
|
n = (16711680 & (e[b] << 16)) + \
|
||||||
|
((e[b + 1] << 8) & 65280) + (e[b + 2] & 255)
|
||||||
|
m.append(tripletToBase64(n))
|
||||||
|
return ''.join(m)
|
||||||
|
|
||||||
|
|
||||||
|
def b64Encode(e):
|
||||||
|
P = len(e)
|
||||||
|
W = P % 3
|
||||||
|
U = []
|
||||||
|
z = 16383
|
||||||
|
H = 0
|
||||||
|
Z = P - W
|
||||||
|
while H < Z:
|
||||||
|
U.append(encodeChunk(e, H, Z if H + z > Z else H + z))
|
||||||
|
H += z
|
||||||
|
if 1 == W:
|
||||||
|
F = e[P - 1]
|
||||||
|
U.append(lookup[F >> 2] + lookup[(F << 4) & 63] + "==")
|
||||||
|
elif 2 == W:
|
||||||
|
F = (e[P - 2] << 8) + e[P - 1]
|
||||||
|
U.append(lookup[F >> 10] + lookup[63 & (F >> 4)] + lookup[(F << 2) & 63] + "=")
|
||||||
|
return "".join(U)
|
||||||
|
|
||||||
|
|
||||||
|
def encodeUtf8(e):
|
||||||
|
b = []
|
||||||
|
m = urllib.parse.quote(e, safe='~()*!.\'')
|
||||||
|
w = 0
|
||||||
|
while w < len(m):
|
||||||
|
T = m[w]
|
||||||
|
if T == "%":
|
||||||
|
E = m[w + 1] + m[w + 2]
|
||||||
|
S = int(E, 16)
|
||||||
|
b.append(S)
|
||||||
|
w += 2
|
||||||
|
else:
|
||||||
|
b.append(ord(T[0]))
|
||||||
|
w += 1
|
||||||
|
return b
|
||||||
|
|
||||||
|
|
||||||
|
def base36encode(number, alphabet='0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'):
|
||||||
|
"""Converts an integer to a base36 string."""
|
||||||
|
if not isinstance(number, int):
|
||||||
|
raise TypeError('number must be an integer')
|
||||||
|
|
||||||
|
base36 = ''
|
||||||
|
sign = ''
|
||||||
|
|
||||||
|
if number < 0:
|
||||||
|
sign = '-'
|
||||||
|
number = -number
|
||||||
|
|
||||||
|
if 0 <= number < len(alphabet):
|
||||||
|
return sign + alphabet[number]
|
||||||
|
|
||||||
|
while number != 0:
|
||||||
|
number, i = divmod(number, len(alphabet))
|
||||||
|
base36 = alphabet[i] + base36
|
||||||
|
|
||||||
|
return sign + base36
|
||||||
|
|
||||||
|
|
||||||
|
def base36decode(number):
|
||||||
|
return int(number, 36)
|
||||||
|
|
||||||
|
|
||||||
|
def xml_to_dict(element):
|
||||||
|
result = {}
|
||||||
|
for child in element:
|
||||||
|
if child:
|
||||||
|
child_dict = xml_to_dict(child)
|
||||||
|
if child.tag in result:
|
||||||
|
if type(result[child.tag]) is list:
|
||||||
|
result[child.tag].append(child_dict)
|
||||||
|
else:
|
||||||
|
result[child.tag] = [result[child.tag], child_dict]
|
||||||
|
else:
|
||||||
|
result[child.tag] = child_dict
|
||||||
|
else:
|
||||||
|
result[child.tag] = child.text
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def parse_xml(xml_string):
|
||||||
|
root = ElementTree.fromstring(xml_string)
|
||||||
|
return xml_to_dict(root)
|
||||||
|
|
||||||
|
|
||||||
|
def get_search_id():
|
||||||
|
e = int(time.time() * 1000) << 64
|
||||||
|
t = int(random.uniform(0, 2147483646))
|
||||||
|
return base36encode((e + t))
|
||||||
|
|
||||||
|
|
||||||
|
def cookie_str_to_cookie_dict(cookie_str: str):
|
||||||
|
cookie_blocks = [cookie_block.split("=")
|
||||||
|
for cookie_block in cookie_str.split(";") if cookie_block]
|
||||||
|
return {cookie[0].strip(): cookie[1].strip() for cookie in cookie_blocks}
|
||||||
|
|
||||||
|
|
||||||
|
def cookie_jar_to_cookie_str(cookie_jar):
|
||||||
|
cookie_dict = requests.utils.dict_from_cookiejar(cookie_jar)
|
||||||
|
return ";".join([f"{key}={value}" for key, value in cookie_dict.items()])
|
||||||
|
|
||||||
|
|
||||||
|
def update_session_cookies_from_cookie(session: requests.Session, cookie: str):
|
||||||
|
cookie_dict = cookie_str_to_cookie_dict(cookie) if cookie else {}
|
||||||
|
if "a1" not in cookie_dict or "webId" not in cookie_dict:
|
||||||
|
# a1, web_id = get_a1_and_web_id()
|
||||||
|
cookie_dict |= {"a1": "187d2defea8dz1fgwydnci40kw265ikh9fsxn66qs50000726043",
|
||||||
|
"webId": "ba57f42593b9e55840a289fa0b755374"}
|
||||||
|
if "gid" not in cookie_dict:
|
||||||
|
cookie_dict |= {
|
||||||
|
"gid.sign": "PSF1M3U6EBC/Jv6eGddPbmsWzLI=",
|
||||||
|
"gid": "yYWfJfi820jSyYWfJfdidiKK0YfuyikEvfISMAM348TEJC28K23TxI888WJK84q8S4WfY2Sy"
|
||||||
|
}
|
||||||
|
new_cookies = requests.utils.cookiejar_from_dict(cookie_dict)
|
||||||
|
session.cookies = new_cookies
|
1
packages/xhs-core/python/readme.md
Normal file
1
packages/xhs-core/python/readme.md
Normal file
@ -0,0 +1 @@
|
|||||||
|
# 参考代码模块
|
300
packages/xhs-core/python/xhs/core.ts
Normal file
300
packages/xhs-core/python/xhs/core.ts
Normal file
@ -0,0 +1,300 @@
|
|||||||
|
import { sign } from './helper.ts';
|
||||||
|
enum FeedType {
|
||||||
|
RECOMMEND = 'homefeed_recommend',
|
||||||
|
FASION = 'homefeed.fashion_v3',
|
||||||
|
FOOD = 'homefeed.food_v3',
|
||||||
|
COSMETICS = 'homefeed.cosmetics_v3',
|
||||||
|
MOVIE = 'homefeed.movie_and_tv_v3',
|
||||||
|
CAREER = 'homefeed.career_v3',
|
||||||
|
EMOTION = 'homefeed.love_v3',
|
||||||
|
HOURSE = 'homefeed.household_product_v3',
|
||||||
|
GAME = 'homefeed.gaming_v3',
|
||||||
|
TRAVEL = 'homefeed.travel_v3',
|
||||||
|
FITNESS = 'homefeed.fitness_v3',
|
||||||
|
}
|
||||||
|
|
||||||
|
enum NoteType {
|
||||||
|
NORMAL = 'normal',
|
||||||
|
VIDEO = 'video',
|
||||||
|
}
|
||||||
|
|
||||||
|
enum SearchSortType {
|
||||||
|
GENERAL = 'general',
|
||||||
|
MOST_POPULAR = 'popularity_descending',
|
||||||
|
LATEST = 'time_descending',
|
||||||
|
}
|
||||||
|
|
||||||
|
enum SearchNoteType {
|
||||||
|
ALL = 0,
|
||||||
|
VIDEO = 1,
|
||||||
|
IMAGE = 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Note {
|
||||||
|
note_id: string;
|
||||||
|
title: string;
|
||||||
|
desc: string;
|
||||||
|
type: string;
|
||||||
|
user: Record<string, any>;
|
||||||
|
img_urls: string[];
|
||||||
|
video_url: string;
|
||||||
|
tag_list: any[];
|
||||||
|
at_user_list: any[];
|
||||||
|
collected_count: string;
|
||||||
|
comment_count: string;
|
||||||
|
liked_count: string;
|
||||||
|
share_count: string;
|
||||||
|
time: number;
|
||||||
|
last_update_time: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ErrorResponse {
|
||||||
|
success?: boolean;
|
||||||
|
code?: number;
|
||||||
|
msg?: string;
|
||||||
|
data?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
class XhsError extends Error {
|
||||||
|
response?: Response;
|
||||||
|
verify_type?: string;
|
||||||
|
verify_uuid?: string;
|
||||||
|
|
||||||
|
constructor(message: string, options?: { response?: Response; verify_type?: string | null; verify_uuid?: string | null }) {
|
||||||
|
super(message);
|
||||||
|
this.response = options?.response;
|
||||||
|
this.verify_type = options?.verify_type ?? '';
|
||||||
|
this.verify_uuid = options?.verify_uuid ?? '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DataFetchError extends XhsError {}
|
||||||
|
class IPBlockError extends XhsError {}
|
||||||
|
class NeedVerifyError extends XhsError {}
|
||||||
|
class SignError extends XhsError {}
|
||||||
|
|
||||||
|
export class XhsClientBase {
|
||||||
|
private timeout: number;
|
||||||
|
private externalSign?: (url: string, data: any, options: { a1?: string; web_session?: string }) => Record<string, string>;
|
||||||
|
private host: string;
|
||||||
|
private creatorHost: string;
|
||||||
|
private home: string;
|
||||||
|
private userAgent: string;
|
||||||
|
private cookies: Record<string, string> = {};
|
||||||
|
private headers: Record<string, string>;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
options: {
|
||||||
|
cookie?: string;
|
||||||
|
user_agent?: string;
|
||||||
|
timeout?: number;
|
||||||
|
sign?: (url: string, data: any, options: { a1?: string; web_session?: string }) => Record<string, string>;
|
||||||
|
} = {},
|
||||||
|
) {
|
||||||
|
this.timeout = options.timeout || 10 * 60 * 1000; // 10 minutes
|
||||||
|
this.externalSign = options.sign;
|
||||||
|
this.host = 'https://edith.xiaohongshu.com';
|
||||||
|
this.creatorHost = 'https://creator.xiaohongshu.com';
|
||||||
|
this.home = 'https://www.xiaohongshu.com';
|
||||||
|
this.userAgent = options.user_agent || 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36';
|
||||||
|
|
||||||
|
this.headers = {
|
||||||
|
'user-agent': this.userAgent,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (options.cookie) {
|
||||||
|
this.cookie = options.cookie;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get cookie(): string {
|
||||||
|
return Object.entries(this.cookies)
|
||||||
|
.map(([key, value]) => `${key}=${value}`)
|
||||||
|
.join('; ');
|
||||||
|
}
|
||||||
|
|
||||||
|
set cookie(cookie: string) {
|
||||||
|
this.cookies = cookie.split(';').reduce((acc, pair) => {
|
||||||
|
const [key, value] = pair.trim().split('=');
|
||||||
|
if (key && value) {
|
||||||
|
acc[key] = value;
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<string, string>);
|
||||||
|
}
|
||||||
|
|
||||||
|
get cookieDict(): Record<string, string> {
|
||||||
|
return { ...this.cookies };
|
||||||
|
}
|
||||||
|
|
||||||
|
private async request(method: string, url: string, options: RequestInit = {}): Promise<any> {
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method,
|
||||||
|
headers: {
|
||||||
|
...this.headers,
|
||||||
|
...options.headers,
|
||||||
|
cookie: this.cookie,
|
||||||
|
},
|
||||||
|
body: options.body,
|
||||||
|
signal: controller.signal,
|
||||||
|
});
|
||||||
|
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.status === 204) {
|
||||||
|
// No content
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json().catch(() => response.text());
|
||||||
|
|
||||||
|
if (response.status === 471 || response.status === 461) {
|
||||||
|
const verify_type = response.headers.get('Verifytype');
|
||||||
|
const verify_uuid = response.headers.get('Verifyuuid');
|
||||||
|
throw new NeedVerifyError(`出现验证码,请求失败,Verifytype: ${verify_type},Verifyuuid: ${verify_uuid}`, { response, verify_type, verify_uuid });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
return data.data || data.success;
|
||||||
|
} else if (data.code === 300007) {
|
||||||
|
// IP_BLOCK
|
||||||
|
throw new IPBlockError('IP blocked', { response });
|
||||||
|
} else if (data.code === 400002) {
|
||||||
|
// SIGN_FAULT
|
||||||
|
throw new SignError('Sign fault', { response });
|
||||||
|
} else {
|
||||||
|
throw new DataFetchError(data, { response });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
if (error instanceof Error) {
|
||||||
|
if (error.name === 'AbortError') {
|
||||||
|
throw new Error(`Request timeout after ${this.timeout} seconds`);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
throw new Error('Unknown error occurred');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
preHeaders(url: string, data?: any, isCreator: boolean = false): void {
|
||||||
|
if (isCreator) {
|
||||||
|
// Implement sign function in TypeScript or import it
|
||||||
|
const signs = sign(url, data, { a1: this.cookies.a1 });
|
||||||
|
this.headers['x-s'] = signs['x-s'];
|
||||||
|
this.headers['x-t'] = signs['x-t'];
|
||||||
|
this.headers['x-s-common'] = signs['x-s-common'];
|
||||||
|
} else if (this.externalSign) {
|
||||||
|
const signs = this.externalSign(url, data, {
|
||||||
|
a1: this.cookies.a1,
|
||||||
|
web_session: this.cookies.web_session || '',
|
||||||
|
});
|
||||||
|
Object.assign(this.headers, signs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async get(uri: string, params?: Record<string, any>, isCreator: boolean = false, options: RequestInit = {}): Promise<any> {
|
||||||
|
let finalUri = uri;
|
||||||
|
if (params) {
|
||||||
|
const query = new URLSearchParams();
|
||||||
|
for (const [key, value] of Object.entries(params)) {
|
||||||
|
if (value !== undefined) {
|
||||||
|
query.append(key, String(value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finalUri = `${uri}?${query.toString()}`;
|
||||||
|
}
|
||||||
|
this.preHeaders(finalUri, undefined, isCreator);
|
||||||
|
const url = `${isCreator ? this.creatorHost : this.host}${finalUri}`;
|
||||||
|
return this.request('GET', url, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
async post(uri: string, data: any, isCreator: boolean = false, options: RequestInit = {}): Promise<any> {
|
||||||
|
this.preHeaders(uri, data, isCreator);
|
||||||
|
const url = `${isCreator ? this.creatorHost : this.host}${uri}`;
|
||||||
|
return this.request('POST', url, {
|
||||||
|
...options,
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getNoteById(noteId: string): Promise<any> {
|
||||||
|
const data = { source_note_id: noteId, image_scenes: ['CRD_WM_WEBP'] };
|
||||||
|
const uri = '/api/sns/web/v1/feed';
|
||||||
|
const res = await this.post(uri, data);
|
||||||
|
return res.items[0].note_card;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getNoteByIdFromHtml(noteId: string): Promise<any> {
|
||||||
|
const url = `${this.home}/explore/${noteId}`;
|
||||||
|
const response = await this.request('GET', url, {
|
||||||
|
headers: {
|
||||||
|
'user-agent': this.userAgent,
|
||||||
|
referer: `${this.home}/`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (typeof response === 'string') {
|
||||||
|
const html = response;
|
||||||
|
const stateMatch = html.match(/window\.__INITIAL_STATE__=({.*?})<\/script>/);
|
||||||
|
if (!stateMatch) {
|
||||||
|
throw new Error('Could not find initial state in HTML');
|
||||||
|
}
|
||||||
|
|
||||||
|
let state = stateMatch[1].replace(/undefined/g, '""');
|
||||||
|
if (state === '{}') {
|
||||||
|
throw new DataFetchError('Empty state');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implement transformJsonKeys in TypeScript if needed
|
||||||
|
const noteDict = this.transformJsonKeys(JSON.parse(state));
|
||||||
|
return noteDict.note.note_detail_map[noteId].note;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new DataFetchError('Invalid response');
|
||||||
|
}
|
||||||
|
|
||||||
|
private transformJsonKeys(data: any): any {
|
||||||
|
// Implement camelToUnderscore and transform logic here
|
||||||
|
// Similar to the Python version
|
||||||
|
return data; // Placeholder
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implement all other methods similarly, converting Python to TypeScript
|
||||||
|
// For example:
|
||||||
|
|
||||||
|
async reportNoteMetrics(
|
||||||
|
noteId: string,
|
||||||
|
noteType: number,
|
||||||
|
noteUserId: string,
|
||||||
|
viewerUserId: string,
|
||||||
|
followedAuthor: number = 0,
|
||||||
|
reportType: number = 1,
|
||||||
|
staySeconds: number = 0,
|
||||||
|
): Promise<any> {
|
||||||
|
const uri = '/api/sns/web/v1/note/metrics_report';
|
||||||
|
const data = {
|
||||||
|
note_id: noteId,
|
||||||
|
note_type: noteType,
|
||||||
|
report_type: reportType,
|
||||||
|
stress_test: false,
|
||||||
|
viewer: { user_id: viewerUserId, followed_author: followedAuthor },
|
||||||
|
author: { user_id: noteUserId },
|
||||||
|
interaction: { like: 0, collect: 0, comment: 0, comment_read: 0 },
|
||||||
|
note: { stay_seconds: staySeconds },
|
||||||
|
other: { platform: 'web' },
|
||||||
|
};
|
||||||
|
return this.post(uri, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Continue with all other methods...
|
||||||
|
}
|
48
packages/xhs-core/python/xhs/exception.ts
Normal file
48
packages/xhs-core/python/xhs/exception.ts
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
export enum ErrorCode {
|
||||||
|
/** 网络连接异常,请检查网络设置或重启试试 */
|
||||||
|
IP_BLOCK = 300012,
|
||||||
|
/** 笔记状态异常,请稍后查看 */
|
||||||
|
NOTE_ABNORMAL = -510001,
|
||||||
|
/** 当前内容无法展示 */
|
||||||
|
NOTE_SECRETE_FAULT = -510001,
|
||||||
|
/** 浏览器异常,请尝试关闭/卸载风险插件或重启试试! */
|
||||||
|
SIGN_FAULT = 300015,
|
||||||
|
/** 登录已过期 */
|
||||||
|
SESSION_EXPIRED = -100
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export class XhsError extends Error {
|
||||||
|
response?: Response;
|
||||||
|
verifyType?: string;
|
||||||
|
verifyUuid?: string;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
message: string,
|
||||||
|
options?: {
|
||||||
|
response?: Response;
|
||||||
|
verifyType?: string;
|
||||||
|
verifyUuid?: string;
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
super(message);
|
||||||
|
this.response = options?.response;
|
||||||
|
this.verifyType = options?.verifyType;
|
||||||
|
this.verifyUuid = options?.verifyUuid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DataFetchError extends XhsError {}
|
||||||
|
export class IPBlockError extends XhsError {}
|
||||||
|
export class SignError extends XhsError {}
|
||||||
|
export class NeedVerifyError extends XhsError {
|
||||||
|
constructor(
|
||||||
|
message: string,
|
||||||
|
options?: {
|
||||||
|
verifyType?: string;
|
||||||
|
verifyUuid?: string;
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
super(message, options);
|
||||||
|
}
|
||||||
|
}
|
401
packages/xhs-core/python/xhs/helper.ts
Normal file
401
packages/xhs-core/python/xhs/helper.ts
Normal file
@ -0,0 +1,401 @@
|
|||||||
|
import { createHash } from 'crypto';
|
||||||
|
import fs from 'node:fs';
|
||||||
|
import { DOMParser } from 'xmldom';
|
||||||
|
|
||||||
|
// Helper functions
|
||||||
|
function randomStr(length: number): string {
|
||||||
|
const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||||
|
return Array.from({ length }, () => alphabet.charAt(Math.floor(Math.random() * alphabet.length))).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function getA1AndWebId(): [string, string] {
|
||||||
|
const d = Math.floor(Date.now()).toString(16) + randomStr(30) + '5' + '0' + '000';
|
||||||
|
const crc32 = (str: string): number => {
|
||||||
|
let crc = 0 ^ (-1);
|
||||||
|
for (let i = 0; i < str.length; i++) {
|
||||||
|
const char = str.charCodeAt(i);
|
||||||
|
crc = (crc >>> 8) ^ crc32Table[(crc ^ char) & 0xFF];
|
||||||
|
}
|
||||||
|
return (crc ^ (-1)) >>> 0;
|
||||||
|
};
|
||||||
|
const g = (d + crc32(d).toString()).slice(0, 52);
|
||||||
|
const webId = Array.from(new Uint8Array(createHash('md5').update(g).digest()))
|
||||||
|
.map(b => b.toString(16).padStart(2, '0')).join('');
|
||||||
|
return [g, webId];
|
||||||
|
}
|
||||||
|
|
||||||
|
// CRC32 table
|
||||||
|
const crc32Table = Array.from({ length: 256 }, (_, i) => {
|
||||||
|
let crc = i;
|
||||||
|
for (let j = 0; j < 8; j++) {
|
||||||
|
crc = crc & 1 ? 0xEDB88320 ^ (crc >>> 1) : crc >>> 1;
|
||||||
|
}
|
||||||
|
return crc;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Image and video CDNs
|
||||||
|
const imgCdns = [
|
||||||
|
"https://sns-img-qc.xhscdn.com",
|
||||||
|
"https://sns-img-hw.xhscdn.com",
|
||||||
|
"https://sns-img-bd.xhscdn.com",
|
||||||
|
"https://sns-img-qn.xhscdn.com",
|
||||||
|
];
|
||||||
|
|
||||||
|
const videoCdns = [
|
||||||
|
"https://sns-video-qc.xhscdn.com",
|
||||||
|
"https://sns-video-hw.xhscdn.com",
|
||||||
|
"https://sns-video-bd.xhscdn.com",
|
||||||
|
"https://sns-video-qn.xhscdn.com",
|
||||||
|
];
|
||||||
|
|
||||||
|
// URL handling functions
|
||||||
|
function getImgUrlByTraceId(traceId: string, format: string = "png"): string {
|
||||||
|
return `${imgCdns[Math.floor(Math.random() * imgCdns.length)]}/${traceId}?imageView2/format/${format}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getImgUrlsByTraceId(traceId: string, format: string = "png"): string[] {
|
||||||
|
return imgCdns.map(cdn => `${cdn}/${traceId}?imageView2/format/${format}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTraceId(imgUrl: string): string {
|
||||||
|
const parts = imgUrl.split('/');
|
||||||
|
const traceId = parts[parts.length - 1].split('!')[0];
|
||||||
|
return imgUrl.includes('spectrum') ? `spectrum/${traceId}` : traceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getImgsUrlFromNote(note: any): string[] {
|
||||||
|
const imgs = note.image_list || [];
|
||||||
|
return imgs.map((img: any) => getImgUrlByTraceId(getTraceId(img.info_list[0].url)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getImgsUrlsFromNote(note: any): string[][] {
|
||||||
|
const imgs = note.image_list || [];
|
||||||
|
return imgs.map((img: any) => getImgUrlsByTraceId(img.trace_id));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getVideoUrlFromNote(note: any): string {
|
||||||
|
if (!note.video) return '';
|
||||||
|
const originVideoKey = note.video.consumer.origin_video_key;
|
||||||
|
return `${videoCdns[Math.floor(Math.random() * videoCdns.length)]}/${originVideoKey}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getVideoUrlsFromNote(note: any): string[] {
|
||||||
|
if (!note.video) return [];
|
||||||
|
const originVideoKey = note.video.consumer.origin_video_key;
|
||||||
|
return videoCdns.map(cdn => `${cdn}/${originVideoKey}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// File download
|
||||||
|
async function downloadFile(url: string, filename: string): Promise<void> {
|
||||||
|
const response = await fetch(url);
|
||||||
|
if (!response.ok) throw new Error(`Failed to download file: ${response.statusText}`);
|
||||||
|
const buffer = await response.arrayBuffer();
|
||||||
|
await fs.promises.writeFile(filename, Buffer.from(buffer));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Path handling
|
||||||
|
function getValidPathName(text: string): string {
|
||||||
|
const invalidChars = '<>:"/\\|?*';
|
||||||
|
return text.replace(new RegExp(`[${invalidChars.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, '\\$1')}]`, 'g'), '_');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Signing functions
|
||||||
|
function mrc(e: string): number {
|
||||||
|
const ie = [
|
||||||
|
0, 1996959894, 3993919788, 2567524794, 124634137, 1886057615, 3915621685,
|
||||||
|
2657392035, 249268274, 2044508324, 3772115230, 2547177864, 162941995,
|
||||||
|
2125561021, 3887607047, 2428444049, 498536548, 1789927666, 4089016648,
|
||||||
|
2227061214, 450548861, 1843258603, 4107580753, 2211677639, 325883990,
|
||||||
|
1684777152, 4251122042, 2321926636, 335633487, 1661365465, 4195302755,
|
||||||
|
2366115317, 997073096, 1281953886, 3579855332, 2724688242, 1006888145,
|
||||||
|
1258607687, 3524101629, 2768942443, 901097722, 1119000684, 3686517206,
|
||||||
|
2898065728, 853044451, 1172266101, 3705015759, 2882616665, 651767980,
|
||||||
|
1373503546, 3369554304, 3218104598, 565507253, 1454621731, 3485111705,
|
||||||
|
3099436303, 671266974, 1594198024, 3322730930, 2970347812, 795835527,
|
||||||
|
1483230225, 3244367275, 3060149565, 1994146192, 31158534, 2563907772,
|
||||||
|
4023717930, 1907459465, 112637215, 2680153253, 3904427059, 2013776290,
|
||||||
|
251722036, 2517215374, 3775830040, 2137656763, 141376813, 2439277719,
|
||||||
|
3865271297, 1802195444, 476864866, 2238001368, 4066508878, 1812370925,
|
||||||
|
453092731, 2181625025, 4111451223, 1706088902, 314042704, 2344532202,
|
||||||
|
4240017532, 1658658271, 366619977, 2362670323, 4224994405, 1303535960,
|
||||||
|
984961486, 2747007092, 3569037538, 1256170817, 1037604311, 2765210733,
|
||||||
|
3554079995, 1131014506, 879679996, 2909243462, 3663771856, 1141124467,
|
||||||
|
855842277, 2852801631, 3708648649, 1342533948, 654459306, 3188396048,
|
||||||
|
3373015174, 1466479909, 544179635, 3110523913, 3462522015, 1591671054,
|
||||||
|
702138776, 2966460450, 3352799412, 1504918807, 783551873, 3082640443,
|
||||||
|
3233442989, 3988292384, 2596254646, 62317068, 1957810842, 3939845945,
|
||||||
|
2647816111, 81470997, 1943803523, 3814918930, 2489596804, 225274430,
|
||||||
|
2053790376, 3826175755, 2466906013, 167816743, 2097651377, 4027552580,
|
||||||
|
2265490386, 503444072, 1762050814, 4150417245, 2154129355, 426522225,
|
||||||
|
1852507879, 4275313526, 2312317920, 282753626, 1742555852, 4189708143,
|
||||||
|
2394877945, 397917763, 1622183637, 3604390888, 2714866558, 953729732,
|
||||||
|
1340076626, 3518719985, 2797360999, 1068828381, 1219638859, 3624741850,
|
||||||
|
2936675148, 906185462, 1090812512, 3747672003, 2825379669, 829329135,
|
||||||
|
1181335161, 3412177804, 3160834842, 628085408, 1382605366, 3423369109,
|
||||||
|
3138078467, 570562233, 1426400815, 3317316542, 2998733608, 733239954,
|
||||||
|
1555261956, 3268935591, 3050360625, 752459403, 1541320221, 2607071920,
|
||||||
|
3965973030, 1969922972, 40735498, 2617837225, 3943577151, 1913087877,
|
||||||
|
83908371, 2512341634, 3803740692, 2075208622, 213261112, 2463272603,
|
||||||
|
3855990285, 2094854071, 198958881, 2262029012, 4057260610, 1759359992,
|
||||||
|
534414190, 2176718541, 4139329115, 1873836001, 414664567, 2282248934,
|
||||||
|
4279200368, 1711684554, 285281116, 2405801727, 4167216745, 1634467795,
|
||||||
|
376229701, 2685067896, 3608007406, 1308918612, 956543938, 2808555105,
|
||||||
|
3495958263, 1231636301, 1047427035, 2932959818, 3654703836, 1088359270,
|
||||||
|
936918000, 2847714899, 3736837829, 1202900863, 817233897, 3183342108,
|
||||||
|
3401237130, 1404277552, 615818150, 3134207493, 3453421203, 1423857449,
|
||||||
|
601450431, 3009837614, 3294710456, 1567103746, 711928724, 3020668471,
|
||||||
|
3272380065, 1510334235, 755167117,
|
||||||
|
];
|
||||||
|
let o = -1;
|
||||||
|
|
||||||
|
const rightWithoutSign = (num: number, bit = 0): number => {
|
||||||
|
const val = num >>> bit;
|
||||||
|
const MAX32INT = 4294967295;
|
||||||
|
return (val + (MAX32INT + 1)) % (2 * (MAX32INT + 1)) - MAX32INT - 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
for (let n = 0; n < 57; n++) {
|
||||||
|
o = ie[(o & 255) ^ e.charCodeAt(n)] ^ rightWithoutSign(o, 8);
|
||||||
|
}
|
||||||
|
return o ^ -1 ^ 3988292384;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Base64 encoding
|
||||||
|
const lookup = [
|
||||||
|
"Z", "m", "s", "e", "r", "b", "B", "o", "H", "Q", "t", "N", "P", "+", "w", "O",
|
||||||
|
"c", "z", "a", "/", "L", "p", "n", "g", "G", "8", "y", "J", "q", "4", "2", "K",
|
||||||
|
"W", "Y", "j", "0", "D", "S", "f", "d", "i", "k", "x", "3", "V", "T", "1", "6",
|
||||||
|
"I", "l", "U", "A", "F", "M", "9", "7", "h", "E", "C", "v", "u", "R", "X", "5",
|
||||||
|
];
|
||||||
|
|
||||||
|
function tripletToBase64(e: number): string {
|
||||||
|
return (
|
||||||
|
lookup[63 & (e >> 18)] +
|
||||||
|
lookup[63 & (e >> 12)] +
|
||||||
|
lookup[(e >> 6) & 63] +
|
||||||
|
lookup[e & 63]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function encodeChunk(e: Uint8Array, t: number, r: number): string {
|
||||||
|
const m: string[] = [];
|
||||||
|
for (let b = t; b < r; b += 3) {
|
||||||
|
const n = (16711680 & (e[b] << 16)) +
|
||||||
|
((e[b + 1] << 8) & 65280) +
|
||||||
|
(e[b + 2] & 255);
|
||||||
|
m.push(tripletToBase64(n));
|
||||||
|
}
|
||||||
|
return m.join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function b64Encode(e: Uint8Array): string {
|
||||||
|
const P = e.length;
|
||||||
|
const W = P % 3;
|
||||||
|
const U: string[] = [];
|
||||||
|
const z = 16383;
|
||||||
|
let H = 0;
|
||||||
|
const Z = P - W;
|
||||||
|
while (H < Z) {
|
||||||
|
U.push(encodeChunk(e, H, Math.min(H + z, Z)));
|
||||||
|
H += z;
|
||||||
|
}
|
||||||
|
if (W === 1) {
|
||||||
|
const F = e[P - 1];
|
||||||
|
U.push(lookup[F >> 2] + lookup[(F << 4) & 63] + "==");
|
||||||
|
} else if (W === 2) {
|
||||||
|
const F = (e[P - 2] << 8) + e[P - 1];
|
||||||
|
U.push(lookup[F >> 10] + lookup[63 & (F >> 4)] + lookup[(F << 2) & 63] + "=");
|
||||||
|
}
|
||||||
|
return U.join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function encodeUtf8(e: string): Uint8Array {
|
||||||
|
const b: number[] = [];
|
||||||
|
const m = encodeURIComponent(e).replace(/%([0-9A-F]{2})/g, (match, p1) => {
|
||||||
|
b.push(parseInt(p1, 16));
|
||||||
|
return '';
|
||||||
|
});
|
||||||
|
for (let w = 0; w < m.length; w++) {
|
||||||
|
b.push(m.charCodeAt(w));
|
||||||
|
}
|
||||||
|
return new Uint8Array(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Base36 encoding
|
||||||
|
function base36encode(number: number, alphabet = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'): string {
|
||||||
|
if (!Number.isInteger(number)) {
|
||||||
|
throw new TypeError('number must be an integer');
|
||||||
|
}
|
||||||
|
|
||||||
|
let base36 = '';
|
||||||
|
let sign = '';
|
||||||
|
|
||||||
|
if (number < 0) {
|
||||||
|
sign = '-';
|
||||||
|
number = -number;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (0 <= number && number < alphabet.length) {
|
||||||
|
return sign + alphabet[number];
|
||||||
|
}
|
||||||
|
|
||||||
|
while (number !== 0) {
|
||||||
|
const remainder = number % alphabet.length;
|
||||||
|
number = Math.floor(number / alphabet.length);
|
||||||
|
base36 = alphabet[remainder] + base36;
|
||||||
|
}
|
||||||
|
|
||||||
|
return sign + base36;
|
||||||
|
}
|
||||||
|
|
||||||
|
function base36decode(number: string): number {
|
||||||
|
return parseInt(number, 36);
|
||||||
|
}
|
||||||
|
|
||||||
|
// XML parsing
|
||||||
|
function xmlToDict(element: Element): Record<string, any> {
|
||||||
|
const result: Record<string, any> = {};
|
||||||
|
for (let i = 0; i < element.children.length; i++) {
|
||||||
|
const child = element.children[i];
|
||||||
|
if (child.children.length > 0) {
|
||||||
|
const childDict = xmlToDict(child);
|
||||||
|
if (child.tagName in result) {
|
||||||
|
if (Array.isArray(result[child.tagName])) {
|
||||||
|
result[child.tagName].push(childDict);
|
||||||
|
} else {
|
||||||
|
result[child.tagName] = [result[child.tagName], childDict];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result[child.tagName] = childDict;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result[child.tagName] = child.textContent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseXml(xmlString: string): Record<string, any> {
|
||||||
|
const parser = new DOMParser();
|
||||||
|
const doc = parser.parseFromString(xmlString, "text/xml");
|
||||||
|
return xmlToDict(doc.documentElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search ID generation
|
||||||
|
function getSearchId(): string {
|
||||||
|
const e = Math.floor(Date.now()) << 64;
|
||||||
|
const t = Math.floor(Math.random() * 2147483646);
|
||||||
|
return base36encode(e + t);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cookie handling
|
||||||
|
function cookieStrToCookieDict(cookieStr: string): Record<string, string> {
|
||||||
|
return cookieStr.split(';').reduce((acc, cookieBlock) => {
|
||||||
|
const [key, value] = cookieBlock.trim().split('=');
|
||||||
|
if (key && value) acc[key] = value;
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<string, string>);
|
||||||
|
}
|
||||||
|
|
||||||
|
function cookieJarToCookieStr(cookieJar: Record<string, string>): string {
|
||||||
|
return Object.entries(cookieJar).map(([key, value]) => `${key}=${value}`).join(';');
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSessionCookiesFromCookie(session: { cookies: Record<string, string> }, cookie: string): void {
|
||||||
|
const cookieDict = cookie ? cookieStrToCookieDict(cookie) : {};
|
||||||
|
if (!cookieDict.a1 || !cookieDict.webId) {
|
||||||
|
const [a1, webId] = getA1AndWebId();
|
||||||
|
cookieDict.a1 = a1;
|
||||||
|
cookieDict.webId = webId;
|
||||||
|
}
|
||||||
|
if (!cookieDict.gid) {
|
||||||
|
cookieDict['gid.sign'] = 'PSF1M3U6EBC/Jv6eGddPbmsWzLI=';
|
||||||
|
cookieDict.gid = 'yYWfJfi820jSyYWfJfdidiKK0YfuyikEvfISMAM348TEJC28K23TxI888WJK84q8S4WfY2Sy';
|
||||||
|
}
|
||||||
|
session.cookies = cookieDict;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main sign function
|
||||||
|
function sign(uri: string, data: any = null, options: { ctime?: number; a1?: string; b1?: string } = {}): {
|
||||||
|
'x-s': string;
|
||||||
|
'x-t': string;
|
||||||
|
'x-s-common': string;
|
||||||
|
} {
|
||||||
|
function h(n: string): string {
|
||||||
|
let m = '';
|
||||||
|
const d = 'A4NjFqYu5wPHsO0XTdDgMa2r1ZQocVte9UJBvk6/7=yRnhISGKblCWi+LpfE8xzm3';
|
||||||
|
for (let i = 0; i < 32; i += 3) {
|
||||||
|
const o = n.charCodeAt(i);
|
||||||
|
const g = i + 1 < 32 ? n.charCodeAt(i + 1) : 0;
|
||||||
|
const h = i + 2 < 32 ? n.charCodeAt(i + 2) : 0;
|
||||||
|
const x = ((o & 3) << 4) | (g >> 4);
|
||||||
|
let p = ((15 & g) << 2) | (h >> 6);
|
||||||
|
const v = o >> 2;
|
||||||
|
let b = h ? h & 63 : 64;
|
||||||
|
if (!g) {
|
||||||
|
p = 64;
|
||||||
|
b = 64;
|
||||||
|
}
|
||||||
|
m += d[v] + d[x] + d[p] + d[b];
|
||||||
|
}
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
|
const v = options.ctime || Math.floor(Date.now());
|
||||||
|
const rawStr = `${v}test${uri}${
|
||||||
|
data && typeof data === 'object'
|
||||||
|
? JSON.stringify(data, null, '')
|
||||||
|
: data || ''
|
||||||
|
}`;
|
||||||
|
const md5Str = createHash('md5').update(rawStr).digest('hex');
|
||||||
|
const x_s = h(md5Str);
|
||||||
|
const x_t = v.toString();
|
||||||
|
|
||||||
|
const common = {
|
||||||
|
s0: 5,
|
||||||
|
s1: '',
|
||||||
|
x0: '1',
|
||||||
|
x1: '3.2.0',
|
||||||
|
x2: 'Windows',
|
||||||
|
x3: 'xhs-pc-web',
|
||||||
|
x4: '2.3.1',
|
||||||
|
x5: options.a1 || '',
|
||||||
|
x6: x_t,
|
||||||
|
x7: x_s,
|
||||||
|
x8: options.b1 || '',
|
||||||
|
x9: mrc(x_t + x_s),
|
||||||
|
x10: 1,
|
||||||
|
};
|
||||||
|
const encodeStr = encodeUtf8(JSON.stringify(common, null, ''));
|
||||||
|
const x_s_common = b64Encode(encodeStr);
|
||||||
|
return {
|
||||||
|
'x-s': x_s,
|
||||||
|
'x-t': x_t,
|
||||||
|
'x-s-common': x_s_common,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export all functions
|
||||||
|
export {
|
||||||
|
sign,
|
||||||
|
getA1AndWebId,
|
||||||
|
getImgUrlByTraceId,
|
||||||
|
getImgUrlsByTraceId,
|
||||||
|
getTraceId,
|
||||||
|
getImgsUrlFromNote,
|
||||||
|
getImgsUrlsFromNote,
|
||||||
|
getVideoUrlFromNote,
|
||||||
|
getVideoUrlsFromNote,
|
||||||
|
downloadFile,
|
||||||
|
getValidPathName,
|
||||||
|
mrc,
|
||||||
|
b64Encode,
|
||||||
|
encodeUtf8,
|
||||||
|
base36encode,
|
||||||
|
base36decode,
|
||||||
|
parseXml,
|
||||||
|
getSearchId,
|
||||||
|
cookieStrToCookieDict,
|
||||||
|
cookieJarToCookieStr,
|
||||||
|
updateSessionCookiesFromCookie,
|
||||||
|
};
|
9
packages/xhs-core/readme.md
Normal file
9
packages/xhs-core/readme.md
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
# sign 部分依赖
|
||||||
|
|
||||||
|
https://github.com/saifeiLee/xhs-js
|
||||||
|
|
||||||
|
## 继承核心模块去开发
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { XhsClient } from '@kevisual/xhs-core';
|
||||||
|
```
|
45
packages/xhs-core/src/exception.js
Normal file
45
packages/xhs-core/src/exception.js
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
// ErrorTuple and ErrorEnum
|
||||||
|
const ErrorEnum = {
|
||||||
|
IP_BLOCK: { code: 300012, 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 };
|
259
packages/xhs-core/src/helper.js
Normal file
259
packages/xhs-core/src/helper.js
Normal file
@ -0,0 +1,259 @@
|
|||||||
|
const lookup = [
|
||||||
|
"Z",
|
||||||
|
"m",
|
||||||
|
"s",
|
||||||
|
"e",
|
||||||
|
"r",
|
||||||
|
"b",
|
||||||
|
"B",
|
||||||
|
"o",
|
||||||
|
"H",
|
||||||
|
"Q",
|
||||||
|
"t",
|
||||||
|
"N",
|
||||||
|
"P",
|
||||||
|
"+",
|
||||||
|
"w",
|
||||||
|
"O",
|
||||||
|
"c",
|
||||||
|
"z",
|
||||||
|
"a",
|
||||||
|
"/",
|
||||||
|
"L",
|
||||||
|
"p",
|
||||||
|
"n",
|
||||||
|
"g",
|
||||||
|
"G",
|
||||||
|
"8",
|
||||||
|
"y",
|
||||||
|
"J",
|
||||||
|
"q",
|
||||||
|
"4",
|
||||||
|
"2",
|
||||||
|
"K",
|
||||||
|
"W",
|
||||||
|
"Y",
|
||||||
|
"j",
|
||||||
|
"0",
|
||||||
|
"D",
|
||||||
|
"S",
|
||||||
|
"f",
|
||||||
|
"d",
|
||||||
|
"i",
|
||||||
|
"k",
|
||||||
|
"x",
|
||||||
|
"3",
|
||||||
|
"V",
|
||||||
|
"T",
|
||||||
|
"1",
|
||||||
|
"6",
|
||||||
|
"I",
|
||||||
|
"l",
|
||||||
|
"U",
|
||||||
|
"A",
|
||||||
|
"F",
|
||||||
|
"M",
|
||||||
|
"9",
|
||||||
|
"7",
|
||||||
|
"h",
|
||||||
|
"E",
|
||||||
|
"C",
|
||||||
|
"v",
|
||||||
|
"u",
|
||||||
|
"R",
|
||||||
|
"X",
|
||||||
|
"5",
|
||||||
|
]
|
||||||
|
|
||||||
|
function encodeUtf8(e) {
|
||||||
|
const b = [];
|
||||||
|
const m = encodeURIComponent(e).replace(/[!'()*]/g, c => `%${c.charCodeAt(0).toString(16).toUpperCase()}`);
|
||||||
|
let w = 0;
|
||||||
|
|
||||||
|
while (w < m.length) {
|
||||||
|
const T = m[w];
|
||||||
|
if (T === "%") {
|
||||||
|
const E = m.slice(w + 1, w + 3);
|
||||||
|
const S = parseInt(E, 16);
|
||||||
|
b.push(S);
|
||||||
|
w += 2;
|
||||||
|
} else {
|
||||||
|
b.push(T.charCodeAt(0));
|
||||||
|
}
|
||||||
|
w += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mrc(e) {
|
||||||
|
const ie = [
|
||||||
|
0, 1996959894, 3993919788, 2567524794, 124634137, 1886057615, 3915621685,
|
||||||
|
2657392035, 249268274, 2044508324, 3772115230, 2547177864, 162941995,
|
||||||
|
2125561021, 3887607047, 2428444049, 498536548, 1789927666, 4089016648,
|
||||||
|
2227061214, 450548861, 1843258603, 4107580753, 2211677639, 325883990,
|
||||||
|
1684777152, 4251122042, 2321926636, 335633487, 1661365465, 4195302755,
|
||||||
|
2366115317, 997073096, 1281953886, 3579855332, 2724688242, 1006888145,
|
||||||
|
1258607687, 3524101629, 2768942443, 901097722, 1119000684, 3686517206,
|
||||||
|
2898065728, 853044451, 1172266101, 3705015759, 2882616665, 651767980,
|
||||||
|
1373503546, 3369554304, 3218104598, 565507253, 1454621731, 3485111705,
|
||||||
|
3099436303, 671266974, 1594198024, 3322730930, 2970347812, 795835527,
|
||||||
|
1483230225, 3244367275, 3060149565, 1994146192, 31158534, 2563907772,
|
||||||
|
4023717930, 1907459465, 112637215, 2680153253, 3904427059, 2013776290,
|
||||||
|
251722036, 2517215374, 3775830040, 2137656763, 141376813, 2439277719,
|
||||||
|
3865271297, 1802195444, 476864866, 2238001368, 4066508878, 1812370925,
|
||||||
|
453092731, 2181625025, 4111451223, 1706088902, 314042704, 2344532202,
|
||||||
|
4240017532, 1658658271, 366619977, 2362670323, 4224994405, 1303535960,
|
||||||
|
984961486, 2747007092, 3569037538, 1256170817, 1037604311, 2765210733,
|
||||||
|
3554079995, 1131014506, 879679996, 2909243462, 3663771856, 1141124467,
|
||||||
|
855842277, 2852801631, 3708648649, 1342533948, 654459306, 3188396048,
|
||||||
|
3373015174, 1466479909, 544179635, 3110523913, 3462522015, 1591671054,
|
||||||
|
702138776, 2966460450, 3352799412, 1504918807, 783551873, 3082640443,
|
||||||
|
3233442989, 3988292384, 2596254646, 62317068, 1957810842, 3939845945,
|
||||||
|
2647816111, 81470997, 1943803523, 3814918930, 2489596804, 225274430,
|
||||||
|
2053790376, 3826175755, 2466906013, 167816743, 2097651377, 4027552580,
|
||||||
|
2265490386, 503444072, 1762050814, 4150417245, 2154129355, 426522225,
|
||||||
|
1852507879, 4275313526, 2312317920, 282753626, 1742555852, 4189708143,
|
||||||
|
2394877945, 397917763, 1622183637, 3604390888, 2714866558, 953729732,
|
||||||
|
1340076626, 3518719985, 2797360999, 1068828381, 1219638859, 3624741850,
|
||||||
|
2936675148, 906185462, 1090812512, 3747672003, 2825379669, 829329135,
|
||||||
|
1181335161, 3412177804, 3160834842, 628085408, 1382605366, 3423369109,
|
||||||
|
3138078467, 570562233, 1426400815, 3317316542, 2998733608, 733239954,
|
||||||
|
1555261956, 3268935591, 3050360625, 752459403, 1541320221, 2607071920,
|
||||||
|
3965973030, 1969922972, 40735498, 2617837225, 3943577151, 1913087877,
|
||||||
|
83908371, 2512341634, 3803740692, 2075208622, 213261112, 2463272603,
|
||||||
|
3855990285, 2094854071, 198958881, 2262029012, 4057260610, 1759359992,
|
||||||
|
534414190, 2176718541, 4139329115, 1873836001, 414664567, 2282248934,
|
||||||
|
4279200368, 1711684554, 285281116, 2405801727, 4167216745, 1634467795,
|
||||||
|
376229701, 2685067896, 3608007406, 1308918612, 956543938, 2808555105,
|
||||||
|
3495958263, 1231636301, 1047427035, 2932959818, 3654703836, 1088359270,
|
||||||
|
936918000, 2847714899, 3736837829, 1202900863, 817233897, 3183342108,
|
||||||
|
3401237130, 1404277552, 615818150, 3134207493, 3453421203, 1423857449,
|
||||||
|
601450431, 3009837614, 3294710456, 1567103746, 711928724, 3020668471,
|
||||||
|
3272380065, 1510334235, 755167117,
|
||||||
|
]
|
||||||
|
let o = -1
|
||||||
|
|
||||||
|
function rightWithoutSign(num, bit = 0) {
|
||||||
|
return (num >>> bit) | 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let n = 0; n < 57; n++) {
|
||||||
|
o = ie[(o & 255) ^ e.charCodeAt(n)] ^ rightWithoutSign(o, 8);
|
||||||
|
}
|
||||||
|
return o ^ -1 ^ 3988292384;
|
||||||
|
}
|
||||||
|
|
||||||
|
function encodeChunk(e, t, r) {
|
||||||
|
const m = [];
|
||||||
|
for (let b = t; b < r; b += 3) {
|
||||||
|
const n = (16711680 & (e[b] << 16)) +
|
||||||
|
((e[b + 1] << 8) & 65280) +
|
||||||
|
(e[b + 2] & 255);
|
||||||
|
m.push(tripletToBase64(n));
|
||||||
|
}
|
||||||
|
return m.join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function tripletToBase64(e) {
|
||||||
|
return (
|
||||||
|
lookup[63 & (e >> 18)] +
|
||||||
|
lookup[63 & (e >> 12)] +
|
||||||
|
lookup[(e >> 6) & 63] +
|
||||||
|
lookup[e & 63]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
function b64Encode(e) {
|
||||||
|
const P = e.length;
|
||||||
|
const W = P % 3;
|
||||||
|
const U = [];
|
||||||
|
const z = 16383;
|
||||||
|
let H = 0;
|
||||||
|
const Z = P - W;
|
||||||
|
|
||||||
|
while (H < Z) {
|
||||||
|
U.push(encodeChunk(e, H, Math.min(H + z, Z)));
|
||||||
|
H += z;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (W === 1) {
|
||||||
|
const F = e[P - 1];
|
||||||
|
U.push(lookup[F >> 2] + lookup[(F << 4) & 63] + "==");
|
||||||
|
} else if (W === 2) {
|
||||||
|
const F = (e[P - 2] << 8) + e[P - 1];
|
||||||
|
U.push(lookup[F >> 10] +
|
||||||
|
lookup[63 & (F >> 4)] +
|
||||||
|
lookup[(F << 2) & 63] +
|
||||||
|
"=");
|
||||||
|
}
|
||||||
|
|
||||||
|
return U.join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export {
|
||||||
|
getXCommon,
|
||||||
|
encodeUtf8,
|
||||||
|
mrc,
|
||||||
|
encodeChunk
|
||||||
|
};
|
||||||
|
|
||||||
|
function getXCommon(a1="", b1 = "", xS = undefined, xT = undefined) {
|
||||||
|
const common = {
|
||||||
|
s0: 5, // getPlatformCode
|
||||||
|
s1: "",
|
||||||
|
x0: "1", // localStorage.getItem("b1b1")
|
||||||
|
x1: "3.7.8-2", // version
|
||||||
|
x2: "Windows",
|
||||||
|
x3: "xhs-pc-web",
|
||||||
|
x4: "4.27.7",
|
||||||
|
x5: a1, // cookie of a1
|
||||||
|
x6: xT,
|
||||||
|
x7: xS,
|
||||||
|
x8: b1, // localStorage.getItem("b1")
|
||||||
|
x9: mrc(xT + xS + b1),
|
||||||
|
x10: 1, // getSigCount
|
||||||
|
};
|
||||||
|
const encodeStr = encodeUtf8(JSON.stringify(common));
|
||||||
|
const x_s_common = b64Encode(encodeStr);
|
||||||
|
return x_s_common;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
// getXCommon,
|
||||||
|
// encodeUtf8,
|
||||||
|
// mrc,
|
||||||
|
// encodeChunk,
|
||||||
|
getSearchId,
|
||||||
|
SearchSortType,
|
||||||
|
SearchNoteType,
|
||||||
|
}
|
425
packages/xhs-core/src/index.js
Normal file
425
packages/xhs-core/src/index.js
Normal file
@ -0,0 +1,425 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
import qs from 'querystring';
|
||||||
|
import { get_xs } from './jsvmp/xhs';
|
||||||
|
import fs from 'fs';
|
||||||
|
|
||||||
|
import { getXCommon, getSearchId, SearchSortType, SearchNoteType } from './helper.js';
|
||||||
|
import { ErrorEnum, DataFetchError, IPBlockError, SignError, NeedVerifyError } from './exception';
|
||||||
|
|
||||||
|
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 {
|
||||||
|
/**
|
||||||
|
* 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.axiosInstance = axios.create({
|
||||||
|
timeout: this.timeout,
|
||||||
|
headers: {
|
||||||
|
'user-agent': this.userAgent,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (cookie) {
|
||||||
|
this.cookie = cookie;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getter for cookie
|
||||||
|
get cookie() {
|
||||||
|
return this.axiosInstance.defaults.headers.Cookie;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setter for cookie
|
||||||
|
set cookie(cookie) {
|
||||||
|
this.axiosInstance.defaults.headers.Cookie = cookie;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getter for cookieDict
|
||||||
|
get cookieDict() {
|
||||||
|
const cookieStr = this.axiosInstance.defaults.headers.Cookie;
|
||||||
|
return cookieStr ? qs.parse(cookieStr.replace(/; /g, '&')) : {};
|
||||||
|
}
|
||||||
|
|
||||||
|
_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);
|
||||||
|
// this.axiosInstance.defaults.headers['X-s'] = X_S;
|
||||||
|
// this.axiosInstance.defaults.headers['X-t'] = X_t;
|
||||||
|
// this.axiosInstance.defaults.headers['X-s-common'] = X_S_COMMON;
|
||||||
|
return {
|
||||||
|
headers: {
|
||||||
|
'X-s': X_S,
|
||||||
|
'X-t': X_t,
|
||||||
|
'X-s-common': X_S_COMMON,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
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 {};
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 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 = {}) {
|
||||||
|
try {
|
||||||
|
const response = await this.axiosInstance({ method, url, ...config });
|
||||||
|
if (!response.data) return response;
|
||||||
|
// console.log('response', response)
|
||||||
|
if (response.status === 471 || response.status === 461) {
|
||||||
|
const verifyType = response.headers['verifytype'];
|
||||||
|
const verifyUuid = response.headers['verifyuuid'];
|
||||||
|
throw new NeedVerifyError(`出现验证码,请求失败,Verifytype: ${verifyType},Verifyuuid: ${verifyUuid}`, response, verifyType, verifyUuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = response.data;
|
||||||
|
if (data.success) {
|
||||||
|
return data.data || data.success;
|
||||||
|
} else if (data.code === ErrorEnum.IP_BLOCK.code) {
|
||||||
|
throw new IPBlockError(ErrorEnum.IP_BLOCK.msg, response);
|
||||||
|
} else if (data.code === ErrorEnum.SIGN_FAULT.code) {
|
||||||
|
throw new SignError(ErrorEnum.SIGN_FAULT.msg, response);
|
||||||
|
} else {
|
||||||
|
throw new DataFetchError(data, response);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (error.response && (error.response.status === 471 || error.response.status) === 461) {
|
||||||
|
// Handle verification error
|
||||||
|
const verifyType = error.response.headers['verifytype'];
|
||||||
|
const verifyUuid = error.response.headers['verifyuuid'];
|
||||||
|
throw new NeedVerifyError(`出现验证码,请求失败,Verifytype: ${verifyType},Verifyuuid: ${verifyUuid}`, error.response, verifyType, verifyUuid);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @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 {*} [config.headers] - XSEC token for authentication
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
async get(uri, params = null, config = {}) {
|
||||||
|
if (params) {
|
||||||
|
uri = `${uri}?${qs.stringify(params)}`;
|
||||||
|
}
|
||||||
|
if (config.sign) {
|
||||||
|
await config.sign(uri, data, config);
|
||||||
|
} else {
|
||||||
|
const { headers } = this._preHeaders(uri, null);
|
||||||
|
config = { ...config, headers: { ...config.headers, ...headers } };
|
||||||
|
}
|
||||||
|
let isCreator = config?.isCreator ?? false;
|
||||||
|
let isCustomer = config?.isCustomer ?? false;
|
||||||
|
let endpoint = this._host;
|
||||||
|
if (isCustomer) {
|
||||||
|
endpoint = this._customerHost;
|
||||||
|
} else if (isCreator) {
|
||||||
|
endpoint = this._creatorHost;
|
||||||
|
}
|
||||||
|
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 {*} [config.headers] - XSEC token for authentication
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
async post(uri, data = null, config = {}) {
|
||||||
|
let jsonStr = data ? JSON.stringify(data) : null;
|
||||||
|
let endpoint = this._host;
|
||||||
|
if (config.sign) {
|
||||||
|
await config.sign(uri, data, config);
|
||||||
|
} else {
|
||||||
|
const { headers } = this._preHeaders(uri, data);
|
||||||
|
config = { ...config, headers: { ...config.headers, ...headers } };
|
||||||
|
}
|
||||||
|
let isCreator = config?.isCreator ?? false;
|
||||||
|
let isCustomer = config?.isCustomer ?? false;
|
||||||
|
if (isCustomer) {
|
||||||
|
endpoint = this._customerHost;
|
||||||
|
} else if (isCreator) {
|
||||||
|
endpoint = this._creatorHost;
|
||||||
|
}
|
||||||
|
if (data) {
|
||||||
|
return this.request('POST', `${endpoint}${uri}`, {
|
||||||
|
...config,
|
||||||
|
data: jsonStr,
|
||||||
|
headers: {
|
||||||
|
...config.headers,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return this.request('POST', `${endpoint}${uri}`, { ...config, data });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取笔记详情
|
||||||
|
* 注意: 需要xsec_token
|
||||||
|
* @param {string} noteId
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
async getNoteById(noteId, xsecToken, xsecSource = 'pc_feed') {
|
||||||
|
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);
|
||||||
|
return res.items[0].note_card;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching note:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getNoteByIdFromHtml(noteId, xsecToken, xsecSource = 'pc_feed') {
|
||||||
|
const url = `https://www.xiaohongshu.com/explore/${noteId}?xsec_token=${xsecToken}&xsec_source=${xsecSource}`;
|
||||||
|
let html = '';
|
||||||
|
try {
|
||||||
|
const response = await this.axiosInstance.get(url, {
|
||||||
|
headers: {
|
||||||
|
'user-agent': this.userAgent,
|
||||||
|
referer: 'https://www.xiaohongshu.com/',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
html = response.data;
|
||||||
|
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));
|
||||||
|
return 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);
|
||||||
|
fs.writeFileSync('a.html', html);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getSelfInfo() {
|
||||||
|
const uri = '/api/sns/web/v1/user/selfinfo';
|
||||||
|
return this.get(uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getSelfInfoV2() {
|
||||||
|
const uri = '/api/sns/web/v2/user/me';
|
||||||
|
return this.get(uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUserInfo(userId) {
|
||||||
|
const uri = '/api/sns/web/v1/user/otherinfo';
|
||||||
|
const params = {
|
||||||
|
target_user_id: userId,
|
||||||
|
};
|
||||||
|
return this.get(uri, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} keyword 关键词
|
||||||
|
* @param {number} page 页码
|
||||||
|
* @param {number} pageSize 分页查询的数量
|
||||||
|
* @param {string} sort 搜索的类型,分为: general, popularity_descending, time_descending
|
||||||
|
* @param {number} noteType 笔记类型
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
async getNoteByKeyword(keyword, page = 1, pageSize = 20, sort = SearchSortType.GENERAL, noteType = SearchNoteType.ALL) {
|
||||||
|
const uri = '/api/sns/web/v1/search/notes';
|
||||||
|
const data = {
|
||||||
|
keyword: keyword,
|
||||||
|
page: page,
|
||||||
|
page_size: pageSize,
|
||||||
|
search_id: getSearchId(),
|
||||||
|
sort: sort.value,
|
||||||
|
note_type: noteType.value,
|
||||||
|
image_formats: ['jpg', 'webp', 'avif'],
|
||||||
|
ext_flags: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.post(uri, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取笔记评论
|
||||||
|
* @param {string} noteId 笔记id
|
||||||
|
* @param {string} cursor 分页查询的下标,默认为""
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
async getNoteComments(noteId, cursor = '') {
|
||||||
|
const uri = '/api/sns/web/v2/comment/page';
|
||||||
|
const params = {
|
||||||
|
note_id: noteId,
|
||||||
|
cursor: cursor,
|
||||||
|
};
|
||||||
|
return this.get(uri, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户笔记
|
||||||
|
* @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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取账号@我通知
|
||||||
|
* @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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取点赞通知
|
||||||
|
* @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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取关注通知
|
||||||
|
* @param {*} num
|
||||||
|
* @param {*} cursor
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
async getFollowNotifications(num = 20, cursor = '') {
|
||||||
|
const uri = '/api/sns/web/v1/you/connections';
|
||||||
|
const params = { num: num, cursor: cursor };
|
||||||
|
return this.get(uri, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUserInfoFromHtml(userId) {
|
||||||
|
const url = `https://www.xiaohongshu.com/user/profile/${userId}`;
|
||||||
|
try {
|
||||||
|
const response = await this.axiosInstance.get(url, {
|
||||||
|
headers: {
|
||||||
|
'user-agent': this.userAgent,
|
||||||
|
referer: 'https://www.xiaohongshu.com/',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const html = response.data;
|
||||||
|
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);
|
||||||
|
const userBasicInfo = transformJsonKeys(parsedState).user.user_page_data.basic_info;
|
||||||
|
return userBasicInfo;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching user info:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { XhsClient };
|
2666
packages/xhs-core/src/jsvmp/xhs.js
Normal file
2666
packages/xhs-core/src/jsvmp/xhs.js
Normal file
File diff suppressed because one or more lines are too long
1
packages/xhs-core/test/common.ts
Normal file
1
packages/xhs-core/test/common.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export 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;"
|
7
packages/xhs-core/test/get_userinfo.ts
Normal file
7
packages/xhs-core/test/get_userinfo.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
// import { XhsClient } from '../src/index.js';
|
||||||
|
import { XhsClient } from '../dist/app';
|
||||||
|
import { cookie } from './common.ts';
|
||||||
|
const client = new XhsClient({ cookie } as any);
|
||||||
|
client.getSelfInfoV2().then((res) => {
|
||||||
|
console.log(res);
|
||||||
|
});
|
2
packages/xhs/.npmrc
Normal file
2
packages/xhs/.npmrc
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
//npm.xiongxiao.me/:_authToken=${ME_NPM_TOKEN}
|
||||||
|
//registry.npmjs.org/:_authToken=${NPM_TOKEN}
|
492
packages/xhs/a.html
Normal file
492
packages/xhs/a.html
Normal file
File diff suppressed because one or more lines are too long
24
packages/xhs/bun.config.mjs
Normal file
24
packages/xhs/bun.config.mjs
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
// @ts-check
|
||||||
|
// https://bun.sh/docs/bundler
|
||||||
|
// @ts-ignore
|
||||||
|
import pkg from './package.json';
|
||||||
|
// import { resolvePath as rp } from '@kevisual/use-config/env';
|
||||||
|
import path from 'path';
|
||||||
|
const rp = (resolvePath) => {
|
||||||
|
return path.resolve(process.cwd(), resolvePath);
|
||||||
|
};
|
||||||
|
// bun run src/index.ts --
|
||||||
|
await Bun.build({
|
||||||
|
target: 'node',
|
||||||
|
format: 'esm',
|
||||||
|
entrypoints: [rp('src/index.js')],
|
||||||
|
outdir: rp('./dist'),
|
||||||
|
naming: {
|
||||||
|
entry: 'app.mjs',
|
||||||
|
},
|
||||||
|
|
||||||
|
define: {
|
||||||
|
VERSION: JSON.stringify(pkg.version),
|
||||||
|
},
|
||||||
|
env: 'KEVISUAL_*',
|
||||||
|
});
|
27
packages/xhs/package.json
Normal file
27
packages/xhs/package.json
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"name": "@kevisual/xhs",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "",
|
||||||
|
"main": "app.mjs",
|
||||||
|
"types": "app.d.ts",
|
||||||
|
"scripts": {
|
||||||
|
"build": "bun run bun.config.mjs",
|
||||||
|
"postbuild": "dts -i src/index.js -o app.d.ts",
|
||||||
|
"dts": "dts -i src/index.js -o app.d.ts"
|
||||||
|
},
|
||||||
|
"file": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "abearxiong <xiongxiao@xiongxiao.me> (https://www.xiongxiao.me)",
|
||||||
|
"license": "MIT",
|
||||||
|
"packageManager": "pnpm@10.10.0",
|
||||||
|
"type": "module",
|
||||||
|
"dependencies": {},
|
||||||
|
"devDependencies": {
|
||||||
|
"@kevisual/xhs-core": "workspace:*"
|
||||||
|
}
|
||||||
|
}
|
66
packages/xhs/src/index.ts
Normal file
66
packages/xhs/src/index.ts
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import { XhsClient as XhsClientBase } from '@kevisual/xhs-core';
|
||||||
|
|
||||||
|
export 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;
|
||||||
|
};
|
||||||
|
type XhsOptions = {
|
||||||
|
cookie?: string;
|
||||||
|
};
|
||||||
|
export class XhsClient extends XhsClientBase {
|
||||||
|
constructor(opts: XhsOptions) {
|
||||||
|
super(opts as any);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 获取未读消息
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
async getUnread(): Promise<UnreadCount> {
|
||||||
|
const url = '/api/sns/web/unread_count';
|
||||||
|
const response = await this.get(url);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
async sign(uri: string, data: any, config: any) {
|
||||||
|
let header = config?.header || {};
|
||||||
|
const cookieDist = this.getCookieMap();
|
||||||
|
const web_session = cookieDist['web_session'];
|
||||||
|
const a1 = cookieDist['a1'];
|
||||||
|
const res = await getSign(uri, data, a1, web_session);
|
||||||
|
header['x-s'] = res['x-s'];
|
||||||
|
header['x-t'] = res['x-t'];
|
||||||
|
config.header = header;
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
async getNoteById(noteId: string, xsecToken?: string) {
|
||||||
|
const data = {
|
||||||
|
source_note_id: noteId,
|
||||||
|
image_scenes: ['CRD_WM_WEBP'],
|
||||||
|
};
|
||||||
|
const uri = '/api/sns/web/v1/feed';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await this.post(uri, data, { sign: this.sign.bind(this) });
|
||||||
|
return response['items'][0]['node_card'];
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type UnreadCount = {
|
||||||
|
unread_count: number;
|
||||||
|
likes: number;
|
||||||
|
connections: number;
|
||||||
|
mentions: number;
|
||||||
|
};
|
4
packages/xhs/src/test/common.ts
Normal file
4
packages/xhs/src/test/common.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import { XhsClient } from '../index.ts';
|
||||||
|
export 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;"
|
||||||
|
|
||||||
|
export const client = new XhsClient({ cookie } as any);
|
15
packages/xhs/src/test/query/get_note.ts
Normal file
15
packages/xhs/src/test/query/get_note.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { client } from '../common.ts';
|
||||||
|
|
||||||
|
// client.getNoteComments()
|
||||||
|
|
||||||
|
// client.getNoteById('67dcc34e000000000602a8eb', 'ABuYS8Xb1o08DlRmMLIabdqnW0OKnLR9nMpDGq5bVRdvk').then((res) => {
|
||||||
|
// console.log(res);
|
||||||
|
// });
|
||||||
|
// client.getNoteByIdFromHtml('67dcc34e000000000602a8eb', 'ABuYS8Xb1o08DlRmMLIabdqnW0OKnLR9nMpDGq5bVRdvk').then((res) => {
|
||||||
|
// console.log(res);
|
||||||
|
// });
|
||||||
|
const id = '6810d722000000002100f139';
|
||||||
|
const x = 'LBEqTFigLzp41AdwQ-E3hbQScnvrx2flLgHElHpQ8zHWc=';
|
||||||
|
client.getNoteByIdFromHtml(id, x).then((res) => {
|
||||||
|
console.log(res);
|
||||||
|
});
|
5
packages/xhs/src/test/query/get_userinfo.ts
Normal file
5
packages/xhs/src/test/query/get_userinfo.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { client } from '../common.ts';
|
||||||
|
|
||||||
|
client.getSelfInfoV2().then((res) => {
|
||||||
|
console.log(res);
|
||||||
|
});
|
5
packages/xhs/src/test/query/unread.ts
Normal file
5
packages/xhs/src/test/query/unread.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { client } from '../common.ts';
|
||||||
|
|
||||||
|
client.getUnread().then((res) => {
|
||||||
|
console.log(res);
|
||||||
|
});
|
843
pnpm-lock.yaml
generated
843
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
3
pnpm-workspace.yaml
Normal file
3
pnpm-workspace.yaml
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
packages:
|
||||||
|
- packages/*
|
||||||
|
- submodules/*
|
@ -1,75 +0,0 @@
|
|||||||
import resolve from '@rollup/plugin-node-resolve';
|
|
||||||
import commonjs from '@rollup/plugin-commonjs';
|
|
||||||
import json from '@rollup/plugin-json';
|
|
||||||
import path from 'path';
|
|
||||||
import esbuild from 'rollup-plugin-esbuild';
|
|
||||||
import alias from '@rollup/plugin-alias';
|
|
||||||
import replace from '@rollup/plugin-replace';
|
|
||||||
import pkgs from './package.json' with {type: 'json'};
|
|
||||||
|
|
||||||
const isDev = process.env.NODE_ENV === 'development';
|
|
||||||
const input = isDev ? './src/dev.ts' : './src/main.ts';
|
|
||||||
/**
|
|
||||||
* @type {import('rollup').RollupOptions}
|
|
||||||
*/
|
|
||||||
const config = {
|
|
||||||
input,
|
|
||||||
output: {
|
|
||||||
dir: './dist',
|
|
||||||
entryFileNames: 'app.mjs',
|
|
||||||
chunkFileNames: '[name]-[hash].mjs',
|
|
||||||
format: 'esm',
|
|
||||||
},
|
|
||||||
plugins: [
|
|
||||||
replace({
|
|
||||||
preventAssignment: true, // 防止意外赋值
|
|
||||||
DEV_SERVER: JSON.stringify(isDev), // 替换 process.env.NODE_ENV
|
|
||||||
APP_VERSION: JSON.stringify(pkgs.version),
|
|
||||||
}),
|
|
||||||
alias({
|
|
||||||
// only esbuild needs to be configured
|
|
||||||
entries: [
|
|
||||||
{ find: '@', replacement: path.resolve('src') }, // 配置 @ 为 src 目录
|
|
||||||
{ find: 'http', replacement: 'node:http' },
|
|
||||||
{ find: 'https', replacement: 'node:https' },
|
|
||||||
{ find: 'fs', replacement: 'node:fs' },
|
|
||||||
{ find: 'path', replacement: 'node:path' },
|
|
||||||
{ find: 'crypto', replacement: 'node:crypto' },
|
|
||||||
{ find: 'zlib', replacement: 'node:zlib' },
|
|
||||||
{ find: 'stream', replacement: 'node:stream' },
|
|
||||||
{ find: 'net', replacement: 'node:net' },
|
|
||||||
{ find: 'tty', replacement: 'node:tty' },
|
|
||||||
{ find: 'tls', replacement: 'node:tls' },
|
|
||||||
{ find: 'buffer', replacement: 'node:buffer' },
|
|
||||||
{ find: 'timers', replacement: 'node:timers' },
|
|
||||||
// { find: 'string_decoder', replacement: 'node:string_decoder' },
|
|
||||||
{ find: 'dns', replacement: 'node:dns' },
|
|
||||||
{ find: 'domain', replacement: 'node:domain' },
|
|
||||||
{ find: 'os', replacement: 'node:os' },
|
|
||||||
{ find: 'events', replacement: 'node:events' },
|
|
||||||
{ find: 'url', replacement: 'node:url' },
|
|
||||||
{ find: 'assert', replacement: 'node:assert' },
|
|
||||||
{ find: 'util', replacement: 'node:util' },
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
resolve({
|
|
||||||
preferBuiltins: true, // 强制优先使用内置模块
|
|
||||||
}),
|
|
||||||
commonjs(),
|
|
||||||
esbuild({
|
|
||||||
target: 'node22', //
|
|
||||||
minify: false, // 启用代码压缩
|
|
||||||
tsconfig: 'tsconfig.json',
|
|
||||||
}),
|
|
||||||
json(),
|
|
||||||
],
|
|
||||||
external: [
|
|
||||||
/@kevisual\/router(\/.*)?/, //, // 路由
|
|
||||||
/@kevisual\/use-config(\/.*)?/, //
|
|
||||||
|
|
||||||
'sequelize', // 数据库 orm
|
|
||||||
'ioredis', // redis
|
|
||||||
'pg', // pg
|
|
||||||
],
|
|
||||||
};
|
|
||||||
export default config;
|
|
0
src/modules/bullmq.ts
Normal file
0
src/modules/bullmq.ts
Normal file
@ -1,5 +1,6 @@
|
|||||||
import { Redis } from 'ioredis';
|
import { Redis } from 'ioredis';
|
||||||
import { config } from './config.ts';
|
import { config } from './config.ts';
|
||||||
|
import { useContextKey } from '@kevisual/use-config/context';
|
||||||
|
|
||||||
type initRedisOpts = {
|
type initRedisOpts = {
|
||||||
onConnect?: () => void; // 连接成功的回调函数
|
onConnect?: () => void; // 连接成功的回调函数
|
||||||
@ -36,7 +37,7 @@ const initRedis = (config?: any, options?: initRedisOpts) => {
|
|||||||
return redis;
|
return redis;
|
||||||
};
|
};
|
||||||
// 配置 Redis 连接
|
// 配置 Redis 连接
|
||||||
export const redis = useConfigKey('redis', () => initRedis(config));
|
export const redis = useContextKey('redis', () => initRedis(config));
|
||||||
|
|
||||||
// 初始化 Redis 客户端
|
// 初始化 Redis 客户端
|
||||||
export const redisPublisher = new Redis(); // 用于发布消息
|
export const redisPublisher = new Redis(); // 用于发布消息
|
||||||
|
2
src/task/common.ts
Normal file
2
src/task/common.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
// https://edith.xiaohongshu.com/api/sns/web/unread_count
|
||||||
|
export const XHS_GET_UNREAD = 'unread_count';
|
0
src/task/index.ts
Normal file
0
src/task/index.ts
Normal file
43
src/task/task.ts
Normal file
43
src/task/task.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import { redis } from '@/modules/redis.ts';
|
||||||
|
import { Queue } from 'bullmq';
|
||||||
|
import { nanoid } from 'nanoid';
|
||||||
|
const XHS_QUEUE_NAME = 'XHS_QUEUE';
|
||||||
|
export const queue = new Queue(XHS_QUEUE_NAME, {
|
||||||
|
connection: redis,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 初始启动
|
||||||
|
async function start() {
|
||||||
|
const res = await queue.add(
|
||||||
|
'start-job',
|
||||||
|
{
|
||||||
|
name: 'initialJob',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
delay: 0, // 立即执行
|
||||||
|
removeOnComplete: true,
|
||||||
|
removeOnFail: true,
|
||||||
|
jobId: nanoid(), // 使用 nanoid 生成唯一 ID
|
||||||
|
},
|
||||||
|
);
|
||||||
|
console.log('Queue started:', res.id);
|
||||||
|
}
|
||||||
|
//
|
||||||
|
start();
|
||||||
|
const getTasks = async () => {
|
||||||
|
const tasks = await queue.getJobs(['waiting', 'active', 'completed', 'failed']);
|
||||||
|
return tasks;
|
||||||
|
};
|
||||||
|
const getTask = async (id: string) => {
|
||||||
|
const task = await queue.getJob(id);
|
||||||
|
return task;
|
||||||
|
};
|
||||||
|
const removeTask = async (id: string) => {
|
||||||
|
const task = await queue.getJob(id);
|
||||||
|
if (task) {
|
||||||
|
await task.remove();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// const task = await getTask('4');
|
||||||
|
// console.log('task', task);
|
52
src/task/worker.ts
Normal file
52
src/task/worker.ts
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import { redis } from '@/modules/redis.ts';
|
||||||
|
import { Queue, Worker } from 'bullmq';
|
||||||
|
import { clamp } from 'lodash-es';
|
||||||
|
import { nanoid } from 'nanoid';
|
||||||
|
|
||||||
|
const XHS_QUEUE_NAME = 'XHS_QUEUE';
|
||||||
|
export const queue = new Queue(XHS_QUEUE_NAME);
|
||||||
|
export const sleep = (ms: number) => {
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
};
|
||||||
|
export const worker = new Worker(
|
||||||
|
XHS_QUEUE_NAME,
|
||||||
|
async (job) => {
|
||||||
|
const startTime = Date.now();
|
||||||
|
console.log('job', job.name, job.data);
|
||||||
|
await sleep(1000);
|
||||||
|
const endTime = Date.now();
|
||||||
|
const duration = endTime - startTime;
|
||||||
|
|
||||||
|
return {
|
||||||
|
startTime: startTime,
|
||||||
|
endTime: endTime,
|
||||||
|
duration: duration,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{ connection: redis, concurrency: 1 },
|
||||||
|
);
|
||||||
|
worker.on('completed', async (job) => {
|
||||||
|
console.log('job completed', job.name, job.id, job.returnvalue);
|
||||||
|
const duration = job.returnvalue.duration || 0;
|
||||||
|
const maxNextTime = 20 * 1000; // 5 minutes
|
||||||
|
const nextTime = clamp(maxNextTime - duration, 0, maxNextTime);
|
||||||
|
const hasJobs = await queue.getJobCounts('waiting', 'wait', 'delayed');
|
||||||
|
console.log('hasJobs', nextTime, 'joblenght', hasJobs);
|
||||||
|
if (hasJobs.delayed + hasJobs.wait > 0) {
|
||||||
|
console.log('======has jobs, no need to add new job');
|
||||||
|
} else {
|
||||||
|
const id = nanoid();
|
||||||
|
queue.add(
|
||||||
|
'repeact-call-job' + id,
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
delay: nextTime,
|
||||||
|
removeOnComplete: true,
|
||||||
|
removeOnFail: {
|
||||||
|
age: 24 * 3600, // keep up to 24 hours
|
||||||
|
},
|
||||||
|
jobId: id,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
Loading…
x
Reference in New Issue
Block a user