@optima-chat/dev-skills 0.16.6 → 0.16.8
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/.claude/commands/logs.md +1 -1
- package/.claude/commands/query-db.md +6 -1
- package/.claude/skills/discount-codes/SKILL.md +4 -4
- package/.claude/skills/generate-test-token/SKILL.md +8 -4
- package/.claude/skills/logs/SKILL.md +5 -3
- package/.claude/skills/show-env/SKILL.md +1 -1
- package/.claude/skills/yzsgo-e2e/SKILL.md +19 -0
- package/.claude/skills/yzsgo-e2e/SYNC.md +26 -2
- package/.claude/skills/yzsgo-e2e/chat_driver.py +221 -47
- package/.claude/skills/yzsgo-e2e/pull_wire.py +39 -8
- package/.claude/skills/yzsgo-e2e/run_e2e.py +17 -2
- package/.codex/skills/generate-test-token/SKILL.md +3 -2
- package/.codex/skills/logs/SKILL.md +4 -2
- package/.codex/skills/show-env/SKILL.md +4 -2
- package/.codex/skills/yzsgo-e2e/SKILL.md +19 -0
- package/.codex/skills/yzsgo-e2e/SYNC.md +26 -2
- package/.codex/skills/yzsgo-e2e/chat_driver.py +221 -47
- package/.codex/skills/yzsgo-e2e/pull_wire.py +39 -8
- package/.codex/skills/yzsgo-e2e/run_e2e.py +17 -2
- package/AGENTS.md +1 -1
- package/bin/cli.js +3 -3
- package/bin/helpers/billing-http.ts +12 -3
- package/bin/helpers/db-utils.ts +8 -5
- package/bin/helpers/entitlement/grant.ts +5 -0
- package/bin/helpers/entitlement/revoke.ts +5 -0
- package/bin/helpers/generate-test-token.ts +40 -3
- package/bin/helpers/grant-credits.ts +8 -2
- package/bin/helpers/grant-subscription.ts +8 -4
- package/bin/helpers/operator.ts +9 -0
- package/bin/helpers/verify-health.ts +77 -29
- package/dist/bin/helpers/billing-http.js +9 -1
- package/dist/bin/helpers/db-utils.js +8 -2
- package/dist/bin/helpers/entitlement/grant.js +7 -0
- package/dist/bin/helpers/entitlement/revoke.js +7 -0
- package/dist/bin/helpers/generate-test-token.js +40 -3
- package/dist/bin/helpers/grant-credits.js +9 -2
- package/dist/bin/helpers/grant-subscription.js +9 -3
- package/dist/bin/helpers/operator.js +11 -0
- package/dist/bin/helpers/verify-health.js +83 -27
- package/package.json +1 -1
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
*
|
|
18
18
|
* 用法:
|
|
19
19
|
* optima-verify-health user-auth # 默认 cn-prod
|
|
20
|
-
* optima-verify-health user-auth --env prod # 环境 stage|prod|cn|all
|
|
21
|
-
* optima-verify-health --all --env all # stage/prod/cn
|
|
20
|
+
* optima-verify-health user-auth --env prod # 环境 stage|prod|cn-prod|cn-stage|all
|
|
21
|
+
* optima-verify-health --all --env all # stage/prod/cn-prod/cn-stage 四环境矩阵
|
|
22
22
|
* optima-verify-health gateway-core --expect-commit a1b2c3d
|
|
23
23
|
* optima-verify-health --url https://auth.yzsgo.com/health
|
|
24
24
|
* optima-verify-health --all --json # 机器可读,接 CI
|
|
@@ -29,23 +29,40 @@ import { promises as dns } from 'node:dns';
|
|
|
29
29
|
import * as tls from 'node:tls';
|
|
30
30
|
import * as https from 'node:https';
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
// 环境名与全仓保持一致(logs.ts 的 CN_ENVS、query-db.ts 的 VALID_ENVS、show-env、
|
|
33
|
+
// billing-http 的 validateEnvCnProd):阿里云生产叫 cn-prod,不叫 cn。
|
|
34
|
+
// 🔴 名字对不上时 resolve() 会把每个 SvcCfg 键查成 undefined,于是走到「无目标」分支,
|
|
35
|
+
// 把「环境名打错」报成「该服务无部署」—— 与 #83 那批 301 被报成「疑似未部署」同一个病:
|
|
36
|
+
// 探针在不确定的时候撒了一个确定的谎。
|
|
37
|
+
type Env = 'stage' | 'prod' | 'cn-prod' | 'cn-stage';
|
|
38
|
+
interface SvcCfg { path: string; stage?: string; prod?: string; 'cn-prod'?: string; 'cn-prod_path'?: string; 'cn-stage'?: string; 'cn-stage_path'?: string; }
|
|
34
39
|
|
|
35
40
|
// 服务 × 环境 FQDN 表。某服务某环境没部署 → 该 env 键缺省,探时跳过。
|
|
36
41
|
// cn-prod 真实 subdomain 抄自 optima-terraform alicloud/stacks/cn-prod-ingress-sae/main.tf。
|
|
37
42
|
// #201 (2026-06-12): yzsgo.com 全量迁移完成,旧 *-cn.optima.chat 路由已下线。
|
|
38
43
|
// cn-stage(阿里云预发)域名 *.stage.optima.chat,抄自 cn-stage-ingress-sae services map。
|
|
44
|
+
// #83 (2026-08-13): AWS stage 是 *.stage.optima.onl(点号),旧的连字符形式 *-stage.optima.onl
|
|
45
|
+
// 已不再路由到服务——三项实测 301 → www.optima.onl,探测恒红并误报成「疑似未部署」。
|
|
46
|
+
// 🔴 别只看 stage 列:agentic-chat 的 prod 也踩同一个坑(见下面那行的注释)。
|
|
39
47
|
const SERVICES: Record<string, SvcCfg> = {
|
|
40
|
-
'user-auth': { path: '/health', stage: 'auth
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
'
|
|
45
|
-
'
|
|
46
|
-
|
|
48
|
+
'user-auth': { path: '/health', stage: 'auth.stage.optima.onl', prod: 'auth.optima.onl', 'cn-prod': 'auth.yzsgo.com', 'cn-stage': 'auth.stage.optima.chat' },
|
|
49
|
+
// agentic-chat 的 prod 入口是 www 不是 ai:prod-ecs/variables.tf 里它的 subdomain = "www",
|
|
50
|
+
// 且 main.tf 有一条 ai_to_www_redirect(priority 309) 专门把 ai.optima.onl 301 到 www。
|
|
51
|
+
// 实测 ai.optima.onl/api/health → 301;www.optima.onl/api/health → 200 service=agentic-chat。
|
|
52
|
+
'agentic-chat': { path: '/api/health', stage: 'ai.stage.optima.onl', prod: 'www.optima.onl', 'cn-prod': 'app.yzsgo.com', 'cn-stage': 'app.stage.optima.chat' },
|
|
53
|
+
'commerce-backend': { path: '/health', stage: 'api.stage.optima.onl', prod: 'api.optima.onl', 'cn-prod': 'commerce.yzsgo.com', 'cn-prod_path': '/health/live', 'cn-stage': 'commerce.stage.optima.chat', 'cn-stage_path': '/health/live' },
|
|
54
|
+
// #83: mcp-host 已于 2025-12-18 下线,故不在表内 —— optima-terraform origin/main(f97adbb) 的
|
|
55
|
+
// stage-ecs/variables.tf:146 与 prod-ecs/variables.tf:508 都写着「MCP 工具服务已移除」,
|
|
56
|
+
// 两个 ecs stack 的 services map 里均无该条目;实测四个候选主机名全是壳(mcp.stage.optima.onl
|
|
57
|
+
// 307 → /en-US/health 前端 locale 路由,mcp-stage.optima.onl 与 prod 的 mcp.optima.onl 均
|
|
58
|
+
// 301 → www)。🔴 别再把它加回来:worstOk 是全局单标志(见 main()),留着它会让 `--all --env stage`
|
|
59
|
+
// 与 `--all --env prod` 恒 exit 1,而那个退出码正是接 CI 卡口时唯一被读的东西。
|
|
60
|
+
// 同一约束在 tests/service-matrix-alignment.test.js 里对 show-env 的清单也钉着。
|
|
61
|
+
'gateway-core': { path: '/health', 'cn-prod': 'gw.yzsgo.com', 'cn-stage': 'gw.stage.optima.chat' },
|
|
62
|
+
'optima-scout': { path: '/health', 'cn-prod': 'scout.yzsgo.com', 'cn-stage': 'scout.stage.optima.chat' },
|
|
63
|
+
'optima-skills': { path: '/health', 'cn-prod': 'skills.yzsgo.com', 'cn-stage': 'skills.stage.optima.chat' },
|
|
47
64
|
};
|
|
48
|
-
const ENVS: Env[] = ['stage', 'prod', 'cn', 'cn-stage'];
|
|
65
|
+
const ENVS: Env[] = ['stage', 'prod', 'cn-prod', 'cn-stage'];
|
|
49
66
|
|
|
50
67
|
const G = '\x1b[32m', R = '\x1b[31m', Y = '\x1b[33m', B = '\x1b[34m', N = '\x1b[0m';
|
|
51
68
|
const MARK: Record<string, string> = { ok: `${G}✅${N}`, fail: `${R}❌${N}`, warn: `${Y}⚠️ ${N}`, na: `${B}··${N}` };
|
|
@@ -165,27 +182,56 @@ function resolve(svc: string, e: Env): [string, string, string] | null {
|
|
|
165
182
|
return [`${svc} [${e}]`, host, path];
|
|
166
183
|
}
|
|
167
184
|
|
|
185
|
+
export interface Args { help: boolean; json: boolean; strict: boolean; all: boolean; env: Env | 'all'; expect?: string; url?: string; service?: string; }
|
|
186
|
+
|
|
187
|
+
// 🔴 不认识的输入一律报错、绝不静默丢弃(同 query-db.ts 的 parseQueryDbArgs、logs.ts 的 parseArgs):
|
|
188
|
+
// 此前 `--env=stage` 不认、`--env` 漏写取值、多敲一个位置参数都会被静默无视而回落默认 cn-prod ——
|
|
189
|
+
// 想探 stage,实际探的是阿里云生产,还 exit 0。
|
|
190
|
+
export function parseArgs(argv: string[]): Args {
|
|
191
|
+
const flags = new Set<string>(), vals: Record<string, string> = {}, positional: string[] = [];
|
|
192
|
+
for (let i = 0; i < argv.length; i++) {
|
|
193
|
+
const arg = argv[i];
|
|
194
|
+
const eq = arg.startsWith('--') ? arg.indexOf('=') : -1;
|
|
195
|
+
const name = eq > 0 ? arg.slice(0, eq) : arg;
|
|
196
|
+
if (['--env', '--expect-commit', '--url'].includes(name)) {
|
|
197
|
+
const v = eq > 0 ? arg.slice(eq + 1) : argv[++i];
|
|
198
|
+
if (!v || v.startsWith('-')) throw new Error(`${name} 缺少取值`);
|
|
199
|
+
vals[name] = v;
|
|
200
|
+
} else if (['--json', '--strict', '--all', '--help', '-h'].includes(arg)) flags.add(arg);
|
|
201
|
+
else if (arg.startsWith('-')) throw new Error(`未知参数:${arg}`);
|
|
202
|
+
else positional.push(arg);
|
|
203
|
+
}
|
|
204
|
+
// 🔴 必须先校验再转型:不校验的话任何不认识的 env 都会让 resolve() 把每个 SvcCfg 键查成
|
|
205
|
+
// undefined,于是落到下面的「无目标」分支,把「环境名打错」报成「该服务无部署」——
|
|
206
|
+
// 一个活着的服务被说成没上线,正是 #83 要根治的那类静默误报。
|
|
207
|
+
let envArg = vals['--env'] || 'cn-prod';
|
|
208
|
+
// 'cn' 是本文件的历史叫法,继续放行但不在 usage 里宣传(同 query-db.ts 的 ENV_ALIASES)。
|
|
209
|
+
if (envArg === 'cn') envArg = 'cn-prod';
|
|
210
|
+
if (envArg !== 'all' && !ENVS.includes(envArg as Env)) throw new Error(`未知环境:${envArg}(可选:${ENVS.join(' | ')} | all)`);
|
|
211
|
+
if (positional.length > 1) throw new Error(`多余参数:${positional.slice(1).join(' ')}(一次探一个服务;环境用 --env 指定)`);
|
|
212
|
+
const service = positional[0];
|
|
213
|
+
if (service && !Object.keys(SERVICES).includes(service)) throw new Error(`未知服务:${service}(可选:${Object.keys(SERVICES).join(' | ')})`);
|
|
214
|
+
const url = vals['--url'], all = flags.has('--all');
|
|
215
|
+
return { help: flags.has('--help') || flags.has('-h') || !(url || all || service), json: flags.has('--json'), strict: flags.has('--strict'), all, env: envArg as Env | 'all', expect: vals['--expect-commit'], url, service };
|
|
216
|
+
}
|
|
217
|
+
|
|
168
218
|
async function main() {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
const
|
|
172
|
-
const
|
|
173
|
-
const expect = val('--expect-commit');
|
|
174
|
-
const env = (val('--env') || 'cn') as Env | 'all';
|
|
175
|
-
const envs: Env[] = env === 'all' ? ENVS : [env as Env];
|
|
176
|
-
const positional = argv.filter((x, i) => !x.startsWith('--') && !(i > 0 && argv[i - 1].startsWith('--') && ['--env', '--expect-commit', '--url'].includes(argv[i - 1])));
|
|
219
|
+
let args: Args;
|
|
220
|
+
try { args = parseArgs(process.argv.slice(2)); } catch (e: any) { console.error(`❌ ${e.message}`); process.exit(2); }
|
|
221
|
+
const { json: asJson, strict, expect, env } = args;
|
|
222
|
+
const envs: Env[] = env === 'all' ? ENVS : [env];
|
|
177
223
|
|
|
178
224
|
let targets: [string, string, string][] = [];
|
|
179
|
-
if (
|
|
180
|
-
const u = new URL(val('--url')!);
|
|
181
|
-
targets = [[u.hostname, u.hostname, u.pathname || '/health']];
|
|
182
|
-
} else if (has('--all')) {
|
|
183
|
-
for (const e of envs) for (const s of Object.keys(SERVICES)) { const t = resolve(s, e); if (t) targets.push(t); }
|
|
184
|
-
} else if (positional[0] && SERVICES[positional[0]]) {
|
|
185
|
-
for (const e of envs) { const t = resolve(positional[0], e); if (t) targets.push(t); }
|
|
186
|
-
} else {
|
|
225
|
+
if (args.help) {
|
|
187
226
|
console.log((require('fs').readFileSync(__filename, 'utf-8').match(/\/\*\*[\s\S]*?\*\//)?.[0] || '').replace(/^\s*\*?/gm, ''));
|
|
188
227
|
process.exit(2);
|
|
228
|
+
} else if (args.url) {
|
|
229
|
+
const u = new URL(args.url);
|
|
230
|
+
targets = [[u.hostname, u.hostname, u.pathname || '/health']];
|
|
231
|
+
} else if (args.all) {
|
|
232
|
+
for (const e of envs) for (const s of Object.keys(SERVICES)) { const t = resolve(s, e); if (t) targets.push(t); }
|
|
233
|
+
} else if (args.service) {
|
|
234
|
+
for (const e of envs) { const t = resolve(args.service, e); if (t) targets.push(t); }
|
|
189
235
|
}
|
|
190
236
|
if (targets.length === 0) { console.log(`(无目标:${env} 环境下该服务无部署)`); process.exit(2); }
|
|
191
237
|
|
|
@@ -207,4 +253,6 @@ async function main() {
|
|
|
207
253
|
process.exitCode = worstOk ? 0 : 1;
|
|
208
254
|
}
|
|
209
255
|
|
|
210
|
-
main
|
|
256
|
+
if (require.main === module) {
|
|
257
|
+
main();
|
|
258
|
+
}
|
|
@@ -156,7 +156,15 @@ function getServiceToken(env, scope) {
|
|
|
156
156
|
const effectiveScope = scope ?? (isCn ? CN_PROD_TOKEN_SCOPE : undefined);
|
|
157
157
|
const scopeParam = effectiveScope ? `&scope=${encodeURIComponent(effectiveScope)}` : '';
|
|
158
158
|
const body = `grant_type=client_credentials&client_id=${encodeURIComponent(clientId)}&client_secret=${encodeURIComponent(clientSecret)}${scopeParam}`;
|
|
159
|
-
|
|
159
|
+
// execFileSync + 参数数组(不经 shell):Windows cmd.exe 不认单引号,shell 拼出的
|
|
160
|
+
// `-d '${body}'` 会被拆碎、curl 收到垃圾参数直接退出(#92)。数组传参绕开 shell、
|
|
161
|
+
// 跨平台一致;函数保持同步。
|
|
162
|
+
const response = (0, child_process_1.execFileSync)('curl', [
|
|
163
|
+
'-s', '-X', 'POST',
|
|
164
|
+
`${authUrl}/api/v1/oauth/token`,
|
|
165
|
+
'-H', 'Content-Type: application/x-www-form-urlencoded',
|
|
166
|
+
'-d', body,
|
|
167
|
+
], { encoding: 'utf-8' });
|
|
160
168
|
let parsed;
|
|
161
169
|
try {
|
|
162
170
|
parsed = JSON.parse(response);
|
|
@@ -117,8 +117,14 @@ function getInfisicalConfig() {
|
|
|
117
117
|
};
|
|
118
118
|
}
|
|
119
119
|
function getInfisicalToken(config) {
|
|
120
|
-
|
|
121
|
-
|
|
120
|
+
// 复用 curlJson(execFileSync + 参数数组、不经 shell):Windows cmd.exe 不认单引号,
|
|
121
|
+
// 原来 shell 拼的 `-d '{...}'` JSON body 会被拆碎、curl 收垃圾参数直接退出(#92)。
|
|
122
|
+
// cn 路径早已用 curlJson,这里对齐;函数保持同步(调用方无需改 async)。
|
|
123
|
+
return curlJson([
|
|
124
|
+
'-X', 'POST', `${config.url}/api/v1/auth/universal-auth/login`,
|
|
125
|
+
'-H', 'Content-Type: application/json',
|
|
126
|
+
'-d', JSON.stringify({ clientId: config.clientId, clientSecret: config.clientSecret }),
|
|
127
|
+
]).accessToken;
|
|
122
128
|
}
|
|
123
129
|
function getInfisicalSecrets(config, token, environment, secretPath) {
|
|
124
130
|
const response = (0, child_process_1.execSync)(`curl -s "${config.url}/api/v3/secrets/raw?workspaceId=${config.projectId}&environment=${environment}&secretPath=${secretPath}" -H "Authorization: Bearer ${token}"`, { encoding: 'utf-8' });
|
|
@@ -4,6 +4,7 @@ exports.runGrant = runGrant;
|
|
|
4
4
|
const billing_http_1 = require("../billing-http");
|
|
5
5
|
const confirm_prompt_1 = require("../confirm-prompt");
|
|
6
6
|
const grant_subscription_1 = require("../grant-subscription");
|
|
7
|
+
const operator_1 = require("../operator");
|
|
7
8
|
function parseArgs(argv) {
|
|
8
9
|
if (argv.length === 0 || argv[0] === '-h' || argv[0] === '--help') {
|
|
9
10
|
console.log(`Usage: optima-entitlement grant <email|phone|userId> --product-key <slug> --justification "..." [options]
|
|
@@ -15,6 +16,7 @@ Required:
|
|
|
15
16
|
|
|
16
17
|
Optional:
|
|
17
18
|
--yes Skip prod/cn-prod confirmation prompt (no-op on stage / cn-stage)
|
|
19
|
+
--operator <name> Operator self-report for billing audit (default: local username)
|
|
18
20
|
--env stage|prod|cn-prod|cn-stage (default: stage)
|
|
19
21
|
|
|
20
22
|
Hardcoded server-side: source=ADMIN_GRANT, priceCents=0, currency=USD, grantedBy=<clientId>.`);
|
|
@@ -40,6 +42,10 @@ Hardcoded server-side: source=ADMIN_GRANT, priceCents=0, currency=USD, grantedBy
|
|
|
40
42
|
case '--yes':
|
|
41
43
|
out.yes = true;
|
|
42
44
|
break;
|
|
45
|
+
case '--operator':
|
|
46
|
+
out.operator = next;
|
|
47
|
+
i++;
|
|
48
|
+
break;
|
|
43
49
|
case '--env':
|
|
44
50
|
out.env = next;
|
|
45
51
|
i++;
|
|
@@ -72,6 +78,7 @@ async function runGrant(argv) {
|
|
|
72
78
|
userId,
|
|
73
79
|
productKey: args.productKey,
|
|
74
80
|
justification: args.justification,
|
|
81
|
+
actorUserId: (0, operator_1.operatorActorId)(args.operator ?? null),
|
|
75
82
|
});
|
|
76
83
|
console.log(`✓ Granted entitlement (HTTP ${res.status}):`);
|
|
77
84
|
console.log(JSON.stringify(res.body, null, 2));
|
|
@@ -4,6 +4,7 @@ exports.runRevoke = runRevoke;
|
|
|
4
4
|
const billing_http_1 = require("../billing-http");
|
|
5
5
|
const confirm_prompt_1 = require("../confirm-prompt");
|
|
6
6
|
const grant_subscription_1 = require("../grant-subscription");
|
|
7
|
+
const operator_1 = require("../operator");
|
|
7
8
|
function parseArgs(argv) {
|
|
8
9
|
if (argv.length === 0 || argv[0] === '-h' || argv[0] === '--help') {
|
|
9
10
|
console.log(`Usage: optima-entitlement revoke <email|phone|userId> --product-key <slug> --reason "..." [options]
|
|
@@ -15,6 +16,7 @@ Required:
|
|
|
15
16
|
|
|
16
17
|
Optional:
|
|
17
18
|
--yes Skip prod/cn-prod confirmation prompt (no-op on stage / cn-stage)
|
|
19
|
+
--operator <name> Operator self-report for billing audit (default: local username)
|
|
18
20
|
--env stage|prod|cn-prod|cn-stage (default: stage)
|
|
19
21
|
|
|
20
22
|
Refuses non-ADMIN_GRANT sources (PAYMENT, PARTNER) with source-specific
|
|
@@ -41,6 +43,10 @@ error pointing to the right reversal flow.`);
|
|
|
41
43
|
case '--yes':
|
|
42
44
|
out.yes = true;
|
|
43
45
|
break;
|
|
46
|
+
case '--operator':
|
|
47
|
+
out.operator = next;
|
|
48
|
+
i++;
|
|
49
|
+
break;
|
|
44
50
|
case '--env':
|
|
45
51
|
out.env = next;
|
|
46
52
|
i++;
|
|
@@ -102,6 +108,7 @@ async function runRevoke(argv) {
|
|
|
102
108
|
entitlementId: target.id,
|
|
103
109
|
refundReason: args.reason,
|
|
104
110
|
refundAmountCents: 0,
|
|
111
|
+
actorUserId: (0, operator_1.operatorActorId)(args.operator ?? null),
|
|
105
112
|
});
|
|
106
113
|
console.log(`✓ Revoked entitlement (HTTP ${res.status}):`);
|
|
107
114
|
console.log(JSON.stringify(res.body, null, 2));
|
|
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
};
|
|
35
35
|
})();
|
|
36
36
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
|
+
exports.isAlreadyExistsError = isAlreadyExistsError;
|
|
37
38
|
const fs = __importStar(require("fs"));
|
|
38
39
|
const os = __importStar(require("os"));
|
|
39
40
|
const path = __importStar(require("path"));
|
|
@@ -88,6 +89,40 @@ async function httpRequest(url, options = {}) {
|
|
|
88
89
|
}
|
|
89
90
|
return response.json();
|
|
90
91
|
}
|
|
92
|
+
/**
|
|
93
|
+
* 判定「账号/商户已存在」——吃 httpRequest 抛出的 `HTTP <status>: <body>` message。
|
|
94
|
+
*
|
|
95
|
+
* 两条腿各自对应一个真实来源(user-auth / commerce-backend 是同一套代码跑在 AWS 与
|
|
96
|
+
* cn 两侧,响应形态一致;下列均为 2026-08-13 直查 origin/main 坐实):
|
|
97
|
+
*
|
|
98
|
+
* - **文案腿** `^HTTP 4xx` 且 `already (exists|registered)`
|
|
99
|
+
* · user-auth 注册已注册邮箱 → 400 "Email already registered"
|
|
100
|
+
* (user-auth `app/services/user.py`)。**这就是 #78 的病灶**:旧判定只认
|
|
101
|
+
* "already exists",措辞对不上 ⇒ cn 侧 `--email` 复用已有账号必挂。
|
|
102
|
+
* 实测 cn-prod 响应体:{"error":"Email already registered","status_code":400}
|
|
103
|
+
* · commerce-backend 重复建 merchant profile → 400 "Merchant profile already
|
|
104
|
+
* exists. Use PUT ..."(commerce-backend `src/api/merchants.py`)
|
|
105
|
+
* · 🔴 必须同时是 4xx:5xx 是服务端故障,body 里恰好带着这些词也不能吞 —— 否则
|
|
106
|
+
* `POST /api/merchants/me` 的 502/503 会被报成 ℹ already exists,一路打印到
|
|
107
|
+
* 「✅ Test token generated successfully!」+ Merchant ID: N/A、exit 0。
|
|
108
|
+
* - **状态码腿** `^HTTP 409`
|
|
109
|
+
* · 本工具只调 `POST /api/v1/auth/register/merchant` 与 `POST /api/merchants/me`
|
|
110
|
+
* 两个端点。**register/merchant 这条路径上唯一的 409** 是企业席位:
|
|
111
|
+
* account_type == ENTERPRISE_SEAT → 409 detail "email_bound_to_seat"。
|
|
112
|
+
* 🔴 **该文案既不含 exists 也不含 registered,所以这条腿不是历史遗留、
|
|
113
|
+
* 是席位场景的唯一命中路径,别删。**
|
|
114
|
+
* ⚠️ 「这条路径上唯一」的限定不是废话:user-auth 全仓有 20+ 处 409
|
|
115
|
+
* (referral / internal-sms / teams / admin),措辞各异且语义完全不同
|
|
116
|
+
* (如 `claim_in_progress` 是并发冲突)。给本工具接新端点时别想当然沿用本判定,
|
|
117
|
+
* 否则会把并发冲突静默吞成「已存在」。
|
|
118
|
+
*
|
|
119
|
+
* 锚 `^HTTP 409` 而不是 issue 建议的 `\b(409|400)\b`:后者会把 body 里夹带的数字当
|
|
120
|
+
* 状态码,把真故障(500 而 body 里提到 409)静默吞成「已存在」;而 400 本身太泛
|
|
121
|
+
* (参数校验失败也是 400),单凭它放行等于吞掉一切请求错误 —— 400 一律交给文案腿判。
|
|
122
|
+
*/
|
|
123
|
+
function isAlreadyExistsError(message) {
|
|
124
|
+
return /^HTTP 409\b/.test(message) || (/^HTTP 4\d\d\b/.test(message) && /already (exists|registered)/i.test(message));
|
|
125
|
+
}
|
|
91
126
|
async function registerMerchant(email, password, businessName, config, phone, address) {
|
|
92
127
|
console.log(`\n📝 Registering merchant: ${email}...`);
|
|
93
128
|
const payload = { email, password, business_name: businessName };
|
|
@@ -105,7 +140,7 @@ async function registerMerchant(email, password, businessName, config, phone, ad
|
|
|
105
140
|
return result;
|
|
106
141
|
}
|
|
107
142
|
catch (error) {
|
|
108
|
-
if (
|
|
143
|
+
if (isAlreadyExistsError(error.message)) {
|
|
109
144
|
console.log(`ℹ Merchant already exists, proceeding to login...`);
|
|
110
145
|
return { email, user_id: '', role: 'merchant', is_active: true, created_at: '', updated_at: '' };
|
|
111
146
|
}
|
|
@@ -153,7 +188,7 @@ async function setupMerchantProfile(token, businessName, config) {
|
|
|
153
188
|
return result;
|
|
154
189
|
}
|
|
155
190
|
catch (error) {
|
|
156
|
-
if (
|
|
191
|
+
if (isAlreadyExistsError(error.message)) {
|
|
157
192
|
console.log(`ℹ Merchant profile already exists`);
|
|
158
193
|
return { merchant_id: '', business_name: businessName, user_id: '' };
|
|
159
194
|
}
|
|
@@ -292,4 +327,6 @@ Example:
|
|
|
292
327
|
process.exit(1);
|
|
293
328
|
}
|
|
294
329
|
}
|
|
295
|
-
main
|
|
330
|
+
if (require.main === module) {
|
|
331
|
+
main();
|
|
332
|
+
}
|
|
@@ -11,6 +11,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
11
11
|
const path_1 = require("path");
|
|
12
12
|
const crypto_1 = require("crypto");
|
|
13
13
|
const billing_http_1 = require("./billing-http");
|
|
14
|
+
const operator_1 = require("./operator");
|
|
14
15
|
const grant_subscription_1 = require("./grant-subscription");
|
|
15
16
|
const CREDITS_PER_USD = 700;
|
|
16
17
|
function parseArgs(args) {
|
|
@@ -27,6 +28,7 @@ Options:
|
|
|
27
28
|
--credits <n> Credits to grant (integer >= 1). Primary unit.
|
|
28
29
|
--amount <usd> Alt: grant by USD ($1 = ${CREDITS_PER_USD} credits). Provide exactly one of --credits / --amount.
|
|
29
30
|
--description <text> Description for audit trail (optional)
|
|
31
|
+
--operator <name> Operator self-report for billing audit (default: local username)
|
|
30
32
|
--env <env> Environment: stage, prod, cn-prod, cn-stage (default: stage)
|
|
31
33
|
-h, --help Show this help
|
|
32
34
|
|
|
@@ -40,6 +42,7 @@ Examples:
|
|
|
40
42
|
let credits = null;
|
|
41
43
|
let amountUsd = null;
|
|
42
44
|
let description = null;
|
|
45
|
+
let operator = null;
|
|
43
46
|
let env = 'stage';
|
|
44
47
|
for (let i = 1; i < args.length; i++) {
|
|
45
48
|
if (args[i] === '--credits' && args[i + 1]) {
|
|
@@ -51,6 +54,9 @@ Examples:
|
|
|
51
54
|
else if (args[i] === '--description' && args[i + 1]) {
|
|
52
55
|
description = args[++i];
|
|
53
56
|
}
|
|
57
|
+
else if (args[i] === '--operator' && args[i + 1]) {
|
|
58
|
+
operator = args[++i];
|
|
59
|
+
}
|
|
54
60
|
else if (args[i] === '--env' && args[i + 1]) {
|
|
55
61
|
env = args[++i];
|
|
56
62
|
}
|
|
@@ -69,14 +75,14 @@ Examples:
|
|
|
69
75
|
process.exit(1);
|
|
70
76
|
}
|
|
71
77
|
(0, billing_http_1.validateEnvCnProd)(env);
|
|
72
|
-
return { identifier, credits, amountUsd, description, env };
|
|
78
|
+
return { identifier, credits, amountUsd, description, operator, env };
|
|
73
79
|
}
|
|
74
80
|
async function main() {
|
|
75
81
|
// Deprecation notice when invoked via the legacy alias `optima-grant-balance`.
|
|
76
82
|
if ((0, path_1.basename)(process.argv[1] || '').includes('grant-balance')) {
|
|
77
83
|
console.warn('⚠️ optima-grant-balance 已更名为 optima-grant-credits(P15 钱包退役后回归积分)。请改用 `optima-grant-credits --credits <n>`;本别名仍可用,后续弃用。\n');
|
|
78
84
|
}
|
|
79
|
-
const { identifier, credits, amountUsd, description, env } = parseArgs(process.argv.slice(2));
|
|
85
|
+
const { identifier, credits, amountUsd, description, operator, env } = parseArgs(process.argv.slice(2));
|
|
80
86
|
const creditsDisplay = credits ?? Math.round(amountUsd * CREDITS_PER_USD);
|
|
81
87
|
console.log(`\n🎁 Granting ${creditsDisplay} credits${amountUsd !== null ? ` ($${amountUsd.toFixed(2)})` : ''} to ${identifier} [${env.toUpperCase()}]\n`);
|
|
82
88
|
if (description)
|
|
@@ -93,6 +99,7 @@ async function main() {
|
|
|
93
99
|
userId,
|
|
94
100
|
...amountField,
|
|
95
101
|
description: description ?? undefined,
|
|
102
|
+
actorUserId: (0, operator_1.operatorActorId)(operator),
|
|
96
103
|
idempotencyKey: `dev-skills-grant:${(0, crypto_1.randomUUID)()}`,
|
|
97
104
|
});
|
|
98
105
|
console.log(`✓ Granted ${body.credits} credits (lot ${body.lotId})`);
|
|
@@ -10,6 +10,7 @@ exports.resolveTargetUser = resolveTargetUser;
|
|
|
10
10
|
// 业务体:supersede 旧授予 + 实得 credits + token quota,重试双发不双倍)。
|
|
11
11
|
const db_utils_1 = require("./db-utils");
|
|
12
12
|
const billing_http_1 = require("./billing-http");
|
|
13
|
+
const operator_1 = require("./operator");
|
|
13
14
|
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
14
15
|
/**
|
|
15
16
|
* Auto-detect what kind of identifier the operator passed. gateway#923: a
|
|
@@ -79,6 +80,7 @@ Options:
|
|
|
79
80
|
cn-prod/cn-stage additionally allow: free
|
|
80
81
|
(legacy *-cn ids are accepted and normalized to canonical)
|
|
81
82
|
--months <n> Duration in months (default: 1)
|
|
83
|
+
--operator <name> Operator self-report for billing audit (default: local username)
|
|
82
84
|
--env <env> Environment: stage, prod, cn-prod, cn-stage (default: stage)
|
|
83
85
|
-h, --help Show this help`);
|
|
84
86
|
process.exit(0);
|
|
@@ -86,6 +88,7 @@ Options:
|
|
|
86
88
|
const identifier = args[0];
|
|
87
89
|
let plan = null;
|
|
88
90
|
let months = 1;
|
|
91
|
+
let operator = null;
|
|
89
92
|
let env = 'stage';
|
|
90
93
|
for (let i = 1; i < args.length; i++) {
|
|
91
94
|
if (args[i] === '--plan' && args[i + 1]) {
|
|
@@ -94,6 +97,9 @@ Options:
|
|
|
94
97
|
else if (args[i] === '--months' && args[i + 1]) {
|
|
95
98
|
months = parseInt(args[++i], 10);
|
|
96
99
|
}
|
|
100
|
+
else if (args[i] === '--operator' && args[i + 1]) {
|
|
101
|
+
operator = args[++i];
|
|
102
|
+
}
|
|
97
103
|
else if (args[i] === '--env' && args[i + 1]) {
|
|
98
104
|
env = args[++i];
|
|
99
105
|
}
|
|
@@ -112,7 +118,7 @@ Options:
|
|
|
112
118
|
console.error('Months must be >= 1');
|
|
113
119
|
process.exit(1);
|
|
114
120
|
}
|
|
115
|
-
return { identifier, plan, months, env };
|
|
121
|
+
return { identifier, plan, months, operator, env };
|
|
116
122
|
}
|
|
117
123
|
/**
|
|
118
124
|
* Resolve a CLI identifier (`<email|phone|userId>`) to a userId + verified
|
|
@@ -167,12 +173,12 @@ async function resolveTargetUser(env, identifier) {
|
|
|
167
173
|
return { userId, kind, identity: { phone: null, email: identifier } };
|
|
168
174
|
}
|
|
169
175
|
async function main() {
|
|
170
|
-
const { identifier, plan, months, env } = parseArgs(process.argv.slice(2));
|
|
176
|
+
const { identifier, plan, months, operator, env } = parseArgs(process.argv.slice(2));
|
|
171
177
|
console.log(`\n🎁 Granting ${plan} subscription to ${identifier} (${classifyIdentifier(identifier)}) for ${months} month(s) [${env.toUpperCase()}]\n`);
|
|
172
178
|
// Shared resolver: classify → resolve → reverse-verify echo → phone-assert
|
|
173
179
|
// (cn-prod/cn-stage), or email-only via SSH tunnel (AWS). See resolveTargetUser.
|
|
174
180
|
const { userId, identity } = await resolveTargetUser(env, identifier);
|
|
175
|
-
const { body } = await (0, billing_http_1.callBilling)(env, 'POST', '/api/billing/admin/grant-subscription', { userId, planId: plan, months });
|
|
181
|
+
const { body } = await (0, billing_http_1.callBilling)(env, 'POST', '/api/billing/admin/grant-subscription', { userId, planId: plan, months, actorUserId: (0, operator_1.operatorActorId)(operator) });
|
|
176
182
|
console.log(`✓ Subscription ${body.subscriptionId} (${body.planId})`);
|
|
177
183
|
console.log(`✓ Credits: ${body.credits.toLocaleString()} (expires ${body.expiresAt})`);
|
|
178
184
|
console.log(`✓ Token limits: session ${body.sessionTokenLimit.toLocaleString()} / weekly ${body.weeklyTokenLimit.toLocaleString()}`);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.operatorActorId = operatorActorId;
|
|
4
|
+
const os_1 = require("os");
|
|
5
|
+
// #429(Owner 拍板 3):CLI 发放族自报操作者——informational,billing 信任 allowlist client
|
|
6
|
+
// 自述(actor 不参与授权);不带 --operator 时回退本机用户名。缺省链最终兜底在 billing
|
|
7
|
+
// 侧(channel=client_id)。格式:dev-skills:<name>。
|
|
8
|
+
function operatorActorId(operatorFlag) {
|
|
9
|
+
const name = (operatorFlag ?? '').trim() || (0, os_1.userInfo)().username;
|
|
10
|
+
return `dev-skills:${name}`;
|
|
11
|
+
}
|
|
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
};
|
|
35
35
|
})();
|
|
36
36
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
|
+
exports.parseArgs = parseArgs;
|
|
37
38
|
/**
|
|
38
39
|
* optima-verify-health —— 服务上线健康探针(L1 DNS / L2 TLS / L3-L5 /health)。
|
|
39
40
|
*
|
|
@@ -52,8 +53,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
52
53
|
*
|
|
53
54
|
* 用法:
|
|
54
55
|
* optima-verify-health user-auth # 默认 cn-prod
|
|
55
|
-
* optima-verify-health user-auth --env prod # 环境 stage|prod|cn|all
|
|
56
|
-
* optima-verify-health --all --env all # stage/prod/cn
|
|
56
|
+
* optima-verify-health user-auth --env prod # 环境 stage|prod|cn-prod|cn-stage|all
|
|
57
|
+
* optima-verify-health --all --env all # stage/prod/cn-prod/cn-stage 四环境矩阵
|
|
57
58
|
* optima-verify-health gateway-core --expect-commit a1b2c3d
|
|
58
59
|
* optima-verify-health --url https://auth.yzsgo.com/health
|
|
59
60
|
* optima-verify-health --all --json # 机器可读,接 CI
|
|
@@ -67,16 +68,28 @@ const https = __importStar(require("node:https"));
|
|
|
67
68
|
// cn-prod 真实 subdomain 抄自 optima-terraform alicloud/stacks/cn-prod-ingress-sae/main.tf。
|
|
68
69
|
// #201 (2026-06-12): yzsgo.com 全量迁移完成,旧 *-cn.optima.chat 路由已下线。
|
|
69
70
|
// cn-stage(阿里云预发)域名 *.stage.optima.chat,抄自 cn-stage-ingress-sae services map。
|
|
71
|
+
// #83 (2026-08-13): AWS stage 是 *.stage.optima.onl(点号),旧的连字符形式 *-stage.optima.onl
|
|
72
|
+
// 已不再路由到服务——三项实测 301 → www.optima.onl,探测恒红并误报成「疑似未部署」。
|
|
73
|
+
// 🔴 别只看 stage 列:agentic-chat 的 prod 也踩同一个坑(见下面那行的注释)。
|
|
70
74
|
const SERVICES = {
|
|
71
|
-
'user-auth': { path: '/health', stage: 'auth
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
'
|
|
76
|
-
'
|
|
77
|
-
|
|
75
|
+
'user-auth': { path: '/health', stage: 'auth.stage.optima.onl', prod: 'auth.optima.onl', 'cn-prod': 'auth.yzsgo.com', 'cn-stage': 'auth.stage.optima.chat' },
|
|
76
|
+
// agentic-chat 的 prod 入口是 www 不是 ai:prod-ecs/variables.tf 里它的 subdomain = "www",
|
|
77
|
+
// 且 main.tf 有一条 ai_to_www_redirect(priority 309) 专门把 ai.optima.onl 301 到 www。
|
|
78
|
+
// 实测 ai.optima.onl/api/health → 301;www.optima.onl/api/health → 200 service=agentic-chat。
|
|
79
|
+
'agentic-chat': { path: '/api/health', stage: 'ai.stage.optima.onl', prod: 'www.optima.onl', 'cn-prod': 'app.yzsgo.com', 'cn-stage': 'app.stage.optima.chat' },
|
|
80
|
+
'commerce-backend': { path: '/health', stage: 'api.stage.optima.onl', prod: 'api.optima.onl', 'cn-prod': 'commerce.yzsgo.com', 'cn-prod_path': '/health/live', 'cn-stage': 'commerce.stage.optima.chat', 'cn-stage_path': '/health/live' },
|
|
81
|
+
// #83: mcp-host 已于 2025-12-18 下线,故不在表内 —— optima-terraform origin/main(f97adbb) 的
|
|
82
|
+
// stage-ecs/variables.tf:146 与 prod-ecs/variables.tf:508 都写着「MCP 工具服务已移除」,
|
|
83
|
+
// 两个 ecs stack 的 services map 里均无该条目;实测四个候选主机名全是壳(mcp.stage.optima.onl
|
|
84
|
+
// 307 → /en-US/health 前端 locale 路由,mcp-stage.optima.onl 与 prod 的 mcp.optima.onl 均
|
|
85
|
+
// 301 → www)。🔴 别再把它加回来:worstOk 是全局单标志(见 main()),留着它会让 `--all --env stage`
|
|
86
|
+
// 与 `--all --env prod` 恒 exit 1,而那个退出码正是接 CI 卡口时唯一被读的东西。
|
|
87
|
+
// 同一约束在 tests/service-matrix-alignment.test.js 里对 show-env 的清单也钉着。
|
|
88
|
+
'gateway-core': { path: '/health', 'cn-prod': 'gw.yzsgo.com', 'cn-stage': 'gw.stage.optima.chat' },
|
|
89
|
+
'optima-scout': { path: '/health', 'cn-prod': 'scout.yzsgo.com', 'cn-stage': 'scout.stage.optima.chat' },
|
|
90
|
+
'optima-skills': { path: '/health', 'cn-prod': 'skills.yzsgo.com', 'cn-stage': 'skills.stage.optima.chat' },
|
|
78
91
|
};
|
|
79
|
-
const ENVS = ['stage', 'prod', 'cn', 'cn-stage'];
|
|
92
|
+
const ENVS = ['stage', 'prod', 'cn-prod', 'cn-stage'];
|
|
80
93
|
const G = '\x1b[32m', R = '\x1b[31m', Y = '\x1b[33m', B = '\x1b[34m', N = '\x1b[0m';
|
|
81
94
|
const MARK = { ok: `${G}✅${N}`, fail: `${R}❌${N}`, warn: `${Y}⚠️ ${N}`, na: `${B}··${N}` };
|
|
82
95
|
const verdict = (ls) => ls.some((l) => l.result === 'fail') ? 'fail' : ls.some((l) => l.result === 'warn') ? 'warn' : 'pass';
|
|
@@ -194,21 +207,66 @@ function resolve(svc, e) {
|
|
|
194
207
|
const path = cfg[`${e}_path`] || cfg.path;
|
|
195
208
|
return [`${svc} [${e}]`, host, path];
|
|
196
209
|
}
|
|
210
|
+
// 🔴 不认识的输入一律报错、绝不静默丢弃(同 query-db.ts 的 parseQueryDbArgs、logs.ts 的 parseArgs):
|
|
211
|
+
// 此前 `--env=stage` 不认、`--env` 漏写取值、多敲一个位置参数都会被静默无视而回落默认 cn-prod ——
|
|
212
|
+
// 想探 stage,实际探的是阿里云生产,还 exit 0。
|
|
213
|
+
function parseArgs(argv) {
|
|
214
|
+
const flags = new Set(), vals = {}, positional = [];
|
|
215
|
+
for (let i = 0; i < argv.length; i++) {
|
|
216
|
+
const arg = argv[i];
|
|
217
|
+
const eq = arg.startsWith('--') ? arg.indexOf('=') : -1;
|
|
218
|
+
const name = eq > 0 ? arg.slice(0, eq) : arg;
|
|
219
|
+
if (['--env', '--expect-commit', '--url'].includes(name)) {
|
|
220
|
+
const v = eq > 0 ? arg.slice(eq + 1) : argv[++i];
|
|
221
|
+
if (!v || v.startsWith('-'))
|
|
222
|
+
throw new Error(`${name} 缺少取值`);
|
|
223
|
+
vals[name] = v;
|
|
224
|
+
}
|
|
225
|
+
else if (['--json', '--strict', '--all', '--help', '-h'].includes(arg))
|
|
226
|
+
flags.add(arg);
|
|
227
|
+
else if (arg.startsWith('-'))
|
|
228
|
+
throw new Error(`未知参数:${arg}`);
|
|
229
|
+
else
|
|
230
|
+
positional.push(arg);
|
|
231
|
+
}
|
|
232
|
+
// 🔴 必须先校验再转型:不校验的话任何不认识的 env 都会让 resolve() 把每个 SvcCfg 键查成
|
|
233
|
+
// undefined,于是落到下面的「无目标」分支,把「环境名打错」报成「该服务无部署」——
|
|
234
|
+
// 一个活着的服务被说成没上线,正是 #83 要根治的那类静默误报。
|
|
235
|
+
let envArg = vals['--env'] || 'cn-prod';
|
|
236
|
+
// 'cn' 是本文件的历史叫法,继续放行但不在 usage 里宣传(同 query-db.ts 的 ENV_ALIASES)。
|
|
237
|
+
if (envArg === 'cn')
|
|
238
|
+
envArg = 'cn-prod';
|
|
239
|
+
if (envArg !== 'all' && !ENVS.includes(envArg))
|
|
240
|
+
throw new Error(`未知环境:${envArg}(可选:${ENVS.join(' | ')} | all)`);
|
|
241
|
+
if (positional.length > 1)
|
|
242
|
+
throw new Error(`多余参数:${positional.slice(1).join(' ')}(一次探一个服务;环境用 --env 指定)`);
|
|
243
|
+
const service = positional[0];
|
|
244
|
+
if (service && !Object.keys(SERVICES).includes(service))
|
|
245
|
+
throw new Error(`未知服务:${service}(可选:${Object.keys(SERVICES).join(' | ')})`);
|
|
246
|
+
const url = vals['--url'], all = flags.has('--all');
|
|
247
|
+
return { help: flags.has('--help') || flags.has('-h') || !(url || all || service), json: flags.has('--json'), strict: flags.has('--strict'), all, env: envArg, expect: vals['--expect-commit'], url, service };
|
|
248
|
+
}
|
|
197
249
|
async function main() {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
250
|
+
let args;
|
|
251
|
+
try {
|
|
252
|
+
args = parseArgs(process.argv.slice(2));
|
|
253
|
+
}
|
|
254
|
+
catch (e) {
|
|
255
|
+
console.error(`❌ ${e.message}`);
|
|
256
|
+
process.exit(2);
|
|
257
|
+
}
|
|
258
|
+
const { json: asJson, strict, expect, env } = args;
|
|
204
259
|
const envs = env === 'all' ? ENVS : [env];
|
|
205
|
-
const positional = argv.filter((x, i) => !x.startsWith('--') && !(i > 0 && argv[i - 1].startsWith('--') && ['--env', '--expect-commit', '--url'].includes(argv[i - 1])));
|
|
206
260
|
let targets = [];
|
|
207
|
-
if (
|
|
208
|
-
|
|
261
|
+
if (args.help) {
|
|
262
|
+
console.log((require('fs').readFileSync(__filename, 'utf-8').match(/\/\*\*[\s\S]*?\*\//)?.[0] || '').replace(/^\s*\*?/gm, ''));
|
|
263
|
+
process.exit(2);
|
|
264
|
+
}
|
|
265
|
+
else if (args.url) {
|
|
266
|
+
const u = new URL(args.url);
|
|
209
267
|
targets = [[u.hostname, u.hostname, u.pathname || '/health']];
|
|
210
268
|
}
|
|
211
|
-
else if (
|
|
269
|
+
else if (args.all) {
|
|
212
270
|
for (const e of envs)
|
|
213
271
|
for (const s of Object.keys(SERVICES)) {
|
|
214
272
|
const t = resolve(s, e);
|
|
@@ -216,17 +274,13 @@ async function main() {
|
|
|
216
274
|
targets.push(t);
|
|
217
275
|
}
|
|
218
276
|
}
|
|
219
|
-
else if (
|
|
277
|
+
else if (args.service) {
|
|
220
278
|
for (const e of envs) {
|
|
221
|
-
const t = resolve(
|
|
279
|
+
const t = resolve(args.service, e);
|
|
222
280
|
if (t)
|
|
223
281
|
targets.push(t);
|
|
224
282
|
}
|
|
225
283
|
}
|
|
226
|
-
else {
|
|
227
|
-
console.log((require('fs').readFileSync(__filename, 'utf-8').match(/\/\*\*[\s\S]*?\*\//)?.[0] || '').replace(/^\s*\*?/gm, ''));
|
|
228
|
-
process.exit(2);
|
|
229
|
-
}
|
|
230
284
|
if (targets.length === 0) {
|
|
231
285
|
console.log(`(无目标:${env} 环境下该服务无部署)`);
|
|
232
286
|
process.exit(2);
|
|
@@ -252,4 +306,6 @@ async function main() {
|
|
|
252
306
|
// 用 exitCode 而非 process.exit():后者会在管道(--json | jq)未 flush 完就退出而截断输出
|
|
253
307
|
process.exitCode = worstOk ? 0 : 1;
|
|
254
308
|
}
|
|
255
|
-
main
|
|
309
|
+
if (require.main === module) {
|
|
310
|
+
main();
|
|
311
|
+
}
|