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,150 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { existsSync } from 'node:fs';
3
+ import { dirname } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { models } from 'specific-ai-assets-jd';
6
+ import { SpecificAIError } from '../../../core/errors.js';
7
+ import { staticFile } from '../../../core/paths.js';
8
+ import { PROFILE } from './profile.js';
9
+ /**
10
+ * 无浏览器的 JCAP 图形验证码(上游 utils/jcap_solver.py):官方 JCAP JS/WASM 在 static/jd/jcap/env/run.js 的
11
+ * 补环境里运行。run.js 依赖进程级状态(读 stdin、改写全局、process.exit),按 AGENTS 7.4 用 node 子进程跑;
12
+ * 它原本用 Python 起的两个子进程(网络桥 http_bridge.py、图像求解 captcha_solver.py)由预加载的 jcap/host.js
13
+ * 换成 specific-ai 自己的 jcap/helper.js(wreq-js 网络、TS 移植的求解器)。
14
+ */
15
+ const RESULT_PREFIX = '__JCAP_RESULT__';
16
+ const LINES = /\r?\n/;
17
+ /** 从 run.js 的输出里取最后一次各阶段的诊断信息(不含票据)。 */
18
+ function diagnostics(output) {
19
+ const latest = {};
20
+ for (const line of output.split(LINES)) {
21
+ const m = /^\[(captcha\.[a-z.-]+|env\.summary)\]\s*(\{.*\})\s*$/.exec(line);
22
+ if (!m)
23
+ continue;
24
+ try {
25
+ const v = JSON.parse(m[2]);
26
+ if (m[1] === 'captcha.check')
27
+ latest.check = { code: v.code, sCode: v.sCode, tp: v.tp, message: v.message };
28
+ else if (m[1] === 'captcha.solve')
29
+ latest.solve = { tp: v.tp, solver: v.solver, attempt: v.attempt, retry: v.retry, reason: v.reason };
30
+ else if (m[1] === 'captcha.solver-process')
31
+ latest.solver_process = { status: v.status, error: v.error };
32
+ else if (m[1] === 'captcha.challenge')
33
+ latest.challenge = { code: v.code, tp: v.tp };
34
+ }
35
+ catch { }
36
+ }
37
+ return JSON.stringify(latest);
38
+ }
39
+ /** 完成 JCAP 会话,返回服务端验证票据 vt(solve_graphic_captcha)。 */
40
+ export async function solveCaptcha(jd, o) {
41
+ if (!o.sessionId)
42
+ throw new SpecificAIError('ERROR', '纯程序图形验证码缺少 sessionId');
43
+ const host = fileURLToPath(new URL('./jcap/host.js', import.meta.url));
44
+ const helper = fileURLToPath(new URL('./jcap/helper.js', import.meta.url));
45
+ if (!existsSync(host) || !existsSync(helper))
46
+ throw new SpecificAIError('ERROR', 'JCAP 运行时缺失(需要编译后的 specific-ai)');
47
+ for (const file of Object.values(models)) {
48
+ if (!existsSync(file))
49
+ throw new SpecificAIError('ERROR', `JCAP 模型缺失:${file}`, { hint: '重新安装 specific-ai-cli(specific-ai-assets-jd)' });
50
+ }
51
+ const timeout = Math.max(30, o.timeout ?? 180);
52
+ const runJs = staticFile('jd', 'jcap/env/run.js');
53
+ const input = {
54
+ sessionId: o.sessionId,
55
+ account: o.account ?? '',
56
+ cookie: jd.cookieStr,
57
+ liveNetwork: true,
58
+ autoSolve: true,
59
+ returnResult: true,
60
+ maxSolveAttempts: Math.max(1, Math.min(30, o.attempts ?? 20)),
61
+ runTimeoutMs: Math.max(30_000, timeout * 1000 - 5_000),
62
+ // run.js 用它起网络桥与求解器子进程:预加载的 host.js 把它们换成 helper.js
63
+ pythonExecutable: process.execPath,
64
+ solverModel: models.u2netp,
65
+ orientationModel: models.orientation,
66
+ userAgent: PROFILE.ua,
67
+ secChUa: PROFILE.secChUa,
68
+ secChUaMobile: PROFILE.secChUaMobile,
69
+ secChUaPlatform: PROFILE.secChUaPlatform,
70
+ localStorage: jd.localStorageFor(o.pageUrl),
71
+ pageUrl: o.pageUrl,
72
+ };
73
+ const output = await new Promise((resolve, reject) => {
74
+ const child = spawn(process.execPath, ['--import', new URL('./jcap/host.js', import.meta.url).href, runJs], {
75
+ cwd: dirname(runJs),
76
+ env: {
77
+ ...process.env,
78
+ SPECIFIC_AI_JD_HELPER: helper,
79
+ SPECIFIC_AI_JD_PROXY: jd.ctx.config.proxy ?? '',
80
+ SPECIFIC_AI_JD_TIMEOUT: String(jd.ctx.config.timeout),
81
+ },
82
+ stdio: ['pipe', 'pipe', 'pipe'],
83
+ windowsHide: true,
84
+ });
85
+ let out = '';
86
+ // 上游的求解器只认 tp=2/3/26/30;遇到别的类型会点"刷新",界面里没有刷新按钮时就一直等到超时。
87
+ // 这里在"不支持的类型"之后 15 秒内没有新题就提前结束(上游要等满整个超时)。
88
+ let stalled;
89
+ const watch = (chunk) => {
90
+ out += chunk;
91
+ if (/\[captcha\.(challenge|check)\]/.test(chunk))
92
+ clearTimeout(stalled);
93
+ const m = /\[captcha\.solve\] (\{.*"unsupported-refresh".*\})/.exec(chunk);
94
+ if (m) {
95
+ clearTimeout(stalled);
96
+ let tp = null;
97
+ try {
98
+ tp = JSON.parse(m[1]).tp;
99
+ }
100
+ catch { }
101
+ stalled = setTimeout(() => {
102
+ child.kill();
103
+ reject(new SpecificAIError('RISK_CONTROL', `京东下发了纯程序还不能求解的验证码(tp=${tp})`, { detail: { kind: 'captcha', tp } }));
104
+ }, 15_000);
105
+ }
106
+ };
107
+ child.stdout.setEncoding('utf8').on('data', watch);
108
+ child.stderr.setEncoding('utf8').on('data', watch);
109
+ const timer = setTimeout(() => {
110
+ child.kill();
111
+ reject(new SpecificAIError('RISK_CONTROL', `纯程序图形验证码求解超时:${diagnostics(out)}`, { detail: { kind: 'captcha', stage: 'timeout' } }));
112
+ }, timeout * 1000);
113
+ child.on('error', (err) => {
114
+ clearTimeout(timer);
115
+ clearTimeout(stalled);
116
+ reject(new SpecificAIError('ERROR', `纯程序图形验证码进程启动失败:${err.message}`));
117
+ });
118
+ child.on('close', () => {
119
+ clearTimeout(timer);
120
+ clearTimeout(stalled);
121
+ resolve(out);
122
+ });
123
+ child.stdin.end(JSON.stringify(input));
124
+ });
125
+ for (const line of output.split(LINES))
126
+ if (line.trim() && !line.startsWith(RESULT_PREFIX))
127
+ jd.ctx.log.debug(`[jcap] ${line.slice(0, 300)}`);
128
+ const marker = output
129
+ .split(LINES)
130
+ .reverse()
131
+ .find((l) => l.startsWith(RESULT_PREFIX));
132
+ if (!marker)
133
+ throw new SpecificAIError('RISK_CONTROL', `纯程序图形验证码未返回验证结果:${diagnostics(output)}`, { detail: { kind: 'captcha' } });
134
+ let payload;
135
+ try {
136
+ payload = JSON.parse(marker.slice(RESULT_PREFIX.length));
137
+ }
138
+ catch {
139
+ throw new SpecificAIError('ERROR', '纯程序图形验证码返回格式异常');
140
+ }
141
+ jd.absorbScript(o.pageUrl, payload);
142
+ if (!payload.ok) {
143
+ const stage = payload.error === 'timeout' ? '超时' : '失败';
144
+ throw new SpecificAIError('RISK_CONTROL', `纯程序图形验证码求解${stage}:${diagnostics(output)}`, { detail: { kind: 'captcha', error: payload.error ?? null } });
145
+ }
146
+ const ticket = String(payload.vt ?? '');
147
+ if (ticket.length < 32 || ticket.length > 4096 || /\s/.test(ticket))
148
+ throw new SpecificAIError('RISK_CONTROL', '纯程序图形验证码票据格式异常', { detail: { kind: 'captcha' } });
149
+ return ticket;
150
+ }
@@ -0,0 +1,420 @@
1
+ import { EventEmitter } from 'node:events';
2
+ import * as realFs from 'node:fs';
3
+ import { createRequire } from 'node:module';
4
+ import { join } from 'node:path';
5
+ import { format } from 'node:util';
6
+ import vm from 'node:vm';
7
+ import { SpecificAIError } from '../../../core/errors.js';
8
+ import { staticFile } from '../../../core/paths.js';
9
+ import * as rand from '../../../core/rand.js';
10
+ import { createContext } from '../../../core/vm.js';
11
+ import { LOGIN_PAGE, PROFILE } from './profile.js';
12
+ /**
13
+ * 上游签名 JS 的宿主(AGENTS 7.4):h5st 5.3(static/h5st5_env.js + h5st5_lib.js)、
14
+ * PC 设备参数(pc_tk_lib.js)、SummerCryptico。
15
+ *
16
+ * 上游把它们作为常驻 node 子进程跑(环境脚本改写整个进程的全局对象);这里每个脚本一个 vm context,
17
+ * 注入一份与 node 主 realm 相同的全局对象,环境脚本对 JSON / Function / Date 的改写只留在 context 里。
18
+ * 脚本里的 `require('https')` 换成走 core/http(wreq-js)的实现,`require('fs')` 里 token 缓存换成凭证里的一段,
19
+ * `require('jsdom')` 补上 jsdom 30 去掉的 ResourceLoader。
20
+ */
21
+ const hostRequire = createRequire(import.meta.url);
22
+ const STATIC = staticFile('jd', '');
23
+ /** node 主 realm 在 ECMAScript 内置对象之外的全局(Node 22)。环境脚本据此判断哪些 jsdom 属性需要提升到全局。 */
24
+ const NODE_GLOBALS = [
25
+ 'process', 'global', 'Buffer', 'clearImmediate', 'setImmediate', 'URL', 'URLSearchParams', 'DOMException',
26
+ 'AbortController', 'AbortSignal', 'Event', 'EventTarget', 'TextEncoder', 'TextDecoder', 'TransformStream',
27
+ 'TransformStreamDefaultController', 'WritableStream', 'WritableStreamDefaultController', 'WritableStreamDefaultWriter',
28
+ 'ReadableStream', 'ReadableStreamDefaultReader', 'ReadableStreamBYOBReader', 'ReadableStreamBYOBRequest',
29
+ 'ReadableByteStreamController', 'ReadableStreamDefaultController', 'ByteLengthQueuingStrategy', 'CountQueuingStrategy',
30
+ 'TextEncoderStream', 'TextDecoderStream', 'CompressionStream', 'DecompressionStream', 'clearInterval', 'clearTimeout',
31
+ 'setInterval', 'setTimeout', 'queueMicrotask', 'structuredClone', 'atob', 'btoa', 'BroadcastChannel', 'MessageChannel',
32
+ 'MessagePort', 'Blob', 'File', 'Performance', 'PerformanceEntry', 'PerformanceMark', 'PerformanceMeasure',
33
+ 'PerformanceObserver', 'PerformanceObserverEntryList', 'PerformanceResourceTiming', 'performance', 'fetch', 'FormData',
34
+ 'Headers', 'Request', 'Response', 'MessageEvent', 'WebSocket', 'Navigator', 'navigator', 'crypto', 'Crypto', 'CryptoKey',
35
+ 'SubtleCrypto', 'CustomEvent',
36
+ ];
37
+ const sources = new Map();
38
+ function source(file) {
39
+ let s = sources.get(file);
40
+ if (s == null)
41
+ sources.set(file, (s = realFs.readFileSync(join(STATIC, file), 'utf8')));
42
+ return s;
43
+ }
44
+ // ---------------------------------------------------------------- 模块替身
45
+ /** jsdom 30 去掉了 ResourceLoader:它在 jsdom 25 里只用来设置 UA,这里换成 jsdom 30 的 resources 对象。 */
46
+ function jsdomModule() {
47
+ const m = hostRequire('jsdom');
48
+ if (m.ResourceLoader)
49
+ return m;
50
+ return {
51
+ ...m,
52
+ ResourceLoader: function ResourceLoader(options) {
53
+ return { userAgent: options?.userAgent };
54
+ },
55
+ };
56
+ }
57
+ /** `https.request` 的替身:脚本里的 XHR 经它走 core/http,与业务请求同一套 TLS 指纹和代理。 */
58
+ function httpsModule(host) {
59
+ return {
60
+ request(options, callback) {
61
+ const req = new EventEmitter();
62
+ const chunks = [];
63
+ req.write = (d) => chunks.push(Buffer.from(d));
64
+ req.end = () => {
65
+ const body = Buffer.concat(chunks);
66
+ const headers = Object.entries(options.headers ?? {}).map(([k, v]) => [k, String(v)]);
67
+ host.http
68
+ .request({ method: options.method ?? 'GET', url: `https://${options.hostname}${options.path}`, headers, body: body.length ? body.toString('utf8') : undefined })
69
+ .then(async (r) => {
70
+ const text = await r.text();
71
+ const res = new EventEmitter();
72
+ res.statusCode = r.status;
73
+ res.headers = {};
74
+ const set = r.headers.getSetCookie();
75
+ r.headers.forEach?.((v, k) => {
76
+ if (k !== 'set-cookie')
77
+ res.headers[k] = v;
78
+ });
79
+ if (set.length)
80
+ res.headers['set-cookie'] = set;
81
+ callback(res);
82
+ res.emit('data', Buffer.from(text, 'utf8'));
83
+ res.emit('end');
84
+ })
85
+ .catch((err) => {
86
+ host.log.debug(`签名脚本的网络请求失败:${err.message}`);
87
+ req.emit('error', err);
88
+ });
89
+ };
90
+ return req;
91
+ },
92
+ };
93
+ }
94
+ /** 只读真实文件;`virtual` 里的路径读写落在内存(token 缓存)。 */
95
+ function fsModule(virtual, onRename) {
96
+ return {
97
+ ...realFs,
98
+ existsSync: (p) => virtual.has(p) || realFs.existsSync(p),
99
+ readFileSync: (p, ...rest) => (typeof p === 'string' && virtual.has(p) ? virtual.get(p) : realFs.readFileSync(p, ...rest)),
100
+ writeFileSync: (p, data) => {
101
+ if (typeof p === 'string' && p.startsWith(VIRTUAL))
102
+ virtual.set(p, String(data));
103
+ else
104
+ throw new Error(`签名脚本不允许写文件:${p}`);
105
+ },
106
+ renameSync: (from, to) => {
107
+ if (!virtual.has(from))
108
+ throw new Error(`签名脚本不允许改名文件:${from}`);
109
+ const text = virtual.get(from);
110
+ virtual.delete(from);
111
+ virtual.set(to, text);
112
+ onRename(to, text);
113
+ },
114
+ appendFileSync: () => { },
115
+ };
116
+ }
117
+ const VIRTUAL = '/specific-ai-virtual/jd/';
118
+ /** 有状态宿主的 context:主 realm 同款全局 + 伪造的 process.env、require。 */
119
+ function scriptContext(env, modules, log) {
120
+ const globals = {};
121
+ for (const k of NODE_GLOBALS)
122
+ if (k in globalThis)
123
+ globals[k] = globalThis[k];
124
+ const fakeProcess = new Proxy(process, { get: (t, k) => (k === 'env' ? env : Reflect.get(t, k)) });
125
+ const quiet = (...a) => log.debug(`[jd.js] ${format(...a)}`.slice(0, 500));
126
+ const context = createContext({
127
+ ...globals,
128
+ process: fakeProcess,
129
+ console: { log: quiet, info: quiet, warn: quiet, error: quiet, debug: quiet, trace: quiet },
130
+ require: (name) => (name in modules ? modules[name] : hostRequire(name)),
131
+ __dirname: STATIC.replace(/[\\/]$/, ''),
132
+ module: { exports: {} },
133
+ exports: {},
134
+ });
135
+ context.global = context;
136
+ return context;
137
+ }
138
+ const TK_CACHE = `${VIRTUAL}h5st_token_cache.json`;
139
+ const isRealToken = (h5st) => {
140
+ const segs = (h5st ?? '').split(';');
141
+ return segs.length > 3 && segs[3].startsWith('tk03');
142
+ };
143
+ /**
144
+ * h5st 签名器(上游常驻签名进程)。cookie 变化时重建 context;origin / referer 只影响向 cactus 换 token 的那一发,
145
+ * 只在还没有 context 时更新(与上游 configure 一致)。
146
+ */
147
+ export class H5st {
148
+ host;
149
+ store;
150
+ cookie = '';
151
+ origin = 'https://search.jd.com';
152
+ referer = 'https://search.jd.com/';
153
+ context = null;
154
+ constructor(host, store) {
155
+ this.host = host;
156
+ this.store = store;
157
+ }
158
+ configure(cookie, origin, referer) {
159
+ const changed = cookie !== this.cookie;
160
+ this.cookie = cookie ?? '';
161
+ if (origin && !this.context)
162
+ this.origin = origin;
163
+ if (referer && !this.context)
164
+ this.referer = referer;
165
+ if (changed)
166
+ this.context = null;
167
+ }
168
+ /** 丢弃 context,下次签名重新加载(上游 shutdown)。 */
169
+ reset() {
170
+ this.context = null;
171
+ }
172
+ ensure() {
173
+ if (this.context)
174
+ return this.context;
175
+ const virtual = new Map();
176
+ const cached = this.store.load();
177
+ if (cached)
178
+ virtual.set(TK_CACHE, JSON.stringify(cached));
179
+ const env = {
180
+ JD_COOKIE: this.cookie,
181
+ JD_ORIGIN: this.origin,
182
+ JD_REFERER: this.referer,
183
+ JD_TK_PROFILE: PROFILE.profileId,
184
+ JD_TK_CACHE: TK_CACHE,
185
+ };
186
+ const modules = {
187
+ jsdom: jsdomModule(),
188
+ https: httpsModule(this.host),
189
+ fs: fsModule(virtual, (_p, text) => this.store.save(JSON.parse(text))),
190
+ };
191
+ const context = scriptContext(env, modules, this.host.log);
192
+ vm.runInContext(source('h5st5_env.js'), context, { filename: join(STATIC, 'h5st5_env.js') });
193
+ vm.runInContext(source('h5st5_lib.js'), context, { filename: 'h5st5_lib.js' });
194
+ this.context = context;
195
+ return context;
196
+ }
197
+ async signOnce(params, appId) {
198
+ const context = this.ensure();
199
+ const inner = vm.runInContext('JSON', context).parse(JSON.stringify(params));
200
+ let result;
201
+ try {
202
+ const ps = new context.window.ParamsSign({ appId: appId || 'f06cc' });
203
+ result = await ps.sign(inner);
204
+ }
205
+ catch (err) {
206
+ throw new SpecificAIError('ERROR', `h5st5 签名失败:${err?.message ?? err}`);
207
+ }
208
+ context.__jdFlushTokenCache?.();
209
+ return JSON.parse(JSON.stringify(result ?? {}));
210
+ }
211
+ /** 冷启动后第一次签名往往还没拿到服务端 token(tk03),短暂等待后重签。 */
212
+ async sign(params, appId = 'f06cc', warmup = true) {
213
+ let res = await this.signOnce(params, appId);
214
+ if (warmup && !isRealToken(String(res.h5st ?? ''))) {
215
+ for (const delay of [350, 600, 1000]) {
216
+ await rand.sleep(delay);
217
+ // 换 token 的 XHR 由库的定时器发起;上游在两次签名之间有一次进程间往返,这里让出一个定时器周期
218
+ await new Promise((r) => setTimeout(r, 30));
219
+ res = await this.signOnce(params, appId);
220
+ if (isRealToken(String(res.h5st ?? '')))
221
+ break;
222
+ }
223
+ }
224
+ return res;
225
+ }
226
+ }
227
+ // ---------------------------------------------------------------- PC 设备参数(utils/device_token.py + static/pc_tk_server.js)
228
+ export const DEVICE_PROFILE = PROFILE.profileId;
229
+ const IDENTITY_COOKIES = new Set(['3AB9D23F7A4B3C9B', '3AB9D23F7A4B3CSS', 'PCA9D23F7A4B3CSS', 'PCTSD23F7A4B3CSS']);
230
+ /** 换设备票据时去掉旧票据(_without_old_identity)。 */
231
+ function withoutOldIdentity(cookie) {
232
+ return String(cookie ?? '')
233
+ .split(';')
234
+ .map((s) => s.trim())
235
+ .filter((s) => s && !IDENTITY_COOKIES.has(s.split('=', 1)[0].trim()))
236
+ .join('; ');
237
+ }
238
+ function validateDevice(result) {
239
+ const f = Object.fromEntries(['eid', 'eid2', 'fp', 'giaD'].map((k) => [k, String(result?.[k] ?? '')]));
240
+ if (!/^[A-Za-z0-9._-]{32,256}$/.test(f.eid))
241
+ throw new SpecificAIError('ERROR', 'PC 设备参数 eid 格式异常');
242
+ if (!(f.eid2.startsWith('jdd03') && f.eid2.endsWith('X') && f.eid2.length >= 64 && f.eid2.length <= 512)) {
243
+ throw new SpecificAIError('ERROR', 'PC 设备参数 eid2/jsToken 格式异常');
244
+ }
245
+ if (!/^[A-Za-z0-9]{32}$/.test(f.fp))
246
+ throw new SpecificAIError('ERROR', 'PC 设备参数 fp 格式异常');
247
+ if (f.giaD && !/^[A-Za-z0-9._-]{1,128}$/.test(f.giaD))
248
+ throw new SpecificAIError('ERROR', 'PC 设备参数 _gia_d 格式异常');
249
+ return f;
250
+ }
251
+ /** pc_tk_server.js 的 getDeviceFields。 */
252
+ const DEVICE_FIELDS_JS = `(() => {
253
+ const jsToken = new Promise((resolve) => { window.getJsToken((value) => resolve(value || {}), 5000); });
254
+ const eid = new Promise((resolve) => {
255
+ window.getJdEid((value, fp, meta) => resolve({ eid: String(value || ""), fp: String(fp || ""), meta: meta || {} }), null, 100);
256
+ });
257
+ return Promise.all([jsToken, eid]).then(([tk, device]) => {
258
+ const cookies = Object.create(null);
259
+ for (const item of String(document.cookie || "").split(";")) {
260
+ const pos = item.indexOf("=");
261
+ if (pos > 0) cookies[item.slice(0, pos).trim()] = item.slice(pos + 1).trim();
262
+ }
263
+ return JSON.stringify({
264
+ eid: device.eid || String(device.meta.eid || ""),
265
+ eid2: String(tk.jsToken || ""),
266
+ fp: String(tk.fp || device.fp || device.meta.fp || ""),
267
+ giaD: String(cookies._gia_d || ""),
268
+ });
269
+ });
270
+ })()`;
271
+ /**
272
+ * jsdom 30 把 CSS 系统颜色(ActiveBorder 等)解析成 rgb(),上游用的 jsdom 25 原样返回小写关键字。
273
+ * pc-tk.js 会采集这组颜色,这里让 getComputedStyle 对行内写了系统颜色的属性返回关键字,与上游一致。
274
+ */
275
+ const SYSTEM_COLOR_COMPAT = `(() => {
276
+ const SYSTEM = new Set('activeborder,activecaption,appworkspace,background,buttonface,buttonhighlight,buttonshadow,buttontext,captiontext,graytext,highlight,highlighttext,inactiveborder,inactivecaption,inactivecaptiontext,infobackground,infotext,menu,menutext,scrollbar,threeddarkshadow,threedface,threedhighlight,threedlightshadow,threedshadow,window,windowframe,windowtext'.split(','));
277
+ const inline = (el, prop) => { try { const v = String(el && el.style && el.style.getPropertyValue(prop) || '').toLowerCase(); return SYSTEM.has(v) ? v : null } catch (e) { return null } };
278
+ const orig = window.getComputedStyle;
279
+ const patched = function getComputedStyle(el, pseudo) {
280
+ const cs = orig.call(window, el, pseudo);
281
+ return new Proxy(cs, {
282
+ get(t, k) {
283
+ if (k === 'getPropertyValue') return (p) => inline(el, p) ?? t.getPropertyValue(p);
284
+ if (typeof k === 'string') { const hit = inline(el, k.replace(/[A-Z]/g, (c) => '-' + c.toLowerCase())); if (hit) return hit }
285
+ const v = Reflect.get(t, k, t);
286
+ return typeof v === 'function' ? v.bind(t) : v;
287
+ },
288
+ });
289
+ };
290
+ window.getComputedStyle = patched;
291
+ if (typeof globalThis.getComputedStyle === 'function') globalThis.getComputedStyle = patched;
292
+ })()`;
293
+ /**
294
+ * 京东 PC `getJsToken / getJdEid`(上游 device_token.get_device_fields)。
295
+ * 页面作用域(page / origin / referer)或 cookie 变化时重建 context;force 时临时去掉旧设备票据。
296
+ */
297
+ export class DeviceToken {
298
+ host;
299
+ cookie = '';
300
+ page = LOGIN_PAGE;
301
+ origin = 'https://passport.jd.com';
302
+ referer = LOGIN_PAGE;
303
+ context = null;
304
+ constructor(host) {
305
+ this.host = host;
306
+ }
307
+ configure(cookie, page, origin, referer) {
308
+ const nextPage = page || LOGIN_PAGE;
309
+ const next = [cookie ?? '', nextPage, origin || 'https://passport.jd.com', referer || nextPage];
310
+ if (next.join('\n') !== [this.cookie, this.page, this.origin, this.referer].join('\n')) {
311
+ ;
312
+ [this.cookie, this.page, this.origin, this.referer] = next;
313
+ this.context = null;
314
+ }
315
+ }
316
+ ensure() {
317
+ if (this.context)
318
+ return this.context;
319
+ const env = { JD_COOKIE: this.cookie, JD_PAGE_URL: this.page, JD_ORIGIN: this.origin, JD_REFERER: this.referer };
320
+ const modules = { jsdom: jsdomModule(), https: httpsModule(this.host), fs: fsModule(new Map(), () => { }) };
321
+ const context = scriptContext(env, modules, this.host.log);
322
+ vm.runInContext(source('h5st5_env.js'), context, { filename: join(STATIC, 'h5st5_env.js') });
323
+ vm.runInContext(SYSTEM_COLOR_COMPAT, context);
324
+ vm.runInContext('console.debug = () => {}; console.info = () => {};', context);
325
+ vm.runInContext(source('pc_tk_lib.js'), context, { filename: 'pc_tk_lib.js' });
326
+ this.context = context;
327
+ return context;
328
+ }
329
+ async get(force = false) {
330
+ const original = this.cookie;
331
+ if (force) {
332
+ this.context = null;
333
+ this.cookie = withoutOldIdentity(original);
334
+ }
335
+ let text;
336
+ try {
337
+ const context = this.ensure();
338
+ text = await vm.runInContext(DEVICE_FIELDS_JS, context);
339
+ }
340
+ finally {
341
+ if (force) {
342
+ this.context = null;
343
+ this.cookie = original;
344
+ }
345
+ }
346
+ const fields = JSON.parse(text);
347
+ if (!fields.eid || !fields.eid2 || !fields.fp)
348
+ throw new SpecificAIError('ERROR', 'PC 设备参数生成失败:DEVICE_FIELDS_EMPTY');
349
+ return validateDevice(fields);
350
+ }
351
+ }
352
+ /** 确定性模式下的 crypto.getRandomValues:每个元素取一次 Math.random(与对拍的 node 预加载一致)。 */
353
+ export function fillRandom(array, random) {
354
+ const a = array;
355
+ const span = 2 ** (8 * a.BYTES_PER_ELEMENT);
356
+ for (let i = 0; i < a.length; i++)
357
+ a[i] = Math.floor(random() * span);
358
+ return array;
359
+ }
360
+ // ---------------------------------------------------------------- SummerCryptico(utils/summer_cryptico.py + static/summer_cryptico_runner.js)
361
+ const SUMMER_URL = 'https://jrsecstatic.jdpay.com/jr-sec-dev-static/summer-cryptico-h5.min.js';
362
+ let summerSource = '';
363
+ /** 官方 SummerCryptico 脚本:与上游一样运行时从京东 CDN 取,进程内缓存。 */
364
+ export async function loadSummerSource(http) {
365
+ if (summerSource)
366
+ return summerSource;
367
+ const res = await http.request({ url: SUMMER_URL, cookies: false, timeout: 20 });
368
+ const text = await res.text();
369
+ if (res.status !== 200 || !text.includes('SummerCryptico'))
370
+ throw new SpecificAIError('UPSTREAM', `SummerCryptico 官方脚本加载失败 HTTP ${res.status}`);
371
+ return (summerSource = text);
372
+ }
373
+ /** 测试用:直接给定脚本内容。 */
374
+ export function setSummerSource(text) {
375
+ summerSource = text;
376
+ }
377
+ /** 用服务端下发的 GMPK 加密一个值(summer_cryptico.encrypt)。 */
378
+ export async function summerEncrypt(http, publicKey, plaintext) {
379
+ const key = String(publicKey ?? '');
380
+ const value = String(plaintext ?? '');
381
+ if (!key || !value)
382
+ throw new SpecificAIError('ERROR', 'SummerCryptico 公钥和明文不能为空');
383
+ if (!/^[A-Za-z0-9+/]*={0,2}$/.test(key) || key.length % 4)
384
+ throw new SpecificAIError('ERROR', 'SummerCryptico 公钥格式异常');
385
+ if (Buffer.from(key, 'base64').length < 65)
386
+ throw new SpecificAIError('ERROR', 'SummerCryptico 公钥长度异常');
387
+ const src = await loadSummerSource(http);
388
+ // runner 把自己的 global 当 window:这里的 host context 就是它
389
+ const host = createContext({});
390
+ const storage = new Map();
391
+ const libraryModule = { exports: {} };
392
+ host.window = host;
393
+ host.navigator = { appName: 'Netscape' };
394
+ host.localStorage = {
395
+ getItem: (k) => (storage.has(k) ? storage.get(k) : null),
396
+ setItem: (k, v) => void storage.set(k, String(v)),
397
+ };
398
+ const hostGet = (name) => vm.runInContext(name, host);
399
+ const hostMath = hostGet('Math');
400
+ host.crypto = rand.isDeterministic() ? { getRandomValues: (a) => fillRandom(a, () => hostMath.random()) } : globalThis.crypto;
401
+ const inner = vm.createContext({
402
+ module: libraryModule,
403
+ exports: libraryModule.exports,
404
+ window: host,
405
+ navigator: host.navigator,
406
+ localStorage: host.localStorage,
407
+ console: { log() { }, info() { }, debug() { }, warn() { }, error() { } },
408
+ ...Object.fromEntries(['Math', 'Date', 'JSON', 'Uint8Array', 'Uint32Array', 'ArrayBuffer', 'Promise', 'encodeURIComponent', 'decodeURIComponent', 'escape', 'unescape'].map((k) => [k, hostGet(k)])),
409
+ setTimeout,
410
+ clearTimeout,
411
+ });
412
+ vm.runInContext(src, inner, { filename: 'summer-cryptico-h5.min.js', timeout: 10000 });
413
+ const cryptico = libraryModule.exports?.SummerCryptico;
414
+ if (!cryptico || typeof cryptico.encryptData !== 'function')
415
+ throw new SpecificAIError('ERROR', 'SummerCryptico 本地加密失败:SUMMER_CRYPTICO_EXPORT_MISSING');
416
+ const encrypted = String(cryptico.encryptData(key, value) ?? '');
417
+ if (!/^[A-Za-z0-9+/]+={0,2}$/.test(encrypted))
418
+ throw new SpecificAIError('ERROR', 'SummerCryptico 本地加密失败:EMPTY');
419
+ return encrypted;
420
+ }