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,241 @@
1
+ import * as n from '../../../core/normalize.js';
2
+ /** 快手原始对象 → 归一化类型(AGENTS 6.2)。字段来源参考上游 utils/data_util.py 的 handle_work_info。 */
3
+ export const photoUrl = (id) => `https://www.kuaishou.com/short-video/${id}`;
4
+ export const profileUrl = (eid) => `https://www.kuaishou.com/profile/${eid}`;
5
+ export const liveUrl = (eid) => `https://live.kuaishou.com/u/${eid}`;
6
+ const first = (o, ...keys) => {
7
+ for (const k of keys)
8
+ if (o && o[k] != null && o[k] !== '')
9
+ return o[k];
10
+ return undefined;
11
+ };
12
+ export function ref(id, name) {
13
+ return n.userRef({ id, name, url: id == null || id === '' ? null : profileUrl(id) });
14
+ }
15
+ /** 取 URL:字符串,或 [{url}] / [{cdn,url}] 列表的第一个。 */
16
+ function urlOf(v) {
17
+ if (typeof v === 'string')
18
+ return n.url(v);
19
+ if (Array.isArray(v) && v.length)
20
+ return urlOf(typeof v[0] === 'object' ? v[0]?.url : v[0]);
21
+ return null;
22
+ }
23
+ /** 图集:photo.atlas 或 photo.ext_params.atlas,`{cdn, list: [path]}`。 */
24
+ function atlasImages(photo) {
25
+ const atlas = photo?.atlas ?? photo?.ext_params?.atlas;
26
+ if (!atlas || typeof atlas !== 'object')
27
+ return [];
28
+ let cdn = atlas.cdn ?? '';
29
+ if (Array.isArray(cdn))
30
+ cdn = cdn[0] ?? '';
31
+ return (atlas.list ?? []).map((p) => (cdn ? `https://${cdn}${p}` : p));
32
+ }
33
+ /** manifest.adaptationSet[].representation[]:挑分辨率最高的一路。 */
34
+ export function bestRepresentation(manifest) {
35
+ const reps = [];
36
+ for (const set of manifest?.adaptationSet ?? [])
37
+ for (const r of set?.representation ?? [])
38
+ if (r?.url)
39
+ reps.push({ ...r, duration: set.duration });
40
+ reps.sort((a, b) => (b.width ?? 0) * (b.height ?? 0) - (a.width ?? 0) * (a.height ?? 0) || (b.maxBitrate ?? b.avgBitrate ?? 0) - (a.maxBitrate ?? a.avgBitrate ?? 0));
41
+ return reps[0];
42
+ }
43
+ /** 作品的媒体:图集为图片,否则取 manifest 最高画质,退回 photoUrl。 */
44
+ export function photoMedia(photo) {
45
+ const images = atlasImages(photo);
46
+ if (images.length)
47
+ return images.map((url, i) => n.media({ id: String(i + 1), type: 'image', url }));
48
+ const manifest = typeof photo?.manifest === 'object' && photo.manifest ? photo.manifest : null;
49
+ const best = bestRepresentation(manifest);
50
+ const duration = n.seconds(photo?.duration != null ? Number(photo.duration) / 1000 : null);
51
+ if (best)
52
+ return [n.media({ id: n.idOrNull(best.id), type: 'video', url: n.url(best.url), width: best.width ?? null, height: best.height ?? null, duration }, best)];
53
+ const url = urlOf(first(photo, 'photoUrl', 'photoUrls', 'mainMvUrls'));
54
+ return url ? [n.media({ type: 'video', url, width: photo?.width ?? null, height: photo?.height ?? null, duration })] : [];
55
+ }
56
+ /** 一条 feed(`{photo, author, ...}`)或单独的 photo。 */
57
+ export function feed(v, authorFallback) {
58
+ const photo = v?.photo ?? v;
59
+ const author = v?.author ?? authorFallback ?? {};
60
+ const id = n.id(first(photo, 'id', 'photoId', 'photo_id'));
61
+ const images = atlasImages(photo);
62
+ const comments = first(photo, 'commentCount', 'comment_count') ?? first(v?.comment, 'us_c', 'commentCount');
63
+ return n.item({
64
+ id,
65
+ kind: images.length ? 'image' : 'video',
66
+ url: photoUrl(id),
67
+ text: n.str(first(photo, 'caption', 'originCaption')),
68
+ author: ref(first(author, 'id', 'authorId', 'user_id', 'eid'), first(author, 'name', 'user_name', 'nickname')),
69
+ created_at: n.time(first(photo, 'timestamp', 'create_time')),
70
+ cover: urlOf(first(photo, 'coverUrl', 'coverUrls')),
71
+ media: photoMedia(photo),
72
+ stats: {
73
+ views: n.count(first(photo, 'viewCount', 'view_count')),
74
+ likes: n.count(first(photo, 'realLikeCount', 'likeCount', 'like_count')),
75
+ comments: n.count(comments),
76
+ collects: n.count(first(photo, 'collectCount', 'collect_count')),
77
+ shares: n.count(first(photo, 'shareCount', 'share_count')),
78
+ },
79
+ }, v);
80
+ }
81
+ /** visionVideoDetail。 */
82
+ export function detail(d) {
83
+ return feed({ ...d, photo: d.photo, author: d.author });
84
+ }
85
+ /** 当前用户资料(/rest/v/profile/get)。 */
86
+ export function selfUser(v) {
87
+ const id = n.id(first(v, 'eid', 'userDefineId', 'userId', 'user_id'));
88
+ return n.user({
89
+ id,
90
+ name: n.str(first(v, 'userName', 'user_name', 'name')),
91
+ handle: n.str(first(v, 'kwaiId', 'userId')),
92
+ avatar: n.url(first(v, 'userHead', 'headurl', 'avatar')),
93
+ url: profileUrl(id),
94
+ bio: n.str(first(v, 'userText', 'user_text', 'description')),
95
+ stats: {
96
+ followers: n.count(first(v, 'fans', 'fan', 'followerCount')),
97
+ following: n.count(first(v, 'follows', 'follow', 'followingCount')),
98
+ items: n.count(first(v, 'photo', 'photoCount', 'photo_public')),
99
+ likes: n.count(first(v, 'liked', 'likedCount')),
100
+ },
101
+ }, v);
102
+ }
103
+ /** visionProfileReduced。 */
104
+ export function reducedUser(v) {
105
+ const p = v?.userProfile?.profile ?? {};
106
+ const id = n.id(p.user_id);
107
+ return n.user({ id, name: n.str(p.user_name), avatar: n.url(p.headurl), url: profileUrl(id), bio: n.str(p.user_text) }, v);
108
+ }
109
+ /** 搜索 / 关注 / 粉丝列表里的用户。 */
110
+ export function listUser(v) {
111
+ const id = n.id(first(v, 'user_id', 'eid', 'id', 'userId'));
112
+ return n.user({
113
+ id,
114
+ name: n.str(first(v, 'user_name', 'name', 'userName')),
115
+ handle: n.str(first(v, 'kwaiId', 'kwai_id')),
116
+ avatar: n.url(first(v, 'headurl', 'headerUrl', 'avatar', 'userHead')),
117
+ url: profileUrl(id),
118
+ bio: n.str(first(v, 'user_text', 'description', 'userText')),
119
+ stats: {
120
+ followers: n.count(first(v, 'fansCount', 'fans', 'fan')),
121
+ following: n.count(first(v, 'followCount', 'follows')),
122
+ items: n.count(first(v, 'photoCount', 'photo')),
123
+ },
124
+ }, v);
125
+ }
126
+ /** GraphQL 评论(rootCommentsV2 / subCommentsV2 / REST 的同名字段)。 */
127
+ export function comment(v, itemId, parentId = null) {
128
+ return n.comment({
129
+ id: n.id(v.commentId ?? v.comment_id),
130
+ item_id: itemId,
131
+ parent_id: parentId,
132
+ author: ref(first(v, 'authorEid', 'authorId', 'author_id'), first(v, 'authorName', 'author_name')),
133
+ text: String(v.content ?? ''),
134
+ created_at: n.time(v.timestamp),
135
+ stats: { likes: n.count(first(v, 'realLikedCount', 'likedCount')), replies: n.count(first(v, 'subCommentCount')) ?? (v.hasSubComments ? null : 0) },
136
+ }, v);
137
+ }
138
+ /** 直播首页 home/list 里的直播间(id 用主播 eid,也就是房间页 /u/<eid>)。 */
139
+ export function liveRoom(v) {
140
+ const author = v?.author ?? {};
141
+ const eid = n.id(author.id);
142
+ return n.live({
143
+ id: eid,
144
+ url: liveUrl(eid),
145
+ title: n.str(v.caption),
146
+ status: v.living || author.living ? 'live' : 'offline',
147
+ host: n.userRef({ id: eid, name: author.name, url: liveUrl(eid) }),
148
+ cover: n.url(v.poster),
149
+ stats: { viewers: n.count(v.watchingCount) },
150
+ }, v);
151
+ }
152
+ /** 直播回放列表。 */
153
+ export function playback(v, eid) {
154
+ const id = n.id(first(v, 'id', 'productId', 'photoId'));
155
+ return n.item({
156
+ id,
157
+ kind: 'video',
158
+ url: `https://live.kuaishou.com/playback/${id}`,
159
+ title: n.str(first(v, 'caption', 'title')),
160
+ author: ref(eid, null),
161
+ created_at: n.time(first(v, 'createTime', 'timestamp')),
162
+ cover: n.url(first(v, 'poster', 'coverUrl')),
163
+ stats: { views: n.count(v.viewCount), likes: n.count(v.likeCount), comments: n.count(v.commentCount) },
164
+ }, v);
165
+ }
166
+ /** 创作者中心作品管理的状态。 */
167
+ function workStatus(v) {
168
+ const s = Number(first(v, 'publishStatus', 'status', 'auditStatus'));
169
+ if (!Number.isFinite(s))
170
+ return null;
171
+ if (v?.photoStatus === 2 || v?.privacy === 2)
172
+ return 'private';
173
+ if (s === 1 || s === 3)
174
+ return 'published';
175
+ if (s === 2 || s === 0)
176
+ return 'reviewing';
177
+ // 真机(2026-09-29):仅自己可见的作品都是 publishStatus 4、judgementStatus 1、没有驳回原因,公开主页上看不到
178
+ if (s === 4)
179
+ return 'private';
180
+ if (s < 0)
181
+ return 'rejected';
182
+ return null;
183
+ }
184
+ /** 作品管理页(photo/list)的一行。 */
185
+ export function work(v) {
186
+ const id = n.id(first(v, 'workId', 'photoId', 'photoIdStr', 'publishId'));
187
+ return n.item({
188
+ id,
189
+ // 作品管理的列表没有 photoType:多图图集有 showAtlasIcon;单图没有图集图标,但不显示时长、时长为 0(视频是 showDuration: true)
190
+ kind: first(v, 'atlasId') || v?.photoType === 1 || v?.showAtlasIcon === true || (v?.showDuration === false && !Number(v?.durationSecond)) ? 'image' : 'video',
191
+ url: first(v, 'workId', 'photoId', 'photoIdStr') ? photoUrl(id) : null,
192
+ text: n.str(first(v, 'caption', 'title')),
193
+ author: v?.userIdStr ? n.userRef({ id: String(v.userIdStr), name: n.str(v.userName), url: profileUrl(v.userIdStr) }) : null,
194
+ created_at: n.time(first(v, 'publishTime', 'uploadTime', 'createTime')),
195
+ cover: n.url(first(v, 'coverUrl', 'publishCoverUrl', 'cover')),
196
+ stats: {
197
+ views: n.count(first(v, 'playCount', 'viewCount')),
198
+ likes: n.count(first(v, 'likeCount')),
199
+ comments: n.count(first(v, 'commentCount')),
200
+ collects: n.count(first(v, 'collectCount')),
201
+ shares: n.count(first(v, 'shareCount')),
202
+ },
203
+ status: workStatus(v),
204
+ }, v);
205
+ }
206
+ /** 礼物面板的一项(gift-list 的 `gifts[]`)。价格是快币。 */
207
+ export function gift(g) {
208
+ return n.gift({ id: n.id(g.id), name: String(g.name ?? ''), price: g.unitPrice != null ? { amount: Number(g.unitPrice), currency: 'KSCOIN' } : null }, g);
209
+ }
210
+ /**
211
+ * 礼物字典的 `[id, 名字]`:gift-list 是 `{gifts: [...]}`;allgifts 是以礼物 id 为键的对象,
212
+ * 也兼容数组形式(`gifts` / `list`)。
213
+ */
214
+ export function giftNames(d) {
215
+ const out = [];
216
+ const push = (id, g) => {
217
+ const key = g?.id ?? id;
218
+ if (key != null && key !== '' && g?.name)
219
+ out.push([String(key), String(g.name)]);
220
+ };
221
+ const list = Array.isArray(d) ? d : (d?.gifts ?? d?.list);
222
+ if (Array.isArray(list))
223
+ for (const g of list)
224
+ push(null, g);
225
+ else if (d && typeof d === 'object')
226
+ for (const [id, g] of Object.entries(d))
227
+ if (g && typeof g === 'object')
228
+ push(id, g);
229
+ return out;
230
+ }
231
+ /**
232
+ * 发布后管理页的那一行,合并 publish/refresh 里同一个 publishId 的状态。
233
+ * 作品真正发布出来之前没有 workId,这时 id 是 publishId、url 为空。
234
+ */
235
+ export function publishedWork(row, refresh) {
236
+ const id = row?.publishId;
237
+ const d = refresh?.data;
238
+ const rows = Array.isArray(d) ? d : Array.isArray(d?.list) ? d.list : d && typeof d === 'object' ? Object.values(d) : [];
239
+ const match = rows.find((r) => r && typeof r === 'object' && String(r.publishId ?? r.id) === String(id));
240
+ return work({ ...row, ...(match ?? {}) });
241
+ }
@@ -0,0 +1,329 @@
1
+ import { createHash } from 'node:crypto';
2
+ import * as fs from 'node:fs';
3
+ import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
4
+ import { createRequire } from 'node:module';
5
+ import { basename, dirname, isAbsolute, join } from 'node:path';
6
+ import { format } from 'node:util';
7
+ import vm from 'node:vm';
8
+ import { SpecificAIError } from '../../../core/errors.js';
9
+ import { cacheDir, staticFile } from '../../../core/paths.js';
10
+ import { DEFAULT_SEED, isDeterministic, now } from '../../../core/rand.js';
11
+ import { createContext } from '../../../core/vm.js';
12
+ import { compactJson } from '../../../core/py.js';
13
+ import { ACCEPT_ENCODING, ACCEPT_LANGUAGE, UA } from './profile.js';
14
+ /**
15
+ * webweapon 预言机(上游 utils/sign/weapon_oracle.py、like_token.py):在 node:vm 里跑官方 kwf / kws 脚本
16
+ * 与 likeData token 引擎,产出 `kwfv1` / `kwscode` / 指纹上报 / like token。
17
+ *
18
+ * 上游把 reverse/tools/*_oracle.js 当命令行脚本起子进程(读环境变量、往 stdout 打印 JSON);
19
+ * 这里在 vm 里按 CommonJS 执行同一份脚本(static/kuaishou/ 原样复制),由包装层喂环境变量、收 stdout。
20
+ * 预言机自己会 `require('vm').createContext` 再跑官方脚本:确定性模式(对拍)下给这些内层 context
21
+ * 也注入固定的 Math.random / Date,与 scripts/golden/kuaishou/vm_determinism.cjs 一致。
22
+ */
23
+ const hostRequire = createRequire(import.meta.url);
24
+ const P = 'kuaishou';
25
+ export const KWF_ORACLE = staticFile(P, 'tools/kwf_oracle.js');
26
+ export const KWS_ORACLE = staticFile(P, 'tools/kws_oracle.js');
27
+ export const LIKE_ORACLE = staticFile(P, 'tools/like_token_oracle.js');
28
+ export const WEAPON_DIR = staticFile(P, 'bundles/weapon');
29
+ const KWF_CURRENT_SCRIPT = staticFile(P, 'js/cp-kwf.js');
30
+ const KWF_LEGACY_NAME = 'kwf-0.0.2.2cee19b4b7dec496.js';
31
+ const KWF_LEGACY_SCRIPT = join(WEAPON_DIR, KWF_LEGACY_NAME);
32
+ const KWF_CURRENT_CAPTURE_NAME = 'kwf-0.1.1.a6d1e5d478c2cafa.js';
33
+ const KWF_NEW_GENERATION_COUNTER = 999;
34
+ const HEX64 = /^[0-9a-f]{64}$/;
35
+ const KWS_FILENAME = /^kws-(\d+)-0\.0\.1-obfuscated\.([0-9a-f]{16})\.js$/;
36
+ const KWS_MIN_BYTES = 32_000;
37
+ const KWS_MAX_BYTES = 128_000;
38
+ const sources = new Map();
39
+ function source(file) {
40
+ let s = sources.get(file);
41
+ if (s == null)
42
+ sources.set(file, (s = fs.readFileSync(file, 'utf8')));
43
+ return s;
44
+ }
45
+ /**
46
+ * 确定性模式下注入内层 context 的代码,与 scripts/golden/kuaishou/vm_determinism.cjs 逐字相同。
47
+ * Date 不能用 `class extends Date`:like token 的 VM 用 `constructor.prototype` 找原型,子类会无限循环。
48
+ */
49
+ const INNER_PATCH = (seed, fixed) => `(() => {
50
+ let a = ${seed} >>> 0
51
+ Math.random = () => {
52
+ a = (a + 0x6d2b79f5) >>> 0
53
+ let t = Math.imul(a ^ (a >>> 15), a | 1)
54
+ t = (t + Math.imul(t ^ (t >>> 7), t | 61)) ^ t
55
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296
56
+ }
57
+ const D = Date
58
+ const F = function Date(...a) {
59
+ if (!new.target) return new D(${fixed}).toString()
60
+ return new D(...(a.length ? a : [${fixed}]))
61
+ }
62
+ F.prototype = D.prototype
63
+ F.now = () => ${fixed}
64
+ F.parse = D.parse
65
+ F.UTC = D.UTC
66
+ globalThis.Date = F
67
+ })()`;
68
+ /** 预言机里 `require('vm')` 拿到的模块:确定性模式下每个新 context 都从种子重新开始。 */
69
+ function oracleVm() {
70
+ if (!isDeterministic())
71
+ return vm;
72
+ const patch = INNER_PATCH(DEFAULT_SEED, now());
73
+ return {
74
+ ...vm,
75
+ createContext(sandbox, options) {
76
+ const ctx = vm.createContext(sandbox, options);
77
+ vm.runInContext(patch, ctx);
78
+ return ctx;
79
+ },
80
+ };
81
+ }
82
+ class OracleExit extends Error {
83
+ code;
84
+ constructor(code) {
85
+ super(`exit ${code}`);
86
+ this.code = code;
87
+ }
88
+ }
89
+ /**
90
+ * 按 CommonJS 执行一个预言机脚本,返回它写到 stdout 的内容。env 只含预言机要的 KS_* 变量。
91
+ */
92
+ function runOracle(file, options) {
93
+ const out = [];
94
+ const err = [];
95
+ const env = {};
96
+ for (const [k, v] of Object.entries(options.env))
97
+ if (v != null)
98
+ env[k] = v;
99
+ const fakeProcess = {
100
+ env,
101
+ argv: ['node', file, ...(options.argv ?? [])],
102
+ platform: process.platform,
103
+ versions: process.versions,
104
+ nextTick: process.nextTick,
105
+ exit(code = 0) {
106
+ throw new OracleExit(code);
107
+ },
108
+ stdout: { write: (s) => (out.push(String(s)), true) },
109
+ stderr: { write: (s) => (err.push(String(s)), true) },
110
+ };
111
+ const fakeConsole = {
112
+ log: (...a) => out.push(format(...a) + '\n'),
113
+ info: (...a) => out.push(format(...a) + '\n'),
114
+ error: (...a) => err.push(format(...a) + '\n'),
115
+ warn: (...a) => err.push(format(...a) + '\n'),
116
+ debug: () => { },
117
+ };
118
+ const vmModule = oracleVm();
119
+ const fsModule = {
120
+ ...fs,
121
+ readFileSync: (p, ...rest) => (p === 0 ? (options.stdin ?? '') : fs.readFileSync(p, ...rest)),
122
+ };
123
+ const module = { exports: {} };
124
+ const req = ((id) => (id === 'vm' ? vmModule : id === 'fs' ? fsModule : hostRequire(id)));
125
+ Object.assign(req, { main: module, resolve: hostRequire.resolve, cache: {} });
126
+ const context = createContext({
127
+ require: req,
128
+ module,
129
+ exports: module.exports,
130
+ __filename: file,
131
+ __dirname: dirname(file),
132
+ process: fakeProcess,
133
+ console: fakeConsole,
134
+ });
135
+ try {
136
+ vm.runInContext(source(file), context, { filename: file });
137
+ }
138
+ catch (e) {
139
+ if (!(e instanceof OracleExit) || e.code !== 0) {
140
+ const detail = err.join('').trim().slice(0, 300);
141
+ throw new SpecificAIError('ERROR', `快手 webweapon 预言机 ${basename(file)} 执行失败${detail ? `:${detail}` : ''}`, { cause: e });
142
+ }
143
+ }
144
+ return out.join('').trim();
145
+ }
146
+ function lastJson(out) {
147
+ const lines = out.split(/\r?\n/);
148
+ try {
149
+ return JSON.parse(lines[lines.length - 1] ?? '');
150
+ }
151
+ catch {
152
+ return null;
153
+ }
154
+ }
155
+ // ---------------------------------------------------------------- kwf
156
+ /**
157
+ * 按持久化的 kwfcv1 计数器或 gdfp 下发的 fpUrl 选择 kwf 脚本(_kwf_script_for_counter):
158
+ * fpUrl 只接受已抓包的两个内容寻址文件名,未知脚本直接失败;没有 fpUrl 时计数器 >= 999 用 0.1.1。
159
+ */
160
+ export function kwfScriptFor(kwfcv1 = '', scriptPath = '') {
161
+ if (scriptPath) {
162
+ if (fs.existsSync(scriptPath))
163
+ return scriptPath;
164
+ let name;
165
+ try {
166
+ name = basename(decodeURIComponent(new URL(scriptPath).pathname)).toLowerCase();
167
+ }
168
+ catch {
169
+ name = basename(scriptPath).toLowerCase();
170
+ }
171
+ if (name === KWF_CURRENT_CAPTURE_NAME)
172
+ return KWF_CURRENT_SCRIPT;
173
+ if (name === KWF_LEGACY_NAME.toLowerCase())
174
+ return KWF_LEGACY_SCRIPT;
175
+ throw new SpecificAIError('UPSTREAM', `快手下发了未收录的 kwf 脚本:${scriptPath}`, { detail: { kind: 'webweapon', fpUrl: scriptPath } });
176
+ }
177
+ if (Number.parseInt(kwfcv1, 10) >= KWF_NEW_GENERATION_COUNTER)
178
+ return KWF_CURRENT_SCRIPT;
179
+ return '';
180
+ }
181
+ function hostnameOf(href, fallback) {
182
+ return /^https?:\/\/([^/]+)/.exec(href)?.[1] ?? fallback;
183
+ }
184
+ /** gen_kwfv1:跑官方 kwf 脚本产出当前 kwfv1(174 或 218 字符),连同更新后的 kwfcv1 计数器。 */
185
+ export function genKwfv1(o) {
186
+ const script = kwfScriptFor(o.kwfcv1 ?? '', o.scriptPath ?? '');
187
+ const out = runOracle(KWF_ORACLE, {
188
+ argv: ['--json'],
189
+ env: {
190
+ KS_DID: o.did,
191
+ KS_HREF: o.href,
192
+ KS_HOSTNAME: hostnameOf(o.href, 'www.kuaishou.com'),
193
+ KS_COOKIE: o.cookie || (o.did ? `did=${o.did}` : ''),
194
+ KS_KWFCV1: o.kwfcv1 ?? '',
195
+ KS_KWFV1: o.currentKwfv1 ?? '',
196
+ KS_HARDWARE_CONCURRENCY: '20',
197
+ KS_KWF_SCRIPT: script || undefined,
198
+ },
199
+ });
200
+ const state = lastJson(out) ?? {};
201
+ const value = String(state.value ?? '');
202
+ return { value, kwfv1: String(state.kwfv1 || value), kwfcv1: String(state.kwfcv1 || o.kwfcv1 || '') };
203
+ }
204
+ /** gen_fingerprint_report:kwf 的 getData(1),作为 gdfp /s/w/p 的 data。 */
205
+ export function genFingerprintReport(o) {
206
+ const script = kwfScriptFor(o.kwfcv1 ?? '', '');
207
+ const out = runOracle(KWF_ORACLE, {
208
+ argv: ['--json'],
209
+ env: {
210
+ KS_DID: o.did,
211
+ KS_HREF: o.href,
212
+ KS_HOSTNAME: hostnameOf(o.href, 'cp.kuaishou.com'),
213
+ KS_COOKIE: o.cookie || (o.did ? `did=${o.did}` : ''),
214
+ KS_KWFCV1: o.kwfcv1 ?? '',
215
+ KS_KWFV1: o.currentKwfv1 ?? '',
216
+ KS_HARDWARE_CONCURRENCY: '20',
217
+ KS_KWF_REPORT_MODE: '1',
218
+ KS_KWF_SCRIPT: script || undefined,
219
+ },
220
+ });
221
+ return String(lastJson(out)?.value ?? '');
222
+ }
223
+ // ---------------------------------------------------------------- kws
224
+ function looksLikeOfficialKws(content) {
225
+ if (content.length < KWS_MIN_BYTES || content.length > KWS_MAX_BYTES)
226
+ return false;
227
+ if (!content.subarray(0, 256).toString('latin1').trimStart().startsWith('(function(){'))
228
+ return false;
229
+ return content.subarray(0, 16_000).includes('function') && content.includes('window') && /[A-Za-z0-9+/=]{20000,}/.test(content.toString('latin1'));
230
+ }
231
+ /** 文件名里的 hash 是脚本 MD5 的中间 16 位。 */
232
+ function kwsHashMatches(name, content) {
233
+ const m = KWS_FILENAME.exec(name);
234
+ return Boolean(m && createHash('md5').update(content).digest('hex').slice(8, 24) === m[2]);
235
+ }
236
+ /** 校验 signUrl(官方内容寻址的 static.yximgs.com 地址),返回文件名。 */
237
+ function kwsName(signUrl) {
238
+ let u;
239
+ try {
240
+ u = new URL(signUrl);
241
+ }
242
+ catch {
243
+ throw new SpecificAIError('UPSTREAM', `KWS signUrl 不合法:${signUrl}`);
244
+ }
245
+ const host = u.hostname.toLowerCase();
246
+ const name = basename(decodeURIComponent(u.pathname));
247
+ const query = [...u.searchParams];
248
+ const ok = u.protocol === 'https:' &&
249
+ !u.username &&
250
+ !u.password &&
251
+ (u.port === '' || u.port === '443') &&
252
+ (host === 'static.yximgs.com' || host.endsWith('.static.yximgs.com')) &&
253
+ !u.hash &&
254
+ u.pathname.toLowerCase().includes('/kws/') &&
255
+ (query.length === 0 || (query.length === 1 && query[0][0] === 'x-kcdn-pid' && /^\d+$/.test(query[0][1]))) &&
256
+ KWS_FILENAME.test(name);
257
+ if (!ok)
258
+ throw new SpecificAIError('UPSTREAM', `KWS signUrl 不符合官方脚本地址:${signUrl}`, { detail: { kind: 'webweapon' } });
259
+ return name;
260
+ }
261
+ /**
262
+ * 找到 signUrl 指定的那一份 kws 脚本:先找随包的已抓副本,再找本机缓存,都没有就下载(校验内容与 MD5)。
263
+ * 绝不用别的变体替代。
264
+ */
265
+ export async function resolveKwsScript(signUrl, href, http) {
266
+ const name = kwsName(signUrl);
267
+ const bundled = join(WEAPON_DIR, name);
268
+ if (fs.existsSync(bundled))
269
+ return bundled;
270
+ const cached = join(cacheDir(P), 'kws', name);
271
+ if (fs.existsSync(cached)) {
272
+ const content = await readFile(cached);
273
+ if (looksLikeOfficialKws(content) && kwsHashMatches(name, content))
274
+ return cached;
275
+ }
276
+ const res = await http.request({
277
+ url: signUrl,
278
+ redirect: 'manual',
279
+ cookies: false,
280
+ headers: [
281
+ ['user-agent', UA],
282
+ ['accept', '*/*'],
283
+ ['accept-encoding', ACCEPT_ENCODING],
284
+ ['accept-language', ACCEPT_LANGUAGE],
285
+ ['referer', href || 'https://www.kuaishou.com/new-reco'],
286
+ ['sec-fetch-dest', 'script'],
287
+ ['sec-fetch-mode', 'no-cors'],
288
+ ['sec-fetch-site', 'cross-site'],
289
+ ],
290
+ });
291
+ if (res.status !== 200)
292
+ throw new SpecificAIError('UPSTREAM', `下载 KWS 脚本失败:HTTP ${res.status}`, { detail: { kind: 'webweapon', url: signUrl } });
293
+ const content = Buffer.from(await res.arrayBuffer());
294
+ if (!looksLikeOfficialKws(content) || !kwsHashMatches(name, content)) {
295
+ throw new SpecificAIError('UPSTREAM', 'KWS 脚本内容与文件名 hash 不一致', { detail: { kind: 'webweapon', url: signUrl } });
296
+ }
297
+ await mkdir(dirname(cached), { recursive: true });
298
+ const tmp = `${cached}.${process.pid}.tmp`;
299
+ await writeFile(tmp, content);
300
+ await rename(tmp, cached);
301
+ return cached;
302
+ }
303
+ /** gen_kwscode:执行 /s/w/c 指派的那一份 kws 脚本,产出 64 位小写 hex 的 kwscode。 */
304
+ export async function genKwscode(o) {
305
+ const script = await resolveKwsScript(o.signUrl, o.href, o.http);
306
+ const out = runOracle(KWS_ORACLE, {
307
+ argv: [isAbsolute(script) ? script : join(WEAPON_DIR, script), '--json'],
308
+ env: {
309
+ KS_SECTOKEN: o.secToken,
310
+ KS_DID: o.did,
311
+ KS_HREF: o.href,
312
+ KS_COOKIE: o.did ? `did=${o.did}` : '',
313
+ KS_KWS_SCRIPT_URL: o.signUrl,
314
+ },
315
+ });
316
+ const code = String(lastJson(out)?.kwscode ?? '');
317
+ if (!HEX64.test(code)) {
318
+ throw new SpecificAIError('UPSTREAM', 'KWS 脚本没有产出合法的 kwscode(环境检测或脚本漂移)', { detail: { kind: 'webweapon', kwscode: code.slice(0, 80) } });
319
+ }
320
+ return code;
321
+ }
322
+ // ---------------------------------------------------------------- like token
323
+ /** likeDataQuery 的 56 位 hex token:输入是 {did, ts, uri} 的值按键排序后用 `:` 连接,交给抓包 bundle 的 $encode 引擎。 */
324
+ export function genLikeToken(did, ts, uri = '/rest/v/feed/myfollow') {
325
+ const out = runOracle(LIKE_ORACLE, { stdin: compactJson({ did, ts, uri }), env: {} });
326
+ if (!/^[0-9a-f]{56}$/.test(out))
327
+ throw new SpecificAIError('ERROR', `like token 引擎输出不合法:${out.slice(0, 80)}`);
328
+ return out;
329
+ }