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.
- package/LICENSE +21 -0
- package/README.en.md +177 -0
- package/README.ja.md +177 -0
- package/README.ko.md +177 -0
- package/README.md +174 -2
- package/README.zh-TW.md +177 -0
- package/assets/banner.txt +3 -0
- package/dist/cli/argv.js +98 -0
- package/dist/cli/commands/auth.js +11 -0
- package/dist/cli/commands/config.js +23 -0
- package/dist/cli/commands/doctor.js +134 -0
- package/dist/cli/commands/platforms.js +24 -0
- package/dist/cli/commands/version.js +4 -0
- package/dist/cli/confirm.js +22 -0
- package/dist/cli/dispatch.js +440 -0
- package/dist/cli/help.js +223 -0
- package/dist/cli/hints.js +60 -0
- package/dist/cli/main.js +18 -0
- package/dist/cli/output.js +66 -0
- package/dist/core/auth-handlers.js +41 -0
- package/dist/core/auth-store.js +119 -0
- package/dist/core/capabilities.js +163 -0
- package/dist/core/config.js +121 -0
- package/dist/core/cookies.js +164 -0
- package/dist/core/errors.js +37 -0
- package/dist/core/files.js +89 -0
- package/dist/core/fsutil.js +52 -0
- package/dist/core/hash.js +4 -0
- package/dist/core/http.js +331 -0
- package/dist/core/image.js +280 -0
- package/dist/core/log.js +40 -0
- package/dist/core/login.js +191 -0
- package/dist/core/mp4.js +130 -0
- package/dist/core/normalize.js +207 -0
- package/dist/core/options.js +110 -0
- package/dist/core/paths.js +38 -0
- package/dist/core/pb.js +59 -0
- package/dist/core/py.js +181 -0
- package/dist/core/rand.js +131 -0
- package/dist/core/registry.js +129 -0
- package/dist/core/schemas.js +41 -0
- package/dist/core/stream.js +104 -0
- package/dist/core/toolkit.js +35 -0
- package/dist/core/vm.js +73 -0
- package/dist/core/vocab.js +157 -0
- package/dist/platforms/_shared/impaas.js +410 -0
- package/dist/platforms/bilibili/index.js +216 -0
- package/dist/platforms/bilibili/web/api.js +806 -0
- package/dist/platforms/bilibili/web/client.js +311 -0
- package/dist/platforms/bilibili/web/commands.js +783 -0
- package/dist/platforms/bilibili/web/ddddocr.js +342 -0
- package/dist/platforms/bilibili/web/gaia.js +130 -0
- package/dist/platforms/bilibili/web/geetest-manual.js +154 -0
- package/dist/platforms/bilibili/web/geetest-vision.js +232 -0
- package/dist/platforms/bilibili/web/geetest.js +450 -0
- package/dist/platforms/bilibili/web/normalize.js +234 -0
- package/dist/platforms/bilibili/web/profile.js +135 -0
- package/dist/platforms/bilibili/web/resolve.js +112 -0
- package/dist/platforms/bilibili/web/sign.js +223 -0
- package/dist/platforms/bilibili/web/upos.js +102 -0
- package/dist/platforms/douyin/index.js +142 -0
- package/dist/platforms/douyin/web/api.js +863 -0
- package/dist/platforms/douyin/web/client.js +413 -0
- package/dist/platforms/douyin/web/commands.js +712 -0
- package/dist/platforms/douyin/web/creator.js +601 -0
- package/dist/platforms/douyin/web/crypto.js +247 -0
- package/dist/platforms/douyin/web/dtrait.js +190 -0
- package/dist/platforms/douyin/web/im.js +502 -0
- package/dist/platforms/douyin/web/image.js +5 -0
- package/dist/platforms/douyin/web/live.js +225 -0
- package/dist/platforms/douyin/web/mssdk.js +148 -0
- package/dist/platforms/douyin/web/normalize.js +326 -0
- package/dist/platforms/douyin/web/passport.js +1247 -0
- package/dist/platforms/douyin/web/profile.js +221 -0
- package/dist/platforms/douyin/web/proto.js +26 -0
- package/dist/platforms/douyin/web/resolve.js +88 -0
- package/dist/platforms/douyin/web/sign.js +436 -0
- package/dist/platforms/douyin/web/tos.js +78 -0
- package/dist/platforms/index.js +17 -0
- package/dist/platforms/jd/index.js +67 -0
- package/dist/platforms/jd/web/aks.js +115 -0
- package/dist/platforms/jd/web/api.js +549 -0
- package/dist/platforms/jd/web/chat.js +132 -0
- package/dist/platforms/jd/web/client.js +470 -0
- package/dist/platforms/jd/web/commands.js +557 -0
- package/dist/platforms/jd/web/jcap/helper.js +90 -0
- package/dist/platforms/jd/web/jcap/host.js +107 -0
- package/dist/platforms/jd/web/jcap/lsd.js +300 -0
- package/dist/platforms/jd/web/jcap/solver.js +995 -0
- package/dist/platforms/jd/web/jcap.js +150 -0
- package/dist/platforms/jd/web/js.js +420 -0
- package/dist/platforms/jd/web/login.js +775 -0
- package/dist/platforms/jd/web/normalize.js +297 -0
- package/dist/platforms/jd/web/profile.js +256 -0
- package/dist/platforms/jd/web/util.js +175 -0
- package/dist/platforms/jd/web/webm.js +133 -0
- package/dist/platforms/kuaishou/index.js +98 -0
- package/dist/platforms/kuaishou/web/api.js +694 -0
- package/dist/platforms/kuaishou/web/captcha-crypto.js +134 -0
- package/dist/platforms/kuaishou/web/captcha-fp.js +180 -0
- package/dist/platforms/kuaishou/web/captcha.js +273 -0
- package/dist/platforms/kuaishou/web/client.js +487 -0
- package/dist/platforms/kuaishou/web/commands.js +863 -0
- package/dist/platforms/kuaishou/web/gap.js +144 -0
- package/dist/platforms/kuaishou/web/gdfp.js +376 -0
- package/dist/platforms/kuaishou/web/gql.js +11 -0
- package/dist/platforms/kuaishou/web/live.js +125 -0
- package/dist/platforms/kuaishou/web/normalize.js +241 -0
- package/dist/platforms/kuaishou/web/oracle.js +329 -0
- package/dist/platforms/kuaishou/web/profile.js +259 -0
- package/dist/platforms/kuaishou/web/resolve.js +84 -0
- package/dist/platforms/kuaishou/web/session.js +592 -0
- package/dist/platforms/kuaishou/web/sign.js +568 -0
- package/dist/platforms/taobao/index.js +56 -0
- package/dist/platforms/taobao/web/api.js +96 -0
- package/dist/platforms/taobao/web/client.js +102 -0
- package/dist/platforms/taobao/web/commands.js +172 -0
- package/dist/platforms/taobao/web/im.js +68 -0
- package/dist/platforms/taobao/web/normalize.js +103 -0
- package/dist/platforms/taobao/web/profile.js +85 -0
- package/dist/platforms/taobao/web/resolve.js +51 -0
- package/dist/platforms/taobao/web/sign.js +35 -0
- package/dist/platforms/tiktok/index.js +120 -0
- package/dist/platforms/tiktok/web/api.js +1234 -0
- package/dist/platforms/tiktok/web/client.js +362 -0
- package/dist/platforms/tiktok/web/commands.js +821 -0
- package/dist/platforms/tiktok/web/im.js +133 -0
- package/dist/platforms/tiktok/web/jsrun.js +126 -0
- package/dist/platforms/tiktok/web/normalize.js +286 -0
- package/dist/platforms/tiktok/web/profile.js +169 -0
- package/dist/platforms/tiktok/web/resolve.js +156 -0
- package/dist/platforms/tiktok/web/sign.js +585 -0
- package/dist/platforms/tiktok/web/upload.js +805 -0
- package/dist/platforms/tiktok/web/wire.js +490 -0
- package/dist/platforms/weibo/index.js +91 -0
- package/dist/platforms/weibo/web/api.js +206 -0
- package/dist/platforms/weibo/web/client.js +155 -0
- package/dist/platforms/weibo/web/commands.js +202 -0
- package/dist/platforms/weibo/web/normalize.js +137 -0
- package/dist/platforms/weibo/web/profile.js +246 -0
- package/dist/platforms/weibo/web/resolve.js +60 -0
- package/dist/platforms/weibo/web/sign.js +85 -0
- package/dist/platforms/x/index.js +107 -0
- package/dist/platforms/x/web/api.js +443 -0
- package/dist/platforms/x/web/article.js +212 -0
- package/dist/platforms/x/web/client.js +202 -0
- package/dist/platforms/x/web/commands.js +363 -0
- package/dist/platforms/x/web/graphql.js +108 -0
- package/dist/platforms/x/web/normalize.js +235 -0
- package/dist/platforms/x/web/profile.js +99 -0
- package/dist/platforms/x/web/resolve.js +44 -0
- package/dist/platforms/x/web/transaction.js +171 -0
- package/dist/platforms/xhs/index.js +101 -0
- package/dist/platforms/xhs/web/api.js +278 -0
- package/dist/platforms/xhs/web/client.js +540 -0
- package/dist/platforms/xhs/web/commands.js +1046 -0
- package/dist/platforms/xhs/web/creator-api.js +609 -0
- package/dist/platforms/xhs/web/creator.js +203 -0
- package/dist/platforms/xhs/web/js.js +194 -0
- package/dist/platforms/xhs/web/login.js +230 -0
- package/dist/platforms/xhs/web/normalize.js +166 -0
- package/dist/platforms/xhs/web/profile.js +127 -0
- package/dist/platforms/xhs/web/push.js +361 -0
- package/dist/platforms/xhs/web/resolve.js +69 -0
- package/dist/platforms/xhs/web/state.js +368 -0
- package/dist/platforms/xianyu/index.js +59 -0
- package/dist/platforms/xianyu/web/api.js +312 -0
- package/dist/platforms/xianyu/web/client.js +134 -0
- package/dist/platforms/xianyu/web/commands.js +305 -0
- package/dist/platforms/xianyu/web/im.js +73 -0
- package/dist/platforms/xianyu/web/normalize.js +123 -0
- package/dist/platforms/xianyu/web/profile.js +146 -0
- package/dist/platforms/xianyu/web/resolve.js +48 -0
- package/dist/platforms/xianyu/web/sign.js +79 -0
- package/package.json +77 -4
- package/static/douyin/Live.proto +194 -0
- package/static/douyin/Request.proto +89 -0
- package/static/douyin/Response.proto +46 -0
- package/static/douyin/acrawler_runtime/ac_vm.js +2 -0
- package/static/douyin/acrawler_runtime/browser_window_shape.json +1 -0
- package/static/douyin/acrawler_runtime/canvas_actual_exact.json +1 -0
- package/static/douyin/acrawler_runtime/run_ac_node.js +1005 -0
- package/static/douyin/acrawler_runtime/runner_window_keys_baseline.txt +240 -0
- package/static/douyin/challenge_profile.json +801 -0
- package/static/douyin/challenge_template_runner.js +217 -0
- package/static/douyin/dtrait_profile.json +66 -0
- package/static/douyin/mstoken_common_profile.json +1 -0
- package/static/douyin/mstoken_profile.json +214 -0
- package/static/jd/h5st5_env.js +750 -0
- package/static/jd/h5st5_lib.js +5 -0
- package/static/jd/h5st5_server.js +49 -0
- package/static/jd/jcap/env/env_core.js +512 -0
- package/static/jd/jcap/env/run.js +1307 -0
- package/static/jd/jcap/run/jcap_ujb96b.js +39 -0
- package/static/jd/package.json +4 -0
- package/static/jd/pc_tk_lib.js +1 -0
- package/static/jd/pc_tk_server.js +63 -0
- package/static/jd/summer_cryptico_runner.js +50 -0
- package/static/jd/webm/env/run.js +453 -0
- package/static/jd/webm/run/jdwebm-riskhandle.js +49 -0
- package/static/kuaishou/bundles/weapon/kwf-0.0.2.2cee19b4b7dec496.js +1 -0
- package/static/kuaishou/bundles/weapon/kws-0-0.0.1-obfuscated.4b7f3de1ab0ddcc2.js +1 -0
- package/static/kuaishou/bundles/weapon/kws-1-0.0.1-obfuscated.346ebdb1c78df316.js +1 -0
- package/static/kuaishou/bundles/weapon/kws-10-0.0.1-obfuscated.a33db41897b983af.js +1 -0
- package/static/kuaishou/bundles/weapon/kws-11-0.0.1-obfuscated.5e0a90af726d8a7e.js +1 -0
- package/static/kuaishou/bundles/weapon/kws-12-0.0.1-obfuscated.20ea29345706ec8c.js +1 -0
- package/static/kuaishou/bundles/weapon/kws-13-0.0.1-obfuscated.6b74e9640ff18648.js +1 -0
- package/static/kuaishou/bundles/weapon/kws-14-0.0.1-obfuscated.551e4429d156bf66.js +1 -0
- package/static/kuaishou/bundles/weapon/kws-15-0.0.1-obfuscated.8625b087aaba4a60.js +1 -0
- package/static/kuaishou/bundles/weapon/kws-16-0.0.1-obfuscated.d5ffa4020f3be99a.js +1 -0
- package/static/kuaishou/bundles/weapon/kws-17-0.0.1-obfuscated.6d13e19b22f50a80.js +1 -0
- package/static/kuaishou/bundles/weapon/kws-18-0.0.1-obfuscated.6d31405044166289.js +1 -0
- package/static/kuaishou/bundles/weapon/kws-19-0.0.1-obfuscated.a9a8d925942f19e4.js +1 -0
- package/static/kuaishou/bundles/weapon/kws-2-0.0.1-obfuscated.b849a136ae4a6b3e.js +1 -0
- package/static/kuaishou/bundles/weapon/kws-3-0.0.1-obfuscated.f9e77ebf6186e2ac.js +1 -0
- package/static/kuaishou/bundles/weapon/kws-4-0.0.1-obfuscated.2c3c155aa52ceb0e.js +1 -0
- package/static/kuaishou/bundles/weapon/kws-5-0.0.1-obfuscated.4e553d17691b3c5a.js +1 -0
- package/static/kuaishou/bundles/weapon/kws-6-0.0.1-obfuscated.f73ddd3ff13e1217.js +1 -0
- package/static/kuaishou/bundles/weapon/kws-7-0.0.1-obfuscated.1628b8729beaab7e.js +1 -0
- package/static/kuaishou/bundles/weapon/kws-8-0.0.1-obfuscated.6da6baef7aa81fff.js +1 -0
- package/static/kuaishou/bundles/weapon/kws-9-0.0.1-obfuscated.8645d55e5323b743.js +1 -0
- package/static/kuaishou/fixtures/current_follow_script_813.network-response +2 -0
- package/static/kuaishou/fixtures/live_ws_maps.json +61 -0
- package/static/kuaishou/fixtures/live_ws_proto.json +1342 -0
- package/static/kuaishou/js/cp-kwf.js +1 -0
- package/static/kuaishou/tools/kwf_oracle.js +799 -0
- package/static/kuaishou/tools/kws_oracle.js +885 -0
- package/static/kuaishou/tools/like_token_oracle.js +70 -0
- package/static/taobao/taobao_js_20260407.js +570 -0
- package/static/tiktok/Tiktok_Request.json +597 -0
- package/static/tiktok/package.json +3 -0
- package/static/tiktok/shop_bsid/core.js +1 -0
- package/static/tiktok/shop_bsid/env_core.js +512 -0
- package/static/tiktok/shop_bsid/loader.js +1 -0
- package/static/tiktok/shop_bsid/run.js +753 -0
- package/static/tiktok/shop_bsid/sign.js +100 -0
- package/static/tiktok/shop_bsid/webmssdk.js +1 -0
- package/static/tiktok/shop_bsid/webmssdk_ex.js +1 -0
- package/static/tiktok/signing/env/env_core.js +512 -0
- package/static/tiktok/signing/env/sign.js +455 -0
- package/static/tiktok/signing/sdk_config.json +230 -0
- package/static/tiktok/signing/source/webmssdk.js +1 -0
- package/static/tiktok/signing/source/webmssdk_ex.js +1 -0
- package/static/tiktok/signing/source/webmssdk_legacy.js +1 -0
- package/static/x/graphql.json +7777 -0
- package/static/x/transaction_l1.json +12 -0
- package/static/xhs/package.json +3 -0
- package/static/xhs/xhs_core/js/b1.js +210 -0
- package/static/xhs/xhs_core/js/mns.js +340 -0
- package/static/xhs/xhs_core/js/mns_0101_keystream.json +5 -0
- package/static/xhs/xhs_core/js/mns_keystreams.json +1 -0
- package/static/xhs/xhs_core/js/sign.js +225 -0
- package/static/xhs/xhs_core/js/websectiga_cli.js +220 -0
- package/static/xhs/xhs_core/js/websectiga_env.js +512 -0
- package/static/xhs/xhs_core/js/xs_common.js +79 -0
- package/static/xhs/xhs_creator/js/profile.js +115 -0
- package/static/xhs/xhs_creator/js/rap_fingerprint_creator.json +6 -0
- package/static/xhs/xhs_creator/js/reference_profile.json +282 -0
- package/static/xhs/xhs_creator/js/xhs_creator_sign.js +29 -0
- package/static/xhs/xhs_creator/js/xhs_creator_signature.js +23 -0
- package/static/xhs/xhs_pc/js/aes_decrypt.js +22 -0
- package/static/xhs/xhs_pc/js/aes_encrypt.js +30 -0
- package/static/xhs/xhs_pc/js/b1.js +9 -0
- package/static/xhs/xhs_pc/js/deflate.js +320 -0
- package/static/xhs/xhs_pc/js/mns.js +4 -0
- package/static/xhs/xhs_pc/js/profile.js +141 -0
- package/static/xhs/xhs_pc/js/rap.js +173 -0
- package/static/xhs/xhs_pc/js/rap_cli.js +30 -0
- package/static/xhs/xhs_pc/js/rap_crypto.js +73 -0
- package/static/xhs/xhs_pc/js/rap_fingerprint_template.json +8 -0
- package/static/xhs/xhs_pc/js/reference_profile.json +262 -0
- package/static/xhs/xhs_pc/js/sign.js +6 -0
- package/static/xhs/xhs_pc/js/web_ssk.js +82 -0
- package/static/xhs/xhs_pc/js/xs_common.js +4 -0
- package/static/xianyu/et_f.js +2 -0
- package/static/xianyu/gen_tfstk.js +410 -0
- package/static/xianyu/goofish_js_version_2.js +568 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { describeOption, FILTER_VALUES, flagName, PAGING, STANDARD_VALUES } from './options.js';
|
|
2
|
+
import { sortCommands } from './registry.js';
|
|
3
|
+
import { VOCAB } from './vocab.js';
|
|
4
|
+
/** 生成 docs/capabilities.md(web 端能力矩阵)。只由注册表决定,`npm run gen:capabilities` 写盘,测试检查是否最新。 */
|
|
5
|
+
const SYMBOL = { full: '✓', partial: '◐', none: '○' };
|
|
6
|
+
const SECTIONS = [
|
|
7
|
+
{
|
|
8
|
+
title: 'auth',
|
|
9
|
+
resources: ['auth'],
|
|
10
|
+
intro: (platforms) => `\`login\` 见上表。\`logout\` / \`list\` / \`use\` 由 core 实现,所有平台都有;服务端登出只有 ${platforms
|
|
11
|
+
.filter((p) => web(p).logout)
|
|
12
|
+
.map((p) => p.id)
|
|
13
|
+
.join('、')}。`,
|
|
14
|
+
},
|
|
15
|
+
{ title: 'user', resources: ['user'] },
|
|
16
|
+
{ title: 'item', resources: ['item'] },
|
|
17
|
+
{
|
|
18
|
+
title: 'product 与商品评价',
|
|
19
|
+
resources: ['product'],
|
|
20
|
+
outro: '商品评价用 `comment list <product>`(见下节):传商品 URL 时自动识别,传纯 ID 时加 `--product`。\n\n' +
|
|
21
|
+
'闲鱼、淘宝、京东的商品本身就是 item:商品详情用 `item get`,商品评价用 `comment list <item>`。',
|
|
22
|
+
},
|
|
23
|
+
{ title: 'comment', resources: ['comment'] },
|
|
24
|
+
{ title: 'feed 与 keyword', resources: ['feed', 'keyword'] },
|
|
25
|
+
{ title: 'notice 与 msg', resources: ['notice', 'msg'] },
|
|
26
|
+
{ title: 'media、folder、series、history、topic、poi', resources: ['media', 'folder', 'series', 'history', 'topic', 'poi'] },
|
|
27
|
+
{ title: 'live', resources: ['live'] },
|
|
28
|
+
];
|
|
29
|
+
/** 不在命令表里出现的命令:login 有单独的表,list / use / logout 由 core 实现。 */
|
|
30
|
+
const OMITTED = new Set(['auth login', 'auth logout', 'auth list', 'auth use']);
|
|
31
|
+
const FILTERS = Object.keys(FILTER_VALUES);
|
|
32
|
+
function web(platform) {
|
|
33
|
+
const ep = platform.endpoints.web;
|
|
34
|
+
if (ep === 'planned')
|
|
35
|
+
throw new Error(`${platform.id} 的 web 端是 planned`);
|
|
36
|
+
return ep;
|
|
37
|
+
}
|
|
38
|
+
function row(cells) {
|
|
39
|
+
return `| ${cells.join(' | ')} |`;
|
|
40
|
+
}
|
|
41
|
+
function header(first, platforms) {
|
|
42
|
+
return [row([first, ...platforms.map((p) => p.id)]), '|' + '---|'.repeat(platforms.length + 1)];
|
|
43
|
+
}
|
|
44
|
+
function loginTable(platforms) {
|
|
45
|
+
const logins = platforms.map((p) => web(p).login);
|
|
46
|
+
const lines = [...header('', platforms)];
|
|
47
|
+
for (const method of ['qrcode', 'sms', 'password', 'cookie']) {
|
|
48
|
+
lines.push(row([method, ...logins.map((l) => (l.methods.includes(method) ? '✓' : ''))]));
|
|
49
|
+
}
|
|
50
|
+
lines.push(row(['默认', ...logins.map((l) => l.default)]));
|
|
51
|
+
if (logins.some((l) => l.scopes?.length))
|
|
52
|
+
lines.push(row(['子站点', ...logins.map((l) => (l.scopes ?? []).join('、'))]));
|
|
53
|
+
return lines;
|
|
54
|
+
}
|
|
55
|
+
function commandRows(platforms, resources) {
|
|
56
|
+
const rows = [];
|
|
57
|
+
for (const key of Object.keys(VOCAB)) {
|
|
58
|
+
const [resource, action] = key.split(' ');
|
|
59
|
+
if (!resources.includes(resource) || OMITTED.has(key))
|
|
60
|
+
continue;
|
|
61
|
+
const commands = platforms.map((p) => web(p).commands.get(key));
|
|
62
|
+
const cells = commands.map((c) => (c?.upstream ? SYMBOL[c.upstream] : '—'));
|
|
63
|
+
const notes = commands.map((c) => c?.note ?? null);
|
|
64
|
+
const prev = rows.at(-1);
|
|
65
|
+
// 同一 resource 下相邻、各平台状态和说明都相同的命令合并成一行,例如 user followers / following
|
|
66
|
+
if (prev && prev.keys[0].startsWith(resource + ' ') && prev.cells.join() === cells.join() && prev.notes.join() === notes.join()) {
|
|
67
|
+
prev.keys.push(key);
|
|
68
|
+
prev.label += ` / ${action}`;
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
rows.push({ label: key, keys: [key], cells, notes });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return rows;
|
|
75
|
+
}
|
|
76
|
+
/** 表下的补充:◐ 的说明、筛选取值、平台私有选项。 */
|
|
77
|
+
function footnotes(platforms, rows) {
|
|
78
|
+
const lines = [];
|
|
79
|
+
for (const r of rows) {
|
|
80
|
+
r.notes.forEach((note, i) => {
|
|
81
|
+
if (note)
|
|
82
|
+
lines.push(`- ${platforms[i].id} \`${r.label}\`:${note}`);
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
for (const r of rows) {
|
|
86
|
+
for (const key of r.keys) {
|
|
87
|
+
const commands = platforms.map((p) => [p, web(p).commands.get(key)]).filter(([, c]) => c);
|
|
88
|
+
// 词表给了、平台不支持的标准选项(用了报 UNSUPPORTED)
|
|
89
|
+
const without = commands.filter(([, c]) => c.unsupported.length);
|
|
90
|
+
if (without.length) {
|
|
91
|
+
lines.push(`- \`${key}\` 不支持的标准选项:${without.map(([p, c]) => `${p.id} ${c.unsupported.map((o) => `--${flagName(o)}`).join(' ')}`).join(';')}`);
|
|
92
|
+
}
|
|
93
|
+
// 有标准取值、平台只支持其中一部分的选项(例如 --visibility)
|
|
94
|
+
for (const option of Object.keys(VOCAB[key].options).filter((o) => STANDARD_VALUES[o] && o !== 'method')) {
|
|
95
|
+
const dflt = describeOption(VOCAB[key].options[option]).values;
|
|
96
|
+
const differ = commands.filter(([, c]) => option in c.options && describeOption(c.options[option]).values?.join() !== dflt?.join());
|
|
97
|
+
if (!differ.length)
|
|
98
|
+
continue;
|
|
99
|
+
const values = differ.map(([p, c]) => `${p.id} ${describeOption(c.options[option]).values?.join(' / ')}`);
|
|
100
|
+
lines.push(`- \`${key} --${flagName(option)}\` 取值:默认 ${dflt?.join(' / ')};${values.join(';')}`);
|
|
101
|
+
}
|
|
102
|
+
const base = new Set([...Object.keys(VOCAB[key].options), ...Object.keys(PAGING)]);
|
|
103
|
+
const extra = [...new Set(commands.flatMap(([, c]) => Object.keys(c.options)))].filter((o) => !base.has(o));
|
|
104
|
+
for (const option of extra) {
|
|
105
|
+
const has = commands.filter(([, c]) => option in c.options);
|
|
106
|
+
if (FILTERS.includes(option) || (STANDARD_VALUES[option] && option !== 'method')) {
|
|
107
|
+
const values = has.map(([p, c]) => `${p.id} ${describeOption(c.options[option]).values?.join(' / ')}`);
|
|
108
|
+
lines.push(`- \`${key} --${flagName(option)}\` 取值:${values.join(';')}`);
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
lines.push(`- \`${key} --${flagName(option)}\`:${has.map(([p]) => p.id).join('、')}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return lines;
|
|
117
|
+
}
|
|
118
|
+
function extensions(platforms) {
|
|
119
|
+
const lines = [row(['平台', '命令']), '|---|---|'];
|
|
120
|
+
for (const p of platforms) {
|
|
121
|
+
const ext = sortCommands(web(p).commands.values()).filter((c) => c.extension);
|
|
122
|
+
if (ext.length)
|
|
123
|
+
lines.push(row([p.id, ext.map((c) => `\`${c.key}\` ${SYMBOL[c.upstream]}`).join(' · ')]));
|
|
124
|
+
}
|
|
125
|
+
return lines;
|
|
126
|
+
}
|
|
127
|
+
export function renderCapabilities(platforms) {
|
|
128
|
+
const out = [
|
|
129
|
+
'# 能力矩阵',
|
|
130
|
+
'',
|
|
131
|
+
'各平台 web 端的能力。命令规范见 [AGENTS.md](../AGENTS.md) 第 4 节,上游代码位置见 [upstream-map.md](upstream-map.md)。',
|
|
132
|
+
'',
|
|
133
|
+
'- 本文件由注册表(`src/platforms/<p>/index.ts`)生成:`npm run gen:capabilities`。不要手改,测试会检查它是否最新。',
|
|
134
|
+
'- app / pc 端目前全部是 planned,不在本表列出。',
|
|
135
|
+
'',
|
|
136
|
+
'| 符号 | 含义 | 注册表 status | 执行结果 |',
|
|
137
|
+
'|---|---|---|---|',
|
|
138
|
+
'| ✓ | 上游已有 | implemented | 正常执行 |',
|
|
139
|
+
'| ◐ | 上游部分支持,限制写在注册表的 `note` 里 | implemented | 正常执行 |',
|
|
140
|
+
'| ○ | 平台有这个概念,上游没有,规划中 | planned | `NOT_IMPLEMENTED`,退出码 4 |',
|
|
141
|
+
'| — | 平台没有这个概念 | 不注册 | `UNSUPPORTED`,退出码 2 |',
|
|
142
|
+
'',
|
|
143
|
+
`列顺序:${platforms.map((p) => p.id).join(' · ')}。`,
|
|
144
|
+
'',
|
|
145
|
+
'## 登录方式',
|
|
146
|
+
'',
|
|
147
|
+
...loginTable(platforms),
|
|
148
|
+
];
|
|
149
|
+
for (const s of SECTIONS) {
|
|
150
|
+
const rows = commandRows(platforms, s.resources);
|
|
151
|
+
out.push('', `## ${s.title}`, '');
|
|
152
|
+
if (s.intro)
|
|
153
|
+
out.push(typeof s.intro === 'function' ? s.intro(platforms) : s.intro, '');
|
|
154
|
+
out.push(...header('命令', platforms), ...rows.map((r) => row([r.label, ...r.cells])));
|
|
155
|
+
const notes = footnotes(platforms, rows);
|
|
156
|
+
if (notes.length)
|
|
157
|
+
out.push('', ...notes);
|
|
158
|
+
if (s.outro)
|
|
159
|
+
out.push('', s.outro);
|
|
160
|
+
}
|
|
161
|
+
out.push('', '## 平台扩展', '', '命令定义见 AGENTS.md 4.7。', '', ...extensions(platforms));
|
|
162
|
+
return out.join('\n') + '\n';
|
|
163
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { parse, stringify } from 'smol-toml';
|
|
2
|
+
import { SpecificAIError } from './errors.js';
|
|
3
|
+
import { readTextIfExists, writeFileAtomic } from './fsutil.js';
|
|
4
|
+
import { configFile } from './paths.js';
|
|
5
|
+
/** config.toml(AGENTS 5.5):proxy、timeout、<platform>.proxy。 */
|
|
6
|
+
export const DEFAULT_TIMEOUT = 30;
|
|
7
|
+
const PROXY_SCHEMES = ['http:', 'https:', 'socks4:', 'socks5:', 'socks5h:'];
|
|
8
|
+
const KEYS_HINT = '可用的配置项:proxy、timeout、<platform>.proxy';
|
|
9
|
+
export function checkProxy(value) {
|
|
10
|
+
let url;
|
|
11
|
+
try {
|
|
12
|
+
url = new URL(value);
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
throw new SpecificAIError('USAGE', `代理地址不合法:${value}`, { hint: '例如 http://127.0.0.1:7890 或 socks5://127.0.0.1:1080' });
|
|
16
|
+
}
|
|
17
|
+
if (!PROXY_SCHEMES.includes(url.protocol)) {
|
|
18
|
+
throw new SpecificAIError('USAGE', `不支持的代理协议:${url.protocol}`, { hint: '支持 http、https、socks4、socks5、socks5h' });
|
|
19
|
+
}
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
function checkTimeout(value) {
|
|
23
|
+
const n = typeof value === 'string' ? Number(value) : value;
|
|
24
|
+
if (typeof n !== 'number' || !Number.isFinite(n) || n <= 0) {
|
|
25
|
+
throw new SpecificAIError('USAGE', `timeout 必须是大于 0 的秒数:${String(value)}`);
|
|
26
|
+
}
|
|
27
|
+
return n;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* 解析配置键,平台段用规范 id。`resolvePlatform` 把平台 id 或别名映射成规范 id。
|
|
31
|
+
*/
|
|
32
|
+
export function parseKey(key, resolvePlatform) {
|
|
33
|
+
if (key === 'proxy' || key === 'timeout')
|
|
34
|
+
return [key];
|
|
35
|
+
const m = /^([^.]+)\.proxy$/.exec(key);
|
|
36
|
+
const id = m && resolvePlatform(m[1]);
|
|
37
|
+
if (!id)
|
|
38
|
+
throw new SpecificAIError('USAGE', `未知的配置项:${key}`, { hint: KEYS_HINT });
|
|
39
|
+
return [id, 'proxy'];
|
|
40
|
+
}
|
|
41
|
+
export async function loadConfig() {
|
|
42
|
+
const file = configFile();
|
|
43
|
+
const text = await readTextIfExists(file);
|
|
44
|
+
if (text == null)
|
|
45
|
+
return {};
|
|
46
|
+
try {
|
|
47
|
+
return parse(text);
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
throw new SpecificAIError('ERROR', `config.toml 格式错误:${err.message.split('\n')[0]}`, { hint: file });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
async function saveConfig(config) {
|
|
54
|
+
await writeFileAtomic(configFile(), stringify(config) + '\n');
|
|
55
|
+
}
|
|
56
|
+
function lookup(config, path) {
|
|
57
|
+
let v = config;
|
|
58
|
+
for (const k of path)
|
|
59
|
+
v = v && typeof v === 'object' ? v[k] : undefined;
|
|
60
|
+
return v ?? null;
|
|
61
|
+
}
|
|
62
|
+
export async function getConfig(path) {
|
|
63
|
+
return lookup(await loadConfig(), path);
|
|
64
|
+
}
|
|
65
|
+
export async function setConfig(path, raw) {
|
|
66
|
+
const key = path.join('.');
|
|
67
|
+
const value = key === 'timeout' ? checkTimeout(raw) : checkProxy(raw);
|
|
68
|
+
const config = await loadConfig();
|
|
69
|
+
if (path.length === 1)
|
|
70
|
+
config[path[0]] = value;
|
|
71
|
+
else {
|
|
72
|
+
const table = config[path[0]];
|
|
73
|
+
config[path[0]] = { ...(table && typeof table === 'object' ? table : {}), [path[1]]: value };
|
|
74
|
+
}
|
|
75
|
+
await saveConfig(config);
|
|
76
|
+
return { key, value };
|
|
77
|
+
}
|
|
78
|
+
export async function unsetConfig(path) {
|
|
79
|
+
const config = await loadConfig();
|
|
80
|
+
if (path.length === 1)
|
|
81
|
+
delete config[path[0]];
|
|
82
|
+
else {
|
|
83
|
+
const table = config[path[0]];
|
|
84
|
+
if (table && typeof table === 'object') {
|
|
85
|
+
delete table[path[1]];
|
|
86
|
+
if (Object.keys(table).length === 0)
|
|
87
|
+
delete config[path[0]];
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
await saveConfig(config);
|
|
91
|
+
return { key: path.join('.'), value: null };
|
|
92
|
+
}
|
|
93
|
+
/** 所有已设置的项,键用点号展开,例如 `{ "proxy": "...", "xhs.proxy": "..." }`。 */
|
|
94
|
+
export async function listConfig() {
|
|
95
|
+
const out = {};
|
|
96
|
+
const walk = (table, prefix) => {
|
|
97
|
+
for (const [k, v] of Object.entries(table)) {
|
|
98
|
+
if (v && typeof v === 'object' && !Array.isArray(v) && !(v instanceof Date))
|
|
99
|
+
walk(v, `${prefix}${k}.`);
|
|
100
|
+
else
|
|
101
|
+
out[prefix + k] = v;
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
walk(await loadConfig(), '');
|
|
105
|
+
return out;
|
|
106
|
+
}
|
|
107
|
+
/** 一次调用的网络设置。代理优先级:--proxy > <platform>.proxy > proxy;不读 HTTP(S)_PROXY。 */
|
|
108
|
+
export async function resolveNetwork(platform, cliProxy) {
|
|
109
|
+
const config = await loadConfig();
|
|
110
|
+
const fromFile = (path) => {
|
|
111
|
+
const v = lookup(config, path);
|
|
112
|
+
if (v == null)
|
|
113
|
+
return null;
|
|
114
|
+
if (typeof v !== 'string')
|
|
115
|
+
throw new SpecificAIError('ERROR', `config.toml 里的 ${path.join('.')} 必须是字符串`, { hint: configFile() });
|
|
116
|
+
return checkProxy(v);
|
|
117
|
+
};
|
|
118
|
+
const proxy = cliProxy != null ? checkProxy(cliProxy) : (fromFile([platform, 'proxy']) ?? fromFile(['proxy']));
|
|
119
|
+
const t = lookup(config, ['timeout']);
|
|
120
|
+
return { proxy, timeout: t == null ? DEFAULT_TIMEOUT : checkTimeout(t) };
|
|
121
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { nowSeconds } from './rand.js';
|
|
2
|
+
/**
|
|
3
|
+
* Cookie 罐:直接读写凭证文件里某个 scope 的 cookie 数组,所以请求中更新的 cookie
|
|
4
|
+
* 会随凭证一起由 core 落盘(AGENTS 5.3「自动刷新」)。
|
|
5
|
+
*
|
|
6
|
+
* domain 以 `.` 开头表示对子域名生效;不带点表示只对该主机生效(host-only)。
|
|
7
|
+
*/
|
|
8
|
+
export class CookieJar {
|
|
9
|
+
cookies;
|
|
10
|
+
constructor(cookies = []) {
|
|
11
|
+
this.cookies = cookies;
|
|
12
|
+
}
|
|
13
|
+
/** 对某个 URL 生效、未过期的 cookie,按存入顺序。 */
|
|
14
|
+
forUrl(url) {
|
|
15
|
+
const u = typeof url === 'string' ? new URL(url) : url;
|
|
16
|
+
const now = nowSeconds();
|
|
17
|
+
return this.cookies.filter((c) => domainMatch(u.hostname, c.domain) && pathMatch(u.pathname, c.path) && (c.expires == null || c.expires > now));
|
|
18
|
+
}
|
|
19
|
+
/** 按名字取值;有多个同名 cookie 时,指定 domain 可以消歧,否则取第一个。 */
|
|
20
|
+
get(name, domain) {
|
|
21
|
+
return this.find(name, domain)?.value;
|
|
22
|
+
}
|
|
23
|
+
has(name) {
|
|
24
|
+
return this.cookies.some((c) => c.name === name);
|
|
25
|
+
}
|
|
26
|
+
/** 写入或覆盖(同名、同 domain、同 path 视为同一个)。 */
|
|
27
|
+
set(name, value, domain, options = {}) {
|
|
28
|
+
const path = options.path ?? '/';
|
|
29
|
+
const existing = this.cookies.find((c) => c.name === name && c.domain === domain && c.path === path);
|
|
30
|
+
if (existing) {
|
|
31
|
+
existing.value = value;
|
|
32
|
+
if (options.expires !== undefined)
|
|
33
|
+
existing.expires = options.expires;
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
this.cookies.push({ name, value, domain, path, expires: options.expires ?? null });
|
|
37
|
+
}
|
|
38
|
+
return this;
|
|
39
|
+
}
|
|
40
|
+
delete(name, domain) {
|
|
41
|
+
for (let i = this.cookies.length - 1; i >= 0; i--) {
|
|
42
|
+
const c = this.cookies[i];
|
|
43
|
+
if (c.name === name && (domain == null || c.domain === domain))
|
|
44
|
+
this.cookies.splice(i, 1);
|
|
45
|
+
}
|
|
46
|
+
return this;
|
|
47
|
+
}
|
|
48
|
+
/** 按名字的顺序表重排;表外的排在最后,保持相对顺序。 */
|
|
49
|
+
sort(order) {
|
|
50
|
+
const index = new Map(order.map((n, i) => [n, i]));
|
|
51
|
+
const ranked = this.cookies.map((c, i) => ({ c, i, r: index.get(c.name) ?? order.length }));
|
|
52
|
+
ranked.sort((a, b) => a.r - b.r || a.i - b.i);
|
|
53
|
+
this.cookies.splice(0, this.cookies.length, ...ranked.map((x) => x.c));
|
|
54
|
+
return this;
|
|
55
|
+
}
|
|
56
|
+
/** Cookie 请求头:对 url 生效的 cookie(不给 url 时为全部),按存入顺序拼成 `a=1; b=2`。 */
|
|
57
|
+
header(url) {
|
|
58
|
+
return (url ? this.forUrl(url) : this.cookies).map((c) => `${c.name}=${c.value}`).join('; ');
|
|
59
|
+
}
|
|
60
|
+
/** 名字 → 值;同名时取第一个。 */
|
|
61
|
+
toObject(url) {
|
|
62
|
+
const out = {};
|
|
63
|
+
for (const c of url ? this.forUrl(url) : this.cookies)
|
|
64
|
+
if (!(c.name in out))
|
|
65
|
+
out[c.name] = c.value;
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
68
|
+
/** 处理响应的 Set-Cookie。 */
|
|
69
|
+
applySetCookie(url, headers) {
|
|
70
|
+
const host = new URL(url).hostname;
|
|
71
|
+
for (const header of headers) {
|
|
72
|
+
const parsed = parseSetCookie(header, host);
|
|
73
|
+
// Domain 属性必须覆盖当前主机(RFC 6265 5.3),否则丢弃
|
|
74
|
+
if (!parsed || !domainMatch(host, parsed.domain))
|
|
75
|
+
continue;
|
|
76
|
+
const { name, value, domain, path, expires } = parsed;
|
|
77
|
+
if (expires != null && expires <= nowSeconds()) {
|
|
78
|
+
const i = this.cookies.findIndex((c) => c.name === name && c.domain === domain && c.path === path);
|
|
79
|
+
if (i >= 0)
|
|
80
|
+
this.cookies.splice(i, 1);
|
|
81
|
+
}
|
|
82
|
+
else
|
|
83
|
+
this.set(name, value, domain, { path, expires });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
find(name, domain) {
|
|
87
|
+
return this.cookies.find((c) => c.name === name && (domain == null || c.domain === domain));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function domainMatch(host, domain) {
|
|
91
|
+
if (!domain)
|
|
92
|
+
return true;
|
|
93
|
+
if (domain.startsWith('.')) {
|
|
94
|
+
const d = domain.slice(1);
|
|
95
|
+
return host === d || host.endsWith(domain);
|
|
96
|
+
}
|
|
97
|
+
return host === domain;
|
|
98
|
+
}
|
|
99
|
+
function pathMatch(path, cookiePath) {
|
|
100
|
+
if (!cookiePath || cookiePath === '/')
|
|
101
|
+
return true;
|
|
102
|
+
return path === cookiePath || path.startsWith(cookiePath.endsWith('/') ? cookiePath : cookiePath + '/');
|
|
103
|
+
}
|
|
104
|
+
/** 解析一条 Set-Cookie。domain 属性统一规范成带点的形式,没有 domain 属性时为 host-only。 */
|
|
105
|
+
export function parseSetCookie(header, host) {
|
|
106
|
+
const [pair, ...attrs] = header.split(';');
|
|
107
|
+
const i = pair.indexOf('=');
|
|
108
|
+
if (i <= 0)
|
|
109
|
+
return null;
|
|
110
|
+
const cookie = { name: pair.slice(0, i).trim(), value: pair.slice(i + 1).trim(), domain: host, path: '/', expires: null };
|
|
111
|
+
let maxAge = null;
|
|
112
|
+
for (const attr of attrs) {
|
|
113
|
+
const j = attr.indexOf('=');
|
|
114
|
+
const key = (j < 0 ? attr : attr.slice(0, j)).trim().toLowerCase();
|
|
115
|
+
const val = j < 0 ? '' : attr.slice(j + 1).trim();
|
|
116
|
+
if (key === 'domain' && val)
|
|
117
|
+
cookie.domain = '.' + val.replace(/^\./, '').toLowerCase();
|
|
118
|
+
else if (key === 'path' && val.startsWith('/'))
|
|
119
|
+
cookie.path = val;
|
|
120
|
+
else if (key === 'max-age' && /^-?\d+$/.test(val))
|
|
121
|
+
maxAge = Number(val);
|
|
122
|
+
else if (key === 'expires') {
|
|
123
|
+
const t = Date.parse(val);
|
|
124
|
+
if (!Number.isNaN(t))
|
|
125
|
+
cookie.expires = Math.floor(t / 1000);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (maxAge != null)
|
|
129
|
+
cookie.expires = nowSeconds() + maxAge;
|
|
130
|
+
return cookie;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* 解析用户导入的 cookie 字符串(`a=1; b=2`),也接受浏览器导出的 JSON 数组
|
|
134
|
+
* (`[{name, value, domain, path, expirationDate}]`)。domain 缺省时取平台默认域。
|
|
135
|
+
*/
|
|
136
|
+
export function parseCookieInput(input, defaultDomain) {
|
|
137
|
+
const text = input.trim();
|
|
138
|
+
if (text.startsWith('[')) {
|
|
139
|
+
const list = JSON.parse(text);
|
|
140
|
+
return list.map((c) => ({
|
|
141
|
+
name: c.name,
|
|
142
|
+
value: c.value,
|
|
143
|
+
domain: c.domain ? (c.domain.startsWith('.') ? c.domain : '.' + c.domain.replace(/^www\./, '')) : defaultDomain,
|
|
144
|
+
path: c.path ?? '/',
|
|
145
|
+
expires: c.expirationDate ?? c.expires ?? null,
|
|
146
|
+
}));
|
|
147
|
+
}
|
|
148
|
+
const out = [];
|
|
149
|
+
for (const part of text.replace(/^cookie:\s*/i, '').split(';')) {
|
|
150
|
+
const i = part.indexOf('=');
|
|
151
|
+
if (i <= 0)
|
|
152
|
+
continue;
|
|
153
|
+
const name = part.slice(0, i).trim();
|
|
154
|
+
if (!name)
|
|
155
|
+
continue;
|
|
156
|
+
const existing = out.find((c) => c.name === name);
|
|
157
|
+
const value = part.slice(i + 1).trim();
|
|
158
|
+
if (existing)
|
|
159
|
+
existing.value = value;
|
|
160
|
+
else
|
|
161
|
+
out.push({ name, value, domain: defaultDomain, path: '/', expires: null });
|
|
162
|
+
}
|
|
163
|
+
return out;
|
|
164
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/** 退出码(AGENTS 6.4)。 */
|
|
2
|
+
export const EXIT_CODES = {
|
|
3
|
+
ERROR: 1,
|
|
4
|
+
USAGE: 2,
|
|
5
|
+
UNSUPPORTED: 2,
|
|
6
|
+
CONFIRM_REQUIRED: 2,
|
|
7
|
+
AUTH_REQUIRED: 3,
|
|
8
|
+
AUTH_EXPIRED: 3,
|
|
9
|
+
NOT_IMPLEMENTED: 4,
|
|
10
|
+
RISK_CONTROL: 5,
|
|
11
|
+
NETWORK: 6,
|
|
12
|
+
UPSTREAM: 7,
|
|
13
|
+
};
|
|
14
|
+
export class SpecificAIError extends Error {
|
|
15
|
+
code;
|
|
16
|
+
hint;
|
|
17
|
+
detail;
|
|
18
|
+
constructor(code, message, options = {}) {
|
|
19
|
+
super(message, { cause: options.cause });
|
|
20
|
+
this.name = 'SpecificAIError';
|
|
21
|
+
this.code = code;
|
|
22
|
+
this.hint = options.hint ?? null;
|
|
23
|
+
this.detail = options.detail ?? null;
|
|
24
|
+
}
|
|
25
|
+
get exitCode() {
|
|
26
|
+
return EXIT_CODES[this.code];
|
|
27
|
+
}
|
|
28
|
+
toJSON() {
|
|
29
|
+
return { code: this.code, message: this.message, hint: this.hint, detail: this.detail };
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export function toSpecificAIError(err) {
|
|
33
|
+
if (err instanceof SpecificAIError)
|
|
34
|
+
return err;
|
|
35
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
36
|
+
return new SpecificAIError('ERROR', message, { cause: err });
|
|
37
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { createWriteStream } from 'node:fs';
|
|
2
|
+
import { mkdir, readFile, rename, rm, stat } from 'node:fs/promises';
|
|
3
|
+
import { basename, extname, join, resolve } from 'node:path';
|
|
4
|
+
import { Readable } from 'node:stream';
|
|
5
|
+
import { pipeline } from 'node:stream/promises';
|
|
6
|
+
import { SpecificAIError } from './errors.js';
|
|
7
|
+
/** `item download` 与发布时的本地文件(AGENTS 4.9「下载」「发布」)。 */
|
|
8
|
+
const EXT_BY_TYPE = {
|
|
9
|
+
'image/jpeg': 'jpg',
|
|
10
|
+
'image/png': 'png',
|
|
11
|
+
'image/webp': 'webp',
|
|
12
|
+
'image/gif': 'gif',
|
|
13
|
+
'image/avif': 'avif',
|
|
14
|
+
'image/heic': 'heic',
|
|
15
|
+
'video/mp4': 'mp4',
|
|
16
|
+
'video/quicktime': 'mov',
|
|
17
|
+
'video/webm': 'webm',
|
|
18
|
+
'audio/mp4': 'm4a',
|
|
19
|
+
'audio/mpeg': 'mp3',
|
|
20
|
+
};
|
|
21
|
+
const TYPE_BY_EXT = Object.fromEntries(Object.entries(EXT_BY_TYPE).map(([t, e]) => [e, t]));
|
|
22
|
+
Object.assign(TYPE_BY_EXT, { jpeg: 'image/jpeg', m4s: 'video/mp4', flv: 'video/x-flv', mkv: 'video/x-matroska' });
|
|
23
|
+
function extFromUrl(url) {
|
|
24
|
+
try {
|
|
25
|
+
const ext = extname(new URL(url).pathname).slice(1).toLowerCase();
|
|
26
|
+
return /^[a-z0-9]{2,5}$/.test(ext) ? ext : null;
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export function contentTypeOf(name) {
|
|
33
|
+
return TYPE_BY_EXT[extname(name).slice(1).toLowerCase()] ?? 'application/octet-stream';
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* 把一组媒体下载到 `--dir`,文件名 `<platform>_<id>_<序号>.<ext>`;已有文件默认跳过,`--overwrite` 时覆盖。
|
|
37
|
+
*/
|
|
38
|
+
export async function downloadMedia(ctx, http, itemId, media, options = {}) {
|
|
39
|
+
const dir = resolve(ctx.options.dir ?? '.');
|
|
40
|
+
const overwrite = Boolean(ctx.options.overwrite);
|
|
41
|
+
await mkdir(dir, { recursive: true });
|
|
42
|
+
const files = [];
|
|
43
|
+
for (const [i, m] of media.entries()) {
|
|
44
|
+
if (!m.url) {
|
|
45
|
+
ctx.log.warn(`第 ${i + 1} 个媒体没有可下载的地址,跳过`);
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
const ext = options.ext?.(m, i) ?? extFromUrl(m.url) ?? (m.type === 'image' ? 'jpg' : m.type === 'audio' ? 'm4a' : 'mp4');
|
|
49
|
+
const path = join(dir, `${ctx.platform.id}_${itemId}_${i + 1}.${ext}`);
|
|
50
|
+
const existing = await stat(path).catch(() => null);
|
|
51
|
+
if (existing && !overwrite) {
|
|
52
|
+
ctx.log.info(`已存在,跳过:${path}`);
|
|
53
|
+
files.push({ path, type: m.type, url: m.url, size: existing.size });
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
ctx.log.info(`下载 ${i + 1}/${media.length}:${basename(path)}`);
|
|
57
|
+
const res = await http.request({ url: m.url, headers: options.headers });
|
|
58
|
+
if (!res.ok)
|
|
59
|
+
throw new SpecificAIError('UPSTREAM', `下载失败:HTTP ${res.status}`, { detail: { status: res.status, url: m.url } });
|
|
60
|
+
const tmp = `${path}.part`;
|
|
61
|
+
try {
|
|
62
|
+
if (res.body)
|
|
63
|
+
await pipeline(Readable.fromWeb(res.body), createWriteStream(tmp));
|
|
64
|
+
else
|
|
65
|
+
await pipeline(Readable.from([Buffer.from(await res.arrayBuffer())]), createWriteStream(tmp));
|
|
66
|
+
await rename(tmp, path);
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
await rm(tmp, { force: true });
|
|
70
|
+
throw err;
|
|
71
|
+
}
|
|
72
|
+
files.push({ path, type: m.type, url: m.url, size: (await stat(path)).size });
|
|
73
|
+
}
|
|
74
|
+
return files;
|
|
75
|
+
}
|
|
76
|
+
/** 发布时的 `--image` / `--video` / `--cover`:本地路径直接读,URL 先下载。 */
|
|
77
|
+
export async function readMedia(http, input, headers) {
|
|
78
|
+
if (/^https?:\/\//i.test(input)) {
|
|
79
|
+
const res = await http.request({ url: input, headers, cookies: false });
|
|
80
|
+
if (!res.ok)
|
|
81
|
+
throw new SpecificAIError('USAGE', `下载失败:HTTP ${res.status} ${input}`);
|
|
82
|
+
const filename = basename(new URL(input).pathname) || 'file';
|
|
83
|
+
return { data: new Uint8Array(await res.arrayBuffer()), filename, contentType: res.headers.get('content-type')?.split(';')[0] ?? contentTypeOf(filename) };
|
|
84
|
+
}
|
|
85
|
+
const data = await readFile(input).catch(() => {
|
|
86
|
+
throw new SpecificAIError('USAGE', `读取文件失败:${input}`);
|
|
87
|
+
});
|
|
88
|
+
return { data: new Uint8Array(data), filename: basename(input), contentType: contentTypeOf(input) };
|
|
89
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { dirname } from 'node:path';
|
|
4
|
+
import { setTimeout as sleep } from 'node:timers/promises';
|
|
5
|
+
/** 目录 0700(Windows 依赖用户目录本身的 ACL)。 */
|
|
6
|
+
export async function ensureDir(dir) {
|
|
7
|
+
await mkdir(dir, { recursive: true, mode: 0o700 });
|
|
8
|
+
}
|
|
9
|
+
/** 原子写入:先写同目录的临时文件(0600),再 rename 覆盖。 */
|
|
10
|
+
export async function writeFileAtomic(file, data) {
|
|
11
|
+
await ensureDir(dirname(file));
|
|
12
|
+
const tmp = `${file}.${process.pid}.${randomBytes(4).toString('hex')}.tmp`;
|
|
13
|
+
await writeFile(tmp, data, { mode: 0o600 });
|
|
14
|
+
try {
|
|
15
|
+
// Windows 上目标文件被其他进程短暂占用时 rename 会失败,重试几次。
|
|
16
|
+
for (let i = 0;; i++) {
|
|
17
|
+
try {
|
|
18
|
+
await rename(tmp, file);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
catch (err) {
|
|
22
|
+
const code = err.code;
|
|
23
|
+
if (i >= 5 || (code !== 'EPERM' && code !== 'EBUSY' && code !== 'EACCES'))
|
|
24
|
+
throw err;
|
|
25
|
+
await sleep(20 * (i + 1));
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
await rm(tmp, { force: true });
|
|
31
|
+
throw err;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export async function readTextIfExists(file) {
|
|
35
|
+
try {
|
|
36
|
+
return await readFile(file, 'utf8');
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
if (err.code === 'ENOENT')
|
|
40
|
+
return null;
|
|
41
|
+
throw err;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/** 带本地时区偏移的 ISO 8601,例如 2026-09-27T12:00:00+08:00。 */
|
|
45
|
+
export function isoNow(date = new Date()) {
|
|
46
|
+
const pad = (n) => String(Math.trunc(Math.abs(n))).padStart(2, '0');
|
|
47
|
+
const offset = -date.getTimezoneOffset();
|
|
48
|
+
const sign = offset >= 0 ? '+' : '-';
|
|
49
|
+
return (`${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}` +
|
|
50
|
+
`T${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}` +
|
|
51
|
+
`${sign}${pad(offset / 60)}:${pad(offset % 60)}`);
|
|
52
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { createHash, createHmac } from 'node:crypto';
|
|
2
|
+
export const md5Hex = (data) => createHash('md5').update(data).digest('hex');
|
|
3
|
+
export const sha256Hex = (data) => createHash('sha256').update(data).digest('hex');
|
|
4
|
+
export const hmacSha256Hex = (key, data) => createHmac('sha256', key).update(data).digest('hex');
|