@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.
Files changed (40) hide show
  1. package/.claude/commands/logs.md +1 -1
  2. package/.claude/commands/query-db.md +6 -1
  3. package/.claude/skills/discount-codes/SKILL.md +4 -4
  4. package/.claude/skills/generate-test-token/SKILL.md +8 -4
  5. package/.claude/skills/logs/SKILL.md +5 -3
  6. package/.claude/skills/show-env/SKILL.md +1 -1
  7. package/.claude/skills/yzsgo-e2e/SKILL.md +19 -0
  8. package/.claude/skills/yzsgo-e2e/SYNC.md +26 -2
  9. package/.claude/skills/yzsgo-e2e/chat_driver.py +221 -47
  10. package/.claude/skills/yzsgo-e2e/pull_wire.py +39 -8
  11. package/.claude/skills/yzsgo-e2e/run_e2e.py +17 -2
  12. package/.codex/skills/generate-test-token/SKILL.md +3 -2
  13. package/.codex/skills/logs/SKILL.md +4 -2
  14. package/.codex/skills/show-env/SKILL.md +4 -2
  15. package/.codex/skills/yzsgo-e2e/SKILL.md +19 -0
  16. package/.codex/skills/yzsgo-e2e/SYNC.md +26 -2
  17. package/.codex/skills/yzsgo-e2e/chat_driver.py +221 -47
  18. package/.codex/skills/yzsgo-e2e/pull_wire.py +39 -8
  19. package/.codex/skills/yzsgo-e2e/run_e2e.py +17 -2
  20. package/AGENTS.md +1 -1
  21. package/bin/cli.js +3 -3
  22. package/bin/helpers/billing-http.ts +12 -3
  23. package/bin/helpers/db-utils.ts +8 -5
  24. package/bin/helpers/entitlement/grant.ts +5 -0
  25. package/bin/helpers/entitlement/revoke.ts +5 -0
  26. package/bin/helpers/generate-test-token.ts +40 -3
  27. package/bin/helpers/grant-credits.ts +8 -2
  28. package/bin/helpers/grant-subscription.ts +8 -4
  29. package/bin/helpers/operator.ts +9 -0
  30. package/bin/helpers/verify-health.ts +77 -29
  31. package/dist/bin/helpers/billing-http.js +9 -1
  32. package/dist/bin/helpers/db-utils.js +8 -2
  33. package/dist/bin/helpers/entitlement/grant.js +7 -0
  34. package/dist/bin/helpers/entitlement/revoke.js +7 -0
  35. package/dist/bin/helpers/generate-test-token.js +40 -3
  36. package/dist/bin/helpers/grant-credits.js +9 -2
  37. package/dist/bin/helpers/grant-subscription.js +9 -3
  38. package/dist/bin/helpers/operator.js +11 -0
  39. package/dist/bin/helpers/verify-health.js +83 -27
  40. 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
- type Env = 'stage' | 'prod' | 'cn' | 'cn-stage';
33
- interface SvcCfg { path: string; stage?: string; prod?: string; cn?: string; cn_path?: string; 'cn-stage'?: string; 'cn-stage_path'?: string; }
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-stage.optima.onl', prod: 'auth.optima.onl', cn: 'auth.yzsgo.com', 'cn-stage': 'auth.stage.optima.chat' },
41
- 'agentic-chat': { path: '/api/health', stage: 'ai-stage.optima.onl', prod: 'ai.optima.onl', cn: 'app.yzsgo.com', 'cn-stage': 'app.stage.optima.chat' },
42
- 'commerce-backend': { path: '/health', stage: 'api-stage.optima.onl', prod: 'api.optima.onl', cn: 'commerce.yzsgo.com', cn_path: '/health/live', 'cn-stage': 'commerce.stage.optima.chat', 'cn-stage_path': '/health/live' },
43
- 'mcp-host': { path: '/health', stage: 'mcp-stage.optima.onl', prod: 'mcp.optima.onl' },
44
- 'gateway-core': { path: '/health', cn: 'gw.yzsgo.com', 'cn-stage': 'gw.stage.optima.chat' },
45
- 'optima-scout': { path: '/health', cn: 'scout.yzsgo.com', 'cn-stage': 'scout.stage.optima.chat' },
46
- 'optima-skills': { path: '/health', cn: 'skills.yzsgo.com', 'cn-stage': 'skills.stage.optima.chat' },
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
- const argv = process.argv.slice(2);
170
- const has = (f: string) => argv.includes(f);
171
- const val = (f: string) => { const i = argv.indexOf(f); return i >= 0 ? argv[i + 1] : undefined; };
172
- const asJson = has('--json'), strict = has('--strict');
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 (has('--url')) {
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
- const response = (0, child_process_1.execSync)(`curl -s -X POST '${authUrl}/api/v1/oauth/token' -H 'Content-Type: application/x-www-form-urlencoded' -d '${body}'`, { encoding: 'utf-8' });
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
- const response = (0, child_process_1.execSync)(`curl -s -X POST "${config.url}/api/v1/auth/universal-auth/login" -H "Content-Type: application/json" -d '{"clientId": "${config.clientId}", "clientSecret": "${config.clientSecret}"}'`, { encoding: 'utf-8' });
121
- return JSON.parse(response).accessToken;
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 (error.message.includes('409') || error.message.includes('already exists')) {
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 (error.message.includes('409') || error.message.includes('already exists')) {
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-stage.optima.onl', prod: 'auth.optima.onl', cn: 'auth.yzsgo.com', 'cn-stage': 'auth.stage.optima.chat' },
72
- 'agentic-chat': { path: '/api/health', stage: 'ai-stage.optima.onl', prod: 'ai.optima.onl', cn: 'app.yzsgo.com', 'cn-stage': 'app.stage.optima.chat' },
73
- 'commerce-backend': { path: '/health', stage: 'api-stage.optima.onl', prod: 'api.optima.onl', cn: 'commerce.yzsgo.com', cn_path: '/health/live', 'cn-stage': 'commerce.stage.optima.chat', 'cn-stage_path': '/health/live' },
74
- 'mcp-host': { path: '/health', stage: 'mcp-stage.optima.onl', prod: 'mcp.optima.onl' },
75
- 'gateway-core': { path: '/health', cn: 'gw.yzsgo.com', 'cn-stage': 'gw.stage.optima.chat' },
76
- 'optima-scout': { path: '/health', cn: 'scout.yzsgo.com', 'cn-stage': 'scout.stage.optima.chat' },
77
- 'optima-skills': { path: '/health', cn: 'skills.yzsgo.com', 'cn-stage': 'skills.stage.optima.chat' },
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
- const argv = process.argv.slice(2);
199
- const has = (f) => argv.includes(f);
200
- const val = (f) => { const i = argv.indexOf(f); return i >= 0 ? argv[i + 1] : undefined; };
201
- const asJson = has('--json'), strict = has('--strict');
202
- const expect = val('--expect-commit');
203
- const env = (val('--env') || 'cn');
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 (has('--url')) {
208
- const u = new URL(val('--url'));
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 (has('--all')) {
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 (positional[0] && SERVICES[positional[0]]) {
277
+ else if (args.service) {
220
278
  for (const e of envs) {
221
- const t = resolve(positional[0], e);
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@optima-chat/dev-skills",
3
- "version": "0.16.6",
3
+ "version": "0.16.8",
4
4
  "description": "Claude Code Skills for Optima development team - cross-environment collaboration tools",
5
5
  "main": "index.js",
6
6
  "bin": {