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,73 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { createRequire } from 'node:module';
3
+ import { format } from 'node:util';
4
+ import vm from 'node:vm';
5
+ import { DEFAULT_NOW, DEFAULT_SEED, isDeterministic, mulberry32, now } from './rand.js';
6
+ /**
7
+ * 上游签名 JS 的执行环境(AGENTS 7.4):每个脚本一个 vm context,懒创建、复用。
8
+ * `require` 指向 specific-ai 自己的 node_modules;console 输出到 stderr,保证 stdout 纯净。
9
+ *
10
+ * 确定性模式(对拍测试)下,每次调用都新建 context,并注入与 scripts/golden/node_determinism.cjs
11
+ * 相同的 Math.random 和 Date——上游每次签名都新起一个 node 进程,序列从头开始,这里保持一致。
12
+ */
13
+ const require = createRequire(import.meta.url);
14
+ const contexts = new Map();
15
+ const sources = new Map();
16
+ const stderrConsole = Object.fromEntries(['log', 'info', 'warn', 'error', 'debug', 'trace'].map((k) => [k, (...a) => process.stderr.write(format(...a) + '\n')]));
17
+ /** 新建 context,带上签名 JS 常用的宿主全局对象。 */
18
+ export function createContext(globals = {}) {
19
+ const context = vm.createContext({
20
+ require,
21
+ console: stderrConsole,
22
+ Buffer,
23
+ URL,
24
+ URLSearchParams,
25
+ TextEncoder,
26
+ TextDecoder,
27
+ atob,
28
+ btoa,
29
+ crypto: globalThis.crypto,
30
+ setTimeout,
31
+ clearTimeout,
32
+ setInterval,
33
+ clearInterval,
34
+ ...globals,
35
+ });
36
+ if (isDeterministic()) {
37
+ context.__specific_ai_random = mulberry32(DEFAULT_SEED);
38
+ context.__specific_ai_now = now() || DEFAULT_NOW;
39
+ vm.runInContext(`Math.random = __specific_ai_random;
40
+ (() => { const D = Date; globalThis.Date = class extends D {
41
+ constructor(...a) { super(...(a.length ? a : [__specific_ai_now])) }
42
+ static now() { return __specific_ai_now }
43
+ } })();`, context);
44
+ }
45
+ return context;
46
+ }
47
+ function source(file) {
48
+ let s = sources.get(file);
49
+ if (s == null)
50
+ sources.set(file, (s = readFileSync(file, 'utf8')));
51
+ return s;
52
+ }
53
+ /** 在该脚本专属的 context 里执行一次,返回 context(即脚本的全局对象),之后复用。 */
54
+ export function loadScript(file, globals) {
55
+ const deterministic = isDeterministic();
56
+ let context = deterministic ? undefined : contexts.get(file);
57
+ if (!context) {
58
+ context = createContext(globals);
59
+ vm.runInContext(source(file), context, { filename: file });
60
+ if (!deterministic)
61
+ contexts.set(file, context);
62
+ }
63
+ return context;
64
+ }
65
+ /** 在脚本的 context 里调用一个全局函数。 */
66
+ export function callScript(file, fn, args, globals) {
67
+ const context = loadScript(file, globals);
68
+ // 顶层 const / let 声明不会挂到全局对象上,按名字在 context 里求值
69
+ const f = context[fn] ?? (/^[\w$]+$/.test(fn) ? vm.runInContext(`typeof ${fn} === 'undefined' ? undefined : ${fn}`, context) : undefined);
70
+ if (typeof f !== 'function')
71
+ throw new Error(`${file} 里没有函数 ${fn}`);
72
+ return f(...args);
73
+ }
@@ -0,0 +1,157 @@
1
+ import { z } from 'zod';
2
+ import { DOWNLOAD, PUBLISH, STREAM } from './options.js';
3
+ const arg = {
4
+ user: { name: 'user', summary: '用户:ID、主页 URL、用户名或 me' },
5
+ userOrMe: { name: 'user', summary: '用户,省略时为 me', optional: true, default: 'me' },
6
+ item: { name: 'item', summary: '{item}:ID 或 URL' },
7
+ keyword: { name: 'keyword', summary: '关键词' },
8
+ comment: { name: 'comment', summary: '评论 ID' },
9
+ room: { name: 'room', summary: '直播间:ID 或 URL' },
10
+ conversation: { name: 'conversation', summary: '会话 ID' },
11
+ };
12
+ function read(summary, args, output, extra = {}) {
13
+ return { summary, args, output, options: {}, auth: 'optional', ...extra };
14
+ }
15
+ function list(summary, args, output, extra = {}) {
16
+ return read(summary, args, output, { paged: true, ...extra });
17
+ }
18
+ function write(summary, args, output = '{id}', extra = {}) {
19
+ return read(summary, args, output, { auth: 'required', ...extra });
20
+ }
21
+ function given(options, keys) {
22
+ return keys.filter((k) => options[k] != null);
23
+ }
24
+ const flags = (keys) => keys.map((k) => (k === 'text' ? '<text>' : `--${k}`)).join('、');
25
+ /**
26
+ * `msg send` 的参数约束(AGENTS 4.8):--to、--conversation、--item 用一个,只有 --to 与 --item 可以同时用;
27
+ * 要有消息内容。平台可以追加目标(京东的 --order)和内容(抖音的 --file、--share)。
28
+ * 某个平台不支持 --to 与 --item 同时用时,由 handler 报 UNSUPPORTED。
29
+ */
30
+ export function checkMsgSend(a, o, extra = {}) {
31
+ const targets = given(o, ['to', 'conversation', 'item']);
32
+ const extraTargets = given(o, extra.targets ?? []);
33
+ if ((targets.length === 0 && extraTargets.length === 0) || (targets.length > 1 && targets.includes('conversation'))) {
34
+ return `${flags(['to', 'conversation', 'item', ...(extra.targets ?? [])])} 需要用一个,只有 --to 与 --item 可以同时用`;
35
+ }
36
+ const content = ['image', 'video', ...(extra.content ?? [])];
37
+ if (a.text == null && given(o, content).length === 0) {
38
+ const all = flags(['text', ...content]).split('、');
39
+ return `需要 ${all.slice(0, -1).join('、')} 或 ${all.at(-1)}`;
40
+ }
41
+ }
42
+ /** 通用词表(AGENTS 4.5、4.8、6.3)。平台只声明与默认不同的部分。 */
43
+ export const VOCAB = {
44
+ 'auth login': read('登录', [], 'Account', {
45
+ options: {
46
+ phone: z.string().optional().describe('手机号(sms)'),
47
+ code: z.string().optional().describe('短信验证码(sms)'),
48
+ username: z.string().optional().describe('用户名(password)'),
49
+ passwordStdin: z.boolean().optional().describe('从 stdin 读取密码(password)'),
50
+ cookie: z.string().optional().describe('cookie 字符串、@文件,或 - 表示从 stdin 读取(cookie)'),
51
+ },
52
+ }),
53
+ 'auth status': read('在线校验登录态', [], 'AuthStatus'),
54
+ 'auth logout': read('登出并删除本地凭证', [], 'Account'),
55
+ 'auth list': read('本平台、本端的账号', [], 'Account[]'),
56
+ 'auth use': read('切换当前账号', [{ name: 'account', summary: '账号名' }], 'Account'),
57
+ 'user get': read('用户资料', [arg.user], 'User'),
58
+ 'user search': list('搜索用户', [arg.keyword], 'User[]'),
59
+ 'user items': list('用户发布的{item}', [arg.user], 'Item[]'),
60
+ 'user likes': list('用户点赞的{item}', [arg.userOrMe], 'Item[]'),
61
+ 'user collects': list('用户收藏的{item}', [arg.userOrMe], 'Item[]'),
62
+ 'user reposts': list('用户转发的{item}', [arg.userOrMe], 'Item[]'),
63
+ 'user followers': list('粉丝列表', [arg.userOrMe], 'User[]'),
64
+ 'user following': list('关注列表', [arg.userOrMe], 'User[]'),
65
+ 'user follow': write('关注', [arg.user]),
66
+ 'user unfollow': write('取消关注', [arg.user]),
67
+ 'item get': read('{item}详情', [arg.item], 'Item'),
68
+ 'item search': list('搜索{item}', [arg.keyword], 'Item[]'),
69
+ 'item related': list('相关推荐', [arg.item], 'Item[]'),
70
+ 'item list': list('我发布的{item},带审核状态', [], 'Item[]', { auth: 'required' }),
71
+ 'item media': read('{item}的媒体地址', [arg.item], 'Media[]'),
72
+ 'item download': read('下载{item}的媒体', [arg.item], 'File[]', { options: DOWNLOAD }),
73
+ 'item like': write('点赞', [arg.item]),
74
+ 'item unlike': write('取消点赞', [arg.item]),
75
+ 'item collect': write('收藏', [arg.item]),
76
+ 'item uncollect': write('取消收藏', [arg.item]),
77
+ 'item repost': write('转发', [arg.item]),
78
+ 'item unrepost': write('取消转发', [arg.item]),
79
+ 'item publish': write('发布{item}', [], 'Item', { options: PUBLISH }),
80
+ 'item delete': write('删除{item}', [arg.item], '{id}', { confirm: true }),
81
+ 'item categories': read('{item}分类,即 --category 的取值', [], 'Category[]'),
82
+ 'product get': read('商品详情', [{ name: 'product', summary: '商品:ID 或 URL' }], 'Item'),
83
+ 'comment list': list('评论列表,也用于商品评价', [arg.item], 'Comment[]'),
84
+ 'comment replies': list('评论的回复', [arg.item, arg.comment], 'Comment[]'),
85
+ 'comment add': write('发表评论', [arg.item, { name: 'text', summary: '评论内容' }], 'Comment', {
86
+ options: { replyTo: z.string().optional().describe('回复某条评论(评论 ID)') },
87
+ }),
88
+ 'comment delete': write('删除评论', [arg.item, arg.comment], '{id}', { confirm: true }),
89
+ 'comment like': write('点赞评论', [arg.item, arg.comment]),
90
+ 'comment unlike': write('取消点赞评论', [arg.item, arg.comment]),
91
+ 'feed list': list('推荐 / 热门 / 关注流', [], 'Item[]'),
92
+ 'feed categories': read('推荐流分类', [], 'Category[]'),
93
+ 'live get': read('直播间信息', [arg.room], 'Live'),
94
+ 'live list': list('直播列表', [], 'Live[]'),
95
+ 'live search': list('搜索直播', [arg.keyword], 'Live[]'),
96
+ 'live categories': read('直播分类', [], 'Category[]'),
97
+ 'live listen': read('监听弹幕、礼物、进场等事件', [arg.room], 'Event', { stream: true, options: STREAM }),
98
+ 'live history': read('最近的弹幕', [arg.room], 'Event[]'),
99
+ 'live send': write('发弹幕,或用 --gift 送礼', [arg.room, { name: 'text', summary: '弹幕内容', optional: true }], '{id}', {
100
+ options: {
101
+ gift: z.string().optional().describe('礼物 id,取值来自 live gifts'),
102
+ count: z.number().int().positive().optional().describe('礼物数量'),
103
+ },
104
+ confirm: (o) => o.gift != null,
105
+ check: (a, o) => {
106
+ if ((a.text == null) === (o.gift == null))
107
+ return '需要 <text> 或 --gift,二选一';
108
+ if (o.count != null && o.gift == null)
109
+ return '--count 只能和 --gift 一起用';
110
+ },
111
+ }),
112
+ 'live like': write('直播间点赞', [arg.room]),
113
+ 'live rank': read('直播间排行榜', [arg.room], 'Rank[]'),
114
+ 'live gifts': read('礼物列表', [arg.room], 'Gift[]'),
115
+ 'live products': read('直播间商品', [arg.room], 'Item[]'),
116
+ 'live media': read('直播流地址', [arg.room], 'Media[]'),
117
+ 'live replays': list('直播回放', [arg.user], 'Item[]'),
118
+ 'live start': write('开播', [], '{id push{url key}}'),
119
+ 'live stop': write('下播', []),
120
+ 'keyword suggest': read('搜索联想词', [{ name: 'prefix', summary: '输入的前缀' }], 'Keyword[]'),
121
+ 'keyword hot': read('热搜词', [], 'Keyword[]'),
122
+ 'notice list': list('通知列表', [], 'Notice[]', { auth: 'required' }),
123
+ 'notice count': read('未读通知数', [], 'NoticeCount', { auth: 'required' }),
124
+ 'msg list': list('会话列表', [], 'Conversation[]', { auth: 'required' }),
125
+ 'msg history': list('会话的消息记录', [arg.conversation], 'Message[]', { auth: 'required' }),
126
+ 'msg send': write('发私信;--item 表示联系商品的卖家或客服', [{ name: 'text', summary: '消息内容', optional: true }], 'Message', {
127
+ options: {
128
+ to: z.string().optional().describe('收信用户'),
129
+ conversation: z.string().optional().describe('会话 ID'),
130
+ item: z.string().optional().describe('商品:ID 或 URL'),
131
+ image: PUBLISH.image,
132
+ video: PUBLISH.video,
133
+ },
134
+ check: (a, o) => checkMsgSend(a, o),
135
+ }),
136
+ 'msg listen': read('监听新消息', [], 'Message', { auth: 'required', stream: true, options: STREAM }),
137
+ 'msg read': write('标记会话已读', [arg.conversation]),
138
+ 'msg revoke': write('撤回消息', [arg.conversation, { name: 'message', summary: '消息 ID' }], '{id}', { confirm: true }),
139
+ 'msg delete': write('删除会话', [arg.conversation], '{id}', { confirm: true }),
140
+ 'media upload': write('上传媒体', [{ name: 'file', summary: '本地文件路径' }], 'Media'),
141
+ 'folder list': list('收藏夹列表', [arg.userOrMe], 'Folder[]'),
142
+ 'folder items': list('收藏夹里的{item}', [{ name: 'folder', summary: '收藏夹 ID' }], 'Item[]'),
143
+ 'folder create': write('新建收藏夹', [{ name: 'name', summary: '收藏夹名字' }], 'Folder'),
144
+ 'folder update': write('修改收藏夹', [{ name: 'folder', summary: '收藏夹 ID' }], 'Folder', {
145
+ options: { name: z.string().describe('新名字') },
146
+ }),
147
+ 'folder delete': write('删除收藏夹', [{ name: 'folder', summary: '收藏夹 ID' }], '{id}', { confirm: true }),
148
+ 'series list': list('合集列表', [arg.userOrMe], 'Series[]'),
149
+ 'series items': list('合集里的{item}', [{ name: 'series', summary: '合集 ID' }], 'Item[]'),
150
+ 'history list': list('浏览历史', [], 'Item[]', { auth: 'required' }),
151
+ 'topic search': list('搜索话题', [arg.keyword], 'Topic[]'),
152
+ 'poi search': list('搜索地点', [arg.keyword], 'Poi[]'),
153
+ };
154
+ /** 词表里的 resource,按 4.5 的顺序。 */
155
+ export const RESOURCES = [...new Set(Object.keys(VOCAB).map((k) => k.split(' ')[0]))];
156
+ /** 由 core 实现、自动注册到每个可用端的命令。 */
157
+ export const CORE_COMMANDS = ['auth list', 'auth use', 'auth logout'];
@@ -0,0 +1,410 @@
1
+ import { SpecificAIError } from '../../core/errors.js';
2
+ import { jsonDumps, jsonLoads } from '../../core/py.js';
3
+ import * as rand from '../../core/rand.js';
4
+ import { openSocket, reconnecting } from '../../core/stream.js';
5
+ import { authError, paged } from '../../core/toolkit.js';
6
+ /** listUserMessages 的起始游标(JS 的 Number.MAX_SAFE_INTEGER)。 */
7
+ export const FIRST_CURSOR = '9007199254740991';
8
+ /** 心跳间隔(上游 heart_beat 为 15 秒)。 */
9
+ const HEARTBEAT_MS = 15_000;
10
+ /** 注册后等服务端的 /s/vulcan 推送,超时后照样继续。 */
11
+ const READY_MS = 10_000;
12
+ // ================================================================ 帧(一个函数对应上游的一处构造)
13
+ const imId = (p, id) => `${id}@${p.domain}`;
14
+ const plainId = (p, id) => String(id ?? '').replace(new RegExp(`@${p.domain}$`), '');
15
+ /** init 的第一帧:/reg。 */
16
+ function regFrame(p, token, deviceId) {
17
+ return {
18
+ lwp: '/reg',
19
+ headers: {
20
+ 'cache-header': 'app-key token ua wv',
21
+ 'app-key': p.appKey,
22
+ token,
23
+ ua: p.ua,
24
+ dt: 'j',
25
+ wv: 'im:3,au:3,sy:6',
26
+ sync: '0,0;0;0;',
27
+ did: deviceId,
28
+ mid: p.mid(),
29
+ },
30
+ };
31
+ }
32
+ /** init 的第二帧:同步位点。 */
33
+ function ackDiffFrame(p) {
34
+ const now = rand.now();
35
+ return {
36
+ lwp: '/r/SyncStatus/ackDiff',
37
+ headers: { mid: p.mid() },
38
+ body: [{ pipeline: 'sync', tooLong2Tag: 'PNM,1', channel: 'sync', topic: 'sync', highPts: 0, pts: now * 1000, seq: 0, timestamp: now }],
39
+ };
40
+ }
41
+ /** heart_beat。 */
42
+ function heartbeatFrame(p) {
43
+ return { lwp: '/!', headers: { mid: p.mid() } };
44
+ }
45
+ /** 收到任何带 headers 的帧都回的 ack(上游 main / list_all_conversations 的循环体)。 */
46
+ function ackFrame(p, h) {
47
+ const ack = { code: 200, headers: { mid: 'mid' in h ? h.mid : p.mid(), sid: 'sid' in h ? h.sid : '' } };
48
+ for (const key of ['app-key', 'ua', 'dt'])
49
+ if (key in h)
50
+ ack.headers[key] = h[key];
51
+ return ack;
52
+ }
53
+ /** list_all_conversations 的请求:一页 20 条,游标从大往小翻。 */
54
+ function listFrame(p, cid, cursor) {
55
+ return { lwp: '/r/MessageManager/listUserMessages', headers: { mid: p.mid() }, body: [imId(p, cid), false, BigInt(cursor), 20, false] };
56
+ }
57
+ /** 从 SingleChatConversation/create 的响应里取会话 ID(去掉后缀)。 */
58
+ function createdCid(p, res) {
59
+ const direct = res.body?.singleChatConversation?.cid ?? res.body?.cid;
60
+ return typeof direct === 'string' && direct ? plainId(p, direct) : null;
61
+ }
62
+ /** 一个平台的 IMPaaS:绑定了平台差异的帧构造与建连。 */
63
+ export function impaas(p) {
64
+ return {
65
+ /** 加上后缀:`<id>@<domain>`。 */
66
+ imId: (id) => imId(p, id),
67
+ /** 去掉后缀。 */
68
+ plainId: (id) => plainId(p, id),
69
+ regFrame: (token, deviceId) => regFrame(p, token, deviceId),
70
+ ackDiffFrame: () => ackDiffFrame(p),
71
+ heartbeatFrame: () => heartbeatFrame(p),
72
+ listFrame: (cid, cursor) => listFrame(p, cid, cursor),
73
+ createdCid: (res) => createdCid(p, res),
74
+ /** 连接并注册,见 {@link Im.open}。 */
75
+ open: (client, options) => Im.open(p, client, options),
76
+ };
77
+ }
78
+ const realConnect = (url, headers, ctx) => openSocket(url, {
79
+ // Host、Connection 由 WebSocket 库自己生成
80
+ headers: headers.filter(([k]) => !/^(host|connection)$/i.test(k)),
81
+ ...(ctx.config.proxy ? { proxy: ctx.config.proxy } : {}),
82
+ signal: ctx.signal,
83
+ });
84
+ let connector = realConnect;
85
+ /** 替换建立长连的函数,返回恢复函数。只给测试用。 */
86
+ export function mockConnect(fn) {
87
+ const prev = connector;
88
+ connector = fn;
89
+ return () => {
90
+ connector = prev;
91
+ };
92
+ }
93
+ /** 一条私信长连:注册、ack、按 mid 配对请求与响应、收集推送。 */
94
+ export class Im {
95
+ p;
96
+ ctx;
97
+ socket;
98
+ pending = new Map();
99
+ pushes = [];
100
+ wake = null;
101
+ failure = null;
102
+ closed = false;
103
+ regMid = '';
104
+ heartbeat = null;
105
+ readyResolve;
106
+ readyReject;
107
+ readyPromise = new Promise((resolve, reject) => {
108
+ this.readyResolve = resolve;
109
+ this.readyReject = reject;
110
+ });
111
+ constructor(p, ctx) {
112
+ this.p = p;
113
+ this.ctx = ctx;
114
+ this.readyPromise.catch(() => { });
115
+ }
116
+ /**
117
+ * 连接并注册(上游 init:先 get_token,再发 /reg 与 ackDiff)。listen 时再发一次心跳,之后每 15 秒一次。
118
+ * 上游先连接再取 token;这里先取 token,登录态无效时不必建连接。
119
+ */
120
+ static async open(p, client, options = {}) {
121
+ const token = await p.token(client);
122
+ const im = new Im(p, client.ctx);
123
+ im.socket = await connector(p.url, p.headers(client.cookieString()), client.ctx);
124
+ try {
125
+ const reg = regFrame(p, token, client.deviceId);
126
+ im.regMid = reg.headers.mid;
127
+ await im.send(reg);
128
+ await im.send(ackDiffFrame(p));
129
+ if (options.heartbeat) {
130
+ await im.send(heartbeatFrame(p));
131
+ im.heartbeat = setInterval(() => void im.send(heartbeatFrame(p)).catch(() => { }), HEARTBEAT_MS);
132
+ im.heartbeat.unref();
133
+ }
134
+ }
135
+ catch (err) {
136
+ im.close();
137
+ throw err;
138
+ }
139
+ void im.read();
140
+ return im;
141
+ }
142
+ send(frame) {
143
+ return this.socket.send(jsonDumps(frame));
144
+ }
145
+ /** 等服务端的 /s/vulcan(上游 list_all_conversations 收到它才发请求);注册失败时报错。 */
146
+ async ready() {
147
+ let timer;
148
+ const timeout = new Promise((resolve) => (timer = setTimeout(resolve, READY_MS)));
149
+ try {
150
+ await Promise.race([this.readyPromise, timeout]);
151
+ }
152
+ finally {
153
+ clearTimeout(timer);
154
+ }
155
+ }
156
+ /** 发请求帧,等同一 mid 的响应;code 不是 200 时报错。 */
157
+ async request(frame) {
158
+ if (this.failure)
159
+ throw this.failure;
160
+ const mid = String(frame.headers.mid);
161
+ const response = new Promise((resolve, reject) => this.pending.set(mid, { resolve, reject }));
162
+ let timer;
163
+ const timeout = new Promise((_, reject) => {
164
+ timer = setTimeout(() => {
165
+ this.pending.delete(mid);
166
+ reject(new SpecificAIError('NETWORK', `私信请求超时:${frame.lwp}`, { detail: { kind: 'timeout' } }));
167
+ }, this.ctx.config.timeout * 1000);
168
+ });
169
+ try {
170
+ await this.send(frame);
171
+ const res = await Promise.race([response, timeout]);
172
+ if (res.code !== 200)
173
+ throw this.responseError(frame.lwp, res);
174
+ return res;
175
+ }
176
+ finally {
177
+ clearTimeout(timer);
178
+ }
179
+ }
180
+ /** 取一页消息记录(上游 list_all_conversations 循环里的一次请求)。游标没往前走时按没有更多处理,免得原地打转。 */
181
+ async historyPage(cid, cursor) {
182
+ const body = (await this.request(listFrame(this.p, cid, cursor))).body ?? {};
183
+ const nextCursor = body.nextCursor == null ? null : String(body.nextCursor);
184
+ const hasMore = Number(body.hasMore) === 1 && nextCursor != null && nextCursor !== cursor;
185
+ return { models: body.userMessageModels ?? [], nextCursor, hasMore };
186
+ }
187
+ /**
188
+ * 在同一条连接上按 nextCursor 往更早翻(上游 list_all_conversations:收到一页就接着发下一页的请求),
189
+ * 直到没有更多;调用方不再需要时提前结束迭代即可。翻页间隔用平台默认值(AGENTS 4.9)。
190
+ */
191
+ async *historyPages(cid, cursor) {
192
+ for (;;) {
193
+ const page = await this.historyPage(cid, cursor);
194
+ yield page;
195
+ if (!page.hasMore)
196
+ return;
197
+ cursor = page.nextCursor;
198
+ await rand.sleep(this.ctx.platform.pageInterval, this.ctx.signal);
199
+ }
200
+ }
201
+ /** 服务端推送(带 syncPushPackage 的帧),直到连接关闭;连接出错时抛出。 */
202
+ async *pushFrames() {
203
+ for (;;) {
204
+ while (this.pushes.length)
205
+ yield this.pushes.shift();
206
+ if (this.failure)
207
+ throw this.failure;
208
+ if (this.closed)
209
+ return;
210
+ await new Promise((r) => (this.wake = r));
211
+ }
212
+ }
213
+ close() {
214
+ if (this.closed)
215
+ return;
216
+ this.closed = true;
217
+ this.socket?.close();
218
+ this.finish(null);
219
+ }
220
+ async read() {
221
+ try {
222
+ for await (const raw of this.socket.messages) {
223
+ let msg;
224
+ try {
225
+ msg = jsonLoads(typeof raw === 'string' ? raw : Buffer.from(raw).toString('utf8'));
226
+ }
227
+ catch {
228
+ continue;
229
+ }
230
+ const h = msg?.headers;
231
+ if (h && typeof h === 'object')
232
+ await this.send(ackFrame(this.p, h)).catch(() => { });
233
+ this.dispatch(msg);
234
+ }
235
+ this.finish(this.closed ? null : new SpecificAIError('NETWORK', '私信长连接已断开', { detail: { kind: 'connect' } }));
236
+ }
237
+ catch (err) {
238
+ this.finish(this.closed ? null : err);
239
+ }
240
+ }
241
+ dispatch(msg) {
242
+ if (msg.lwp === '/s/vulcan')
243
+ this.readyResolve();
244
+ const mid = msg.headers?.mid;
245
+ if (mid != null && 'code' in msg) {
246
+ if (mid === this.regMid && msg.code !== 200) {
247
+ // 注册失败(token 无效等):等待中的请求和推送都以它结束
248
+ const err = this.responseError('/reg', msg);
249
+ this.failure ??= err;
250
+ this.readyReject(err);
251
+ this.notify();
252
+ return;
253
+ }
254
+ const waiter = this.pending.get(String(mid));
255
+ if (waiter) {
256
+ this.pending.delete(String(mid));
257
+ waiter.resolve(msg);
258
+ return;
259
+ }
260
+ }
261
+ if (msg.body?.syncPushPackage) {
262
+ this.pushes.push(msg);
263
+ this.notify();
264
+ }
265
+ }
266
+ responseError(lwp, res) {
267
+ const reason = String(res.body?.reason ?? res.body?.message ?? res.body?.msg ?? '');
268
+ if (res.code === 401)
269
+ return authError(this.ctx, `私信登录失败:${reason || res.code}`);
270
+ return new SpecificAIError('UPSTREAM', `私信请求 ${lwp} 失败:${reason || res.code}`, { detail: { code: res.code, body: res.body ?? null } });
271
+ }
272
+ /** 连接结束(主动关闭或断线):停心跳,等待中的请求和推送都以错误结束。 */
273
+ finish(err) {
274
+ if (err && !this.failure)
275
+ this.failure = err;
276
+ this.closed = true;
277
+ // 断线时 read() 先走到这里,之后的 close() 会直接返回,心跳必须在这里停
278
+ if (this.heartbeat)
279
+ clearInterval(this.heartbeat);
280
+ this.heartbeat = null;
281
+ const e = this.failure ?? new SpecificAIError('NETWORK', '私信长连接已关闭', { detail: { kind: 'connect' } });
282
+ for (const w of this.pending.values())
283
+ w.reject(e);
284
+ this.pending.clear();
285
+ this.readyReject(e);
286
+ this.notify();
287
+ }
288
+ notify() {
289
+ const w = this.wake;
290
+ this.wake = null;
291
+ w?.();
292
+ }
293
+ }
294
+ // ================================================================ 命令里的共同流程
295
+ /**
296
+ * msg history 截在一页中间时的游标:`<这一页的起始游标>+<N>`,续翻时重取这一页、跳过已经输出的 N 条。
297
+ * 与 core 的 `<游标>#skip=N` 同义(cli/dispatch 的 runPaged);写法不同,是因为 core 见到 `#skip=N` 会自己从结果的
298
+ * 前面丢掉 N 条,而这里的结果是反转过的(从旧到新),要丢的是这一页最新的 N 条,只能由 handler 自己跳过。
299
+ */
300
+ const RESUME_RE = /^(\d+)\+(\d+)$/;
301
+ /**
302
+ * msg history:上游 list_all_conversations 在一条连接上按 nextCursor 一直翻到底;这里也在同一条连接上翻,
303
+ * 翻到 `--limit` 条或(`--all`)没有更多为止,不带时只取一页,不让 core 每页重新取 token、建连接、注册、等 /s/vulcan。
304
+ * 接口从新到旧给;取最新的那些条,再像上游一样反转成从旧到新。`page.cursor` 接着往更早翻。
305
+ */
306
+ export async function history(ctx, open, cid, toMessage) {
307
+ const resume = RESUME_RE.exec(ctx.cursor ?? '');
308
+ const cursor = resume ? resume[1] : (ctx.cursor ?? FIRST_CURSOR);
309
+ if (!/^\d+$/.test(cursor))
310
+ throw new SpecificAIError('USAGE', `--cursor 不对:${ctx.cursor}`, { hint: '用上次输出的 page.cursor' });
311
+ let skip = resume ? Number(resume[2]) : 0;
312
+ const { limit, all } = ctx.options;
313
+ // 不带 --limit / --all 时只取一页
314
+ const want = limit ?? (all ? Infinity : 0);
315
+ const im = await open();
316
+ try {
317
+ await im.ready();
318
+ const models = [];
319
+ const done = (next, more) => paged(models.reverse().map(toMessage), next, more);
320
+ let start = cursor;
321
+ let last = null;
322
+ for await (const page of im.historyPages(cid, cursor)) {
323
+ const fresh = page.models.slice(skip);
324
+ if (want > 0 && models.length + fresh.length > want) {
325
+ // --limit 截在这一页中间:留下最新的,游标指回这一页的起始,续翻时跳过已经输出的
326
+ const take = want - models.length;
327
+ models.push(...fresh.slice(0, take));
328
+ return done(`${start}+${skip + take}`, true);
329
+ }
330
+ models.push(...fresh);
331
+ skip = 0;
332
+ last = page;
333
+ if (models.length >= want)
334
+ break;
335
+ start = page.nextCursor;
336
+ }
337
+ return done(last?.nextCursor, last?.hasMore ?? false);
338
+ }
339
+ finally {
340
+ im.close();
341
+ }
342
+ }
343
+ /** base64 解开后就是 JSON 文本(以 `{` 开头):会话唤起(contentType 8)等状态推送,不是 MessagePack。 */
344
+ function isBase64Json(data) {
345
+ const head = Buffer.from(data.slice(0, 8), 'base64');
346
+ return head[0] === 0x7b;
347
+ }
348
+ /**
349
+ * 推送帧里的聊天消息:syncPushPackage.data[].data 是 base64 + MessagePack,用平台的上游 JS 解码成 JSON;
350
+ * 能直接解析成 JSON 的是状态类推送,跳过(上游 handle_message)。base64 里直接是 JSON 的也是状态推送
351
+ * (真机 2026-09-29:建连后会收到一批会话唤起),同样跳过,不当成解码失败。
352
+ */
353
+ export function* pushedPayloads(frame, decrypt, ctx) {
354
+ for (const entry of frame?.body?.syncPushPackage?.data ?? []) {
355
+ const data = entry?.data;
356
+ if (typeof data !== 'string')
357
+ continue;
358
+ try {
359
+ jsonLoads(data);
360
+ continue;
361
+ }
362
+ catch { }
363
+ if (isBase64Json(data))
364
+ continue;
365
+ let decoded;
366
+ try {
367
+ decoded = jsonLoads(decrypt(data));
368
+ }
369
+ catch (err) {
370
+ ctx.log.debug(`推送解码失败:${err.message}`);
371
+ continue;
372
+ }
373
+ yield decoded;
374
+ }
375
+ }
376
+ const isAuthError = (err) => err instanceof SpecificAIError && (err.code === 'AUTH_REQUIRED' || err.code === 'AUTH_EXPIRED');
377
+ /** 登录态失效时不再重连:由重连循环里的连接交出来,在外面抛出。 */
378
+ class Fatal {
379
+ error;
380
+ constructor(error) {
381
+ this.error = error;
382
+ }
383
+ }
384
+ /**
385
+ * msg listen:带心跳的长连(上游 main),断线自动重连;登录态失效时直接抛出,不再重连。
386
+ * messages 从一帧推送里取要输出的消息。
387
+ */
388
+ export async function* listen(ctx, open, messages) {
389
+ const stream = reconnecting(ctx, async function* () {
390
+ let im = null;
391
+ try {
392
+ im = await open();
393
+ for await (const frame of im.pushFrames())
394
+ yield* messages(frame);
395
+ }
396
+ catch (err) {
397
+ if (!isAuthError(err))
398
+ throw err;
399
+ yield new Fatal(err);
400
+ }
401
+ finally {
402
+ im?.close();
403
+ }
404
+ });
405
+ for await (const item of stream) {
406
+ if (item instanceof Fatal)
407
+ throw item.error;
408
+ yield item;
409
+ }
410
+ }