specific-ai-cli 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (277) hide show
  1. package/LICENSE +21 -0
  2. package/README.en.md +177 -0
  3. package/README.ja.md +177 -0
  4. package/README.ko.md +177 -0
  5. package/README.md +174 -2
  6. package/README.zh-TW.md +177 -0
  7. package/assets/banner.txt +3 -0
  8. package/dist/cli/argv.js +98 -0
  9. package/dist/cli/commands/auth.js +11 -0
  10. package/dist/cli/commands/config.js +23 -0
  11. package/dist/cli/commands/doctor.js +134 -0
  12. package/dist/cli/commands/platforms.js +24 -0
  13. package/dist/cli/commands/version.js +4 -0
  14. package/dist/cli/confirm.js +22 -0
  15. package/dist/cli/dispatch.js +440 -0
  16. package/dist/cli/help.js +223 -0
  17. package/dist/cli/hints.js +60 -0
  18. package/dist/cli/main.js +18 -0
  19. package/dist/cli/output.js +66 -0
  20. package/dist/core/auth-handlers.js +41 -0
  21. package/dist/core/auth-store.js +119 -0
  22. package/dist/core/capabilities.js +163 -0
  23. package/dist/core/config.js +121 -0
  24. package/dist/core/cookies.js +164 -0
  25. package/dist/core/errors.js +37 -0
  26. package/dist/core/files.js +89 -0
  27. package/dist/core/fsutil.js +52 -0
  28. package/dist/core/hash.js +4 -0
  29. package/dist/core/http.js +331 -0
  30. package/dist/core/image.js +280 -0
  31. package/dist/core/log.js +40 -0
  32. package/dist/core/login.js +191 -0
  33. package/dist/core/mp4.js +130 -0
  34. package/dist/core/normalize.js +207 -0
  35. package/dist/core/options.js +110 -0
  36. package/dist/core/paths.js +38 -0
  37. package/dist/core/pb.js +59 -0
  38. package/dist/core/py.js +181 -0
  39. package/dist/core/rand.js +131 -0
  40. package/dist/core/registry.js +129 -0
  41. package/dist/core/schemas.js +41 -0
  42. package/dist/core/stream.js +104 -0
  43. package/dist/core/toolkit.js +35 -0
  44. package/dist/core/vm.js +73 -0
  45. package/dist/core/vocab.js +157 -0
  46. package/dist/platforms/_shared/impaas.js +410 -0
  47. package/dist/platforms/bilibili/index.js +216 -0
  48. package/dist/platforms/bilibili/web/api.js +806 -0
  49. package/dist/platforms/bilibili/web/client.js +311 -0
  50. package/dist/platforms/bilibili/web/commands.js +783 -0
  51. package/dist/platforms/bilibili/web/ddddocr.js +342 -0
  52. package/dist/platforms/bilibili/web/gaia.js +130 -0
  53. package/dist/platforms/bilibili/web/geetest-manual.js +154 -0
  54. package/dist/platforms/bilibili/web/geetest-vision.js +232 -0
  55. package/dist/platforms/bilibili/web/geetest.js +450 -0
  56. package/dist/platforms/bilibili/web/normalize.js +234 -0
  57. package/dist/platforms/bilibili/web/profile.js +135 -0
  58. package/dist/platforms/bilibili/web/resolve.js +112 -0
  59. package/dist/platforms/bilibili/web/sign.js +223 -0
  60. package/dist/platforms/bilibili/web/upos.js +102 -0
  61. package/dist/platforms/douyin/index.js +142 -0
  62. package/dist/platforms/douyin/web/api.js +863 -0
  63. package/dist/platforms/douyin/web/client.js +413 -0
  64. package/dist/platforms/douyin/web/commands.js +712 -0
  65. package/dist/platforms/douyin/web/creator.js +601 -0
  66. package/dist/platforms/douyin/web/crypto.js +247 -0
  67. package/dist/platforms/douyin/web/dtrait.js +190 -0
  68. package/dist/platforms/douyin/web/im.js +502 -0
  69. package/dist/platforms/douyin/web/image.js +5 -0
  70. package/dist/platforms/douyin/web/live.js +225 -0
  71. package/dist/platforms/douyin/web/mssdk.js +148 -0
  72. package/dist/platforms/douyin/web/normalize.js +326 -0
  73. package/dist/platforms/douyin/web/passport.js +1247 -0
  74. package/dist/platforms/douyin/web/profile.js +221 -0
  75. package/dist/platforms/douyin/web/proto.js +26 -0
  76. package/dist/platforms/douyin/web/resolve.js +88 -0
  77. package/dist/platforms/douyin/web/sign.js +436 -0
  78. package/dist/platforms/douyin/web/tos.js +78 -0
  79. package/dist/platforms/index.js +17 -0
  80. package/dist/platforms/jd/index.js +67 -0
  81. package/dist/platforms/jd/web/aks.js +115 -0
  82. package/dist/platforms/jd/web/api.js +549 -0
  83. package/dist/platforms/jd/web/chat.js +132 -0
  84. package/dist/platforms/jd/web/client.js +470 -0
  85. package/dist/platforms/jd/web/commands.js +557 -0
  86. package/dist/platforms/jd/web/jcap/helper.js +90 -0
  87. package/dist/platforms/jd/web/jcap/host.js +107 -0
  88. package/dist/platforms/jd/web/jcap/lsd.js +300 -0
  89. package/dist/platforms/jd/web/jcap/solver.js +995 -0
  90. package/dist/platforms/jd/web/jcap.js +150 -0
  91. package/dist/platforms/jd/web/js.js +420 -0
  92. package/dist/platforms/jd/web/login.js +775 -0
  93. package/dist/platforms/jd/web/normalize.js +297 -0
  94. package/dist/platforms/jd/web/profile.js +256 -0
  95. package/dist/platforms/jd/web/util.js +175 -0
  96. package/dist/platforms/jd/web/webm.js +133 -0
  97. package/dist/platforms/kuaishou/index.js +98 -0
  98. package/dist/platforms/kuaishou/web/api.js +694 -0
  99. package/dist/platforms/kuaishou/web/captcha-crypto.js +134 -0
  100. package/dist/platforms/kuaishou/web/captcha-fp.js +180 -0
  101. package/dist/platforms/kuaishou/web/captcha.js +273 -0
  102. package/dist/platforms/kuaishou/web/client.js +487 -0
  103. package/dist/platforms/kuaishou/web/commands.js +863 -0
  104. package/dist/platforms/kuaishou/web/gap.js +144 -0
  105. package/dist/platforms/kuaishou/web/gdfp.js +376 -0
  106. package/dist/platforms/kuaishou/web/gql.js +11 -0
  107. package/dist/platforms/kuaishou/web/live.js +125 -0
  108. package/dist/platforms/kuaishou/web/normalize.js +241 -0
  109. package/dist/platforms/kuaishou/web/oracle.js +329 -0
  110. package/dist/platforms/kuaishou/web/profile.js +259 -0
  111. package/dist/platforms/kuaishou/web/resolve.js +84 -0
  112. package/dist/platforms/kuaishou/web/session.js +592 -0
  113. package/dist/platforms/kuaishou/web/sign.js +568 -0
  114. package/dist/platforms/taobao/index.js +56 -0
  115. package/dist/platforms/taobao/web/api.js +96 -0
  116. package/dist/platforms/taobao/web/client.js +102 -0
  117. package/dist/platforms/taobao/web/commands.js +172 -0
  118. package/dist/platforms/taobao/web/im.js +68 -0
  119. package/dist/platforms/taobao/web/normalize.js +103 -0
  120. package/dist/platforms/taobao/web/profile.js +85 -0
  121. package/dist/platforms/taobao/web/resolve.js +51 -0
  122. package/dist/platforms/taobao/web/sign.js +35 -0
  123. package/dist/platforms/tiktok/index.js +120 -0
  124. package/dist/platforms/tiktok/web/api.js +1234 -0
  125. package/dist/platforms/tiktok/web/client.js +362 -0
  126. package/dist/platforms/tiktok/web/commands.js +821 -0
  127. package/dist/platforms/tiktok/web/im.js +133 -0
  128. package/dist/platforms/tiktok/web/jsrun.js +126 -0
  129. package/dist/platforms/tiktok/web/normalize.js +286 -0
  130. package/dist/platforms/tiktok/web/profile.js +169 -0
  131. package/dist/platforms/tiktok/web/resolve.js +156 -0
  132. package/dist/platforms/tiktok/web/sign.js +585 -0
  133. package/dist/platforms/tiktok/web/upload.js +805 -0
  134. package/dist/platforms/tiktok/web/wire.js +490 -0
  135. package/dist/platforms/weibo/index.js +91 -0
  136. package/dist/platforms/weibo/web/api.js +206 -0
  137. package/dist/platforms/weibo/web/client.js +155 -0
  138. package/dist/platforms/weibo/web/commands.js +202 -0
  139. package/dist/platforms/weibo/web/normalize.js +137 -0
  140. package/dist/platforms/weibo/web/profile.js +246 -0
  141. package/dist/platforms/weibo/web/resolve.js +60 -0
  142. package/dist/platforms/weibo/web/sign.js +85 -0
  143. package/dist/platforms/x/index.js +107 -0
  144. package/dist/platforms/x/web/api.js +443 -0
  145. package/dist/platforms/x/web/article.js +212 -0
  146. package/dist/platforms/x/web/client.js +202 -0
  147. package/dist/platforms/x/web/commands.js +363 -0
  148. package/dist/platforms/x/web/graphql.js +108 -0
  149. package/dist/platforms/x/web/normalize.js +235 -0
  150. package/dist/platforms/x/web/profile.js +99 -0
  151. package/dist/platforms/x/web/resolve.js +44 -0
  152. package/dist/platforms/x/web/transaction.js +171 -0
  153. package/dist/platforms/xhs/index.js +101 -0
  154. package/dist/platforms/xhs/web/api.js +278 -0
  155. package/dist/platforms/xhs/web/client.js +540 -0
  156. package/dist/platforms/xhs/web/commands.js +1046 -0
  157. package/dist/platforms/xhs/web/creator-api.js +609 -0
  158. package/dist/platforms/xhs/web/creator.js +203 -0
  159. package/dist/platforms/xhs/web/js.js +194 -0
  160. package/dist/platforms/xhs/web/login.js +230 -0
  161. package/dist/platforms/xhs/web/normalize.js +166 -0
  162. package/dist/platforms/xhs/web/profile.js +127 -0
  163. package/dist/platforms/xhs/web/push.js +361 -0
  164. package/dist/platforms/xhs/web/resolve.js +69 -0
  165. package/dist/platforms/xhs/web/state.js +368 -0
  166. package/dist/platforms/xianyu/index.js +59 -0
  167. package/dist/platforms/xianyu/web/api.js +312 -0
  168. package/dist/platforms/xianyu/web/client.js +134 -0
  169. package/dist/platforms/xianyu/web/commands.js +305 -0
  170. package/dist/platforms/xianyu/web/im.js +73 -0
  171. package/dist/platforms/xianyu/web/normalize.js +123 -0
  172. package/dist/platforms/xianyu/web/profile.js +146 -0
  173. package/dist/platforms/xianyu/web/resolve.js +48 -0
  174. package/dist/platforms/xianyu/web/sign.js +79 -0
  175. package/package.json +77 -4
  176. package/static/douyin/Live.proto +194 -0
  177. package/static/douyin/Request.proto +89 -0
  178. package/static/douyin/Response.proto +46 -0
  179. package/static/douyin/acrawler_runtime/ac_vm.js +2 -0
  180. package/static/douyin/acrawler_runtime/browser_window_shape.json +1 -0
  181. package/static/douyin/acrawler_runtime/canvas_actual_exact.json +1 -0
  182. package/static/douyin/acrawler_runtime/run_ac_node.js +1005 -0
  183. package/static/douyin/acrawler_runtime/runner_window_keys_baseline.txt +240 -0
  184. package/static/douyin/challenge_profile.json +801 -0
  185. package/static/douyin/challenge_template_runner.js +217 -0
  186. package/static/douyin/dtrait_profile.json +66 -0
  187. package/static/douyin/mstoken_common_profile.json +1 -0
  188. package/static/douyin/mstoken_profile.json +214 -0
  189. package/static/jd/h5st5_env.js +750 -0
  190. package/static/jd/h5st5_lib.js +5 -0
  191. package/static/jd/h5st5_server.js +49 -0
  192. package/static/jd/jcap/env/env_core.js +512 -0
  193. package/static/jd/jcap/env/run.js +1307 -0
  194. package/static/jd/jcap/run/jcap_ujb96b.js +39 -0
  195. package/static/jd/package.json +4 -0
  196. package/static/jd/pc_tk_lib.js +1 -0
  197. package/static/jd/pc_tk_server.js +63 -0
  198. package/static/jd/summer_cryptico_runner.js +50 -0
  199. package/static/jd/webm/env/run.js +453 -0
  200. package/static/jd/webm/run/jdwebm-riskhandle.js +49 -0
  201. package/static/kuaishou/bundles/weapon/kwf-0.0.2.2cee19b4b7dec496.js +1 -0
  202. package/static/kuaishou/bundles/weapon/kws-0-0.0.1-obfuscated.4b7f3de1ab0ddcc2.js +1 -0
  203. package/static/kuaishou/bundles/weapon/kws-1-0.0.1-obfuscated.346ebdb1c78df316.js +1 -0
  204. package/static/kuaishou/bundles/weapon/kws-10-0.0.1-obfuscated.a33db41897b983af.js +1 -0
  205. package/static/kuaishou/bundles/weapon/kws-11-0.0.1-obfuscated.5e0a90af726d8a7e.js +1 -0
  206. package/static/kuaishou/bundles/weapon/kws-12-0.0.1-obfuscated.20ea29345706ec8c.js +1 -0
  207. package/static/kuaishou/bundles/weapon/kws-13-0.0.1-obfuscated.6b74e9640ff18648.js +1 -0
  208. package/static/kuaishou/bundles/weapon/kws-14-0.0.1-obfuscated.551e4429d156bf66.js +1 -0
  209. package/static/kuaishou/bundles/weapon/kws-15-0.0.1-obfuscated.8625b087aaba4a60.js +1 -0
  210. package/static/kuaishou/bundles/weapon/kws-16-0.0.1-obfuscated.d5ffa4020f3be99a.js +1 -0
  211. package/static/kuaishou/bundles/weapon/kws-17-0.0.1-obfuscated.6d13e19b22f50a80.js +1 -0
  212. package/static/kuaishou/bundles/weapon/kws-18-0.0.1-obfuscated.6d31405044166289.js +1 -0
  213. package/static/kuaishou/bundles/weapon/kws-19-0.0.1-obfuscated.a9a8d925942f19e4.js +1 -0
  214. package/static/kuaishou/bundles/weapon/kws-2-0.0.1-obfuscated.b849a136ae4a6b3e.js +1 -0
  215. package/static/kuaishou/bundles/weapon/kws-3-0.0.1-obfuscated.f9e77ebf6186e2ac.js +1 -0
  216. package/static/kuaishou/bundles/weapon/kws-4-0.0.1-obfuscated.2c3c155aa52ceb0e.js +1 -0
  217. package/static/kuaishou/bundles/weapon/kws-5-0.0.1-obfuscated.4e553d17691b3c5a.js +1 -0
  218. package/static/kuaishou/bundles/weapon/kws-6-0.0.1-obfuscated.f73ddd3ff13e1217.js +1 -0
  219. package/static/kuaishou/bundles/weapon/kws-7-0.0.1-obfuscated.1628b8729beaab7e.js +1 -0
  220. package/static/kuaishou/bundles/weapon/kws-8-0.0.1-obfuscated.6da6baef7aa81fff.js +1 -0
  221. package/static/kuaishou/bundles/weapon/kws-9-0.0.1-obfuscated.8645d55e5323b743.js +1 -0
  222. package/static/kuaishou/fixtures/current_follow_script_813.network-response +2 -0
  223. package/static/kuaishou/fixtures/live_ws_maps.json +61 -0
  224. package/static/kuaishou/fixtures/live_ws_proto.json +1342 -0
  225. package/static/kuaishou/js/cp-kwf.js +1 -0
  226. package/static/kuaishou/tools/kwf_oracle.js +799 -0
  227. package/static/kuaishou/tools/kws_oracle.js +885 -0
  228. package/static/kuaishou/tools/like_token_oracle.js +70 -0
  229. package/static/taobao/taobao_js_20260407.js +570 -0
  230. package/static/tiktok/Tiktok_Request.json +597 -0
  231. package/static/tiktok/package.json +3 -0
  232. package/static/tiktok/shop_bsid/core.js +1 -0
  233. package/static/tiktok/shop_bsid/env_core.js +512 -0
  234. package/static/tiktok/shop_bsid/loader.js +1 -0
  235. package/static/tiktok/shop_bsid/run.js +753 -0
  236. package/static/tiktok/shop_bsid/sign.js +100 -0
  237. package/static/tiktok/shop_bsid/webmssdk.js +1 -0
  238. package/static/tiktok/shop_bsid/webmssdk_ex.js +1 -0
  239. package/static/tiktok/signing/env/env_core.js +512 -0
  240. package/static/tiktok/signing/env/sign.js +455 -0
  241. package/static/tiktok/signing/sdk_config.json +230 -0
  242. package/static/tiktok/signing/source/webmssdk.js +1 -0
  243. package/static/tiktok/signing/source/webmssdk_ex.js +1 -0
  244. package/static/tiktok/signing/source/webmssdk_legacy.js +1 -0
  245. package/static/x/graphql.json +7777 -0
  246. package/static/x/transaction_l1.json +12 -0
  247. package/static/xhs/package.json +3 -0
  248. package/static/xhs/xhs_core/js/b1.js +210 -0
  249. package/static/xhs/xhs_core/js/mns.js +340 -0
  250. package/static/xhs/xhs_core/js/mns_0101_keystream.json +5 -0
  251. package/static/xhs/xhs_core/js/mns_keystreams.json +1 -0
  252. package/static/xhs/xhs_core/js/sign.js +225 -0
  253. package/static/xhs/xhs_core/js/websectiga_cli.js +220 -0
  254. package/static/xhs/xhs_core/js/websectiga_env.js +512 -0
  255. package/static/xhs/xhs_core/js/xs_common.js +79 -0
  256. package/static/xhs/xhs_creator/js/profile.js +115 -0
  257. package/static/xhs/xhs_creator/js/rap_fingerprint_creator.json +6 -0
  258. package/static/xhs/xhs_creator/js/reference_profile.json +282 -0
  259. package/static/xhs/xhs_creator/js/xhs_creator_sign.js +29 -0
  260. package/static/xhs/xhs_creator/js/xhs_creator_signature.js +23 -0
  261. package/static/xhs/xhs_pc/js/aes_decrypt.js +22 -0
  262. package/static/xhs/xhs_pc/js/aes_encrypt.js +30 -0
  263. package/static/xhs/xhs_pc/js/b1.js +9 -0
  264. package/static/xhs/xhs_pc/js/deflate.js +320 -0
  265. package/static/xhs/xhs_pc/js/mns.js +4 -0
  266. package/static/xhs/xhs_pc/js/profile.js +141 -0
  267. package/static/xhs/xhs_pc/js/rap.js +173 -0
  268. package/static/xhs/xhs_pc/js/rap_cli.js +30 -0
  269. package/static/xhs/xhs_pc/js/rap_crypto.js +73 -0
  270. package/static/xhs/xhs_pc/js/rap_fingerprint_template.json +8 -0
  271. package/static/xhs/xhs_pc/js/reference_profile.json +262 -0
  272. package/static/xhs/xhs_pc/js/sign.js +6 -0
  273. package/static/xhs/xhs_pc/js/web_ssk.js +82 -0
  274. package/static/xhs/xhs_pc/js/xs_common.js +4 -0
  275. package/static/xianyu/et_f.js +2 -0
  276. package/static/xianyu/gen_tfstk.js +410 -0
  277. package/static/xianyu/goofish_js_version_2.js +568 -0
@@ -0,0 +1,59 @@
1
+ import protobuf from 'protobufjs/minimal.js';
2
+ /** 解一层消息。varint 为 bigint,fixed32 / fixed64 为 number / bigint,length-delimited 为字节。 */
3
+ export function decode(buf) {
4
+ const r = protobuf.Reader.create(buf);
5
+ const out = new Map();
6
+ while (r.pos < r.len) {
7
+ const tag = r.uint32();
8
+ const field = tag >>> 3;
9
+ let value;
10
+ switch (tag & 7) {
11
+ case 0:
12
+ value = BigInt(r.uint64().toString());
13
+ break;
14
+ case 1:
15
+ value = BigInt(r.fixed64().toString());
16
+ break;
17
+ case 2:
18
+ value = r.bytes();
19
+ break;
20
+ case 5:
21
+ value = r.fixed32();
22
+ break;
23
+ default:
24
+ throw new Error(`protobuf:不支持的 wire type ${tag & 7}`);
25
+ }
26
+ const list = out.get(field);
27
+ if (list)
28
+ list.push(value);
29
+ else
30
+ out.set(field, [value]);
31
+ }
32
+ return out;
33
+ }
34
+ /** 取字段的第一个值。 */
35
+ export function first(m, field) {
36
+ return m.get(field)?.[0];
37
+ }
38
+ export function str(m, field) {
39
+ const v = first(m, field);
40
+ return v instanceof Uint8Array ? Buffer.from(v).toString('utf8') : null;
41
+ }
42
+ export function int(m, field) {
43
+ const v = first(m, field);
44
+ return typeof v === 'bigint' ? Number(BigInt.asIntN(64, v)) : typeof v === 'number' ? v : null;
45
+ }
46
+ /** 64 位整数按字符串取(ID 用)。 */
47
+ export function int64(m, field) {
48
+ const v = first(m, field);
49
+ return typeof v === 'bigint' ? BigInt.asIntN(64, v).toString() : null;
50
+ }
51
+ /** repeated 的子消息。 */
52
+ export function messages(m, field) {
53
+ return (m.get(field) ?? []).filter((v) => v instanceof Uint8Array).map(decode);
54
+ }
55
+ export function message(m, field) {
56
+ const v = first(m, field);
57
+ return v instanceof Uint8Array ? decode(v) : null;
58
+ }
59
+ export { protobuf };
@@ -0,0 +1,181 @@
1
+ /**
2
+ * 与 Python 标准库逐字节一致的编码函数。上游的签名常常算在 `urlencode` / `json.dumps` 的结果上,
3
+ * 移植时必须产出同样的字符串,否则签名对不上。
4
+ */
5
+ const ALWAYS_SAFE = /[A-Za-z0-9_.\-~]/;
6
+ export function toPairs(init) {
7
+ if (!init)
8
+ return [];
9
+ return Array.isArray(init) ? init : Object.entries(init);
10
+ }
11
+ /** Python 的 `str(value)`:True / False / None 的写法与 JS 不同。 */
12
+ export function pyStr(value) {
13
+ if (value === true)
14
+ return 'True';
15
+ if (value === false)
16
+ return 'False';
17
+ if (value == null)
18
+ return 'None';
19
+ return String(value);
20
+ }
21
+ /** 有限 double 的精确十进制:|x| = digits / 10^scale。 */
22
+ function exactDecimal(x) {
23
+ const view = new DataView(new ArrayBuffer(8));
24
+ view.setFloat64(0, Math.abs(x));
25
+ const bits = view.getBigUint64(0);
26
+ const exponent = Number((bits >> 52n) & 0x7ffn);
27
+ let mantissa = bits & ((1n << 52n) - 1n);
28
+ let e = -1074;
29
+ if (exponent) {
30
+ mantissa |= 1n << 52n;
31
+ e = exponent - 1075;
32
+ }
33
+ if (e >= 0)
34
+ return { digits: mantissa << BigInt(e), scale: 0 };
35
+ return { digits: mantissa * 5n ** BigInt(-e), scale: -e };
36
+ }
37
+ /**
38
+ * Python 的 `round(x, ndigits)`(float):按 x 的精确二进制值舍入到 ndigits 位小数,恰好一半时取偶,
39
+ * 结果为 0 时保留符号。`Number.prototype.toFixed` 在恰好一半时进位,不能直接用。
40
+ */
41
+ export function pyRound(x, ndigits = 0) {
42
+ if (!Number.isFinite(x) || x === 0)
43
+ return x;
44
+ const { digits, scale } = exactDecimal(x);
45
+ if (scale <= ndigits)
46
+ return x;
47
+ const divisor = 10n ** BigInt(scale - ndigits);
48
+ let q = digits / divisor;
49
+ const twice = (digits % divisor) * 2n;
50
+ if (twice > divisor || (twice === divisor && q % 2n === 1n))
51
+ q += 1n;
52
+ const out = Number(`${q}e-${ndigits}`);
53
+ return x < 0 ? -out : out;
54
+ }
55
+ /**
56
+ * Python 的 `str(float)` / `repr(float)`:最短往返表示;整数值带 `.0`;
57
+ * 小于 1e-4 或不小于 1e16 时用指数形式,指数至少两位(`1e-05`)。
58
+ */
59
+ export function pyFloatStr(x) {
60
+ if (Number.isNaN(x))
61
+ return 'nan';
62
+ if (!Number.isFinite(x))
63
+ return x > 0 ? 'inf' : '-inf';
64
+ if (x === 0)
65
+ return Object.is(x, -0) ? '-0.0' : '0.0';
66
+ const abs = Math.abs(x);
67
+ if (abs < 1e-4 || abs >= 1e16) {
68
+ const [mantissa, exp] = x.toExponential().split('e');
69
+ const sign = exp.startsWith('-') ? '-' : '+';
70
+ return `${mantissa}e${sign}${exp.replace(/^[+-]/, '').padStart(2, '0')}`;
71
+ }
72
+ const s = String(x);
73
+ return s.includes('.') ? s : `${s}.0`;
74
+ }
75
+ /** `urllib.parse.quote(s, safe)`,默认 safe 为 `/`。 */
76
+ export function quote(s, safe = '/') {
77
+ let out = '';
78
+ for (const byte of Buffer.from(s, 'utf8')) {
79
+ const ch = String.fromCharCode(byte);
80
+ out += byte < 0x80 && (ALWAYS_SAFE.test(ch) || safe.includes(ch)) ? ch : '%' + byte.toString(16).toUpperCase().padStart(2, '0');
81
+ }
82
+ return out;
83
+ }
84
+ /** `urllib.parse.quote_plus(s, safe)`:空格编码成 `+`。 */
85
+ export function quotePlus(s, safe = '') {
86
+ return s.includes(' ') ? quote(s, safe + ' ').replaceAll(' ', '+') : quote(s, safe);
87
+ }
88
+ /** `urllib.parse.unquote(s)`。 */
89
+ export function unquote(s) {
90
+ return s.replace(/(%[0-9A-Fa-f]{2})+/g, (m) => Buffer.from(m.replaceAll('%', ''), 'hex').toString('utf8'));
91
+ }
92
+ /** `urllib.parse.urlencode(pairs, safe=safe, quote_via=...)`,值先经 Python 的 `str()`。 */
93
+ export function urlencode(init, options = {}) {
94
+ const q = options.via === 'percent' ? quote : quotePlus;
95
+ const safe = options.safe ?? '';
96
+ return toPairs(init)
97
+ .map(([k, v]) => `${q(k, safe)}=${q(pyStr(v), safe)}`)
98
+ .join('&');
99
+ }
100
+ /** `urllib.parse.parse_qsl(query, keep_blank_values=True)`。 */
101
+ export function parseQsl(query) {
102
+ return query
103
+ .split(/[&;]/)
104
+ .filter(Boolean)
105
+ .map((part) => {
106
+ const i = part.indexOf('=');
107
+ const [k, v] = i < 0 ? [part, ''] : [part.slice(0, i), part.slice(i + 1)];
108
+ return [unquote(k.replaceAll('+', ' ')), unquote(v.replaceAll('+', ' '))];
109
+ });
110
+ }
111
+ const REQUOTE_SAFE = "!#$%&'()*+,/:;=?@[]~|";
112
+ const UNRESERVED = /[A-Za-z0-9\-._~]/;
113
+ /** curl_cffi 的 `requote_uri`:已编码的部分保持不变,其余非法字符补上编码(requests 的版本不把 `|` 当安全字符)。 */
114
+ export function requoteUri(uri) {
115
+ // unquote_unreserved:%XX 表示的是非保留字符时解开
116
+ const unreserved = uri.replace(/%([0-9A-Fa-f]{2})/g, (m, h) => {
117
+ const ch = String.fromCharCode(parseInt(h, 16));
118
+ return UNRESERVED.test(ch) ? ch : m.toUpperCase();
119
+ });
120
+ return quote(unreserved, REQUOTE_SAFE);
121
+ }
122
+ const ESCAPES = { '"': '\\"', '\\': '\\\\', '\n': '\\n', '\r': '\\r', '\t': '\\t', '\b': '\\b', '\f': '\\f' };
123
+ function dumpString(s, ensureAscii) {
124
+ let out = '"';
125
+ for (let i = 0; i < s.length; i++) {
126
+ const ch = s[i];
127
+ const code = s.charCodeAt(i);
128
+ if (ESCAPES[ch])
129
+ out += ESCAPES[ch];
130
+ else if (code < 0x20 || (ensureAscii && code > 0x7e))
131
+ out += '\\u' + code.toString(16).padStart(4, '0');
132
+ else
133
+ out += ch;
134
+ }
135
+ return out + '"';
136
+ }
137
+ /**
138
+ * `json.loads`:与 Python 一样保留大整数。超出 JS 安全整数范围(2^53)的整数(各平台的 64 位 ID,
139
+ * 例如 TikTok 的 collectionId)保留成原文字符串,不按 double 取近似值;其余与 JSON.parse 相同。
140
+ */
141
+ export function jsonLoads(text) {
142
+ return JSON.parse(text, (_key, value, context) => typeof value === 'number' && !Number.isSafeInteger(value) && context?.source != null && /^-?\d+$/.test(context.source) ? context.source : value);
143
+ }
144
+ /**
145
+ * `json.dumps(value, separators=..., ensure_ascii=...)`。对象按插入顺序输出,与 Python dict 一致。
146
+ * 注意 JS 对象里形如整数的键(如 `"3064"`)总是排在最前;需要精确键序时传 Map。
147
+ */
148
+ export function jsonDumps(value, options = {}) {
149
+ const [itemSep, keySep] = options.separators ?? [', ', ': '];
150
+ const ascii = options.ensureAscii ?? true;
151
+ const dump = (v) => {
152
+ if (v === null || v === undefined)
153
+ return 'null';
154
+ if (v === true)
155
+ return 'true';
156
+ if (v === false)
157
+ return 'false';
158
+ if (typeof v === 'number') {
159
+ if (!Number.isFinite(v))
160
+ return Number.isNaN(v) ? 'NaN' : v > 0 ? 'Infinity' : '-Infinity';
161
+ return String(v);
162
+ }
163
+ if (typeof v === 'bigint')
164
+ return v.toString();
165
+ if (typeof v === 'string')
166
+ return dumpString(v, ascii);
167
+ if (Array.isArray(v))
168
+ return '[' + v.map(dump).join(itemSep) + ']';
169
+ if (v instanceof Map)
170
+ return '{' + [...v].map(([k, x]) => dumpString(String(k), ascii) + keySep + dump(x)).join(itemSep) + '}';
171
+ if (typeof v === 'object') {
172
+ return '{' + Object.entries(v).map(([k, x]) => dumpString(k, ascii) + keySep + dump(x)).join(itemSep) + '}';
173
+ }
174
+ throw new TypeError(`jsonDumps:不支持的类型 ${typeof v}`);
175
+ };
176
+ return dump(value);
177
+ }
178
+ /** 紧凑、不转义非 ASCII:`json.dumps(v, separators=(',', ':'), ensure_ascii=False)`。 */
179
+ export function compactJson(value) {
180
+ return jsonDumps(value, { separators: [',', ':'], ensureAscii: false });
181
+ }
@@ -0,0 +1,131 @@
1
+ import { randomBytes } from 'node:crypto';
2
+ import { setTimeout as delay } from 'node:timers/promises';
3
+ /**
4
+ * 随机数与时钟。平台代码一律经由这里取随机数和当前时间,不直接用 Math.random / Date.now,
5
+ * 这样对拍测试可以换成确定性的序列(见 {@link deterministic}),与上游 Python 的输出逐字节比较。
6
+ *
7
+ * 各方法的推导公式与 scripts/golden/specific_ai_golden.py 里对 Python random 的替换一一对应,改动时两边一起改。
8
+ */
9
+ let source = Math.random;
10
+ let clock = Date.now;
11
+ /** [0, 1) 的浮点数。对应 Python 的 `random.random()`。 */
12
+ export function random() {
13
+ return source();
14
+ }
15
+ /** [a, b] 的整数,含两端。对应 `random.randint(a, b)`。 */
16
+ export function randint(a, b) {
17
+ return a + Math.floor(source() * (b - a + 1));
18
+ }
19
+ /** 对应 `random.uniform(a, b)`。 */
20
+ export function uniform(a, b) {
21
+ return a + (b - a) * source();
22
+ }
23
+ /** 对应 `random.choice(seq)`。 */
24
+ export function choice(seq) {
25
+ return seq[Math.floor(source() * seq.length)];
26
+ }
27
+ /** 从 alphabet 里取 n 个字符。对应 `''.join(random.choice(alphabet) for _ in range(n))`。 */
28
+ export function string(n, alphabet) {
29
+ let s = '';
30
+ for (let i = 0; i < n; i++)
31
+ s += choice(alphabet);
32
+ return s;
33
+ }
34
+ const TWOPI = 2 * Math.PI;
35
+ let gaussNext = null;
36
+ /**
37
+ * 对应 `random.gauss(mu, sigma)`:Box-Muller 一次算出两个,第二个缓存到下一次调用(Python 的 `gauss_next`)。
38
+ * 缓存在 {@link deterministic} 切换时清空。cos / sin / log 用的是 V8 的实现,与 C libm 可能差最后一位。
39
+ */
40
+ export function gauss(mu = 0, sigma = 1) {
41
+ let z = gaussNext;
42
+ gaussNext = null;
43
+ if (z === null) {
44
+ const x2pi = source() * TWOPI;
45
+ const g2rad = Math.sqrt(-2 * Math.log(1 - source()));
46
+ z = Math.cos(x2pi) * g2rad;
47
+ gaussNext = Math.sin(x2pi) * g2rad;
48
+ }
49
+ return mu + z * sigma;
50
+ }
51
+ /** 对应 `random.sample(population, k)`(部分 Fisher-Yates)。 */
52
+ export function sample(population, k) {
53
+ const pool = Array.from(population);
54
+ for (let i = 0; i < k; i++) {
55
+ const j = i + Math.floor(source() * (pool.length - i));
56
+ [pool[i], pool[j]] = [pool[j], pool[i]];
57
+ }
58
+ return pool.slice(0, k);
59
+ }
60
+ /** 原地打乱。对应 `random.shuffle(x)`。 */
61
+ export function shuffle(x) {
62
+ for (let i = x.length - 1; i > 0; i--) {
63
+ const j = Math.floor(source() * (i + 1));
64
+ [x[i], x[j]] = [x[j], x[i]];
65
+ }
66
+ return x;
67
+ }
68
+ /** n 个随机字节。对应 `os.urandom(n)` / `secrets.token_bytes(n)`。 */
69
+ export function bytes(n) {
70
+ if (source === Math.random)
71
+ return new Uint8Array(randomBytes(n));
72
+ const out = new Uint8Array(n);
73
+ for (let i = 0; i < n; i++)
74
+ out[i] = Math.floor(source() * 256);
75
+ return out;
76
+ }
77
+ /** 2n 位小写十六进制。对应 `secrets.token_hex(n)`。 */
78
+ export function hex(n) {
79
+ return Buffer.from(bytes(n)).toString('hex');
80
+ }
81
+ /** 对应 `str(uuid.uuid4())`。 */
82
+ export function uuid4() {
83
+ const b = bytes(16);
84
+ b[6] = (b[6] & 0x0f) | 0x40;
85
+ b[8] = (b[8] & 0x3f) | 0x80;
86
+ const h = Buffer.from(b).toString('hex');
87
+ return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20)}`;
88
+ }
89
+ /** 当前时间,毫秒。 */
90
+ export function now() {
91
+ return clock();
92
+ }
93
+ /** 当前时间,秒(整数)。对应 `int(time.time())`。 */
94
+ export function nowSeconds() {
95
+ return Math.floor(clock() / 1000);
96
+ }
97
+ /** 等待(毫秒)。确定性模式下不等,对应对拍时被替换成空操作的 `time.sleep`。 */
98
+ export async function sleep(ms, signal) {
99
+ if (isDeterministic() || ms <= 0)
100
+ return;
101
+ await delay(ms, undefined, { signal }).catch(() => { });
102
+ }
103
+ /** mulberry32:Python 侧有逐位相同的实现。 */
104
+ export function mulberry32(seed) {
105
+ let a = seed >>> 0;
106
+ return () => {
107
+ a = (a + 0x6d2b79f5) >>> 0;
108
+ let t = Math.imul(a ^ (a >>> 15), a | 1);
109
+ t = (t + Math.imul(t ^ (t >>> 7), t | 61)) ^ t;
110
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
111
+ };
112
+ }
113
+ export const DEFAULT_SEED = 20260927;
114
+ export const DEFAULT_NOW = 1790000000123;
115
+ /** 切换成确定性的随机数与固定时钟,返回恢复函数。只给测试用。 */
116
+ export function deterministic(options = {}) {
117
+ const prev = [source, clock];
118
+ source = mulberry32(options.seed ?? DEFAULT_SEED);
119
+ gaussNext = null;
120
+ const fixed = options.now ?? DEFAULT_NOW;
121
+ clock = () => fixed;
122
+ return () => {
123
+ ;
124
+ [source, clock] = prev;
125
+ gaussNext = null;
126
+ };
127
+ }
128
+ /** 当前是否处于确定性模式(签名 vm 据此给脚本注入固定的 Math.random / Date)。 */
129
+ export function isDeterministic() {
130
+ return source !== Math.random;
131
+ }
@@ -0,0 +1,129 @@
1
+ import { z } from 'zod';
2
+ import { PAGING } from './options.js';
3
+ import { OUTPUT_TYPES } from './schemas.js';
4
+ import { CORE_COMMANDS, RESOURCES, VOCAB } from './vocab.js';
5
+ export const ENDPOINTS = ['web', 'app', 'pc'];
6
+ const coreHandler = (name) => () => import('./auth-handlers.js').then((m) => m[name]);
7
+ /**
8
+ * 平台 index.ts 用的 handler 声明:`h('itemGet')` 懒加载 handler,`impl('full', 'itemGet', {...})` 连同 upstream 一起声明。
9
+ * `wrap` 给每个 handler 套一层(例如抖音遇到 Uifid 失效时重试)。
10
+ */
11
+ export function handlers(load, wrap) {
12
+ const h = (name) => Object.assign(() => load().then((m) => {
13
+ const handler = m[name];
14
+ return wrap ? wrap(m, handler) : handler;
15
+ }), { handlerName: name });
16
+ const impl = (upstream, name, extra = {}) => ({ upstream, handler: h(name), ...extra });
17
+ return { h, impl };
18
+ }
19
+ export function definePlatform(decl) {
20
+ const endpoints = {};
21
+ for (const endpoint of ENDPOINTS) {
22
+ const e = decl.endpoints[endpoint];
23
+ endpoints[endpoint] =
24
+ e === 'planned' ? 'planned' : { login: e.login, logout: e.logout, guest: e.guest ?? false, commands: resolveCommands(decl, e) };
25
+ }
26
+ return {
27
+ id: decl.id,
28
+ name: decl.name,
29
+ aliases: decl.aliases,
30
+ item: decl.item,
31
+ pageInterval: decl.pageInterval ?? 1000,
32
+ endpoints,
33
+ };
34
+ }
35
+ function resolveCommands(decl, e) {
36
+ const commands = new Map();
37
+ const fail = (key, message) => {
38
+ throw new Error(`注册表错误 ${decl.id} "${key}":${message}`);
39
+ };
40
+ for (const [key, value] of Object.entries(e.commands)) {
41
+ const parts = key.split(' ');
42
+ if (parts.length !== 2 || !parts.every((p) => /^[a-z]+$/.test(p)))
43
+ fail(key, '命令名必须是 "resource action"');
44
+ if (CORE_COMMANDS.includes(key))
45
+ fail(key, '由 core 自动注册,不要声明');
46
+ const d = typeof value === 'string' ? { upstream: value } : value;
47
+ const base = VOCAB[key];
48
+ if (!base) {
49
+ for (const field of ['summary', 'args', 'auth', 'output']) {
50
+ if (d[field] === undefined)
51
+ fail(key, `扩展命令必须写全字段,缺少 ${field}`);
52
+ }
53
+ }
54
+ const spec = { ...base, ...d };
55
+ if (!e.guest && parts[0] !== 'auth') {
56
+ if (d.auth === 'optional')
57
+ fail(key, '该端不支持游客态,auth 只能是 required');
58
+ spec.auth = 'required';
59
+ }
60
+ const options = { ...(spec.paged ? PAGING : {}), ...base?.options, ...d.options };
61
+ const unsupported = [];
62
+ if (d.supports) {
63
+ const standard = Object.keys(base?.options ?? {});
64
+ for (const k of d.supports)
65
+ if (!standard.includes(k))
66
+ fail(key, `supports 里的 ${k} 不是这个命令的标准选项`);
67
+ for (const k of standard) {
68
+ if (d.supports.includes(k))
69
+ continue;
70
+ if (d.options && k in d.options)
71
+ fail(key, `选项 ${k} 既在 options 里声明,又没列进 supports`);
72
+ unsupported.push(k);
73
+ delete options[k];
74
+ }
75
+ }
76
+ if (key === 'auth login') {
77
+ options.method = z.enum(e.login.methods).default(e.login.default).describe('登录方式');
78
+ if (e.login.scopes?.length)
79
+ options.scope = z.enum(e.login.scopes).optional().describe('子站点');
80
+ }
81
+ const output = spec.output.replace(/\[\]$/, '');
82
+ if (!output.startsWith('{') && !OUTPUT_TYPES.has(output))
83
+ fail(key, `未知的输出类型 ${spec.output}`);
84
+ commands.set(key, {
85
+ ...spec,
86
+ key,
87
+ resource: parts[0],
88
+ action: parts[1],
89
+ summary: spec.summary.replaceAll('{item}', decl.item),
90
+ args: spec.args.map((a) => ({ ...a, summary: a.summary.replaceAll('{item}', decl.item) })),
91
+ options,
92
+ note: d.note ?? null,
93
+ status: d.handler ? 'implemented' : 'planned',
94
+ extension: !base,
95
+ unsupported,
96
+ handler: d.handler ?? null,
97
+ });
98
+ }
99
+ for (const key of CORE_COMMANDS) {
100
+ const [resource, action] = key.split(' ');
101
+ commands.set(key, {
102
+ ...VOCAB[key],
103
+ key,
104
+ resource,
105
+ action,
106
+ upstream: null,
107
+ note: null,
108
+ status: 'implemented',
109
+ extension: false,
110
+ unsupported: [],
111
+ handler: coreHandler(action),
112
+ });
113
+ }
114
+ return commands;
115
+ }
116
+ const VOCAB_ORDER = new Map(Object.keys(VOCAB).map((k, i) => [k, i]));
117
+ /** 按词表顺序排列命令;扩展 action 排在同 resource 的词表命令之后,新增的 resource 排在最后。 */
118
+ export function sortCommands(commands) {
119
+ const list = [...commands];
120
+ const extra = [...new Set(list.map((c) => c.resource).filter((r) => !RESOURCES.includes(r)))];
121
+ const rank = (c) => {
122
+ const r = RESOURCES.indexOf(c.resource);
123
+ return [r >= 0 ? r : RESOURCES.length + extra.indexOf(c.resource), VOCAB_ORDER.get(c.key) ?? Infinity];
124
+ };
125
+ return list
126
+ .map((c, i) => ({ c, i, k: rank(c) }))
127
+ .sort((a, b) => a.k[0] - b.k[0] || a.k[1] - b.k[1] || a.i - b.i)
128
+ .map((x) => x.c);
129
+ }
@@ -0,0 +1,41 @@
1
+ import { z } from 'zod';
2
+ import { LOGIN_METHODS } from './options.js';
3
+ /** 输出类型名,注册表的 `output` 只能用这些(加 `[]` 表示数组),或 `{...}` 形式的内联结构。 */
4
+ export const OUTPUT_TYPES = new Set([
5
+ 'User', 'Item', 'Comment', 'Folder', 'Series', 'Category', 'Keyword', 'Topic', 'Poi', 'Live', 'Event', 'Gift',
6
+ 'Rank', 'Conversation', 'Message', 'Notice', 'NoticeCount', 'Media', 'AuthStatus', 'Account', 'File', 'Subtitle',
7
+ 'Danmaku', 'Order', 'Coupon',
8
+ ]);
9
+ /** 归一化对象上挂平台原始对象,`--raw` 时用它替换。JSON 序列化会忽略 symbol 键。 */
10
+ export const RAW = Symbol.for('specific-ai.raw');
11
+ export function withRaw(value, raw) {
12
+ Object.defineProperty(value, RAW, { value: raw, enumerable: false });
13
+ return value;
14
+ }
15
+ // ---- 凭证文件(AGENTS 5.4) ----
16
+ const UserRefSchema = z.object({ id: z.string(), name: z.string().nullable(), url: z.string().nullable() });
17
+ const CookieSchema = z.object({
18
+ name: z.string(),
19
+ value: z.string(),
20
+ domain: z.string(),
21
+ path: z.string().default('/'),
22
+ /** Unix 秒;会话 cookie 为 null。 */
23
+ expires: z.number().nullable().default(null),
24
+ });
25
+ const ScopeSchema = z.object({
26
+ cookies: z.array(CookieSchema).default([]),
27
+ tokens: z.record(z.string(), z.unknown()).default({}),
28
+ });
29
+ export const CredentialSchema = z.object({
30
+ schema: z.literal(1),
31
+ platform: z.string(),
32
+ endpoint: z.enum(['web', 'app', 'pc']),
33
+ account: z.string(),
34
+ user: UserRefSchema.nullable(),
35
+ method: z.enum([...LOGIN_METHODS, 'guest']),
36
+ created_at: z.string(),
37
+ updated_at: z.string(),
38
+ scopes: z.record(z.string(), ScopeSchema),
39
+ device: z.record(z.string(), z.unknown()).default({}),
40
+ extra: z.record(z.string(), z.unknown()).default({}),
41
+ });
@@ -0,0 +1,104 @@
1
+ import { websocket } from 'wreq-js';
2
+ import { SpecificAIError } from './errors.js';
3
+ import { toNetworkError } from './http.js';
4
+ import { sleep } from './rand.js';
5
+ /** 打开一个 WebSocket。消息按到达顺序进入 messages;连接关闭或 signal 中止时迭代结束,出错时抛出。 */
6
+ export async function openSocket(url, options = {}) {
7
+ const { signal, ...wsOptions } = options;
8
+ const queue = [];
9
+ let wake = null;
10
+ let done = false;
11
+ let failure = null;
12
+ const notify = () => {
13
+ const w = wake;
14
+ wake = null;
15
+ w?.();
16
+ };
17
+ let ws;
18
+ try {
19
+ ws = await websocket(url, { browser: 'chrome', os: 'windows', ...wsOptions, binaryType: 'nodebuffer' });
20
+ }
21
+ catch (err) {
22
+ throw toNetworkError(err);
23
+ }
24
+ ws.onmessage = (e) => {
25
+ queue.push(e.data);
26
+ notify();
27
+ };
28
+ ws.onclose = () => {
29
+ done = true;
30
+ notify();
31
+ };
32
+ ws.onerror = (e) => {
33
+ failure = new Error(String(e.message ?? 'WebSocket 出错'));
34
+ done = true;
35
+ notify();
36
+ };
37
+ const onAbort = () => {
38
+ ws.close();
39
+ done = true;
40
+ notify();
41
+ };
42
+ signal?.addEventListener('abort', onAbort, { once: true });
43
+ return {
44
+ send: async (data) => {
45
+ await ws.send(data);
46
+ },
47
+ close: () => ws.close(),
48
+ messages: {
49
+ async *[Symbol.asyncIterator]() {
50
+ try {
51
+ for (;;) {
52
+ while (queue.length)
53
+ yield queue.shift();
54
+ if (done)
55
+ break;
56
+ await new Promise((r) => (wake = r));
57
+ }
58
+ if (failure)
59
+ throw failure;
60
+ }
61
+ finally {
62
+ signal?.removeEventListener('abort', onAbort);
63
+ if (!done)
64
+ ws.close();
65
+ }
66
+ },
67
+ },
68
+ };
69
+ }
70
+ /** 连续多少次 UPSTREAM 错误后不再重连。 */
71
+ const MAX_UPSTREAM_FAILURES = 5;
72
+ /**
73
+ * 断线自动重连:connect 每次返回一段事件流,流结束或出错后按指数退避重连,直到 signal 中止。
74
+ * 重连解决不了的错误直接抛出:登录态失效、风控、参数错误等(NETWORK、UPSTREAM 以外的 SpecificAIError);
75
+ * UPSTREAM(例如房间不存在)连续 5 次、中间一条事件都没收到时也抛出。其余错误(断线)一直重连。
76
+ */
77
+ export async function* reconnecting(ctx, connect, options = {}) {
78
+ let attempt = 0;
79
+ let upstreamFailures = 0;
80
+ while (!ctx.signal.aborted) {
81
+ try {
82
+ for await (const value of connect(attempt)) {
83
+ attempt = 0;
84
+ upstreamFailures = 0;
85
+ yield value;
86
+ }
87
+ }
88
+ catch (err) {
89
+ if (ctx.signal.aborted)
90
+ return;
91
+ if (err instanceof SpecificAIError && err.code !== 'NETWORK') {
92
+ if (err.code !== 'UPSTREAM' || ++upstreamFailures >= MAX_UPSTREAM_FAILURES)
93
+ throw err;
94
+ }
95
+ ctx.log.warn(`连接断开:${err instanceof Error ? err.message : String(err)}`);
96
+ }
97
+ if (ctx.signal.aborted)
98
+ return;
99
+ attempt++;
100
+ const delay = Math.min(1000 * 2 ** (attempt - 1), options.maxDelay ?? 30_000);
101
+ ctx.log.info(`${Math.round(delay / 1000)} 秒后重连(第 ${attempt} 次)`);
102
+ await sleep(delay, ctx.signal);
103
+ }
104
+ }
@@ -0,0 +1,35 @@
1
+ import { CookieJar } from './cookies.js';
2
+ import { SpecificAIError } from './errors.js';
3
+ import { HttpClient } from './http.js';
4
+ import { endpointFlag, GUEST } from './auth-store.js';
5
+ /** 凭证里的一个子站点分区,不存在时创建。 */
6
+ export function scope(credential, name = 'main') {
7
+ return (credential.scopes[name] ??= { cookies: [], tokens: {} });
8
+ }
9
+ /** 绑定到凭证某个 scope 的 cookie 罐:请求中更新的 cookie 会随凭证由 core 落盘。 */
10
+ export function jarOf(credential, name = 'main') {
11
+ return new CookieJar(scope(credential, name).cookies);
12
+ }
13
+ /** 按本次调用的代理和超时创建 HTTP 客户端。 */
14
+ export function httpClient(ctx, options = {}) {
15
+ const { scope: name, ...rest } = options;
16
+ return new HttpClient({ jar: jarOf(ctx.credential, name), ...rest, proxy: ctx.config.proxy, timeout: ctx.config.timeout, log: ctx.log });
17
+ }
18
+ /** 当前身份是否是游客。 */
19
+ export function isGuest(ctx) {
20
+ return ctx.account == null || ctx.account === GUEST || ctx.credential.method === 'guest';
21
+ }
22
+ /** 平台返回登录墙时:游客 → AUTH_REQUIRED;已登录 → AUTH_EXPIRED(AGENTS 5.2)。 */
23
+ export function authError(ctx, message) {
24
+ const p = ctx.platform.id;
25
+ const e = endpointFlag(ctx.endpoint);
26
+ if (isGuest(ctx))
27
+ return new SpecificAIError('AUTH_REQUIRED', message ?? '需要登录', { hint: `specific-ai ${p} auth login${e}` });
28
+ return new SpecificAIError('AUTH_EXPIRED', message ?? `账号 ${ctx.account} 的登录态已失效`, {
29
+ hint: `specific-ai ${p} auth login -a ${ctx.account}${e}`,
30
+ });
31
+ }
32
+ /** 分页结果。 */
33
+ export function paged(data, cursor, hasMore) {
34
+ return { data, page: { cursor: hasMore && cursor != null ? String(cursor) : null, has_more: hasMore && cursor != null } };
35
+ }