@optima-chat/dev-skills 0.7.43 → 0.8.0

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.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: "grant-balance"
3
- description: "当用户请求赠送余额、充值 USD 余额、grant balance、加余额、奖励、补偿、推荐奖励、运营发放时,使用此技能。支持 Stage、Prod、cn-prod 三个环境。"
3
+ description: "当用户请求赠送余额、充值 USD 余额、grant balance、加余额、奖励、补偿、推荐奖励、运营发放时,使用此技能。支持 Stage、Prod、cn-prod、cn-stage 四个环境。"
4
4
  allowed-tools: ["Bash"]
5
5
  ---
6
6
 
@@ -50,9 +50,11 @@ optima-grant-balance user@example.com --amount 20 --description "服务中断补
50
50
  | `<email>` | 用户邮箱(必填) | - |
51
51
  | `--amount <usd>` | USD 金额(必填,> 0) | - |
52
52
  | `--description <text>` | 描述/原因(仅 console 输出) | - |
53
- | `--env <env>` | 环境:stage, prod, cn-prod | stage |
53
+ | `--env <env>` | 环境:stage, prod, cn-prod, cn-stage | stage |
54
54
 
55
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
+ >
57
+ > **cn-stage(阿里云预发)**:同 cn-prod 走 HTTPS(auth.stage.optima.chat / billing-api.stage.optima.chat)。M2M 凭证读 cn Infisical **staging** /shared-secrets/oauth-clients,运行时需 `INFISICAL_CN_EMAIL`/`INFISICAL_CN_PASSWORD`(admin user)。例:`optima-grant-balance user@example.com --amount 1 --env cn-stage`
56
58
 
57
59
  ## 与 grant-subscription 的区别
58
60
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: "grant-subscription"
3
- description: "当用户请求开通会员、赠送订阅、grant subscription、开通 Pro/Starter/Enterprise、升级计划、给用户开会员时,使用此技能。支持 Stage、Prod、cn-prod 三个环境。"
3
+ description: "当用户请求开通会员、赠送订阅、grant subscription、开通 Pro/Starter/Enterprise、升级计划、给用户开会员时,使用此技能。支持 Stage、Prod、cn-prod、cn-stage 四个环境。"
4
4
  allowed-tools: ["Bash"]
5
5
  ---
6
6
 
@@ -50,6 +50,10 @@ optima-grant-subscription user@example.com --plan enterprise --env prod
50
50
  # cn-prod(国内环境):plan 用 -cn 档,默认 pro-cn
51
51
  optima-grant-subscription user@example.com --env cn-prod
52
52
  optima-grant-subscription user@example.com --plan starter-cn --env cn-prod
53
+
54
+ # cn-stage(阿里云预发):同 cn-prod 用 -cn 档;需 INFISICAL_CN_EMAIL/PASSWORD。
55
+ # ⚠️ cn-stage billing 的 plan 目录可能未 seed(报 403 Plan not found)——属 cn-stage 基建缺口,非本工具问题。
56
+ optima-grant-subscription user@example.com --plan pro-cn --env cn-stage
53
57
  ```
54
58
 
55
59
  ### 参数说明
@@ -49,9 +49,10 @@ optima-grant-balance user@example.com --amount 20 --description "服务中断补
49
49
  | `<email>` | 用户邮箱(必填) | - |
50
50
  | `--amount <usd>` | USD 金额(必填,> 0) | - |
51
51
  | `--description <text>` | 描述/原因(仅 console 输出) | - |
52
- | `--env <env>` | 环境:stage, prod, cn-prod | stage |
52
+ | `--env <env>` | 环境:stage, prod, cn-prod, cn-stage | stage |
53
53
 
54
54
  > **cn-prod**:走 HTTPS(auth.yzsgo.com / billing-api.yzsgo.com),email 查找经 user-auth internal lookup API(无 SSH 隧道)。金额输入仍是 USD($1 = 700 积分 = ¥7 档积分口径一致)。
55
+ > **cn-stage(阿里云预发)**:同 cn-prod 走 HTTPS(auth.stage.optima.chat / billing-api.stage.optima.chat);M2M 凭证读 cn Infisical staging,需 `INFISICAL_CN_EMAIL`/`INFISICAL_CN_PASSWORD`。
55
56
 
56
57
  ## 与 grant-subscription 的区别
57
58
 
@@ -25,12 +25,16 @@ optima-grant-subscription user@example.com --plan enterprise --env prod
25
25
  # cn-prod(国内环境):plan 用 -cn 档,默认 pro-cn
26
26
  optima-grant-subscription user@example.com --env cn-prod
27
27
  optima-grant-subscription user@example.com --plan starter-cn --env cn-prod
28
+
29
+ # cn-stage(阿里云预发):同 cn-prod 用 -cn 档;需 INFISICAL_CN_EMAIL/PASSWORD。
30
+ # ⚠️ cn-stage billing plan 目录可能未 seed(403 Plan not found)——cn-stage 基建缺口,非本工具问题。
31
+ optima-grant-subscription user@example.com --plan pro-cn --env cn-stage
28
32
  ```
29
33
 
30
34
  ## Guidance
31
35
 
32
36
  - Default to `stage`.
33
- - Confirm the user email before running on `prod` / `cn-prod`.
34
- - `cn-prod` plans are the CNY-priced `-cn` ids (`starter-cn`, `pro-cn`, `enterprise-cn`, plus `trial`); bare USD ids are rejected client-side to avoid granting a USD-priced plan to a CN user.
37
+ - Confirm the user email before running on `prod` / `cn-prod` / `cn-stage`.
38
+ - `cn-prod` / `cn-stage` plans are the CNY-priced `-cn` ids (`starter-cn`, `pro-cn`, `enterprise-cn`, plus `trial`); bare USD ids are rejected client-side to avoid granting a USD-priced plan to a CN user.
35
39
  - This operation replaces existing subscription state and resets the wallet granted balance according to the selected plan.
36
40
  - Use `optima-query-db` afterward if the user asks for verification.
package/bin/cli.js CHANGED
@@ -44,7 +44,7 @@ switch (command) {
44
44
  log(' session-gateway optima-scout billing browser-backend optima-generation', 'cyan');
45
45
 
46
46
  log('\nEnvironments:', 'yellow');
47
- log(' stage (default) prod', 'cyan');
47
+ log(' stage (default) prod cn-prod (阿里云生产) cn-stage (阿里云预发)', 'cyan');
48
48
 
49
49
  log('\nExamples:', 'yellow');
50
50
  log(' /logs commerce-backend 100 prod', 'cyan');
@@ -1,19 +1,22 @@
1
1
  import { execSync } from 'child_process';
2
2
  import { fetchInfisicalSecret } from './infisical-secrets';
3
- import { getInfisicalConfig, getInfisicalToken } from './db-utils';
3
+ import { getInfisicalConfig, getInfisicalToken, getCnInfisicalToken, getCnSecrets } from './db-utils';
4
4
 
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
9
  'cn-prod': 'https://auth.yzsgo.com',
10
+ // cn-stage(阿里云预发,独立于 AWS stage `.optima.onl`)
11
+ 'cn-stage': 'https://auth.stage.optima.chat',
10
12
  };
11
13
 
12
- // cn-prod URLs are hardcoded: cn Infisical (secrets-cn.optima.chat) is a
14
+ // cn URLs are hardcoded: cn Infisical (secrets-cn.optima.chat) is a
13
15
  // separate instance dev-skills has no machine identity for, and these domains
14
16
  // are stable. AWS envs keep reading /shared-secrets/domain-urls.
15
17
  // #201: yzsgo.com 全量迁移 (2026-06-12), 旧 billing-cn.optima.chat 路由已下线
16
18
  const CN_PROD_BILLING_URL = 'https://billing-api.yzsgo.com';
19
+ const CN_STAGE_BILLING_URL = 'https://billing-api.stage.optima.chat';
17
20
 
18
21
  /**
19
22
  * Validate the --env flag value at command entry, before any I/O.
@@ -39,9 +42,9 @@ export function validateEnv(env: string): 'stage' | 'prod' {
39
42
  * exist for cn-prod (Aliyun VPC-internal RDS) — keep them on validateEnv so a
40
43
  * cn-prod typo fails fast instead of dying inside the tunnel setup.
41
44
  */
42
- export function validateEnvCnProd(env: string): 'stage' | 'prod' | 'cn-prod' {
43
- if (env !== 'stage' && env !== 'prod' && env !== 'cn-prod') {
44
- throw new Error(`--env must be "stage", "prod" or "cn-prod" (got: ${env})`);
45
+ export function validateEnvCnProd(env: string): 'stage' | 'prod' | 'cn-prod' | 'cn-stage' {
46
+ if (env !== 'stage' && env !== 'prod' && env !== 'cn-prod' && env !== 'cn-stage') {
47
+ throw new Error(`--env must be "stage", "prod", "cn-prod" or "cn-stage" (got: ${env})`);
45
48
  }
46
49
  return env;
47
50
  }
@@ -84,6 +87,7 @@ const skillsUrlCache: Record<string, string> = {};
84
87
 
85
88
  function getBillingUrl(env: string): string {
86
89
  if (env === 'cn-prod') return CN_PROD_BILLING_URL;
90
+ if (env === 'cn-stage') return CN_STAGE_BILLING_URL;
87
91
  if (billingUrlCache[env]) return billingUrlCache[env];
88
92
  const url = fetchInfisicalSecret(env, '/shared-secrets/domain-urls', 'BILLING_URL');
89
93
  billingUrlCache[env] = url;
@@ -100,17 +104,33 @@ function getSkillsUrl(env: string): string {
100
104
  export function getServiceToken(env: string): string {
101
105
  if (tokenCache[env]) return tokenCache[env];
102
106
 
103
- const cfg = getInfisicalConfig();
104
- const tok = getInfisicalToken(cfg);
105
- // Fetch BOTH client_id and client_secret from Infisical they differ per env.
106
- // cn-prod credentials are mirrored in the AWS Infisical *prod* environment
107
- // (fetchInfisicalSecret has no cn-prod env slug), under CN-specific keys.
108
- const isCn = env === 'cn-prod';
109
- const infisicalEnv = isCn ? 'prod' : env;
110
- const idKey = isCn ? DEV_SKILLS_CN_CLIENT_ID_KEY : DEV_SKILLS_CLIENT_ID_KEY;
111
- const secretKey = isCn ? DEV_SKILLS_CN_CLIENT_SECRET_KEY : DEV_SKILLS_CLIENT_SECRET_KEY;
112
- const clientId = fetchInfisicalSecret(infisicalEnv, DEV_SKILLS_OAUTH_PATH, idKey, cfg, tok);
113
- const clientSecret = fetchInfisicalSecret(infisicalEnv, DEV_SKILLS_OAUTH_PATH, secretKey, cfg, tok);
107
+ // cn-prod 与 cn-stage 都用 client_credentials M2M token,scope=internal:users:write。
108
+ // 凭证来源不同:
109
+ // · cn-prod —— 镜像在 AWS Infisical prod 环境(CN_PROD_ 前缀键),复用既有 AWS 访问。
110
+ // · cn-stage —— canonical cn Infisical staging /shared-secrets/oauth-clients
111
+ // (dev-skills 无该 client AWS 镜像;用 admin email/password 直读,
112
+ // 运行时需 INFISICAL_CN_EMAIL/PASSWORD,与 query-db cn 同一前提)。
113
+ const isCn = env === 'cn-prod' || env === 'cn-stage';
114
+ let clientId: string;
115
+ let clientSecret: string;
116
+ if (env === 'cn-stage') {
117
+ const cnTok = getCnInfisicalToken();
118
+ const oc = getCnSecrets(cnTok, '/shared-secrets/oauth-clients', false, 'staging');
119
+ clientId = oc['DEV_SKILLS_OAUTH_CLIENT_ID'];
120
+ clientSecret = oc['DEV_SKILLS_OAUTH_CLIENT_SECRET'];
121
+ if (!clientId || !clientSecret) {
122
+ throw new Error('cn-stage dev-skills OAuth client 凭证缺失(cn Infisical staging /shared-secrets/oauth-clients 的 DEV_SKILLS_OAUTH_CLIENT_ID/SECRET)。');
123
+ }
124
+ } else {
125
+ const cfg = getInfisicalConfig();
126
+ const tok = getInfisicalToken(cfg);
127
+ // Fetch BOTH client_id and client_secret from Infisical — they differ per env.
128
+ const infisicalEnv = env === 'cn-prod' ? 'prod' : env;
129
+ const idKey = env === 'cn-prod' ? DEV_SKILLS_CN_CLIENT_ID_KEY : DEV_SKILLS_CLIENT_ID_KEY;
130
+ const secretKey = env === 'cn-prod' ? DEV_SKILLS_CN_CLIENT_SECRET_KEY : DEV_SKILLS_CLIENT_SECRET_KEY;
131
+ clientId = fetchInfisicalSecret(infisicalEnv, DEV_SKILLS_OAUTH_PATH, idKey, cfg, tok);
132
+ clientSecret = fetchInfisicalSecret(infisicalEnv, DEV_SKILLS_OAUTH_PATH, secretKey, cfg, tok);
133
+ }
114
134
 
115
135
  const authUrl = USER_AUTH_URLS[env];
116
136
  if (!authUrl) throw new Error(`Unknown env: ${env}`);
@@ -29,15 +29,33 @@ const EC2_HOST = '3.0.210.113';
29
29
  // ─── cn-prod (阿里云) 常量 ────────────────────────────────────────────────────
30
30
  // cn-prod 跑在阿里云 SAE,与海外 AWS 完全独立:独立 Infisical (secrets-cn.optima.chat)
31
31
  // + 内网 RDS(无公网端点)经 buildbox ECS 跳板 SSH 隧道访问。设计见 optima-dev-skills#21。
32
+ // cn Infisical 是【单实例双环境】:prod=cn-prod、staging=cn-stage(URL/org/project 共享,
33
+ // 只切 environment 参数)。cn-stage 数据面共用 prod VPC,故走【同一 buildbox】跳板。
32
34
  const CN_INFISICAL_URL = process.env.INFISICAL_CN_URL || 'https://secrets-cn.optima.chat';
33
35
  const CN_INFISICAL_ORG = process.env.INFISICAL_CN_ORG || 'f44012fa-0659-4f7e-b0ac-ed01244efc8a';
34
36
  const CN_INFISICAL_PROJECT = process.env.INFISICAL_CN_PROJECT || '4deef229-11be-40a5-8f56-b61f0bce7240';
35
- const CN_RDS_HOST = 'pgm-2zexwx9eso9e4yla.pg.rds.aliyuncs.com';
37
+ const CN_RDS_HOST = 'pgm-2zexwx9eso9e4yla.pg.rds.aliyuncs.com'; // cn-prod RDS
38
+ const CN_STAGE_RDS_HOST = 'pgm-2zem1u9zdh06boim.pg.rds.aliyuncs.com'; // cn-stage RDS(独立实例,共用 prod VPC)
36
39
  const CN_BUILDBOX_HOST = process.env.OPTIMA_CN_BUILDBOX_HOST || '47.94.105.163';
37
40
 
38
- /** env 是否指向 cn-prod(阿里云)。接受 `cn` / `cn-prod`。 */
41
+ /** env 是否指向阿里云(cn-prod 或 cn-stage)。接受 `cn` / `cn-prod` / `cn-stage`。 */
39
42
  export function isCnEnv(env: string): boolean {
40
- return env === 'cn' || env === 'cn-prod';
43
+ return env === 'cn' || env === 'cn-prod' || env === 'cn-stage';
44
+ }
45
+
46
+ /** env 是否指向 cn-stage(阿里云预发)。 */
47
+ export function isCnStageEnv(env: string): boolean {
48
+ return env === 'cn-stage';
49
+ }
50
+
51
+ /** cn env → cn Infisical environment slug。cn-stage→staging,其余 cn→prod。 */
52
+ export function cnInfisicalEnv(env: string): 'prod' | 'staging' {
53
+ return env === 'cn-stage' ? 'staging' : 'prod';
54
+ }
55
+
56
+ /** cn env → 内网 RDS host。cn-stage→stage 实例,其余 cn→prod 实例。 */
57
+ export function cnRdsHost(env: string): string {
58
+ return env === 'cn-stage' ? CN_STAGE_RDS_HOST : CN_RDS_HOST;
41
59
  }
42
60
 
43
61
  // ─── SQL escaping ───────────────────────────────────────────────────────────
@@ -117,10 +135,13 @@ export function getCnInfisicalToken(): string {
117
135
  return org.token;
118
136
  }
119
137
 
120
- /** 读 cn Infisical 某 folder(prod env)→ key→value map。expand=true 让服务端解析 `${...}` 引用。 */
121
- export function getCnSecrets(token: string, secretPath: string, expand = false): Record<string, string> {
138
+ /**
139
+ * cn Infisical folder key→value map。expand=true 让服务端解析 `${...}` 引用。
140
+ * environment:cn-prod 用 `prod`、cn-stage 用 `staging`(同一实例双环境)。
141
+ */
142
+ export function getCnSecrets(token: string, secretPath: string, expand = false, environment: 'prod' | 'staging' = 'prod'): Record<string, string> {
122
143
  const data = curlJson([
123
- `${CN_INFISICAL_URL}/api/v3/secrets/raw?workspaceId=${CN_INFISICAL_PROJECT}&environment=prod&secretPath=${encodeURIComponent(secretPath)}&expandSecretReferences=${expand}`,
144
+ `${CN_INFISICAL_URL}/api/v3/secrets/raw?workspaceId=${CN_INFISICAL_PROJECT}&environment=${environment}&secretPath=${encodeURIComponent(secretPath)}&expandSecretReferences=${expand}`,
124
145
  '-H', `Authorization: Bearer ${token}`,
125
146
  ]);
126
147
  const secrets: Record<string, string> = {};
@@ -417,17 +438,18 @@ export function queryDB(conn: DBConnection, sql: string): string {
417
438
  * 连 cn-prod 某服务库:creds 取自 cn Infisical 的 /shared-secrets/database-users +
418
439
  * /database-names(按 prefix,如 AUTH/BILLING),经 buildbox 跳板隧道(动态端口)。
419
440
  */
420
- export function connectCnDB(prefix: string): { query: (sql: string) => string } {
441
+ export function connectCnDB(prefix: string, cnEnv: string = 'cn-prod'): { query: (sql: string) => string } {
442
+ const infEnv = cnInfisicalEnv(cnEnv);
421
443
  const token = getCnInfisicalToken();
422
- const users = getCnSecrets(token, '/shared-secrets/database-users');
423
- const names = getCnSecrets(token, '/shared-secrets/database-names');
444
+ const users = getCnSecrets(token, '/shared-secrets/database-users', false, infEnv);
445
+ const names = getCnSecrets(token, '/shared-secrets/database-names', false, infEnv);
424
446
  const user = users[`${prefix}_DB_USER`];
425
447
  const password = users[`${prefix}_DB_PASSWORD`];
426
448
  const database = names[`${prefix}_DB_NAME`];
427
449
  if (!user || !password || !database) {
428
450
  throw new Error(`cn DB creds 不全 for ${prefix}(需 ${prefix}_DB_USER/PASSWORD@database-users + ${prefix}_DB_NAME@database-names)。该服务 cn 可能用字面 DATABASE_URL,见 optima-dev-skills#21。`);
429
451
  }
430
- const port = ensureTunnel(CN_RDS_HOST, 'cn-buildbox');
452
+ const port = ensureTunnel(cnRdsHost(cnEnv), 'cn-buildbox');
431
453
  const conn: DBConnection = { host: 'localhost', port, user, password, database };
432
454
  return { query: (sql: string) => queryDB(conn, sql) };
433
455
  }
@@ -437,9 +459,9 @@ export function connectCnDB(prefix: string): { query: (sql: string) => string }
437
459
  * 用于 cred 不在 shared-secrets/database-users 的服务(如 gateway-core)。
438
460
  * expandSecretReferences=true 让 cn Infisical 解析 `${...}` 引用为字面值。
439
461
  */
440
- export function connectCnDBFromUrl(servicePath: string): { query: (sql: string) => string } {
462
+ export function connectCnDBFromUrl(servicePath: string, cnEnv: string = 'cn-prod'): { query: (sql: string) => string } {
441
463
  const token = getCnInfisicalToken();
442
- const secrets = getCnSecrets(token, servicePath, true);
464
+ const secrets = getCnSecrets(token, servicePath, true, cnInfisicalEnv(cnEnv));
443
465
  const dbUrl = secrets['DATABASE_URL'];
444
466
  if (!dbUrl) throw new Error(`cn DATABASE_URL 未找到 at ${servicePath}`);
445
467
  // 注意:DATABASE_URL 含凭证,报错只说事实,不回显值
@@ -24,7 +24,7 @@ interface MerchantSetupResponse {
24
24
  user_id: string;
25
25
  }
26
26
 
27
- type Environment = 'ci' | 'stage' | 'prod' | 'cn-prod';
27
+ type Environment = 'ci' | 'stage' | 'prod' | 'cn-prod' | 'cn-stage';
28
28
 
29
29
  interface EnvironmentConfig {
30
30
  authUrl: string;
@@ -60,6 +60,14 @@ const ENV_CONFIG: Record<Environment, EnvironmentConfig> = {
60
60
  // 注意不要换成同名的 confidential client `commerce-cli-cn-prod-*`,那个需要 client_secret,走不通 CLI 的 public ROPC flow。
61
61
  clientId: 'dev-skill-cli-cn-pro-acvkmcuq',
62
62
  envName: 'cn-prod'
63
+ },
64
+ 'cn-stage': {
65
+ // 阿里云预发(独立于 AWS stage `.optima.onl`)。域名 *.stage.optima.chat。
66
+ authUrl: 'https://auth.stage.optima.chat',
67
+ apiUrl: 'https://commerce.stage.optima.chat',
68
+ // cn-stage user-auth 里注册的 public client(开 password grant,ROPC flow)。
69
+ clientId: 'dev-skill-cli-cn-sta-3dvsxzdo',
70
+ envName: 'cn-stage'
63
71
  }
64
72
  };
65
73
 
@@ -183,6 +191,7 @@ async function main() {
183
191
  let phone: string | undefined;
184
192
  let address: string | undefined;
185
193
  let environment: Environment = 'ci';
194
+ let skipMerchant = false;
186
195
 
187
196
  // 解析命令行参数
188
197
  for (let i = 0; i < args.length; i++) {
@@ -198,12 +207,14 @@ async function main() {
198
207
  phone = args[++i];
199
208
  } else if (arg === '--address' && args[i + 1]) {
200
209
  address = args[++i];
210
+ } else if (arg === '--skip-merchant') {
211
+ skipMerchant = true;
201
212
  } else if (arg === '--env' && args[i + 1]) {
202
213
  const envArg = args[++i];
203
- if (envArg === 'ci' || envArg === 'stage' || envArg === 'prod' || envArg === 'cn-prod') {
214
+ if (envArg === 'ci' || envArg === 'stage' || envArg === 'prod' || envArg === 'cn-prod' || envArg === 'cn-stage') {
204
215
  environment = envArg as Environment;
205
216
  } else {
206
- console.error(`❌ Invalid environment: ${envArg}. Must be 'ci', 'stage', 'prod', or 'cn-prod'.`);
217
+ console.error(`❌ Invalid environment: ${envArg}. Must be 'ci', 'stage', 'prod', 'cn-prod', or 'cn-stage'.`);
207
218
  process.exit(1);
208
219
  }
209
220
  } else if (arg === '--help' || arg === '-h') {
@@ -216,20 +227,24 @@ Options:
216
227
  --business-name <name> Merchant business name (default: auto-generated)
217
228
  --phone <phone> Merchant phone number (optional)
218
229
  --address <address> Merchant address (optional)
219
- --env <environment> Environment: ci (default), stage, prod, or cn-prod
230
+ --skip-merchant 只注册用户 + token,跳过 Commerce merchant profile 设置
231
+ (token-only;用于 gateway E2E、或 commerce 未就绪的环境)
232
+ --env <environment> Environment: ci (default), stage, prod, cn-prod, or cn-stage
220
233
  --help, -h Show this help message
221
234
 
222
235
  Environments:
223
- ci CI 环境 (auth.optima.chat, api.optima.chat)
224
- stage Stage 环境 (auth.stage.optima.onl, api.stage.optima.onl)
225
- prod Prod 环境 (auth.optima.onl, api.optima.onl)
226
- cn-prod 阿里云生产环境 (auth.yzsgo.com, commerce.yzsgo.com) [#201]
236
+ ci CI 环境 (auth.optima.chat, api.optima.chat)
237
+ stage Stage 环境 (auth.stage.optima.onl, api.stage.optima.onl)
238
+ prod Prod 环境 (auth.optima.onl, api.optima.onl)
239
+ cn-prod 阿里云生产环境 (auth.yzsgo.com, commerce.yzsgo.com) [#201]
240
+ cn-stage 阿里云预发环境 (auth.stage.optima.chat, commerce.stage.optima.chat)
227
241
 
228
242
  Example:
229
243
  optima-generate-test-token
230
244
  optima-generate-test-token --env stage
231
245
  optima-generate-test-token --env prod
232
246
  optima-generate-test-token --env cn-prod
247
+ optima-generate-test-token --env cn-stage
233
248
  optima-generate-test-token --business-name "My Test Shop" --env stage
234
249
  `);
235
250
  process.exit(0);
@@ -254,8 +269,11 @@ Example:
254
269
  // 2. 获取 token
255
270
  const token = await getToken(email, password, config);
256
271
 
257
- // 3. 设置 merchant profile(Commerce API)
258
- const merchantProfile = await setupMerchantProfile(token, businessName, config);
272
+ // 3. 设置 merchant profile(Commerce API);--skip-merchant 时跳过(token-only
273
+ const merchantProfile = skipMerchant
274
+ ? { merchant_id: 'N/A (skipped)' } as MerchantSetupResponse
275
+ : await setupMerchantProfile(token, businessName, config);
276
+ if (skipMerchant) console.log('\n⏭️ Skipped merchant profile setup (--skip-merchant)');
259
277
 
260
278
  // 4. 保存 token 到临时文件
261
279
  const tmpDir = os.tmpdir();
@@ -19,13 +19,14 @@ $1 = 700 credits (P15 unified ledger; the USD wallet is retired).
19
19
  Options:
20
20
  --amount <usd> USD amount to grant (required, e.g. 5 for $5.00 = 3500 credits)
21
21
  --description <text> Description for audit trail (optional)
22
- --env <env> Environment: stage, prod, cn-prod (default: stage)
22
+ --env <env> Environment: stage, prod, cn-prod, cn-stage (default: stage)
23
23
  -h, --help Show this help
24
24
 
25
25
  Examples:
26
26
  optima-grant-balance user@example.com --amount 5 --env prod
27
27
  optima-grant-balance user@example.com --amount 10 --description "Service outage compensation"
28
- optima-grant-balance user@example.com --amount 1 --env cn-prod # ¥-priced env, still USD input ($1 = 700 credits)`);
28
+ optima-grant-balance user@example.com --amount 1 --env cn-prod # ¥-priced env, still USD input ($1 = 700 credits)
29
+ optima-grant-balance user@example.com --amount 1 --env cn-stage # 阿里云预发`);
29
30
  process.exit(0);
30
31
  }
31
32
 
@@ -55,10 +56,10 @@ async function main() {
55
56
  console.log(`\n🎁 Granting $${amountUsd.toFixed(2)} (${Math.round(amountUsd * 700)} credits) to ${email} [${env.toUpperCase()}]\n`);
56
57
  if (description) console.log(` Reason: ${description}`);
57
58
 
58
- // cn-prod has no SSH tunnel into the Aliyun RDS — resolve via user-auth's
59
- // internal lookup API instead of the direct SQL path.
59
+ // cn-prod / cn-stage have no SSH tunnel into the Aliyun RDS — resolve via
60
+ // user-auth's internal lookup API instead of the direct SQL path.
60
61
  let userId: string;
61
- if (env === 'cn-prod') {
62
+ if (env === 'cn-prod' || env === 'cn-stage') {
62
63
  userId = await resolveUserIdByEmail(env, email);
63
64
  } else {
64
65
  const infisicalConfig = getInfisicalConfig();
@@ -60,7 +60,7 @@ export function assertPhoneMatch(inputPhone: string, accountPhone: string | null
60
60
  */
61
61
  export function assertAwsEmailOnly(env: string, kind: 'email' | 'phone' | 'userId'): void {
62
62
  if ((env === 'stage' || env === 'prod') && kind !== 'email') {
63
- throw new Error('stage/prod 仅支持 email 标识;手机号/userId 解析仅 cn-prod 可用');
63
+ throw new Error('stage/prod 仅支持 email 标识;手机号/userId 解析仅 cn-prod / cn-stage 可用');
64
64
  }
65
65
  }
66
66
 
@@ -71,6 +71,8 @@ const PLANS_BY_ENV: Record<string, string[]> = {
71
71
  stage: ['trial', 'starter', 'pro', 'enterprise'],
72
72
  prod: ['trial', 'starter', 'pro', 'enterprise'],
73
73
  'cn-prod': ['trial', 'starter-cn', 'pro-cn', 'enterprise-cn'],
74
+ // cn-stage 卖同一套 CNY -cn plan(billing plan 目录与 cn-prod 一致)
75
+ 'cn-stage': ['trial', 'starter-cn', 'pro-cn', 'enterprise-cn'],
74
76
  };
75
77
 
76
78
  function parseArgs(args: string[]): { identifier: string; plan: string; months: number; env: string } {
@@ -79,9 +81,9 @@ function parseArgs(args: string[]): { identifier: string; plan: string; months:
79
81
 
80
82
  Options:
81
83
  --plan <id> Plan: trial, starter, pro, enterprise (default: pro)
82
- cn-prod plans: trial, starter-cn, pro-cn, enterprise-cn (default: pro-cn)
84
+ cn-prod/cn-stage plans: trial, starter-cn, pro-cn, enterprise-cn (default: pro-cn)
83
85
  --months <n> Duration in months (default: 1)
84
- --env <env> Environment: stage, prod, cn-prod (default: stage)
86
+ --env <env> Environment: stage, prod, cn-prod, cn-stage (default: stage)
85
87
  -h, --help Show this help`);
86
88
  process.exit(0);
87
89
  }
@@ -98,7 +100,7 @@ Options:
98
100
  }
99
101
 
100
102
  validateEnvCnProd(env);
101
- plan = plan ?? (env === 'cn-prod' ? 'pro-cn' : 'pro');
103
+ plan = plan ?? (env === 'cn-prod' || env === 'cn-stage' ? 'pro-cn' : 'pro');
102
104
  const allowed = PLANS_BY_ENV[env];
103
105
  if (!allowed.includes(plan)) {
104
106
  console.error(`Unknown plan for ${env}: ${plan}. Available: ${allowed.join(', ')}`);
@@ -118,13 +120,13 @@ async function main() {
118
120
 
119
121
  // identity is the verified target account to print on success. AWS keeps the
120
122
  // pre-change behavior (email-only, no internal HTTP reverse-verify); only
121
- // cn-prod runs the full classify→resolve→getUserById→phone-assert防呆 path.
123
+ // cn-prod / cn-stage run the full classify→resolve→getUserById→phone-assert防呆 path.
122
124
  let userId: string;
123
125
  let identity: { phone: string | null; email: string | null };
124
126
 
125
- if (env === 'cn-prod') {
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.
127
+ if (env === 'cn-prod' || env === 'cn-stage') {
128
+ // cn-prod / cn-stage have no SSH tunnel into the Aliyun RDS — resolve via
129
+ // user-auth, and the dev-skills token carries internal:users:write for lookups.
128
130
  if (kind === 'userId') {
129
131
  userId = identifier;
130
132
  } else if (kind === 'phone') {
@@ -157,7 +157,7 @@ async function main() {
157
157
  console.error('Usage: query-db.ts <service> <sql> [environment]');
158
158
  console.error('');
159
159
  console.error('Services: commerce-backend, user-auth, agentic-chat, bi-backend, session-gateway, gateway-core, optima-logistics, billing, ads-backend, amazon-backend, browser-backend, shopify-backend, optima-generation, optima-sentinel');
160
- console.error('Environments: ci (default), stage, prod, cn (阿里云 cn-prod)');
160
+ console.error('Environments: ci (default), stage, prod, cn-prod (阿里云生产), cn-stage (阿里云预发)');
161
161
  console.error('');
162
162
  console.error('Example: query-db.ts user-auth "SELECT COUNT(*) FROM users" prod');
163
163
  process.exit(1);
@@ -174,17 +174,20 @@ async function main() {
174
174
  // cn-prod(阿里云):独立 Infisical + 经 buildbox 跳板连内网 RDS(动态端口隧道)。
175
175
  // 两类 cred:① shared-secrets/database-users(按 prefix)② 服务自己的 DATABASE_URL(展开引用)。
176
176
  if (isCnEnv(environment)) {
177
+ // cn-prod / cn-stage 共用 SERVICE_DB_MAP[*].prod 的 userKey/databaseUrlPath(prefix、
178
+ // /services/<svc> 路径两环境一致);connectCn* 按 env 切 cn Infisical 环境 + RDS 实例。
179
+ const label = environment === 'cn-stage' ? 'CN-STAGE' : 'CN-PROD';
177
180
  const prodCfg = SERVICE_DB_MAP[service as keyof typeof SERVICE_DB_MAP].prod as any;
178
181
  let db: { query: (sql: string) => string };
179
182
  if (prodCfg?.userKey) {
180
183
  const prefix = prodCfg.userKey.replace(/_DB_USER$/, '');
181
- console.log(`\n🔍 Querying ${service} (CN-PROD, prefix ${prefix})...`);
182
- db = connectCnDB(prefix);
184
+ console.log(`\n🔍 Querying ${service} (${label}, prefix ${prefix})...`);
185
+ db = connectCnDB(prefix, environment);
183
186
  } else if (prodCfg?.databaseUrlPath) {
184
- console.log(`\n🔍 Querying ${service} (CN-PROD, DATABASE_URL @ ${prodCfg.databaseUrlPath})...`);
185
- db = connectCnDBFromUrl(prodCfg.databaseUrlPath);
187
+ console.log(`\n🔍 Querying ${service} (${label}, DATABASE_URL @ ${prodCfg.databaseUrlPath})...`);
188
+ db = connectCnDBFromUrl(prodCfg.databaseUrlPath, environment);
186
189
  } else {
187
- console.error(`cn-prod query 暂不支持 ${service}(既无 userKey 也无 databaseUrlPath)。见 optima-dev-skills#21。`);
190
+ console.error(`${label} query 暂不支持 ${service}(既无 userKey 也无 databaseUrlPath)。见 optima-dev-skills#21。`);
188
191
  process.exit(1);
189
192
  }
190
193
  console.log('\n' + db.query(sql));
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { execSync } from 'child_process';
4
- import { getInfisicalConfig, getInfisicalToken, InfisicalConfig } from './db-utils';
4
+ import { getInfisicalConfig, getInfisicalToken, InfisicalConfig, isCnEnv, cnInfisicalEnv, getCnInfisicalToken, getCnSecrets } from './db-utils';
5
5
 
6
6
  // 支持的服务列表(Infisical 路径为 /services/<service-name>)
7
7
  const SUPPORTED_SERVICES = [
@@ -32,12 +32,17 @@ const SUPPORTED_SERVICES = [
32
32
  'shopify-backend',
33
33
  ];
34
34
 
35
- // 环境到 Infisical environment 的映射
35
+ // 环境到 AWS Infisical environment 的映射(cn 走独立 cn Infisical,见下方分支)
36
36
  const ENV_MAP: Record<string, string> = {
37
37
  stage: 'staging',
38
38
  prod: 'prod'
39
39
  };
40
40
 
41
+ /** 该 environment 是否被支持(AWS stage/prod 或阿里云 cn-prod/cn-stage)。 */
42
+ function isSupportedEnv(env: string): boolean {
43
+ return ENV_MAP[env] !== undefined || isCnEnv(env);
44
+ }
45
+
41
46
  // NOTE: getInfisicalSecrets is kept local because it encodes secretPath (encodeURIComponent),
42
47
  // unlike db-utils' raw-path variant; getGitHubVariable/getInfisicalConfig/getInfisicalToken are shared from db-utils.
43
48
  function getInfisicalSecrets(config: InfisicalConfig, token: string, environment: string, secretPath: string): Record<string, string> {
@@ -60,17 +65,22 @@ Usage: optima-show-env <service> <environment> [options]
60
65
 
61
66
  Arguments:
62
67
  service Service name (${SUPPORTED_SERVICES.join(', ')})
63
- environment Environment (stage, prod)
68
+ environment Environment (stage, prod, cn-prod, cn-stage)
64
69
 
65
70
  Options:
66
71
  --filter Filter by key pattern (e.g., --filter DATABASE)
67
72
  --keys-only Show only key names, not values
68
73
  --help Show this help message
69
74
 
75
+ Note:
76
+ cn-prod / cn-stage 读阿里云 cn Infisical(需 INFISICAL_CN_EMAIL/PASSWORD 环境变量,
77
+ admin user,见 optima-dev-skills#21)。stage/prod 读 AWS Infisical(GitHub Variables)。
78
+
70
79
  Examples:
71
80
  optima-show-env commerce-backend stage
72
81
  optima-show-env user-auth prod --filter DATABASE
73
- optima-show-env bi stage --keys-only
82
+ optima-show-env gateway-core cn-stage
83
+ optima-show-env bi-backend stage --keys-only
74
84
  `);
75
85
  }
76
86
 
@@ -114,9 +124,9 @@ async function main() {
114
124
  }
115
125
 
116
126
  // 验证环境
117
- if (!ENV_MAP[environment]) {
127
+ if (!isSupportedEnv(environment)) {
118
128
  console.error(`Error: Unknown environment '${environment}'`);
119
- console.error('Available environments: stage, prod');
129
+ console.error('Available environments: stage, prod, cn-prod, cn-stage');
120
130
  process.exit(1);
121
131
  }
122
132
 
@@ -126,15 +136,24 @@ async function main() {
126
136
  console.log(`\n🔍 Fetching environment variables for ${service} (${environment.toUpperCase()})...\n`);
127
137
 
128
138
  try {
129
- const infisicalConfig = getInfisicalConfig();
130
- console.log('✓ Loaded Infisical config from GitHub Variables');
131
-
132
- const token = getInfisicalToken(infisicalConfig);
133
- console.log('✓ Obtained Infisical access token');
134
-
135
- const infisicalEnv = ENV_MAP[environment];
136
- const secrets = getInfisicalSecrets(infisicalConfig, token, infisicalEnv, secretPath);
137
- console.log(`✓ Retrieved secrets from Infisical (path: ${secretPath})\n`);
139
+ let secrets: Record<string, string>;
140
+ if (isCnEnv(environment)) {
141
+ // 阿里云 cn Infisical(独立实例,admin email/password 认证;prod / staging 双环境)
142
+ const cnToken = getCnInfisicalToken();
143
+ console.log('✓ Obtained cn Infisical access token');
144
+ secrets = getCnSecrets(cnToken, secretPath, false, cnInfisicalEnv(environment));
145
+ console.log(`✓ Retrieved secrets from cn Infisical (env: ${cnInfisicalEnv(environment)}, path: ${secretPath})\n`);
146
+ } else {
147
+ const infisicalConfig = getInfisicalConfig();
148
+ console.log('✓ Loaded Infisical config from GitHub Variables');
149
+
150
+ const token = getInfisicalToken(infisicalConfig);
151
+ console.log('✓ Obtained Infisical access token');
152
+
153
+ const infisicalEnv = ENV_MAP[environment];
154
+ secrets = getInfisicalSecrets(infisicalConfig, token, infisicalEnv, secretPath);
155
+ console.log(`✓ Retrieved secrets from Infisical (path: ${secretPath})\n`);
156
+ }
138
157
 
139
158
  const keys = Object.keys(secrets).sort();
140
159
 
@@ -29,22 +29,23 @@ 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';
33
- interface SvcCfg { path: string; stage?: string; prod?: string; cn?: string; cn_path?: string; }
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; }
34
34
 
35
35
  // 服务 × 环境 FQDN 表。某服务某环境没部署 → 该 env 键缺省,探时跳过。
36
36
  // cn-prod 真实 subdomain 抄自 optima-terraform alicloud/stacks/cn-prod-ingress-sae/main.tf。
37
37
  // #201 (2026-06-12): yzsgo.com 全量迁移完成,旧 *-cn.optima.chat 路由已下线。
38
+ // cn-stage(阿里云预发)域名 *.stage.optima.chat,抄自 cn-stage-ingress-sae services map。
38
39
  const SERVICES: Record<string, SvcCfg> = {
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' },
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' },
42
43
  'mcp-host': { path: '/health', stage: 'mcp-stage.optima.onl', prod: 'mcp.optima.onl' },
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' },
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' },
46
47
  };
47
- const ENVS: Env[] = ['stage', 'prod', 'cn'];
48
+ const ENVS: Env[] = ['stage', 'prod', 'cn', 'cn-stage'];
48
49
 
49
50
  const G = '\x1b[32m', R = '\x1b[31m', Y = '\x1b[33m', B = '\x1b[34m', N = '\x1b[0m';
50
51
  const MARK: Record<string, string> = { ok: `${G}✅${N}`, fail: `${R}❌${N}`, warn: `${Y}⚠️ ${N}`, na: `${B}··${N}` };
@@ -16,12 +16,15 @@ const USER_AUTH_URLS = {
16
16
  prod: 'https://auth.optima.onl',
17
17
  // #201: yzsgo.com 全量迁移 (2026-06-12), 旧 auth-cn.optima.chat 路由已下线
18
18
  'cn-prod': 'https://auth.yzsgo.com',
19
+ // cn-stage(阿里云预发,独立于 AWS stage `.optima.onl`)
20
+ 'cn-stage': 'https://auth.stage.optima.chat',
19
21
  };
20
- // cn-prod URLs are hardcoded: cn Infisical (secrets-cn.optima.chat) is a
22
+ // cn URLs are hardcoded: cn Infisical (secrets-cn.optima.chat) is a
21
23
  // separate instance dev-skills has no machine identity for, and these domains
22
24
  // are stable. AWS envs keep reading /shared-secrets/domain-urls.
23
25
  // #201: yzsgo.com 全量迁移 (2026-06-12), 旧 billing-cn.optima.chat 路由已下线
24
26
  const CN_PROD_BILLING_URL = 'https://billing-api.yzsgo.com';
27
+ const CN_STAGE_BILLING_URL = 'https://billing-api.stage.optima.chat';
25
28
  /**
26
29
  * Validate the --env flag value at command entry, before any I/O.
27
30
  *
@@ -46,8 +49,8 @@ function validateEnv(env) {
46
49
  * cn-prod typo fails fast instead of dying inside the tunnel setup.
47
50
  */
48
51
  function validateEnvCnProd(env) {
49
- if (env !== 'stage' && env !== 'prod' && env !== 'cn-prod') {
50
- throw new Error(`--env must be "stage", "prod" or "cn-prod" (got: ${env})`);
52
+ if (env !== 'stage' && env !== 'prod' && env !== 'cn-prod' && env !== 'cn-stage') {
53
+ throw new Error(`--env must be "stage", "prod", "cn-prod" or "cn-stage" (got: ${env})`);
51
54
  }
52
55
  return env;
53
56
  }
@@ -86,6 +89,8 @@ const skillsUrlCache = {};
86
89
  function getBillingUrl(env) {
87
90
  if (env === 'cn-prod')
88
91
  return CN_PROD_BILLING_URL;
92
+ if (env === 'cn-stage')
93
+ return CN_STAGE_BILLING_URL;
89
94
  if (billingUrlCache[env])
90
95
  return billingUrlCache[env];
91
96
  const url = (0, infisical_secrets_1.fetchInfisicalSecret)(env, '/shared-secrets/domain-urls', 'BILLING_URL');
@@ -102,17 +107,34 @@ function getSkillsUrl(env) {
102
107
  function getServiceToken(env) {
103
108
  if (tokenCache[env])
104
109
  return tokenCache[env];
105
- const cfg = (0, db_utils_1.getInfisicalConfig)();
106
- const tok = (0, db_utils_1.getInfisicalToken)(cfg);
107
- // Fetch BOTH client_id and client_secret from Infisical they differ per env.
108
- // cn-prod credentials are mirrored in the AWS Infisical *prod* environment
109
- // (fetchInfisicalSecret has no cn-prod env slug), under CN-specific keys.
110
- const isCn = env === 'cn-prod';
111
- const infisicalEnv = isCn ? 'prod' : env;
112
- const idKey = isCn ? DEV_SKILLS_CN_CLIENT_ID_KEY : DEV_SKILLS_CLIENT_ID_KEY;
113
- const secretKey = isCn ? DEV_SKILLS_CN_CLIENT_SECRET_KEY : DEV_SKILLS_CLIENT_SECRET_KEY;
114
- const clientId = (0, infisical_secrets_1.fetchInfisicalSecret)(infisicalEnv, DEV_SKILLS_OAUTH_PATH, idKey, cfg, tok);
115
- const clientSecret = (0, infisical_secrets_1.fetchInfisicalSecret)(infisicalEnv, DEV_SKILLS_OAUTH_PATH, secretKey, cfg, tok);
110
+ // cn-prod cn-stage 都用 client_credentials M2M token,scope=internal:users:write。
111
+ // 凭证来源不同:
112
+ // · cn-prod —— 镜像在 AWS Infisical prod 环境(CN_PROD_ 前缀键),复用既有 AWS 访问。
113
+ // · cn-stage —— canonical cn Infisical staging /shared-secrets/oauth-clients
114
+ // (dev-skills 无该 client AWS 镜像;用 admin email/password 直读,
115
+ // 运行时需 INFISICAL_CN_EMAIL/PASSWORD,与 query-db cn 同一前提)。
116
+ const isCn = env === 'cn-prod' || env === 'cn-stage';
117
+ let clientId;
118
+ let clientSecret;
119
+ if (env === 'cn-stage') {
120
+ const cnTok = (0, db_utils_1.getCnInfisicalToken)();
121
+ const oc = (0, db_utils_1.getCnSecrets)(cnTok, '/shared-secrets/oauth-clients', false, 'staging');
122
+ clientId = oc['DEV_SKILLS_OAUTH_CLIENT_ID'];
123
+ clientSecret = oc['DEV_SKILLS_OAUTH_CLIENT_SECRET'];
124
+ if (!clientId || !clientSecret) {
125
+ throw new Error('cn-stage dev-skills OAuth client 凭证缺失(cn Infisical staging /shared-secrets/oauth-clients 的 DEV_SKILLS_OAUTH_CLIENT_ID/SECRET)。');
126
+ }
127
+ }
128
+ else {
129
+ const cfg = (0, db_utils_1.getInfisicalConfig)();
130
+ const tok = (0, db_utils_1.getInfisicalToken)(cfg);
131
+ // Fetch BOTH client_id and client_secret from Infisical — they differ per env.
132
+ const infisicalEnv = env === 'cn-prod' ? 'prod' : env;
133
+ const idKey = env === 'cn-prod' ? DEV_SKILLS_CN_CLIENT_ID_KEY : DEV_SKILLS_CLIENT_ID_KEY;
134
+ const secretKey = env === 'cn-prod' ? DEV_SKILLS_CN_CLIENT_SECRET_KEY : DEV_SKILLS_CLIENT_SECRET_KEY;
135
+ clientId = (0, infisical_secrets_1.fetchInfisicalSecret)(infisicalEnv, DEV_SKILLS_OAUTH_PATH, idKey, cfg, tok);
136
+ clientSecret = (0, infisical_secrets_1.fetchInfisicalSecret)(infisicalEnv, DEV_SKILLS_OAUTH_PATH, secretKey, cfg, tok);
137
+ }
116
138
  const authUrl = USER_AUTH_URLS[env];
117
139
  if (!authUrl)
118
140
  throw new Error(`Unknown env: ${env}`);
@@ -35,6 +35,9 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.RDS_HOSTS = void 0;
37
37
  exports.isCnEnv = isCnEnv;
38
+ exports.isCnStageEnv = isCnStageEnv;
39
+ exports.cnInfisicalEnv = cnInfisicalEnv;
40
+ exports.cnRdsHost = cnRdsHost;
38
41
  exports.escapeSQL = escapeSQL;
39
42
  exports.getGitHubVariable = getGitHubVariable;
40
43
  exports.getInfisicalConfig = getInfisicalConfig;
@@ -67,14 +70,29 @@ const EC2_HOST = '3.0.210.113';
67
70
  // ─── cn-prod (阿里云) 常量 ────────────────────────────────────────────────────
68
71
  // cn-prod 跑在阿里云 SAE,与海外 AWS 完全独立:独立 Infisical (secrets-cn.optima.chat)
69
72
  // + 内网 RDS(无公网端点)经 buildbox ECS 跳板 SSH 隧道访问。设计见 optima-dev-skills#21。
73
+ // cn Infisical 是【单实例双环境】:prod=cn-prod、staging=cn-stage(URL/org/project 共享,
74
+ // 只切 environment 参数)。cn-stage 数据面共用 prod VPC,故走【同一 buildbox】跳板。
70
75
  const CN_INFISICAL_URL = process.env.INFISICAL_CN_URL || 'https://secrets-cn.optima.chat';
71
76
  const CN_INFISICAL_ORG = process.env.INFISICAL_CN_ORG || 'f44012fa-0659-4f7e-b0ac-ed01244efc8a';
72
77
  const CN_INFISICAL_PROJECT = process.env.INFISICAL_CN_PROJECT || '4deef229-11be-40a5-8f56-b61f0bce7240';
73
- const CN_RDS_HOST = 'pgm-2zexwx9eso9e4yla.pg.rds.aliyuncs.com';
78
+ const CN_RDS_HOST = 'pgm-2zexwx9eso9e4yla.pg.rds.aliyuncs.com'; // cn-prod RDS
79
+ const CN_STAGE_RDS_HOST = 'pgm-2zem1u9zdh06boim.pg.rds.aliyuncs.com'; // cn-stage RDS(独立实例,共用 prod VPC)
74
80
  const CN_BUILDBOX_HOST = process.env.OPTIMA_CN_BUILDBOX_HOST || '47.94.105.163';
75
- /** env 是否指向 cn-prod(阿里云)。接受 `cn` / `cn-prod`。 */
81
+ /** env 是否指向阿里云(cn-prod 或 cn-stage)。接受 `cn` / `cn-prod` / `cn-stage`。 */
76
82
  function isCnEnv(env) {
77
- return env === 'cn' || env === 'cn-prod';
83
+ return env === 'cn' || env === 'cn-prod' || env === 'cn-stage';
84
+ }
85
+ /** env 是否指向 cn-stage(阿里云预发)。 */
86
+ function isCnStageEnv(env) {
87
+ return env === 'cn-stage';
88
+ }
89
+ /** cn env → cn Infisical environment slug。cn-stage→staging,其余 cn→prod。 */
90
+ function cnInfisicalEnv(env) {
91
+ return env === 'cn-stage' ? 'staging' : 'prod';
92
+ }
93
+ /** cn env → 内网 RDS host。cn-stage→stage 实例,其余 cn→prod 实例。 */
94
+ function cnRdsHost(env) {
95
+ return env === 'cn-stage' ? CN_STAGE_RDS_HOST : CN_RDS_HOST;
78
96
  }
79
97
  // ─── SQL escaping ───────────────────────────────────────────────────────────
80
98
  /** Escape a string value for safe inclusion in SQL single-quoted literals. */
@@ -146,10 +164,13 @@ function getCnInfisicalToken() {
146
164
  throw new Error(`cn Infisical select-organization 失败: ${JSON.stringify(org).slice(0, 200)}`);
147
165
  return org.token;
148
166
  }
149
- /** 读 cn Infisical 某 folder(prod env)→ key→value map。expand=true 让服务端解析 `${...}` 引用。 */
150
- function getCnSecrets(token, secretPath, expand = false) {
167
+ /**
168
+ * cn Infisical 某 folder → key→value map。expand=true 让服务端解析 `${...}` 引用。
169
+ * environment:cn-prod 用 `prod`、cn-stage 用 `staging`(同一实例双环境)。
170
+ */
171
+ function getCnSecrets(token, secretPath, expand = false, environment = 'prod') {
151
172
  const data = curlJson([
152
- `${CN_INFISICAL_URL}/api/v3/secrets/raw?workspaceId=${CN_INFISICAL_PROJECT}&environment=prod&secretPath=${encodeURIComponent(secretPath)}&expandSecretReferences=${expand}`,
173
+ `${CN_INFISICAL_URL}/api/v3/secrets/raw?workspaceId=${CN_INFISICAL_PROJECT}&environment=${environment}&secretPath=${encodeURIComponent(secretPath)}&expandSecretReferences=${expand}`,
153
174
  '-H', `Authorization: Bearer ${token}`,
154
175
  ]);
155
176
  const secrets = {};
@@ -460,17 +481,18 @@ function queryDB(conn, sql) {
460
481
  * 连 cn-prod 某服务库:creds 取自 cn Infisical 的 /shared-secrets/database-users +
461
482
  * /database-names(按 prefix,如 AUTH/BILLING),经 buildbox 跳板隧道(动态端口)。
462
483
  */
463
- function connectCnDB(prefix) {
484
+ function connectCnDB(prefix, cnEnv = 'cn-prod') {
485
+ const infEnv = cnInfisicalEnv(cnEnv);
464
486
  const token = getCnInfisicalToken();
465
- const users = getCnSecrets(token, '/shared-secrets/database-users');
466
- const names = getCnSecrets(token, '/shared-secrets/database-names');
487
+ const users = getCnSecrets(token, '/shared-secrets/database-users', false, infEnv);
488
+ const names = getCnSecrets(token, '/shared-secrets/database-names', false, infEnv);
467
489
  const user = users[`${prefix}_DB_USER`];
468
490
  const password = users[`${prefix}_DB_PASSWORD`];
469
491
  const database = names[`${prefix}_DB_NAME`];
470
492
  if (!user || !password || !database) {
471
493
  throw new Error(`cn DB creds 不全 for ${prefix}(需 ${prefix}_DB_USER/PASSWORD@database-users + ${prefix}_DB_NAME@database-names)。该服务 cn 可能用字面 DATABASE_URL,见 optima-dev-skills#21。`);
472
494
  }
473
- const port = ensureTunnel(CN_RDS_HOST, 'cn-buildbox');
495
+ const port = ensureTunnel(cnRdsHost(cnEnv), 'cn-buildbox');
474
496
  const conn = { host: 'localhost', port, user, password, database };
475
497
  return { query: (sql) => queryDB(conn, sql) };
476
498
  }
@@ -479,9 +501,9 @@ function connectCnDB(prefix) {
479
501
  * 用于 cred 不在 shared-secrets/database-users 的服务(如 gateway-core)。
480
502
  * expandSecretReferences=true 让 cn Infisical 解析 `${...}` 引用为字面值。
481
503
  */
482
- function connectCnDBFromUrl(servicePath) {
504
+ function connectCnDBFromUrl(servicePath, cnEnv = 'cn-prod') {
483
505
  const token = getCnInfisicalToken();
484
- const secrets = getCnSecrets(token, servicePath, true);
506
+ const secrets = getCnSecrets(token, servicePath, true, cnInfisicalEnv(cnEnv));
485
507
  const dbUrl = secrets['DATABASE_URL'];
486
508
  if (!dbUrl)
487
509
  throw new Error(`cn DATABASE_URL 未找到 at ${servicePath}`);
@@ -64,6 +64,14 @@ const ENV_CONFIG = {
64
64
  // 注意不要换成同名的 confidential client `commerce-cli-cn-prod-*`,那个需要 client_secret,走不通 CLI 的 public ROPC flow。
65
65
  clientId: 'dev-skill-cli-cn-pro-acvkmcuq',
66
66
  envName: 'cn-prod'
67
+ },
68
+ 'cn-stage': {
69
+ // 阿里云预发(独立于 AWS stage `.optima.onl`)。域名 *.stage.optima.chat。
70
+ authUrl: 'https://auth.stage.optima.chat',
71
+ apiUrl: 'https://commerce.stage.optima.chat',
72
+ // cn-stage user-auth 里注册的 public client(开 password grant,ROPC flow)。
73
+ clientId: 'dev-skill-cli-cn-sta-3dvsxzdo',
74
+ envName: 'cn-stage'
67
75
  }
68
76
  };
69
77
  async function httpRequest(url, options = {}) {
@@ -166,6 +174,7 @@ async function main() {
166
174
  let phone;
167
175
  let address;
168
176
  let environment = 'ci';
177
+ let skipMerchant = false;
169
178
  // 解析命令行参数
170
179
  for (let i = 0; i < args.length; i++) {
171
180
  const arg = args[i];
@@ -184,13 +193,16 @@ async function main() {
184
193
  else if (arg === '--address' && args[i + 1]) {
185
194
  address = args[++i];
186
195
  }
196
+ else if (arg === '--skip-merchant') {
197
+ skipMerchant = true;
198
+ }
187
199
  else if (arg === '--env' && args[i + 1]) {
188
200
  const envArg = args[++i];
189
- if (envArg === 'ci' || envArg === 'stage' || envArg === 'prod' || envArg === 'cn-prod') {
201
+ if (envArg === 'ci' || envArg === 'stage' || envArg === 'prod' || envArg === 'cn-prod' || envArg === 'cn-stage') {
190
202
  environment = envArg;
191
203
  }
192
204
  else {
193
- console.error(`❌ Invalid environment: ${envArg}. Must be 'ci', 'stage', 'prod', or 'cn-prod'.`);
205
+ console.error(`❌ Invalid environment: ${envArg}. Must be 'ci', 'stage', 'prod', 'cn-prod', or 'cn-stage'.`);
194
206
  process.exit(1);
195
207
  }
196
208
  }
@@ -204,20 +216,24 @@ Options:
204
216
  --business-name <name> Merchant business name (default: auto-generated)
205
217
  --phone <phone> Merchant phone number (optional)
206
218
  --address <address> Merchant address (optional)
207
- --env <environment> Environment: ci (default), stage, prod, or cn-prod
219
+ --skip-merchant 只注册用户 + token,跳过 Commerce merchant profile 设置
220
+ (token-only;用于 gateway E2E、或 commerce 未就绪的环境)
221
+ --env <environment> Environment: ci (default), stage, prod, cn-prod, or cn-stage
208
222
  --help, -h Show this help message
209
223
 
210
224
  Environments:
211
- ci CI 环境 (auth.optima.chat, api.optima.chat)
212
- stage Stage 环境 (auth.stage.optima.onl, api.stage.optima.onl)
213
- prod Prod 环境 (auth.optima.onl, api.optima.onl)
214
- cn-prod 阿里云生产环境 (auth.yzsgo.com, commerce.yzsgo.com) [#201]
225
+ ci CI 环境 (auth.optima.chat, api.optima.chat)
226
+ stage Stage 环境 (auth.stage.optima.onl, api.stage.optima.onl)
227
+ prod Prod 环境 (auth.optima.onl, api.optima.onl)
228
+ cn-prod 阿里云生产环境 (auth.yzsgo.com, commerce.yzsgo.com) [#201]
229
+ cn-stage 阿里云预发环境 (auth.stage.optima.chat, commerce.stage.optima.chat)
215
230
 
216
231
  Example:
217
232
  optima-generate-test-token
218
233
  optima-generate-test-token --env stage
219
234
  optima-generate-test-token --env prod
220
235
  optima-generate-test-token --env cn-prod
236
+ optima-generate-test-token --env cn-stage
221
237
  optima-generate-test-token --business-name "My Test Shop" --env stage
222
238
  `);
223
239
  process.exit(0);
@@ -237,8 +253,12 @@ Example:
237
253
  const user = await registerMerchant(email, password, businessName, config, phone, address);
238
254
  // 2. 获取 token
239
255
  const token = await getToken(email, password, config);
240
- // 3. 设置 merchant profile(Commerce API)
241
- const merchantProfile = await setupMerchantProfile(token, businessName, config);
256
+ // 3. 设置 merchant profile(Commerce API);--skip-merchant 时跳过(token-only
257
+ const merchantProfile = skipMerchant
258
+ ? { merchant_id: 'N/A (skipped)' }
259
+ : await setupMerchantProfile(token, businessName, config);
260
+ if (skipMerchant)
261
+ console.log('\n⏭️ Skipped merchant profile setup (--skip-merchant)');
242
262
  // 4. 保存 token 到临时文件
243
263
  const tmpDir = os.tmpdir();
244
264
  const tokenFileName = `optima-test-token-${Date.now()}.txt`;
@@ -19,13 +19,14 @@ $1 = 700 credits (P15 unified ledger; the USD wallet is retired).
19
19
  Options:
20
20
  --amount <usd> USD amount to grant (required, e.g. 5 for $5.00 = 3500 credits)
21
21
  --description <text> Description for audit trail (optional)
22
- --env <env> Environment: stage, prod, cn-prod (default: stage)
22
+ --env <env> Environment: stage, prod, cn-prod, cn-stage (default: stage)
23
23
  -h, --help Show this help
24
24
 
25
25
  Examples:
26
26
  optima-grant-balance user@example.com --amount 5 --env prod
27
27
  optima-grant-balance user@example.com --amount 10 --description "Service outage compensation"
28
- optima-grant-balance user@example.com --amount 1 --env cn-prod # ¥-priced env, still USD input ($1 = 700 credits)`);
28
+ optima-grant-balance user@example.com --amount 1 --env cn-prod # ¥-priced env, still USD input ($1 = 700 credits)
29
+ optima-grant-balance user@example.com --amount 1 --env cn-stage # 阿里云预发`);
29
30
  process.exit(0);
30
31
  }
31
32
  const email = args[0];
@@ -55,10 +56,10 @@ async function main() {
55
56
  console.log(`\n🎁 Granting $${amountUsd.toFixed(2)} (${Math.round(amountUsd * 700)} credits) to ${email} [${env.toUpperCase()}]\n`);
56
57
  if (description)
57
58
  console.log(` Reason: ${description}`);
58
- // cn-prod has no SSH tunnel into the Aliyun RDS — resolve via user-auth's
59
- // internal lookup API instead of the direct SQL path.
59
+ // cn-prod / cn-stage have no SSH tunnel into the Aliyun RDS — resolve via
60
+ // user-auth's internal lookup API instead of the direct SQL path.
60
61
  let userId;
61
- if (env === 'cn-prod') {
62
+ if (env === 'cn-prod' || env === 'cn-stage') {
62
63
  userId = await (0, billing_http_1.resolveUserIdByEmail)(env, email);
63
64
  }
64
65
  else {
@@ -55,7 +55,7 @@ function assertPhoneMatch(inputPhone, accountPhone) {
55
55
  */
56
56
  function assertAwsEmailOnly(env, kind) {
57
57
  if ((env === 'stage' || env === 'prod') && kind !== 'email') {
58
- throw new Error('stage/prod 仅支持 email 标识;手机号/userId 解析仅 cn-prod 可用');
58
+ throw new Error('stage/prod 仅支持 email 标识;手机号/userId 解析仅 cn-prod / cn-stage 可用');
59
59
  }
60
60
  }
61
61
  // cn-prod sells the CNY-priced -cn plans (P12); the bare USD plan ids also
@@ -65,6 +65,8 @@ const PLANS_BY_ENV = {
65
65
  stage: ['trial', 'starter', 'pro', 'enterprise'],
66
66
  prod: ['trial', 'starter', 'pro', 'enterprise'],
67
67
  'cn-prod': ['trial', 'starter-cn', 'pro-cn', 'enterprise-cn'],
68
+ // cn-stage 卖同一套 CNY -cn plan(billing plan 目录与 cn-prod 一致)
69
+ 'cn-stage': ['trial', 'starter-cn', 'pro-cn', 'enterprise-cn'],
68
70
  };
69
71
  function parseArgs(args) {
70
72
  if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
@@ -72,9 +74,9 @@ function parseArgs(args) {
72
74
 
73
75
  Options:
74
76
  --plan <id> Plan: trial, starter, pro, enterprise (default: pro)
75
- cn-prod plans: trial, starter-cn, pro-cn, enterprise-cn (default: pro-cn)
77
+ cn-prod/cn-stage plans: trial, starter-cn, pro-cn, enterprise-cn (default: pro-cn)
76
78
  --months <n> Duration in months (default: 1)
77
- --env <env> Environment: stage, prod, cn-prod (default: stage)
79
+ --env <env> Environment: stage, prod, cn-prod, cn-stage (default: stage)
78
80
  -h, --help Show this help`);
79
81
  process.exit(0);
80
82
  }
@@ -94,7 +96,7 @@ Options:
94
96
  }
95
97
  }
96
98
  (0, billing_http_1.validateEnvCnProd)(env);
97
- plan = plan ?? (env === 'cn-prod' ? 'pro-cn' : 'pro');
99
+ plan = plan ?? (env === 'cn-prod' || env === 'cn-stage' ? 'pro-cn' : 'pro');
98
100
  const allowed = PLANS_BY_ENV[env];
99
101
  if (!allowed.includes(plan)) {
100
102
  console.error(`Unknown plan for ${env}: ${plan}. Available: ${allowed.join(', ')}`);
@@ -113,12 +115,12 @@ async function main() {
113
115
  console.log(`\n🎁 Granting ${plan} subscription to ${identifier} (${kind}) for ${months} month(s) [${env.toUpperCase()}]\n`);
114
116
  // identity is the verified target account to print on success. AWS keeps the
115
117
  // pre-change behavior (email-only, no internal HTTP reverse-verify); only
116
- // cn-prod runs the full classify→resolve→getUserById→phone-assert防呆 path.
118
+ // cn-prod / cn-stage run the full classify→resolve→getUserById→phone-assert防呆 path.
117
119
  let userId;
118
120
  let identity;
119
- if (env === 'cn-prod') {
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.
121
+ if (env === 'cn-prod' || env === 'cn-stage') {
122
+ // cn-prod / cn-stage have no SSH tunnel into the Aliyun RDS — resolve via
123
+ // user-auth, and the dev-skills token carries internal:users:write for lookups.
122
124
  if (kind === 'userId') {
123
125
  userId = identifier;
124
126
  }
@@ -170,7 +170,7 @@ async function main() {
170
170
  console.error('Usage: query-db.ts <service> <sql> [environment]');
171
171
  console.error('');
172
172
  console.error('Services: commerce-backend, user-auth, agentic-chat, bi-backend, session-gateway, gateway-core, optima-logistics, billing, ads-backend, amazon-backend, browser-backend, shopify-backend, optima-generation, optima-sentinel');
173
- console.error('Environments: ci (default), stage, prod, cn (阿里云 cn-prod)');
173
+ console.error('Environments: ci (default), stage, prod, cn-prod (阿里云生产), cn-stage (阿里云预发)');
174
174
  console.error('');
175
175
  console.error('Example: query-db.ts user-auth "SELECT COUNT(*) FROM users" prod');
176
176
  process.exit(1);
@@ -184,19 +184,22 @@ async function main() {
184
184
  // cn-prod(阿里云):独立 Infisical + 经 buildbox 跳板连内网 RDS(动态端口隧道)。
185
185
  // 两类 cred:① shared-secrets/database-users(按 prefix)② 服务自己的 DATABASE_URL(展开引用)。
186
186
  if ((0, db_utils_1.isCnEnv)(environment)) {
187
+ // cn-prod / cn-stage 共用 SERVICE_DB_MAP[*].prod 的 userKey/databaseUrlPath(prefix、
188
+ // /services/<svc> 路径两环境一致);connectCn* 按 env 切 cn Infisical 环境 + RDS 实例。
189
+ const label = environment === 'cn-stage' ? 'CN-STAGE' : 'CN-PROD';
187
190
  const prodCfg = SERVICE_DB_MAP[service].prod;
188
191
  let db;
189
192
  if (prodCfg?.userKey) {
190
193
  const prefix = prodCfg.userKey.replace(/_DB_USER$/, '');
191
- console.log(`\n🔍 Querying ${service} (CN-PROD, prefix ${prefix})...`);
192
- db = (0, db_utils_1.connectCnDB)(prefix);
194
+ console.log(`\n🔍 Querying ${service} (${label}, prefix ${prefix})...`);
195
+ db = (0, db_utils_1.connectCnDB)(prefix, environment);
193
196
  }
194
197
  else if (prodCfg?.databaseUrlPath) {
195
- console.log(`\n🔍 Querying ${service} (CN-PROD, DATABASE_URL @ ${prodCfg.databaseUrlPath})...`);
196
- db = (0, db_utils_1.connectCnDBFromUrl)(prodCfg.databaseUrlPath);
198
+ console.log(`\n🔍 Querying ${service} (${label}, DATABASE_URL @ ${prodCfg.databaseUrlPath})...`);
199
+ db = (0, db_utils_1.connectCnDBFromUrl)(prodCfg.databaseUrlPath, environment);
197
200
  }
198
201
  else {
199
- console.error(`cn-prod query 暂不支持 ${service}(既无 userKey 也无 databaseUrlPath)。见 optima-dev-skills#21。`);
202
+ console.error(`${label} query 暂不支持 ${service}(既无 userKey 也无 databaseUrlPath)。见 optima-dev-skills#21。`);
200
203
  process.exit(1);
201
204
  }
202
205
  console.log('\n' + db.query(sql));
@@ -31,11 +31,15 @@ const SUPPORTED_SERVICES = [
31
31
  'optima-sentinel-worker',
32
32
  'shopify-backend',
33
33
  ];
34
- // 环境到 Infisical environment 的映射
34
+ // 环境到 AWS Infisical environment 的映射(cn 走独立 cn Infisical,见下方分支)
35
35
  const ENV_MAP = {
36
36
  stage: 'staging',
37
37
  prod: 'prod'
38
38
  };
39
+ /** 该 environment 是否被支持(AWS stage/prod 或阿里云 cn-prod/cn-stage)。 */
40
+ function isSupportedEnv(env) {
41
+ return ENV_MAP[env] !== undefined || (0, db_utils_1.isCnEnv)(env);
42
+ }
39
43
  // NOTE: getInfisicalSecrets is kept local because it encodes secretPath (encodeURIComponent),
40
44
  // unlike db-utils' raw-path variant; getGitHubVariable/getInfisicalConfig/getInfisicalToken are shared from db-utils.
41
45
  function getInfisicalSecrets(config, token, environment, secretPath) {
@@ -53,17 +57,22 @@ Usage: optima-show-env <service> <environment> [options]
53
57
 
54
58
  Arguments:
55
59
  service Service name (${SUPPORTED_SERVICES.join(', ')})
56
- environment Environment (stage, prod)
60
+ environment Environment (stage, prod, cn-prod, cn-stage)
57
61
 
58
62
  Options:
59
63
  --filter Filter by key pattern (e.g., --filter DATABASE)
60
64
  --keys-only Show only key names, not values
61
65
  --help Show this help message
62
66
 
67
+ Note:
68
+ cn-prod / cn-stage 读阿里云 cn Infisical(需 INFISICAL_CN_EMAIL/PASSWORD 环境变量,
69
+ admin user,见 optima-dev-skills#21)。stage/prod 读 AWS Infisical(GitHub Variables)。
70
+
63
71
  Examples:
64
72
  optima-show-env commerce-backend stage
65
73
  optima-show-env user-auth prod --filter DATABASE
66
- optima-show-env bi stage --keys-only
74
+ optima-show-env gateway-core cn-stage
75
+ optima-show-env bi-backend stage --keys-only
67
76
  `);
68
77
  }
69
78
  async function main() {
@@ -101,22 +110,32 @@ async function main() {
101
110
  process.exit(1);
102
111
  }
103
112
  // 验证环境
104
- if (!ENV_MAP[environment]) {
113
+ if (!isSupportedEnv(environment)) {
105
114
  console.error(`Error: Unknown environment '${environment}'`);
106
- console.error('Available environments: stage, prod');
115
+ console.error('Available environments: stage, prod, cn-prod, cn-stage');
107
116
  process.exit(1);
108
117
  }
109
118
  // Infisical 路径为 /services/<service-name>
110
119
  const secretPath = `/services/${service}`;
111
120
  console.log(`\n🔍 Fetching environment variables for ${service} (${environment.toUpperCase()})...\n`);
112
121
  try {
113
- const infisicalConfig = (0, db_utils_1.getInfisicalConfig)();
114
- console.log('✓ Loaded Infisical config from GitHub Variables');
115
- const token = (0, db_utils_1.getInfisicalToken)(infisicalConfig);
116
- console.log('✓ Obtained Infisical access token');
117
- const infisicalEnv = ENV_MAP[environment];
118
- const secrets = getInfisicalSecrets(infisicalConfig, token, infisicalEnv, secretPath);
119
- console.log(`✓ Retrieved secrets from Infisical (path: ${secretPath})\n`);
122
+ let secrets;
123
+ if ((0, db_utils_1.isCnEnv)(environment)) {
124
+ // 阿里云 cn Infisical(独立实例,admin email/password 认证;prod / staging 双环境)
125
+ const cnToken = (0, db_utils_1.getCnInfisicalToken)();
126
+ console.log('✓ Obtained cn Infisical access token');
127
+ secrets = (0, db_utils_1.getCnSecrets)(cnToken, secretPath, false, (0, db_utils_1.cnInfisicalEnv)(environment));
128
+ console.log(`✓ Retrieved secrets from cn Infisical (env: ${(0, db_utils_1.cnInfisicalEnv)(environment)}, path: ${secretPath})\n`);
129
+ }
130
+ else {
131
+ const infisicalConfig = (0, db_utils_1.getInfisicalConfig)();
132
+ console.log('✓ Loaded Infisical config from GitHub Variables');
133
+ const token = (0, db_utils_1.getInfisicalToken)(infisicalConfig);
134
+ console.log('✓ Obtained Infisical access token');
135
+ const infisicalEnv = ENV_MAP[environment];
136
+ secrets = getInfisicalSecrets(infisicalConfig, token, infisicalEnv, secretPath);
137
+ console.log(`✓ Retrieved secrets from Infisical (path: ${secretPath})\n`);
138
+ }
120
139
  const keys = Object.keys(secrets).sort();
121
140
  if (keys.length === 0) {
122
141
  console.log('No environment variables found.');
@@ -66,16 +66,17 @@ const https = __importStar(require("node:https"));
66
66
  // 服务 × 环境 FQDN 表。某服务某环境没部署 → 该 env 键缺省,探时跳过。
67
67
  // cn-prod 真实 subdomain 抄自 optima-terraform alicloud/stacks/cn-prod-ingress-sae/main.tf。
68
68
  // #201 (2026-06-12): yzsgo.com 全量迁移完成,旧 *-cn.optima.chat 路由已下线。
69
+ // cn-stage(阿里云预发)域名 *.stage.optima.chat,抄自 cn-stage-ingress-sae services map。
69
70
  const SERVICES = {
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' },
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' },
73
74
  'mcp-host': { path: '/health', stage: 'mcp-stage.optima.onl', prod: 'mcp.optima.onl' },
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' },
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' },
77
78
  };
78
- const ENVS = ['stage', 'prod', 'cn'];
79
+ const ENVS = ['stage', 'prod', 'cn', 'cn-stage'];
79
80
  const G = '\x1b[32m', R = '\x1b[31m', Y = '\x1b[33m', B = '\x1b[34m', N = '\x1b[0m';
80
81
  const MARK = { ok: `${G}✅${N}`, fail: `${R}❌${N}`, warn: `${Y}⚠️ ${N}`, na: `${B}··${N}` };
81
82
  const verdict = (ls) => ls.some((l) => l.result === 'fail') ? 'fail' : ls.some((l) => l.result === 'warn') ? 'warn' : 'pass';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@optima-chat/dev-skills",
3
- "version": "0.7.43",
3
+ "version": "0.8.0",
4
4
  "description": "Claude Code Skills for Optima development team - cross-environment collaboration tools",
5
5
  "main": "index.js",
6
6
  "bin": {