@optima-chat/dev-skills 0.7.40 → 0.7.42
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/skills/grant-balance/SKILL.md +1 -1
- package/.codex/skills/grant-balance/SKILL.md +1 -1
- package/bin/cli.js +1 -1
- package/bin/helpers/billing-http.ts +76 -2
- package/bin/helpers/generate-test-token.ts +17 -8
- package/bin/helpers/grant-subscription.ts +111 -16
- package/bin/helpers/verify-health.ts +9 -8
- package/dist/bin/helpers/billing-http.js +75 -2
- package/dist/bin/helpers/discount.js +0 -0
- package/dist/bin/helpers/entitlement.js +0 -0
- package/dist/bin/helpers/generate-test-token.js +15 -6
- package/dist/bin/helpers/grant-balance.js +0 -0
- package/dist/bin/helpers/grant-subscription.js +98 -14
- package/dist/bin/helpers/plugin.js +0 -0
- package/dist/bin/helpers/product.js +0 -0
- package/dist/bin/helpers/query-db.js +0 -0
- package/dist/bin/helpers/show-env.js +0 -0
- package/dist/bin/helpers/verify-health.js +9 -8
- package/package.json +3 -2
|
@@ -52,7 +52,7 @@ optima-grant-balance user@example.com --amount 20 --description "服务中断补
|
|
|
52
52
|
| `--description <text>` | 描述/原因(仅 console 输出) | - |
|
|
53
53
|
| `--env <env>` | 环境:stage, prod, cn-prod | stage |
|
|
54
54
|
|
|
55
|
-
> **cn-prod(国内环境)**:全程 HTTPS(auth
|
|
55
|
+
> **cn-prod(国内环境)**:全程 HTTPS(auth.yzsgo.com / billing-api.yzsgo.com),email 查找走 user-auth internal lookup API(无 SSH 隧道)。金额输入仍是 USD($1 = 700 积分,与 CN ¥1 = 100 积分同一账本单位)。例:`optima-grant-balance user@example.com --amount 1 --env cn-prod`
|
|
56
56
|
|
|
57
57
|
## 与 grant-subscription 的区别
|
|
58
58
|
|
|
@@ -51,7 +51,7 @@ optima-grant-balance user@example.com --amount 20 --description "服务中断补
|
|
|
51
51
|
| `--description <text>` | 描述/原因(仅 console 输出) | - |
|
|
52
52
|
| `--env <env>` | 环境:stage, prod, cn-prod | stage |
|
|
53
53
|
|
|
54
|
-
> **cn-prod**:走 HTTPS(auth
|
|
54
|
+
> **cn-prod**:走 HTTPS(auth.yzsgo.com / billing-api.yzsgo.com),email 查找经 user-auth internal lookup API(无 SSH 隧道)。金额输入仍是 USD($1 = 700 积分 = ¥7 档积分口径一致)。
|
|
55
55
|
|
|
56
56
|
## 与 grant-subscription 的区别
|
|
57
57
|
|
package/bin/cli.js
CHANGED
|
@@ -35,7 +35,7 @@ switch (command) {
|
|
|
35
35
|
log(' optima-verify-health <service> [--env cn|prod|all] Probe L1-L5 上线健康', 'cyan');
|
|
36
36
|
log(' optima-generate-test-token [--env production] Generate test token', 'cyan');
|
|
37
37
|
log(' optima-grant-balance <email> --amount <usd> [--env] Grant USD wallet balance', 'cyan');
|
|
38
|
-
log(' optima-grant-subscription <email> --plan <p> [--env] Grant subscription', 'cyan');
|
|
38
|
+
log(' optima-grant-subscription <email|phone|userId> --plan <p> [--env] Grant subscription', 'cyan');
|
|
39
39
|
log(' /logs <service> [lines] [env] View service logs (skill)', 'cyan');
|
|
40
40
|
log(' /restart-ecs <service> [env] Restart ECS service (skill)', 'cyan');
|
|
41
41
|
|
|
@@ -5,13 +5,15 @@ import { getInfisicalConfig, getInfisicalToken } from './db-utils';
|
|
|
5
5
|
const USER_AUTH_URLS: Record<string, string> = {
|
|
6
6
|
stage: 'https://auth.stage.optima.onl',
|
|
7
7
|
prod: 'https://auth.optima.onl',
|
|
8
|
-
|
|
8
|
+
// #201: yzsgo.com 全量迁移 (2026-06-12), 旧 auth-cn.optima.chat 路由已下线
|
|
9
|
+
'cn-prod': 'https://auth.yzsgo.com',
|
|
9
10
|
};
|
|
10
11
|
|
|
11
12
|
// cn-prod URLs are hardcoded: cn Infisical (secrets-cn.optima.chat) is a
|
|
12
13
|
// separate instance dev-skills has no machine identity for, and these domains
|
|
13
14
|
// are stable. AWS envs keep reading /shared-secrets/domain-urls.
|
|
14
|
-
|
|
15
|
+
// #201: yzsgo.com 全量迁移 (2026-06-12), 旧 billing-cn.optima.chat 路由已下线
|
|
16
|
+
const CN_PROD_BILLING_URL = 'https://billing-api.yzsgo.com';
|
|
15
17
|
|
|
16
18
|
/**
|
|
17
19
|
* Validate the --env flag value at command entry, before any I/O.
|
|
@@ -269,3 +271,75 @@ export async function resolveUserIdByEmail(env: string, email: string): Promise<
|
|
|
269
271
|
console.log(`✓ Found user: ${parsed.user_id}`);
|
|
270
272
|
return parsed.user_id;
|
|
271
273
|
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Resolve a user's id by phone via user-auth's internal lookup endpoint
|
|
277
|
+
* (POST /api/v1/internal/users/lookup with {phone}). Mirrors
|
|
278
|
+
* resolveUserIdByEmail — pure-phone CN users have no email, so this is the
|
|
279
|
+
* only id path for them (gateway#923 root cause: email-only lookup couldn't
|
|
280
|
+
* resolve them and a wrong userId got hand-fed instead).
|
|
281
|
+
*/
|
|
282
|
+
export async function resolveUserIdByPhone(env: string, phone: string): Promise<string> {
|
|
283
|
+
console.log(`Looking up user by phone: ${phone}`);
|
|
284
|
+
const token = getServiceToken(env);
|
|
285
|
+
const authUrl = USER_AUTH_URLS[env];
|
|
286
|
+
if (!authUrl) throw new Error(`Unknown env: ${env}`);
|
|
287
|
+
|
|
288
|
+
const res = await fetch(`${authUrl}/api/v1/internal/users/lookup`, {
|
|
289
|
+
method: 'POST',
|
|
290
|
+
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
|
|
291
|
+
body: JSON.stringify({ phone }),
|
|
292
|
+
});
|
|
293
|
+
const text = await res.text();
|
|
294
|
+
if (res.status === 404) {
|
|
295
|
+
throw new Error(`User not found by phone (${env}): ${phone}`);
|
|
296
|
+
}
|
|
297
|
+
if (!res.ok) {
|
|
298
|
+
throw new Error(formatServiceError(res.status, res.statusText, text));
|
|
299
|
+
}
|
|
300
|
+
let parsed: { user_id?: string };
|
|
301
|
+
try {
|
|
302
|
+
parsed = JSON.parse(text);
|
|
303
|
+
} catch {
|
|
304
|
+
throw new Error(`user-auth lookup returned non-JSON 2xx body: ${text.slice(0, 200)}`);
|
|
305
|
+
}
|
|
306
|
+
if (!parsed.user_id) {
|
|
307
|
+
throw new Error(`user-auth lookup response missing user_id: ${text.slice(0, 200)}`);
|
|
308
|
+
}
|
|
309
|
+
console.log(`✓ Found user: ${parsed.user_id}`);
|
|
310
|
+
return parsed.user_id;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Fetch a user's identity by id via user-auth's internal endpoint
|
|
315
|
+
* (GET /api/v1/internal/users/{userId}). Used to reverse-verify the target
|
|
316
|
+
* account before granting — prints phone/email/current_plan so the operator
|
|
317
|
+
* can confirm they're hitting the right account (gateway#923).
|
|
318
|
+
*/
|
|
319
|
+
export async function getUserById(
|
|
320
|
+
env: string,
|
|
321
|
+
userId: string,
|
|
322
|
+
): Promise<{ user_id: string; phone: string | null; email: string | null; current_plan?: string }> {
|
|
323
|
+
const token = getServiceToken(env);
|
|
324
|
+
const authUrl = USER_AUTH_URLS[env];
|
|
325
|
+
if (!authUrl) throw new Error(`Unknown env: ${env}`);
|
|
326
|
+
|
|
327
|
+
const res = await fetch(`${authUrl}/api/v1/internal/users/${encodeURIComponent(userId)}`, {
|
|
328
|
+
method: 'GET',
|
|
329
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
330
|
+
});
|
|
331
|
+
const text = await res.text();
|
|
332
|
+
if (res.status === 404) {
|
|
333
|
+
throw new Error(`User not found by id (${env}): ${userId}`);
|
|
334
|
+
}
|
|
335
|
+
if (!res.ok) {
|
|
336
|
+
throw new Error(formatServiceError(res.status, res.statusText, text));
|
|
337
|
+
}
|
|
338
|
+
let parsed: { user_id: string; phone: string | null; email: string | null; current_plan?: string };
|
|
339
|
+
try {
|
|
340
|
+
parsed = JSON.parse(text);
|
|
341
|
+
} catch {
|
|
342
|
+
throw new Error(`user-auth user lookup returned non-JSON 2xx body: ${text.slice(0, 200)}`);
|
|
343
|
+
}
|
|
344
|
+
return parsed;
|
|
345
|
+
}
|
|
@@ -24,7 +24,7 @@ interface MerchantSetupResponse {
|
|
|
24
24
|
user_id: string;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
type Environment = 'ci' | 'stage' | 'prod';
|
|
27
|
+
type Environment = 'ci' | 'stage' | 'prod' | 'cn-prod';
|
|
28
28
|
|
|
29
29
|
interface EnvironmentConfig {
|
|
30
30
|
authUrl: string;
|
|
@@ -51,6 +51,13 @@ const ENV_CONFIG: Record<Environment, EnvironmentConfig> = {
|
|
|
51
51
|
apiUrl: 'https://api.optima.onl',
|
|
52
52
|
clientId: 'commerce-cli-ecs-pro-i2r5of1h',
|
|
53
53
|
envName: 'prod'
|
|
54
|
+
},
|
|
55
|
+
'cn-prod': {
|
|
56
|
+
// #201: yzsgo.com 全量迁移 (2026-06-12), 旧 *-cn.optima.chat 路由已下线
|
|
57
|
+
authUrl: 'https://auth.yzsgo.com',
|
|
58
|
+
apiUrl: 'https://commerce.yzsgo.com',
|
|
59
|
+
clientId: 'commerce-cli-cn-prod',
|
|
60
|
+
envName: 'cn-prod'
|
|
54
61
|
}
|
|
55
62
|
};
|
|
56
63
|
|
|
@@ -190,10 +197,10 @@ async function main() {
|
|
|
190
197
|
address = args[++i];
|
|
191
198
|
} else if (arg === '--env' && args[i + 1]) {
|
|
192
199
|
const envArg = args[++i];
|
|
193
|
-
if (envArg === 'ci' || envArg === 'stage' || envArg === 'prod') {
|
|
194
|
-
environment = envArg;
|
|
200
|
+
if (envArg === 'ci' || envArg === 'stage' || envArg === 'prod' || envArg === 'cn-prod') {
|
|
201
|
+
environment = envArg as Environment;
|
|
195
202
|
} else {
|
|
196
|
-
console.error(`❌ Invalid environment: ${envArg}. Must be 'ci', 'stage', or 'prod'.`);
|
|
203
|
+
console.error(`❌ Invalid environment: ${envArg}. Must be 'ci', 'stage', 'prod', or 'cn-prod'.`);
|
|
197
204
|
process.exit(1);
|
|
198
205
|
}
|
|
199
206
|
} else if (arg === '--help' || arg === '-h') {
|
|
@@ -206,18 +213,20 @@ Options:
|
|
|
206
213
|
--business-name <name> Merchant business name (default: auto-generated)
|
|
207
214
|
--phone <phone> Merchant phone number (optional)
|
|
208
215
|
--address <address> Merchant address (optional)
|
|
209
|
-
--env <environment> Environment: ci (default), stage, or prod
|
|
216
|
+
--env <environment> Environment: ci (default), stage, prod, or cn-prod
|
|
210
217
|
--help, -h Show this help message
|
|
211
218
|
|
|
212
219
|
Environments:
|
|
213
|
-
ci
|
|
214
|
-
stage
|
|
215
|
-
prod
|
|
220
|
+
ci CI 环境 (auth.optima.chat, api.optima.chat)
|
|
221
|
+
stage Stage 环境 (auth.stage.optima.onl, api.stage.optima.onl)
|
|
222
|
+
prod Prod 环境 (auth.optima.onl, api.optima.onl)
|
|
223
|
+
cn-prod 阿里云生产环境 (auth.yzsgo.com, commerce.yzsgo.com) [#201]
|
|
216
224
|
|
|
217
225
|
Example:
|
|
218
226
|
optima-generate-test-token
|
|
219
227
|
optima-generate-test-token --env stage
|
|
220
228
|
optima-generate-test-token --env prod
|
|
229
|
+
optima-generate-test-token --env cn-prod
|
|
221
230
|
optima-generate-test-token --business-name "My Test Shop" --env stage
|
|
222
231
|
`);
|
|
223
232
|
process.exit(0);
|
|
@@ -4,7 +4,65 @@
|
|
|
4
4
|
// 作废,改调 billing 服务态端点(与用户态 /api/admin/grant-subscription 同
|
|
5
5
|
// 业务体:supersede 旧授予 + 实得 credits + token quota,重试双发不双倍)。
|
|
6
6
|
import { getInfisicalConfig, getInfisicalToken, resolveUserId } from './db-utils';
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
callBilling,
|
|
9
|
+
resolveUserIdByEmail,
|
|
10
|
+
resolveUserIdByPhone,
|
|
11
|
+
getUserById,
|
|
12
|
+
validateEnvCnProd,
|
|
13
|
+
} from './billing-http';
|
|
14
|
+
|
|
15
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Auto-detect what kind of identifier the operator passed. gateway#923: a
|
|
19
|
+
* pure-phone CN user (no email) couldn't be resolved by the email-only CLI,
|
|
20
|
+
* so a wrong userId was hand-fed. Accepting phone/userId directly + this
|
|
21
|
+
* classifier removes the manual-userId footgun.
|
|
22
|
+
*/
|
|
23
|
+
export function classifyIdentifier(s: string): 'email' | 'phone' | 'userId' {
|
|
24
|
+
if (s.includes('@')) return 'email';
|
|
25
|
+
if (UUID_RE.test(s)) return 'userId';
|
|
26
|
+
const digits = s.replace(/\D/g, '');
|
|
27
|
+
if (/^\d{6,}$/.test(digits)) return 'phone';
|
|
28
|
+
throw new Error('无法识别 identifier(需 email / 手机号 / userId)');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Hard guard: when the operator gave a phone number, the resolved account's
|
|
33
|
+
* phone MUST match it. Normalizes both sides (strip non-digits) before
|
|
34
|
+
* comparing. A mismatch (or an account with no phone) throws to abort the
|
|
35
|
+
* grant — this is the assertion that would have stopped gateway#923.
|
|
36
|
+
*/
|
|
37
|
+
export function assertPhoneMatch(inputPhone: string, accountPhone: string | null): void {
|
|
38
|
+
const input = inputPhone.replace(/\D/g, '');
|
|
39
|
+
const account = (accountPhone ?? '').replace(/\D/g, '');
|
|
40
|
+
if (!account || input !== account) {
|
|
41
|
+
throw new Error(
|
|
42
|
+
'手机号不匹配:请求 ' +
|
|
43
|
+
input +
|
|
44
|
+
',但该 userId 的手机号是 ' +
|
|
45
|
+
(accountPhone || '(无)') +
|
|
46
|
+
'——拒绝授予以防发错账号',
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* AWS (stage/prod) only supports email identifiers. The HTTP reverse-verify
|
|
53
|
+
* (getUserById) and phone/userId resolution rely on user-auth's internal
|
|
54
|
+
* endpoints, which require the token scope `internal:users:write` — only the
|
|
55
|
+
* cn-prod dev-skills client carries it (see billing-http getServiceToken).
|
|
56
|
+
* AWS resolves users via the RDS SSH tunnel (resolveUserId) and never hits
|
|
57
|
+
* those endpoints, so phone/userId here would 403. gateway#923 (wrong-account
|
|
58
|
+
* grant) is a cn-prod-only, pure-phone-user problem; AWS users have unambiguous
|
|
59
|
+
* emails, so restricting AWS to email is a zero-regression no-op for them.
|
|
60
|
+
*/
|
|
61
|
+
export function assertAwsEmailOnly(env: string, kind: 'email' | 'phone' | 'userId'): void {
|
|
62
|
+
if ((env === 'stage' || env === 'prod') && kind !== 'email') {
|
|
63
|
+
throw new Error('stage/prod 仅支持 email 标识;手机号/userId 解析仅 cn-prod 可用');
|
|
64
|
+
}
|
|
65
|
+
}
|
|
8
66
|
|
|
9
67
|
// cn-prod sells the CNY-priced -cn plans (P12); the bare USD plan ids also
|
|
10
68
|
// exist in the cn DB, so a per-env whitelist (not billing-side validation)
|
|
@@ -15,9 +73,9 @@ const PLANS_BY_ENV: Record<string, string[]> = {
|
|
|
15
73
|
'cn-prod': ['trial', 'starter-cn', 'pro-cn', 'enterprise-cn'],
|
|
16
74
|
};
|
|
17
75
|
|
|
18
|
-
function parseArgs(args: string[]): {
|
|
76
|
+
function parseArgs(args: string[]): { identifier: string; plan: string; months: number; env: string } {
|
|
19
77
|
if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
|
|
20
|
-
console.log(`Usage: optima-grant-subscription <email> [options]
|
|
78
|
+
console.log(`Usage: optima-grant-subscription <email|phone|userId> [options]
|
|
21
79
|
|
|
22
80
|
Options:
|
|
23
81
|
--plan <id> Plan: trial, starter, pro, enterprise (default: pro)
|
|
@@ -28,7 +86,7 @@ Options:
|
|
|
28
86
|
process.exit(0);
|
|
29
87
|
}
|
|
30
88
|
|
|
31
|
-
const
|
|
89
|
+
const identifier = args[0];
|
|
32
90
|
let plan: string | null = null;
|
|
33
91
|
let months = 1;
|
|
34
92
|
let env = 'stage';
|
|
@@ -48,23 +106,54 @@ Options:
|
|
|
48
106
|
}
|
|
49
107
|
if (months < 1) { console.error('Months must be >= 1'); process.exit(1); }
|
|
50
108
|
|
|
51
|
-
return {
|
|
109
|
+
return { identifier, plan, months, env };
|
|
52
110
|
}
|
|
53
111
|
|
|
54
112
|
async function main() {
|
|
55
|
-
const {
|
|
113
|
+
const { identifier, plan, months, env } = parseArgs(process.argv.slice(2));
|
|
56
114
|
|
|
57
|
-
|
|
115
|
+
const kind = classifyIdentifier(identifier);
|
|
116
|
+
assertAwsEmailOnly(env, kind);
|
|
117
|
+
console.log(`\n🎁 Granting ${plan} subscription to ${identifier} (${kind}) for ${months} month(s) [${env.toUpperCase()}]\n`);
|
|
58
118
|
|
|
59
|
-
//
|
|
60
|
-
//
|
|
119
|
+
// identity is the verified target account to print on success. AWS keeps the
|
|
120
|
+
// pre-change behavior (email-only, no internal HTTP reverse-verify); only
|
|
121
|
+
// cn-prod runs the full classify→resolve→getUserById→phone-assert防呆 path.
|
|
61
122
|
let userId: string;
|
|
123
|
+
let identity: { phone: string | null; email: string | null };
|
|
124
|
+
|
|
62
125
|
if (env === 'cn-prod') {
|
|
63
|
-
|
|
126
|
+
// cn-prod has no SSH tunnel into the Aliyun RDS — resolve via user-auth,
|
|
127
|
+
// and its dev-skills token carries internal:users:write for the lookups.
|
|
128
|
+
if (kind === 'userId') {
|
|
129
|
+
userId = identifier;
|
|
130
|
+
} else if (kind === 'phone') {
|
|
131
|
+
userId = await resolveUserIdByPhone(env, identifier);
|
|
132
|
+
} else {
|
|
133
|
+
userId = await resolveUserIdByEmail(env, identifier);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Reverse-verify: fetch and loudly print the target account identity
|
|
137
|
+
// before granting, so a wrong userId is caught by eye (gateway#923).
|
|
138
|
+
const acct = await getUserById(env, userId);
|
|
139
|
+
console.log(
|
|
140
|
+
`🎯 目标账号: userId=${userId} 手机=${acct.phone || '(无)'} email=${acct.email || '(无)'} 当前plan=${acct.current_plan || '?'}`,
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
// Hard assertion: a phone-input grant must land on an account whose phone
|
|
144
|
+
// matches. Runs BEFORE callBilling — a mismatch aborts without granting.
|
|
145
|
+
if (kind === 'phone') {
|
|
146
|
+
assertPhoneMatch(identifier, acct.phone);
|
|
147
|
+
}
|
|
148
|
+
identity = { phone: acct.phone, email: acct.email };
|
|
64
149
|
} else {
|
|
150
|
+
// AWS (stage/prod): email-only, resolved via the RDS SSH tunnel. No
|
|
151
|
+
// internal HTTP reverse-verify (token lacks internal:users:write → 403).
|
|
65
152
|
const infisicalConfig = getInfisicalConfig();
|
|
66
153
|
const token = getInfisicalToken(infisicalConfig);
|
|
67
|
-
userId = await resolveUserId(
|
|
154
|
+
userId = await resolveUserId(identifier, env, infisicalConfig, token);
|
|
155
|
+
console.log(`🎯 目标账号: userId=${userId} email=${identifier}`);
|
|
156
|
+
identity = { phone: null, email: identifier };
|
|
68
157
|
}
|
|
69
158
|
|
|
70
159
|
const { body } = await callBilling<{
|
|
@@ -82,10 +171,16 @@ async function main() {
|
|
|
82
171
|
console.log(`✓ Credits: ${body.credits.toLocaleString()} (expires ${body.expiresAt})`);
|
|
83
172
|
console.log(`✓ Token limits: session ${body.sessionTokenLimit.toLocaleString()} / weekly ${body.weeklyTokenLimit.toLocaleString()}`);
|
|
84
173
|
if (body.warning) console.log(`⚠️ ${body.warning}`);
|
|
85
|
-
console.log(
|
|
174
|
+
console.log(
|
|
175
|
+
`\n✅ Done! 用户 userId=${userId} 手机=${identity.phone || '(无)'} email=${identity.email || '(无)'} now has ${body.planId} until ${body.expiresAt}\n`,
|
|
176
|
+
);
|
|
86
177
|
}
|
|
87
178
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
179
|
+
// Only run the CLI flow when invoked directly — being require()'d (e.g. by the
|
|
180
|
+
// unit tests for classifyIdentifier/assertPhoneMatch) must not trigger main().
|
|
181
|
+
if (require.main === module) {
|
|
182
|
+
main().catch(error => {
|
|
183
|
+
console.error('\n❌ Error:', error.message);
|
|
184
|
+
process.exit(1);
|
|
185
|
+
});
|
|
186
|
+
}
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* optima-verify-health user-auth --env prod # 环境 stage|prod|cn|all
|
|
21
21
|
* optima-verify-health --all --env all # stage/prod/cn 三环境矩阵
|
|
22
22
|
* optima-verify-health gateway-core --expect-commit a1b2c3d
|
|
23
|
-
* optima-verify-health --url https://auth
|
|
23
|
+
* optima-verify-health --url https://auth.yzsgo.com/health
|
|
24
24
|
* optima-verify-health --all --json # 机器可读,接 CI
|
|
25
25
|
* optima-verify-health --all --strict # warn 也算不通过
|
|
26
26
|
* 退出码:fail → 非 0;warn 默认放行(0),--strict 让 warn 也非 0。
|
|
@@ -33,15 +33,16 @@ type Env = 'stage' | 'prod' | 'cn';
|
|
|
33
33
|
interface SvcCfg { path: string; stage?: string; prod?: string; cn?: string; cn_path?: string; }
|
|
34
34
|
|
|
35
35
|
// 服务 × 环境 FQDN 表。某服务某环境没部署 → 该 env 键缺省,探时跳过。
|
|
36
|
-
// cn-prod 真实 subdomain 抄自 optima-terraform
|
|
36
|
+
// cn-prod 真实 subdomain 抄自 optima-terraform alicloud/stacks/cn-prod-ingress-sae/main.tf。
|
|
37
|
+
// #201 (2026-06-12): yzsgo.com 全量迁移完成,旧 *-cn.optima.chat 路由已下线。
|
|
37
38
|
const SERVICES: Record<string, SvcCfg> = {
|
|
38
|
-
'user-auth': { path: '/health', stage: 'auth-stage.optima.onl', prod: 'auth.optima.onl', cn: 'auth
|
|
39
|
-
'agentic-chat': { path: '/api/health', stage: 'ai-stage.optima.onl', prod: 'ai.optima.onl', cn: '
|
|
40
|
-
'commerce-backend': { path: '/health', stage: 'api-stage.optima.onl', prod: 'api.optima.onl', cn: '
|
|
39
|
+
'user-auth': { path: '/health', stage: 'auth-stage.optima.onl', prod: 'auth.optima.onl', cn: 'auth.yzsgo.com' },
|
|
40
|
+
'agentic-chat': { path: '/api/health', stage: 'ai-stage.optima.onl', prod: 'ai.optima.onl', cn: 'app.yzsgo.com' },
|
|
41
|
+
'commerce-backend': { path: '/health', stage: 'api-stage.optima.onl', prod: 'api.optima.onl', cn: 'commerce.yzsgo.com', cn_path: '/health/live' },
|
|
41
42
|
'mcp-host': { path: '/health', stage: 'mcp-stage.optima.onl', prod: 'mcp.optima.onl' },
|
|
42
|
-
'gateway-core': { path: '/health', cn: 'gw
|
|
43
|
-
'optima-scout': { path: '/health', cn: 'scout
|
|
44
|
-
'optima-skills': { path: '/health', cn: 'skills
|
|
43
|
+
'gateway-core': { path: '/health', cn: 'gw.yzsgo.com' },
|
|
44
|
+
'optima-scout': { path: '/health', cn: 'scout.yzsgo.com' },
|
|
45
|
+
'optima-skills': { path: '/health', cn: 'skills.yzsgo.com' },
|
|
45
46
|
};
|
|
46
47
|
const ENVS: Env[] = ['stage', 'prod', 'cn'];
|
|
47
48
|
|
|
@@ -6,18 +6,22 @@ exports.getServiceToken = getServiceToken;
|
|
|
6
6
|
exports.callBilling = callBilling;
|
|
7
7
|
exports.callSkills = callSkills;
|
|
8
8
|
exports.resolveUserIdByEmail = resolveUserIdByEmail;
|
|
9
|
+
exports.resolveUserIdByPhone = resolveUserIdByPhone;
|
|
10
|
+
exports.getUserById = getUserById;
|
|
9
11
|
const child_process_1 = require("child_process");
|
|
10
12
|
const infisical_secrets_1 = require("./infisical-secrets");
|
|
11
13
|
const db_utils_1 = require("./db-utils");
|
|
12
14
|
const USER_AUTH_URLS = {
|
|
13
15
|
stage: 'https://auth.stage.optima.onl',
|
|
14
16
|
prod: 'https://auth.optima.onl',
|
|
15
|
-
|
|
17
|
+
// #201: yzsgo.com 全量迁移 (2026-06-12), 旧 auth-cn.optima.chat 路由已下线
|
|
18
|
+
'cn-prod': 'https://auth.yzsgo.com',
|
|
16
19
|
};
|
|
17
20
|
// cn-prod URLs are hardcoded: cn Infisical (secrets-cn.optima.chat) is a
|
|
18
21
|
// separate instance dev-skills has no machine identity for, and these domains
|
|
19
22
|
// are stable. AWS envs keep reading /shared-secrets/domain-urls.
|
|
20
|
-
|
|
23
|
+
// #201: yzsgo.com 全量迁移 (2026-06-12), 旧 billing-cn.optima.chat 路由已下线
|
|
24
|
+
const CN_PROD_BILLING_URL = 'https://billing-api.yzsgo.com';
|
|
21
25
|
/**
|
|
22
26
|
* Validate the --env flag value at command entry, before any I/O.
|
|
23
27
|
*
|
|
@@ -230,3 +234,72 @@ async function resolveUserIdByEmail(env, email) {
|
|
|
230
234
|
console.log(`✓ Found user: ${parsed.user_id}`);
|
|
231
235
|
return parsed.user_id;
|
|
232
236
|
}
|
|
237
|
+
/**
|
|
238
|
+
* Resolve a user's id by phone via user-auth's internal lookup endpoint
|
|
239
|
+
* (POST /api/v1/internal/users/lookup with {phone}). Mirrors
|
|
240
|
+
* resolveUserIdByEmail — pure-phone CN users have no email, so this is the
|
|
241
|
+
* only id path for them (gateway#923 root cause: email-only lookup couldn't
|
|
242
|
+
* resolve them and a wrong userId got hand-fed instead).
|
|
243
|
+
*/
|
|
244
|
+
async function resolveUserIdByPhone(env, phone) {
|
|
245
|
+
console.log(`Looking up user by phone: ${phone}`);
|
|
246
|
+
const token = getServiceToken(env);
|
|
247
|
+
const authUrl = USER_AUTH_URLS[env];
|
|
248
|
+
if (!authUrl)
|
|
249
|
+
throw new Error(`Unknown env: ${env}`);
|
|
250
|
+
const res = await fetch(`${authUrl}/api/v1/internal/users/lookup`, {
|
|
251
|
+
method: 'POST',
|
|
252
|
+
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
|
|
253
|
+
body: JSON.stringify({ phone }),
|
|
254
|
+
});
|
|
255
|
+
const text = await res.text();
|
|
256
|
+
if (res.status === 404) {
|
|
257
|
+
throw new Error(`User not found by phone (${env}): ${phone}`);
|
|
258
|
+
}
|
|
259
|
+
if (!res.ok) {
|
|
260
|
+
throw new Error(formatServiceError(res.status, res.statusText, text));
|
|
261
|
+
}
|
|
262
|
+
let parsed;
|
|
263
|
+
try {
|
|
264
|
+
parsed = JSON.parse(text);
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
267
|
+
throw new Error(`user-auth lookup returned non-JSON 2xx body: ${text.slice(0, 200)}`);
|
|
268
|
+
}
|
|
269
|
+
if (!parsed.user_id) {
|
|
270
|
+
throw new Error(`user-auth lookup response missing user_id: ${text.slice(0, 200)}`);
|
|
271
|
+
}
|
|
272
|
+
console.log(`✓ Found user: ${parsed.user_id}`);
|
|
273
|
+
return parsed.user_id;
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Fetch a user's identity by id via user-auth's internal endpoint
|
|
277
|
+
* (GET /api/v1/internal/users/{userId}). Used to reverse-verify the target
|
|
278
|
+
* account before granting — prints phone/email/current_plan so the operator
|
|
279
|
+
* can confirm they're hitting the right account (gateway#923).
|
|
280
|
+
*/
|
|
281
|
+
async function getUserById(env, userId) {
|
|
282
|
+
const token = getServiceToken(env);
|
|
283
|
+
const authUrl = USER_AUTH_URLS[env];
|
|
284
|
+
if (!authUrl)
|
|
285
|
+
throw new Error(`Unknown env: ${env}`);
|
|
286
|
+
const res = await fetch(`${authUrl}/api/v1/internal/users/${encodeURIComponent(userId)}`, {
|
|
287
|
+
method: 'GET',
|
|
288
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
289
|
+
});
|
|
290
|
+
const text = await res.text();
|
|
291
|
+
if (res.status === 404) {
|
|
292
|
+
throw new Error(`User not found by id (${env}): ${userId}`);
|
|
293
|
+
}
|
|
294
|
+
if (!res.ok) {
|
|
295
|
+
throw new Error(formatServiceError(res.status, res.statusText, text));
|
|
296
|
+
}
|
|
297
|
+
let parsed;
|
|
298
|
+
try {
|
|
299
|
+
parsed = JSON.parse(text);
|
|
300
|
+
}
|
|
301
|
+
catch {
|
|
302
|
+
throw new Error(`user-auth user lookup returned non-JSON 2xx body: ${text.slice(0, 200)}`);
|
|
303
|
+
}
|
|
304
|
+
return parsed;
|
|
305
|
+
}
|
|
File without changes
|
|
File without changes
|
|
@@ -55,6 +55,13 @@ const ENV_CONFIG = {
|
|
|
55
55
|
apiUrl: 'https://api.optima.onl',
|
|
56
56
|
clientId: 'commerce-cli-ecs-pro-i2r5of1h',
|
|
57
57
|
envName: 'prod'
|
|
58
|
+
},
|
|
59
|
+
'cn-prod': {
|
|
60
|
+
// #201: yzsgo.com 全量迁移 (2026-06-12), 旧 *-cn.optima.chat 路由已下线
|
|
61
|
+
authUrl: 'https://auth.yzsgo.com',
|
|
62
|
+
apiUrl: 'https://commerce.yzsgo.com',
|
|
63
|
+
clientId: 'commerce-cli-cn-prod',
|
|
64
|
+
envName: 'cn-prod'
|
|
58
65
|
}
|
|
59
66
|
};
|
|
60
67
|
async function httpRequest(url, options = {}) {
|
|
@@ -176,11 +183,11 @@ async function main() {
|
|
|
176
183
|
}
|
|
177
184
|
else if (arg === '--env' && args[i + 1]) {
|
|
178
185
|
const envArg = args[++i];
|
|
179
|
-
if (envArg === 'ci' || envArg === 'stage' || envArg === 'prod') {
|
|
186
|
+
if (envArg === 'ci' || envArg === 'stage' || envArg === 'prod' || envArg === 'cn-prod') {
|
|
180
187
|
environment = envArg;
|
|
181
188
|
}
|
|
182
189
|
else {
|
|
183
|
-
console.error(`❌ Invalid environment: ${envArg}. Must be 'ci', 'stage', or 'prod'.`);
|
|
190
|
+
console.error(`❌ Invalid environment: ${envArg}. Must be 'ci', 'stage', 'prod', or 'cn-prod'.`);
|
|
184
191
|
process.exit(1);
|
|
185
192
|
}
|
|
186
193
|
}
|
|
@@ -194,18 +201,20 @@ Options:
|
|
|
194
201
|
--business-name <name> Merchant business name (default: auto-generated)
|
|
195
202
|
--phone <phone> Merchant phone number (optional)
|
|
196
203
|
--address <address> Merchant address (optional)
|
|
197
|
-
--env <environment> Environment: ci (default), stage, or prod
|
|
204
|
+
--env <environment> Environment: ci (default), stage, prod, or cn-prod
|
|
198
205
|
--help, -h Show this help message
|
|
199
206
|
|
|
200
207
|
Environments:
|
|
201
|
-
ci
|
|
202
|
-
stage
|
|
203
|
-
prod
|
|
208
|
+
ci CI 环境 (auth.optima.chat, api.optima.chat)
|
|
209
|
+
stage Stage 环境 (auth.stage.optima.onl, api.stage.optima.onl)
|
|
210
|
+
prod Prod 环境 (auth.optima.onl, api.optima.onl)
|
|
211
|
+
cn-prod 阿里云生产环境 (auth.yzsgo.com, commerce.yzsgo.com) [#201]
|
|
204
212
|
|
|
205
213
|
Example:
|
|
206
214
|
optima-generate-test-token
|
|
207
215
|
optima-generate-test-token --env stage
|
|
208
216
|
optima-generate-test-token --env prod
|
|
217
|
+
optima-generate-test-token --env cn-prod
|
|
209
218
|
optima-generate-test-token --business-name "My Test Shop" --env stage
|
|
210
219
|
`);
|
|
211
220
|
process.exit(0);
|
|
File without changes
|
|
@@ -1,11 +1,63 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.classifyIdentifier = classifyIdentifier;
|
|
5
|
+
exports.assertPhoneMatch = assertPhoneMatch;
|
|
6
|
+
exports.assertAwsEmailOnly = assertAwsEmailOnly;
|
|
4
7
|
// P15 D8b:USD 钱包退役——原「SSH 直写 subscriptions/usd_wallets/token_quotas」
|
|
5
8
|
// 作废,改调 billing 服务态端点(与用户态 /api/admin/grant-subscription 同
|
|
6
9
|
// 业务体:supersede 旧授予 + 实得 credits + token quota,重试双发不双倍)。
|
|
7
10
|
const db_utils_1 = require("./db-utils");
|
|
8
11
|
const billing_http_1 = require("./billing-http");
|
|
12
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
13
|
+
/**
|
|
14
|
+
* Auto-detect what kind of identifier the operator passed. gateway#923: a
|
|
15
|
+
* pure-phone CN user (no email) couldn't be resolved by the email-only CLI,
|
|
16
|
+
* so a wrong userId was hand-fed. Accepting phone/userId directly + this
|
|
17
|
+
* classifier removes the manual-userId footgun.
|
|
18
|
+
*/
|
|
19
|
+
function classifyIdentifier(s) {
|
|
20
|
+
if (s.includes('@'))
|
|
21
|
+
return 'email';
|
|
22
|
+
if (UUID_RE.test(s))
|
|
23
|
+
return 'userId';
|
|
24
|
+
const digits = s.replace(/\D/g, '');
|
|
25
|
+
if (/^\d{6,}$/.test(digits))
|
|
26
|
+
return 'phone';
|
|
27
|
+
throw new Error('无法识别 identifier(需 email / 手机号 / userId)');
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Hard guard: when the operator gave a phone number, the resolved account's
|
|
31
|
+
* phone MUST match it. Normalizes both sides (strip non-digits) before
|
|
32
|
+
* comparing. A mismatch (or an account with no phone) throws to abort the
|
|
33
|
+
* grant — this is the assertion that would have stopped gateway#923.
|
|
34
|
+
*/
|
|
35
|
+
function assertPhoneMatch(inputPhone, accountPhone) {
|
|
36
|
+
const input = inputPhone.replace(/\D/g, '');
|
|
37
|
+
const account = (accountPhone ?? '').replace(/\D/g, '');
|
|
38
|
+
if (!account || input !== account) {
|
|
39
|
+
throw new Error('手机号不匹配:请求 ' +
|
|
40
|
+
input +
|
|
41
|
+
',但该 userId 的手机号是 ' +
|
|
42
|
+
(accountPhone || '(无)') +
|
|
43
|
+
'——拒绝授予以防发错账号');
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* AWS (stage/prod) only supports email identifiers. The HTTP reverse-verify
|
|
48
|
+
* (getUserById) and phone/userId resolution rely on user-auth's internal
|
|
49
|
+
* endpoints, which require the token scope `internal:users:write` — only the
|
|
50
|
+
* cn-prod dev-skills client carries it (see billing-http getServiceToken).
|
|
51
|
+
* AWS resolves users via the RDS SSH tunnel (resolveUserId) and never hits
|
|
52
|
+
* those endpoints, so phone/userId here would 403. gateway#923 (wrong-account
|
|
53
|
+
* grant) is a cn-prod-only, pure-phone-user problem; AWS users have unambiguous
|
|
54
|
+
* emails, so restricting AWS to email is a zero-regression no-op for them.
|
|
55
|
+
*/
|
|
56
|
+
function assertAwsEmailOnly(env, kind) {
|
|
57
|
+
if ((env === 'stage' || env === 'prod') && kind !== 'email') {
|
|
58
|
+
throw new Error('stage/prod 仅支持 email 标识;手机号/userId 解析仅 cn-prod 可用');
|
|
59
|
+
}
|
|
60
|
+
}
|
|
9
61
|
// cn-prod sells the CNY-priced -cn plans (P12); the bare USD plan ids also
|
|
10
62
|
// exist in the cn DB, so a per-env whitelist (not billing-side validation)
|
|
11
63
|
// is what prevents accidentally granting a USD-priced plan to a CN user.
|
|
@@ -16,7 +68,7 @@ const PLANS_BY_ENV = {
|
|
|
16
68
|
};
|
|
17
69
|
function parseArgs(args) {
|
|
18
70
|
if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
|
|
19
|
-
console.log(`Usage: optima-grant-subscription <email> [options]
|
|
71
|
+
console.log(`Usage: optima-grant-subscription <email|phone|userId> [options]
|
|
20
72
|
|
|
21
73
|
Options:
|
|
22
74
|
--plan <id> Plan: trial, starter, pro, enterprise (default: pro)
|
|
@@ -26,7 +78,7 @@ Options:
|
|
|
26
78
|
-h, --help Show this help`);
|
|
27
79
|
process.exit(0);
|
|
28
80
|
}
|
|
29
|
-
const
|
|
81
|
+
const identifier = args[0];
|
|
30
82
|
let plan = null;
|
|
31
83
|
let months = 1;
|
|
32
84
|
let env = 'stage';
|
|
@@ -52,21 +104,49 @@ Options:
|
|
|
52
104
|
console.error('Months must be >= 1');
|
|
53
105
|
process.exit(1);
|
|
54
106
|
}
|
|
55
|
-
return {
|
|
107
|
+
return { identifier, plan, months, env };
|
|
56
108
|
}
|
|
57
109
|
async function main() {
|
|
58
|
-
const {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
110
|
+
const { identifier, plan, months, env } = parseArgs(process.argv.slice(2));
|
|
111
|
+
const kind = classifyIdentifier(identifier);
|
|
112
|
+
assertAwsEmailOnly(env, kind);
|
|
113
|
+
console.log(`\n🎁 Granting ${plan} subscription to ${identifier} (${kind}) for ${months} month(s) [${env.toUpperCase()}]\n`);
|
|
114
|
+
// identity is the verified target account to print on success. AWS keeps the
|
|
115
|
+
// pre-change behavior (email-only, no internal HTTP reverse-verify); only
|
|
116
|
+
// cn-prod runs the full classify→resolve→getUserById→phone-assert防呆 path.
|
|
62
117
|
let userId;
|
|
118
|
+
let identity;
|
|
63
119
|
if (env === 'cn-prod') {
|
|
64
|
-
|
|
120
|
+
// cn-prod has no SSH tunnel into the Aliyun RDS — resolve via user-auth,
|
|
121
|
+
// and its dev-skills token carries internal:users:write for the lookups.
|
|
122
|
+
if (kind === 'userId') {
|
|
123
|
+
userId = identifier;
|
|
124
|
+
}
|
|
125
|
+
else if (kind === 'phone') {
|
|
126
|
+
userId = await (0, billing_http_1.resolveUserIdByPhone)(env, identifier);
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
userId = await (0, billing_http_1.resolveUserIdByEmail)(env, identifier);
|
|
130
|
+
}
|
|
131
|
+
// Reverse-verify: fetch and loudly print the target account identity
|
|
132
|
+
// before granting, so a wrong userId is caught by eye (gateway#923).
|
|
133
|
+
const acct = await (0, billing_http_1.getUserById)(env, userId);
|
|
134
|
+
console.log(`🎯 目标账号: userId=${userId} 手机=${acct.phone || '(无)'} email=${acct.email || '(无)'} 当前plan=${acct.current_plan || '?'}`);
|
|
135
|
+
// Hard assertion: a phone-input grant must land on an account whose phone
|
|
136
|
+
// matches. Runs BEFORE callBilling — a mismatch aborts without granting.
|
|
137
|
+
if (kind === 'phone') {
|
|
138
|
+
assertPhoneMatch(identifier, acct.phone);
|
|
139
|
+
}
|
|
140
|
+
identity = { phone: acct.phone, email: acct.email };
|
|
65
141
|
}
|
|
66
142
|
else {
|
|
143
|
+
// AWS (stage/prod): email-only, resolved via the RDS SSH tunnel. No
|
|
144
|
+
// internal HTTP reverse-verify (token lacks internal:users:write → 403).
|
|
67
145
|
const infisicalConfig = (0, db_utils_1.getInfisicalConfig)();
|
|
68
146
|
const token = (0, db_utils_1.getInfisicalToken)(infisicalConfig);
|
|
69
|
-
userId = await (0, db_utils_1.resolveUserId)(
|
|
147
|
+
userId = await (0, db_utils_1.resolveUserId)(identifier, env, infisicalConfig, token);
|
|
148
|
+
console.log(`🎯 目标账号: userId=${userId} email=${identifier}`);
|
|
149
|
+
identity = { phone: null, email: identifier };
|
|
70
150
|
}
|
|
71
151
|
const { body } = await (0, billing_http_1.callBilling)(env, 'POST', '/api/billing/admin/grant-subscription', { userId, planId: plan, months });
|
|
72
152
|
console.log(`✓ Subscription ${body.subscriptionId} (${body.planId})`);
|
|
@@ -74,9 +154,13 @@ async function main() {
|
|
|
74
154
|
console.log(`✓ Token limits: session ${body.sessionTokenLimit.toLocaleString()} / weekly ${body.weeklyTokenLimit.toLocaleString()}`);
|
|
75
155
|
if (body.warning)
|
|
76
156
|
console.log(`⚠️ ${body.warning}`);
|
|
77
|
-
console.log(`\n✅ Done!
|
|
157
|
+
console.log(`\n✅ Done! 用户 userId=${userId} 手机=${identity.phone || '(无)'} email=${identity.email || '(无)'} now has ${body.planId} until ${body.expiresAt}\n`);
|
|
158
|
+
}
|
|
159
|
+
// Only run the CLI flow when invoked directly — being require()'d (e.g. by the
|
|
160
|
+
// unit tests for classifyIdentifier/assertPhoneMatch) must not trigger main().
|
|
161
|
+
if (require.main === module) {
|
|
162
|
+
main().catch(error => {
|
|
163
|
+
console.error('\n❌ Error:', error.message);
|
|
164
|
+
process.exit(1);
|
|
165
|
+
});
|
|
78
166
|
}
|
|
79
|
-
main().catch(error => {
|
|
80
|
-
console.error('\n❌ Error:', error.message);
|
|
81
|
-
process.exit(1);
|
|
82
|
-
});
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -55,7 +55,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
55
55
|
* optima-verify-health user-auth --env prod # 环境 stage|prod|cn|all
|
|
56
56
|
* optima-verify-health --all --env all # stage/prod/cn 三环境矩阵
|
|
57
57
|
* optima-verify-health gateway-core --expect-commit a1b2c3d
|
|
58
|
-
* optima-verify-health --url https://auth
|
|
58
|
+
* optima-verify-health --url https://auth.yzsgo.com/health
|
|
59
59
|
* optima-verify-health --all --json # 机器可读,接 CI
|
|
60
60
|
* optima-verify-health --all --strict # warn 也算不通过
|
|
61
61
|
* 退出码:fail → 非 0;warn 默认放行(0),--strict 让 warn 也非 0。
|
|
@@ -64,15 +64,16 @@ const node_dns_1 = require("node:dns");
|
|
|
64
64
|
const tls = __importStar(require("node:tls"));
|
|
65
65
|
const https = __importStar(require("node:https"));
|
|
66
66
|
// 服务 × 环境 FQDN 表。某服务某环境没部署 → 该 env 键缺省,探时跳过。
|
|
67
|
-
// cn-prod 真实 subdomain 抄自 optima-terraform
|
|
67
|
+
// cn-prod 真实 subdomain 抄自 optima-terraform alicloud/stacks/cn-prod-ingress-sae/main.tf。
|
|
68
|
+
// #201 (2026-06-12): yzsgo.com 全量迁移完成,旧 *-cn.optima.chat 路由已下线。
|
|
68
69
|
const SERVICES = {
|
|
69
|
-
'user-auth': { path: '/health', stage: 'auth-stage.optima.onl', prod: 'auth.optima.onl', cn: 'auth
|
|
70
|
-
'agentic-chat': { path: '/api/health', stage: 'ai-stage.optima.onl', prod: 'ai.optima.onl', cn: '
|
|
71
|
-
'commerce-backend': { path: '/health', stage: 'api-stage.optima.onl', prod: 'api.optima.onl', cn: '
|
|
70
|
+
'user-auth': { path: '/health', stage: 'auth-stage.optima.onl', prod: 'auth.optima.onl', cn: 'auth.yzsgo.com' },
|
|
71
|
+
'agentic-chat': { path: '/api/health', stage: 'ai-stage.optima.onl', prod: 'ai.optima.onl', cn: 'app.yzsgo.com' },
|
|
72
|
+
'commerce-backend': { path: '/health', stage: 'api-stage.optima.onl', prod: 'api.optima.onl', cn: 'commerce.yzsgo.com', cn_path: '/health/live' },
|
|
72
73
|
'mcp-host': { path: '/health', stage: 'mcp-stage.optima.onl', prod: 'mcp.optima.onl' },
|
|
73
|
-
'gateway-core': { path: '/health', cn: 'gw
|
|
74
|
-
'optima-scout': { path: '/health', cn: 'scout
|
|
75
|
-
'optima-skills': { path: '/health', cn: 'skills
|
|
74
|
+
'gateway-core': { path: '/health', cn: 'gw.yzsgo.com' },
|
|
75
|
+
'optima-scout': { path: '/health', cn: 'scout.yzsgo.com' },
|
|
76
|
+
'optima-skills': { path: '/health', cn: 'skills.yzsgo.com' },
|
|
76
77
|
};
|
|
77
78
|
const ENVS = ['stage', 'prod', 'cn'];
|
|
78
79
|
const G = '\x1b[32m', R = '\x1b[31m', Y = '\x1b[33m', B = '\x1b[34m', N = '\x1b[0m';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@optima-chat/dev-skills",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.42",
|
|
4
4
|
"description": "Claude Code Skills for Optima development team - cross-environment collaboration tools",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
"scripts": {
|
|
20
20
|
"postinstall": "node scripts/install.js",
|
|
21
21
|
"build": "tsc",
|
|
22
|
-
"prepare": "npm run build"
|
|
22
|
+
"prepare": "npm run build",
|
|
23
|
+
"test": "node --test tests/*.test.js"
|
|
23
24
|
},
|
|
24
25
|
"keywords": [
|
|
25
26
|
"claude-code",
|