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,22 @@
|
|
|
1
|
+
import { createInterface } from 'node:readline/promises';
|
|
2
|
+
import { SpecificAIError } from '../core/errors.js';
|
|
3
|
+
/**
|
|
4
|
+
* 危险操作确认(AGENTS 4.11):stdin 和 stderr 都是 TTY 时在 stderr 上问 y/N;
|
|
5
|
+
* 否则必须带 -y,不带时报 CONFIRM_REQUIRED。
|
|
6
|
+
*/
|
|
7
|
+
export async function confirm(question, yes) {
|
|
8
|
+
if (yes)
|
|
9
|
+
return;
|
|
10
|
+
if (!process.stdin.isTTY || !process.stderr.isTTY) {
|
|
11
|
+
throw new SpecificAIError('CONFIRM_REQUIRED', `危险操作需要确认:${question}`, { hint: '确认无误后加 -y 重新执行' });
|
|
12
|
+
}
|
|
13
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
14
|
+
try {
|
|
15
|
+
const answer = await rl.question(`[specific-ai] ${question}?[y/N] `);
|
|
16
|
+
if (!/^y(es)?$/i.test(answer.trim()))
|
|
17
|
+
throw new SpecificAIError('CONFIRM_REQUIRED', '已取消');
|
|
18
|
+
}
|
|
19
|
+
finally {
|
|
20
|
+
rl.close();
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { resolve as resolvePath } from 'node:path';
|
|
3
|
+
import { text as readStdin } from 'node:stream/consumers';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { checkAccountName, endpointFlag, getCurrent, GUEST, newCredential, readCredential, writeCredential } from '../core/auth-store.js';
|
|
6
|
+
import { resolveNetwork } from '../core/config.js';
|
|
7
|
+
import { SpecificAIError, toSpecificAIError } from '../core/errors.js';
|
|
8
|
+
import { createLogger } from '../core/log.js';
|
|
9
|
+
import { describeOption, flagName, PAGING, parseDuration, STANDARD_VALUES } from '../core/options.js';
|
|
10
|
+
import { ENDPOINTS, } from '../core/registry.js';
|
|
11
|
+
import * as rand from '../core/rand.js';
|
|
12
|
+
import { VOCAB } from '../core/vocab.js';
|
|
13
|
+
import { findPlatform, PLATFORMS } from '../platforms/index.js';
|
|
14
|
+
import { collectOptionKinds, parseArgv } from './argv.js';
|
|
15
|
+
import { allAccounts } from './commands/auth.js';
|
|
16
|
+
import { config } from './commands/config.js';
|
|
17
|
+
import { platformInfo } from './commands/platforms.js';
|
|
18
|
+
import { version } from './commands/version.js';
|
|
19
|
+
import { confirm } from './confirm.js';
|
|
20
|
+
import { nativeHint } from './hints.js';
|
|
21
|
+
import { commandHelp, GLOBAL_HELP, platformHelp, resourceHelp, rootHelp } from './help.js';
|
|
22
|
+
import { FORMATS, Output } from './output.js';
|
|
23
|
+
z.config(z.locales.zhCN());
|
|
24
|
+
const GLOBAL_COMMANDS = ['platforms', 'doctor', 'auth', 'config', 'version'];
|
|
25
|
+
function printHelp(text) {
|
|
26
|
+
process.stdout.write(text + '\n');
|
|
27
|
+
return 0;
|
|
28
|
+
}
|
|
29
|
+
/** 执行一次 specific-ai 调用,返回退出码。结果和错误都以信封写到 stdout(jsonl 时摘要写到 stderr)。 */
|
|
30
|
+
export async function run(argv) {
|
|
31
|
+
const out = new Output();
|
|
32
|
+
try {
|
|
33
|
+
const parsed = parseArgv(argv, collectOptionKinds(PLATFORMS));
|
|
34
|
+
const g = parsed.global;
|
|
35
|
+
if (g.output != null) {
|
|
36
|
+
if (!FORMATS.includes(g.output))
|
|
37
|
+
throw new SpecificAIError('USAGE', `不支持的输出格式:${g.output}`, { hint: '-o json 或 -o jsonl' });
|
|
38
|
+
out.format = g.output;
|
|
39
|
+
}
|
|
40
|
+
out.raw = g.raw ?? false;
|
|
41
|
+
const log = createLogger({ verbose: g.verbose, quiet: g.quiet });
|
|
42
|
+
const [first, ...rest] = parsed.positionals;
|
|
43
|
+
if (first == null) {
|
|
44
|
+
if (g.version && !g.help)
|
|
45
|
+
return await runGlobal('version', [], parsed, out);
|
|
46
|
+
return printHelp(rootHelp());
|
|
47
|
+
}
|
|
48
|
+
if (GLOBAL_COMMANDS.includes(first))
|
|
49
|
+
return await runGlobal(first, rest, parsed, out);
|
|
50
|
+
const platform = findPlatform(first);
|
|
51
|
+
if (!platform) {
|
|
52
|
+
throw new SpecificAIError('USAGE', `未知的平台或命令:${first}`, { hint: 'specific-ai platforms 列出所有平台;specific-ai --help 查看用法' });
|
|
53
|
+
}
|
|
54
|
+
return await runPlatform(platform, rest, parsed, out, log);
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
const e = toSpecificAIError(err);
|
|
58
|
+
out.fail(e);
|
|
59
|
+
return e.exitCode;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
async function runGlobal(name, words, parsed, out) {
|
|
63
|
+
const action = words[0] ?? null;
|
|
64
|
+
out.meta = { platform: null, endpoint: null, resource: name, action: null, account: null };
|
|
65
|
+
if (parsed.global.help)
|
|
66
|
+
return printHelp(GLOBAL_HELP[name]);
|
|
67
|
+
const extra = Object.keys(parsed.options);
|
|
68
|
+
if (extra.length)
|
|
69
|
+
throw new SpecificAIError('USAGE', `specific-ai ${name} 不接受选项 --${flagName(extra[0])}`, { hint: `specific-ai ${name} --help` });
|
|
70
|
+
const noArgs = () => {
|
|
71
|
+
if (words.length)
|
|
72
|
+
throw new SpecificAIError('USAGE', `多余的参数:${words.join(' ')}`, { hint: `specific-ai ${name} --help` });
|
|
73
|
+
};
|
|
74
|
+
switch (name) {
|
|
75
|
+
case 'version':
|
|
76
|
+
noArgs();
|
|
77
|
+
out.result(version());
|
|
78
|
+
return 0;
|
|
79
|
+
case 'platforms': {
|
|
80
|
+
if (words.length > 1)
|
|
81
|
+
throw new SpecificAIError('USAGE', `多余的参数:${words.slice(1).join(' ')}`, { hint: 'specific-ai platforms --help' });
|
|
82
|
+
if (action == null) {
|
|
83
|
+
out.result(PLATFORMS.map(platformInfo));
|
|
84
|
+
return 0;
|
|
85
|
+
}
|
|
86
|
+
const platform = findPlatform(action);
|
|
87
|
+
if (!platform)
|
|
88
|
+
throw new SpecificAIError('USAGE', `未知的平台:${action}`, { hint: 'specific-ai platforms' });
|
|
89
|
+
out.result(platformInfo(platform));
|
|
90
|
+
return 0;
|
|
91
|
+
}
|
|
92
|
+
case 'doctor': {
|
|
93
|
+
noArgs();
|
|
94
|
+
const { doctor } = await import('./commands/doctor.js');
|
|
95
|
+
const checks = await doctor();
|
|
96
|
+
const failed = checks.filter((c) => !c.ok);
|
|
97
|
+
if (!failed.length) {
|
|
98
|
+
out.result(checks);
|
|
99
|
+
return 0;
|
|
100
|
+
}
|
|
101
|
+
const error = new SpecificAIError('ERROR', `${failed.length} 项检查未通过:${failed.map((c) => c.name).join('、')}`);
|
|
102
|
+
out.result(checks, { error });
|
|
103
|
+
return error.exitCode;
|
|
104
|
+
}
|
|
105
|
+
case 'auth':
|
|
106
|
+
if (action == null)
|
|
107
|
+
return printHelp(GLOBAL_HELP.auth);
|
|
108
|
+
out.meta.action = action;
|
|
109
|
+
if (action !== 'list') {
|
|
110
|
+
throw new SpecificAIError('USAGE', `auth ${action} 需要指定平台`, { hint: `specific-ai <platform> auth ${action}` });
|
|
111
|
+
}
|
|
112
|
+
if (words.length > 1)
|
|
113
|
+
throw new SpecificAIError('USAGE', `多余的参数:${words.slice(1).join(' ')}`, { hint: 'specific-ai auth --help' });
|
|
114
|
+
out.result(await allAccounts());
|
|
115
|
+
return 0;
|
|
116
|
+
case 'config':
|
|
117
|
+
if (action == null)
|
|
118
|
+
return printHelp(GLOBAL_HELP.config);
|
|
119
|
+
out.meta.action = action;
|
|
120
|
+
out.result(await config(action, words.slice(1)));
|
|
121
|
+
return 0;
|
|
122
|
+
}
|
|
123
|
+
throw new SpecificAIError('ERROR', `未处理的全局命令:${name}`);
|
|
124
|
+
}
|
|
125
|
+
function parseEndpoint(value) {
|
|
126
|
+
if (value == null)
|
|
127
|
+
return 'web';
|
|
128
|
+
if (!ENDPOINTS.includes(value)) {
|
|
129
|
+
throw new SpecificAIError('USAGE', `不支持的端:${value}`, { hint: '-e web、-e app 或 -e pc' });
|
|
130
|
+
}
|
|
131
|
+
return value;
|
|
132
|
+
}
|
|
133
|
+
/** 该平台任何可用端上声明过的扩展命令。端为 planned 时,词表命令和扩展命令都报 NOT_IMPLEMENTED。 */
|
|
134
|
+
function isExtension(platform, key) {
|
|
135
|
+
return ENDPOINTS.some((e) => {
|
|
136
|
+
const ep = platform.endpoints[e];
|
|
137
|
+
return ep !== 'planned' && ep.commands.get(key)?.extension === true;
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
function hasResource(platform, resource) {
|
|
141
|
+
return ENDPOINTS.some((e) => {
|
|
142
|
+
const ep = platform.endpoints[e];
|
|
143
|
+
return ep !== 'planned' && [...ep.commands.values()].some((c) => c.resource === resource);
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
function unsupported(platform, endpoint, resource, action) {
|
|
147
|
+
const p = platform.id;
|
|
148
|
+
const key = action == null ? resource : `${resource} ${action}`;
|
|
149
|
+
const message = action == null ? `${p} 没有 ${resource} 这类命令` : `${p} (${endpoint}) 不支持 ${key}`;
|
|
150
|
+
const other = ENDPOINTS.find((e) => {
|
|
151
|
+
const ep = platform.endpoints[e];
|
|
152
|
+
return e !== endpoint && ep !== 'planned' && ep.commands.has(key);
|
|
153
|
+
});
|
|
154
|
+
const hint = other
|
|
155
|
+
? `这个命令在 ${other} 端:specific-ai ${p} ${key} ... -e ${other}`
|
|
156
|
+
: (nativeHint(p, resource, action) ??
|
|
157
|
+
(hasResource(platform, resource) ? `specific-ai ${p} ${resource} --help` : `specific-ai ${p} --help`));
|
|
158
|
+
return new SpecificAIError('UNSUPPORTED', message, { hint });
|
|
159
|
+
}
|
|
160
|
+
async function runPlatform(platform, words, parsed, out, log) {
|
|
161
|
+
const g = parsed.global;
|
|
162
|
+
const endpoint = parseEndpoint(g.endpoint);
|
|
163
|
+
const p = platform.id;
|
|
164
|
+
const [resource, action, ...argWords] = words;
|
|
165
|
+
out.meta = { platform: p, endpoint, resource: resource ?? null, action: action ?? null, account: null };
|
|
166
|
+
// 命令不完整或带 -h 时,输出已到达层级的帮助
|
|
167
|
+
if (resource == null)
|
|
168
|
+
return printHelp(platformHelp(platform, endpoint));
|
|
169
|
+
const ep = platform.endpoints[endpoint];
|
|
170
|
+
const known = hasResource(platform, resource);
|
|
171
|
+
if (action == null || g.help) {
|
|
172
|
+
const listed = ep === 'planned' ? platform.endpoints.web : ep;
|
|
173
|
+
const found = action != null && listed !== 'planned' ? listed.commands.get(`${resource} ${action}`) : undefined;
|
|
174
|
+
if (found)
|
|
175
|
+
return printHelp(commandHelp(platform, endpoint, found));
|
|
176
|
+
if (known)
|
|
177
|
+
return printHelp(resourceHelp(platform, endpoint, resource));
|
|
178
|
+
if (g.help)
|
|
179
|
+
return printHelp(platformHelp(platform, endpoint));
|
|
180
|
+
throw unsupported(platform, endpoint, resource, undefined);
|
|
181
|
+
}
|
|
182
|
+
const key = `${resource} ${action}`;
|
|
183
|
+
if (ep === 'planned') {
|
|
184
|
+
if (VOCAB[key] || isExtension(platform, key)) {
|
|
185
|
+
throw new SpecificAIError('NOT_IMPLEMENTED', `${p} 的 ${endpoint} 端尚未实现`, { detail: { endpoint, status: 'planned' } });
|
|
186
|
+
}
|
|
187
|
+
throw unsupported(platform, endpoint, resource, action);
|
|
188
|
+
}
|
|
189
|
+
const command = ep.commands.get(key);
|
|
190
|
+
if (!command)
|
|
191
|
+
throw unsupported(platform, endpoint, resource, action);
|
|
192
|
+
const { args, options } = validate(platform, command, argWords, parsed.options);
|
|
193
|
+
if (command.status === 'planned') {
|
|
194
|
+
throw new SpecificAIError('NOT_IMPLEMENTED', `${p} (${endpoint}) 的 ${key} 尚未实现`, {
|
|
195
|
+
detail: { upstream: command.upstream },
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
const identity = await resolveIdentity(platform, endpoint, command, args, g, log);
|
|
199
|
+
// auth login 没有当前账号时登录到 default(AGENTS 5.3),信封里写实际登录的账号
|
|
200
|
+
out.meta.account = identity.account ?? (key === 'auth login' ? 'default' : null);
|
|
201
|
+
const needConfirm = typeof command.confirm === 'function' ? command.confirm(options) : command.confirm === true;
|
|
202
|
+
if (needConfirm)
|
|
203
|
+
await confirm(`specific-ai ${p} ${key} ${argWords.join(' ')}`.trim(), g.yes ?? false);
|
|
204
|
+
await readFileOptions(options);
|
|
205
|
+
const network = await resolveNetwork(p, g.proxy);
|
|
206
|
+
log.debug(`${p} (${endpoint}) ${key}`, { account: identity.account, ...network });
|
|
207
|
+
const handler = await command.handler();
|
|
208
|
+
const controller = new AbortController();
|
|
209
|
+
const ctx = {
|
|
210
|
+
platform,
|
|
211
|
+
endpoint,
|
|
212
|
+
resource,
|
|
213
|
+
action,
|
|
214
|
+
args,
|
|
215
|
+
options,
|
|
216
|
+
account: identity.account,
|
|
217
|
+
credential: identity.credential,
|
|
218
|
+
cursor: options.cursor ?? null,
|
|
219
|
+
raw: out.raw,
|
|
220
|
+
config: network,
|
|
221
|
+
log,
|
|
222
|
+
signal: controller.signal,
|
|
223
|
+
saveCredential: async (credential) => {
|
|
224
|
+
if (credential.platform !== p || credential.endpoint !== endpoint)
|
|
225
|
+
throw new Error('saveCredential:平台或端不匹配');
|
|
226
|
+
await writeCredential(credential);
|
|
227
|
+
},
|
|
228
|
+
};
|
|
229
|
+
// 不支持游客态的端上,游客凭证只在内存里(见 resolveIdentity)
|
|
230
|
+
const snapshot = ctx.credential.account === GUEST && !ep.guest ? null : JSON.stringify(ctx.credential);
|
|
231
|
+
try {
|
|
232
|
+
if (command.stream) {
|
|
233
|
+
out.format = 'jsonl';
|
|
234
|
+
await runStream(ctx, handler(ctx), controller, out);
|
|
235
|
+
}
|
|
236
|
+
else if (command.paged) {
|
|
237
|
+
await runPaged(platform, ctx, handler, out);
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
out.result(await handler(ctx));
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
finally {
|
|
244
|
+
await persistCredential(ctx.credential, snapshot);
|
|
245
|
+
}
|
|
246
|
+
return 0;
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* 请求中更新过的凭证(Set-Cookie、刷新的 token、新生成的游客设备数据)落盘。
|
|
250
|
+
* 账号文件已被删除(logout)时不再写回。
|
|
251
|
+
*/
|
|
252
|
+
async function persistCredential(credential, snapshot) {
|
|
253
|
+
if (snapshot == null || JSON.stringify(credential) === snapshot)
|
|
254
|
+
return;
|
|
255
|
+
const { platform, endpoint, account } = credential;
|
|
256
|
+
if (account !== GUEST && !(await readCredential(platform, endpoint, account).catch(() => null)))
|
|
257
|
+
return;
|
|
258
|
+
await writeCredential(credential);
|
|
259
|
+
}
|
|
260
|
+
/** 选项与参数校验:不适用的选项 → 标准取值 → 位置参数个数 → zod → 跨参数约束。 */
|
|
261
|
+
function validate(platform, command, words, given) {
|
|
262
|
+
const p = platform.id;
|
|
263
|
+
const help = `specific-ai ${p} ${command.key} --help`;
|
|
264
|
+
for (const key of Object.keys(given)) {
|
|
265
|
+
if (command.unsupported.includes(key)) {
|
|
266
|
+
const usable = Object.keys(command.options).filter((k) => !(k in PAGING));
|
|
267
|
+
throw new SpecificAIError('UNSUPPORTED', `${p} 的 ${command.key} 不支持 --${flagName(key)}`, {
|
|
268
|
+
hint: usable.length ? `可用的选项:${usable.map((k) => `--${flagName(k)}`).join('、')}` : help,
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
if (!(key in command.options)) {
|
|
272
|
+
throw new SpecificAIError('USAGE', `选项 --${flagName(key)} 不适用于 ${p} ${command.key}`, { hint: help });
|
|
273
|
+
}
|
|
274
|
+
const standard = STANDARD_VALUES[key];
|
|
275
|
+
if (!standard)
|
|
276
|
+
continue;
|
|
277
|
+
const value = String(given[key]);
|
|
278
|
+
const flag = `--${flagName(key)}`;
|
|
279
|
+
// 两种错误的 hint 都只列本平台支持的取值,照着 hint 改不会再撞 UNSUPPORTED
|
|
280
|
+
const supported = describeOption(command.options[key]).values ?? standard;
|
|
281
|
+
if (!standard.includes(value)) {
|
|
282
|
+
throw new SpecificAIError('USAGE', `${flag} 的取值不对:${value}`, { hint: `可选:${supported.join('、')}` });
|
|
283
|
+
}
|
|
284
|
+
if (!supported.includes(value)) {
|
|
285
|
+
throw new SpecificAIError('UNSUPPORTED', `${p} 的 ${command.key} 不支持 ${flag} ${value}`, { hint: `可选:${supported.join('、')}` });
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
const min = command.args.filter((a) => !a.optional).length;
|
|
289
|
+
if (words.length < min) {
|
|
290
|
+
throw new SpecificAIError('USAGE', `缺少参数 <${command.args[words.length].name}>`, { hint: help });
|
|
291
|
+
}
|
|
292
|
+
if (words.length > command.args.length) {
|
|
293
|
+
throw new SpecificAIError('USAGE', `多余的参数:${words.slice(command.args.length).join(' ')}`, {
|
|
294
|
+
hint: `含空格的文本请加引号;以 - 开头的参数放在 -- 之后。${help}`,
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
const result = z.object(command.options).safeParse(given);
|
|
298
|
+
if (!result.success) {
|
|
299
|
+
const issue = result.error.issues[0];
|
|
300
|
+
const key = String(issue.path[0] ?? '');
|
|
301
|
+
throw new SpecificAIError('USAGE', `${key ? `--${flagName(key)}:` : ''}${issue.message}`, { hint: help });
|
|
302
|
+
}
|
|
303
|
+
const options = result.data;
|
|
304
|
+
const args = Object.fromEntries(command.args.map((a, i) => [a.name, words[i] ?? a.default]));
|
|
305
|
+
const problem = command.check?.(args, options);
|
|
306
|
+
if (problem)
|
|
307
|
+
throw new SpecificAIError('USAGE', problem, { hint: help });
|
|
308
|
+
return { args, options };
|
|
309
|
+
}
|
|
310
|
+
/** 身份:-a > _current > 游客(AGENTS 5.2)。auth 命令只解析账号名,不做登录检查。 */
|
|
311
|
+
async function resolveIdentity(platform, endpoint, command, args, g, log) {
|
|
312
|
+
const p = platform.id;
|
|
313
|
+
const e = endpointFlag(endpoint);
|
|
314
|
+
const ep = platform.endpoints[endpoint];
|
|
315
|
+
const blank = () => newCredential({ platform: p, endpoint, account: GUEST, method: 'guest' });
|
|
316
|
+
// 不支持游客态的端上没有 guest.json:auth 命令没有账号时用一份不落盘的空凭证(AGENTS 5.2)
|
|
317
|
+
const guest = async () => (ep.guest ? ((await readCredential(p, endpoint, GUEST).catch(() => null)) ?? blank()) : blank());
|
|
318
|
+
if (command.resource === 'auth') {
|
|
319
|
+
// -a guest 只对 auth status 有意义;登录、登出到 guest 都不行(guest 是保留名)
|
|
320
|
+
const allowGuest = command.key === 'auth status';
|
|
321
|
+
const account = g.account != null ? checkAccountName(g.account, { allowGuest }) : await getCurrent(p, endpoint);
|
|
322
|
+
const credential = account == null || account === GUEST ? null : await readCredential(p, endpoint, account).catch(() => null);
|
|
323
|
+
return { account: account === GUEST ? null : account, credential: credential ?? (await guest()) };
|
|
324
|
+
}
|
|
325
|
+
if (g.account != null && g.account !== GUEST) {
|
|
326
|
+
const account = checkAccountName(g.account);
|
|
327
|
+
const credential = await readCredential(p, endpoint, account);
|
|
328
|
+
if (!credential) {
|
|
329
|
+
throw new SpecificAIError('AUTH_REQUIRED', `账号 ${account} 不存在`, { hint: `specific-ai ${p} auth login -a ${account}${e}` });
|
|
330
|
+
}
|
|
331
|
+
return { account, credential };
|
|
332
|
+
}
|
|
333
|
+
const current = g.account == null ? await getCurrent(p, endpoint) : null;
|
|
334
|
+
if (current)
|
|
335
|
+
return { account: current, credential: (await readCredential(p, endpoint, current)) };
|
|
336
|
+
const login = `specific-ai ${p} auth login${e}`;
|
|
337
|
+
if (command.auth === 'required')
|
|
338
|
+
throw new SpecificAIError('AUTH_REQUIRED', `${command.key} 需要登录`, { hint: login });
|
|
339
|
+
if (command.args.some((a) => a.name === 'user' && args[a.name] === 'me')) {
|
|
340
|
+
throw new SpecificAIError('AUTH_REQUIRED', 'me 表示当前账号,需要先登录', { hint: login });
|
|
341
|
+
}
|
|
342
|
+
if (g.account !== GUEST) {
|
|
343
|
+
log.info(`未登录 ${p} (${endpoint}),本次以游客身份访问,结果可能不完整;很多操作需要登录。登录:${login}`);
|
|
344
|
+
}
|
|
345
|
+
return { account: GUEST, credential: await guest() };
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* `--text @file`、`--cookie @file|-`:从文件或 stdin 读取。
|
|
349
|
+
* `--text @file` 同时把文件的绝对路径记在 `textFile` 里,正文里的相对路径(如 Markdown 插图)按它所在目录解析。
|
|
350
|
+
*/
|
|
351
|
+
async function readFileOptions(options) {
|
|
352
|
+
for (const key of ['text', 'cookie']) {
|
|
353
|
+
const v = options[key];
|
|
354
|
+
if (typeof v !== 'string')
|
|
355
|
+
continue;
|
|
356
|
+
if (key === 'cookie' && v === '-')
|
|
357
|
+
options[key] = (await readStdin(process.stdin)).trim();
|
|
358
|
+
else if (v.startsWith('@')) {
|
|
359
|
+
options[key] = await readFile(v.slice(1), 'utf8').catch(() => {
|
|
360
|
+
throw new SpecificAIError('USAGE', `读取文件失败:${v.slice(1)}`);
|
|
361
|
+
});
|
|
362
|
+
if (key === 'text')
|
|
363
|
+
options.textFile = resolvePath(v.slice(1));
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
/** 截在一页中间时的游标后缀:`<这一页的游标>#skip=N`,续翻时重取这一页、跳过已经输出的 N 条。 */
|
|
368
|
+
const SKIP_RE = /#skip=(\d+)$/;
|
|
369
|
+
export function splitCursor(cursor) {
|
|
370
|
+
const m = cursor == null ? null : SKIP_RE.exec(cursor);
|
|
371
|
+
if (!m)
|
|
372
|
+
return { cursor, skip: 0 };
|
|
373
|
+
const base = cursor.slice(0, m.index);
|
|
374
|
+
return { cursor: base === '' ? null : base, skip: Number(m[1]) };
|
|
375
|
+
}
|
|
376
|
+
/** 分页:默认一页;--limit N 翻到取满 N 条;--all 翻到没有更多。jsonl 时边翻边输出。 */
|
|
377
|
+
async function runPaged(platform, ctx, handler, out) {
|
|
378
|
+
const { limit, all } = ctx.options;
|
|
379
|
+
const target = limit ?? (all ? Infinity : null);
|
|
380
|
+
const collected = [];
|
|
381
|
+
let count = 0;
|
|
382
|
+
let { cursor, skip } = splitCursor(ctx.cursor);
|
|
383
|
+
let page;
|
|
384
|
+
for (;;) {
|
|
385
|
+
const result = (await handler({ ...ctx, cursor }));
|
|
386
|
+
let items = result.data.slice(skip);
|
|
387
|
+
const offset = skip;
|
|
388
|
+
skip = 0;
|
|
389
|
+
page = result.page;
|
|
390
|
+
if (target != null && count + items.length > target) {
|
|
391
|
+
// --limit 截在一页中间:游标指回这一页,续翻时跳过已经输出的,剩下的不丢
|
|
392
|
+
const take = target - count;
|
|
393
|
+
items = items.slice(0, take);
|
|
394
|
+
page = { cursor: `${cursor ?? ''}#skip=${offset + take}`, has_more: true };
|
|
395
|
+
}
|
|
396
|
+
count += items.length;
|
|
397
|
+
if (out.format === 'jsonl')
|
|
398
|
+
items.forEach((v) => out.item(v));
|
|
399
|
+
else
|
|
400
|
+
collected.push(...items);
|
|
401
|
+
if (target == null || count >= target || !page.has_more || page.cursor == null)
|
|
402
|
+
break;
|
|
403
|
+
// 服务端把同一个游标又返回一遍:再翻只会重复拿到同一页,停在这里
|
|
404
|
+
if (page.cursor === cursor) {
|
|
405
|
+
ctx.log.warn(`翻页游标没有变化(${page.cursor}),停止翻页`);
|
|
406
|
+
page = { ...page, has_more: false };
|
|
407
|
+
break;
|
|
408
|
+
}
|
|
409
|
+
cursor = page.cursor;
|
|
410
|
+
ctx.log.debug(`翻页:已取 ${count} 条`);
|
|
411
|
+
await rand.sleep(platform.pageInterval);
|
|
412
|
+
}
|
|
413
|
+
out.result(collected, { page, streamed: out.format === 'jsonl' });
|
|
414
|
+
}
|
|
415
|
+
/** 长连接:逐条输出 jsonl,直到 Ctrl-C 或 --duration 到期(退出码 0)。 */
|
|
416
|
+
async function runStream(ctx, iterable, controller, out) {
|
|
417
|
+
const stop = () => controller.abort();
|
|
418
|
+
process.once('SIGINT', stop);
|
|
419
|
+
const duration = ctx.options.duration;
|
|
420
|
+
const timer = duration ? setTimeout(stop, parseDuration(duration)) : null;
|
|
421
|
+
const aborted = new Promise((resolve) => controller.signal.addEventListener('abort', () => resolve(null), { once: true }));
|
|
422
|
+
const it = iterable[Symbol.asyncIterator]();
|
|
423
|
+
try {
|
|
424
|
+
for (;;) {
|
|
425
|
+
const next = await Promise.race([it.next(), aborted]);
|
|
426
|
+
if (next == null || next.done)
|
|
427
|
+
break;
|
|
428
|
+
out.item(next.value);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
finally {
|
|
432
|
+
if (timer)
|
|
433
|
+
clearTimeout(timer);
|
|
434
|
+
process.off('SIGINT', stop);
|
|
435
|
+
controller.abort();
|
|
436
|
+
// 生成器可能正卡在 await 上,return() 要等它恢复才会完成;不等它,免得进程挂住
|
|
437
|
+
void Promise.resolve(it.return?.()).catch(() => { });
|
|
438
|
+
}
|
|
439
|
+
out.result(undefined, { streamed: true });
|
|
440
|
+
}
|