@optima-chat/dev-skills 0.7.42 → 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.
- package/.claude/skills/grant-balance/SKILL.md +4 -2
- package/.claude/skills/grant-subscription/SKILL.md +5 -1
- package/.codex/skills/grant-balance/SKILL.md +2 -1
- package/.codex/skills/grant-subscription/SKILL.md +6 -2
- package/bin/cli.js +1 -1
- package/bin/helpers/billing-http.ts +36 -16
- package/bin/helpers/db-utils.ts +34 -12
- package/bin/helpers/generate-test-token.ts +33 -12
- package/bin/helpers/grant-balance.ts +6 -5
- package/bin/helpers/grant-subscription.ts +10 -8
- package/bin/helpers/query-db.ts +9 -6
- package/bin/helpers/show-env.ts +34 -15
- package/bin/helpers/verify-health.ts +10 -9
- package/dist/bin/helpers/billing-http.js +36 -14
- package/dist/bin/helpers/db-utils.js +34 -12
- package/dist/bin/helpers/generate-test-token.js +34 -11
- package/dist/bin/helpers/grant-balance.js +6 -5
- package/dist/bin/helpers/grant-subscription.js +10 -8
- package/dist/bin/helpers/query-db.js +9 -6
- package/dist/bin/helpers/show-env.js +31 -12
- package/dist/bin/helpers/verify-health.js +8 -7
- package/package.json +2 -3
|
@@ -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
|
|
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-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
//
|
|
106
|
-
// cn-
|
|
107
|
-
//
|
|
108
|
-
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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}`);
|
package/bin/helpers/db-utils.ts
CHANGED
|
@@ -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
|
|
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
|
-
/**
|
|
121
|
-
|
|
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
|
|
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(
|
|
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;
|
|
@@ -56,8 +56,18 @@ const ENV_CONFIG: Record<Environment, EnvironmentConfig> = {
|
|
|
56
56
|
// #201: yzsgo.com 全量迁移 (2026-06-12), 旧 *-cn.optima.chat 路由已下线
|
|
57
57
|
authUrl: 'https://auth.yzsgo.com',
|
|
58
58
|
apiUrl: 'https://commerce.yzsgo.com',
|
|
59
|
-
|
|
59
|
+
// #31: 必须用 cn user-auth 里注册的 public client(开 password grant)。
|
|
60
|
+
// 注意不要换成同名的 confidential client `commerce-cli-cn-prod-*`,那个需要 client_secret,走不通 CLI 的 public ROPC flow。
|
|
61
|
+
clientId: 'dev-skill-cli-cn-pro-acvkmcuq',
|
|
60
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'
|
|
61
71
|
}
|
|
62
72
|
};
|
|
63
73
|
|
|
@@ -98,7 +108,8 @@ async function registerMerchant(
|
|
|
98
108
|
body: JSON.stringify(payload)
|
|
99
109
|
});
|
|
100
110
|
|
|
101
|
-
|
|
111
|
+
// #31: cn-prod register 返回体字段名是 `id`,AWS 是 `user_id`,两者兼容
|
|
112
|
+
console.log(`✓ Merchant registered successfully (ID: ${result.user_id ?? (result as any).id})`);
|
|
102
113
|
return result;
|
|
103
114
|
} catch (error: any) {
|
|
104
115
|
if (error.message.includes('409') || error.message.includes('already exists')) {
|
|
@@ -180,6 +191,7 @@ async function main() {
|
|
|
180
191
|
let phone: string | undefined;
|
|
181
192
|
let address: string | undefined;
|
|
182
193
|
let environment: Environment = 'ci';
|
|
194
|
+
let skipMerchant = false;
|
|
183
195
|
|
|
184
196
|
// 解析命令行参数
|
|
185
197
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -195,12 +207,14 @@ async function main() {
|
|
|
195
207
|
phone = args[++i];
|
|
196
208
|
} else if (arg === '--address' && args[i + 1]) {
|
|
197
209
|
address = args[++i];
|
|
210
|
+
} else if (arg === '--skip-merchant') {
|
|
211
|
+
skipMerchant = true;
|
|
198
212
|
} else if (arg === '--env' && args[i + 1]) {
|
|
199
213
|
const envArg = args[++i];
|
|
200
|
-
if (envArg === 'ci' || envArg === 'stage' || envArg === 'prod' || envArg === 'cn-prod') {
|
|
214
|
+
if (envArg === 'ci' || envArg === 'stage' || envArg === 'prod' || envArg === 'cn-prod' || envArg === 'cn-stage') {
|
|
201
215
|
environment = envArg as Environment;
|
|
202
216
|
} else {
|
|
203
|
-
console.error(`❌ Invalid environment: ${envArg}. Must be 'ci', 'stage', 'prod', or 'cn-
|
|
217
|
+
console.error(`❌ Invalid environment: ${envArg}. Must be 'ci', 'stage', 'prod', 'cn-prod', or 'cn-stage'.`);
|
|
204
218
|
process.exit(1);
|
|
205
219
|
}
|
|
206
220
|
} else if (arg === '--help' || arg === '-h') {
|
|
@@ -213,20 +227,24 @@ Options:
|
|
|
213
227
|
--business-name <name> Merchant business name (default: auto-generated)
|
|
214
228
|
--phone <phone> Merchant phone number (optional)
|
|
215
229
|
--address <address> Merchant address (optional)
|
|
216
|
-
--
|
|
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
|
|
217
233
|
--help, -h Show this help message
|
|
218
234
|
|
|
219
235
|
Environments:
|
|
220
|
-
ci
|
|
221
|
-
stage
|
|
222
|
-
prod
|
|
223
|
-
cn-prod
|
|
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)
|
|
224
241
|
|
|
225
242
|
Example:
|
|
226
243
|
optima-generate-test-token
|
|
227
244
|
optima-generate-test-token --env stage
|
|
228
245
|
optima-generate-test-token --env prod
|
|
229
246
|
optima-generate-test-token --env cn-prod
|
|
247
|
+
optima-generate-test-token --env cn-stage
|
|
230
248
|
optima-generate-test-token --business-name "My Test Shop" --env stage
|
|
231
249
|
`);
|
|
232
250
|
process.exit(0);
|
|
@@ -251,8 +269,11 @@ Example:
|
|
|
251
269
|
// 2. 获取 token
|
|
252
270
|
const token = await getToken(email, password, config);
|
|
253
271
|
|
|
254
|
-
// 3. 设置 merchant profile(Commerce API)
|
|
255
|
-
const merchantProfile =
|
|
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)');
|
|
256
277
|
|
|
257
278
|
// 4. 保存 token 到临时文件
|
|
258
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
|
|
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
|
|
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
|
|
127
|
-
// and
|
|
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') {
|
package/bin/helpers/query-db.ts
CHANGED
|
@@ -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 (
|
|
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} (
|
|
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} (
|
|
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(
|
|
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));
|
package/bin/helpers/show-env.ts
CHANGED
|
@@ -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
|
|
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 (!
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
|
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-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
//
|
|
108
|
-
// cn-
|
|
109
|
-
//
|
|
110
|
-
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
|
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
|
-
/**
|
|
150
|
-
|
|
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
|
|
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(
|
|
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}`);
|
|
@@ -60,8 +60,18 @@ const ENV_CONFIG = {
|
|
|
60
60
|
// #201: yzsgo.com 全量迁移 (2026-06-12), 旧 *-cn.optima.chat 路由已下线
|
|
61
61
|
authUrl: 'https://auth.yzsgo.com',
|
|
62
62
|
apiUrl: 'https://commerce.yzsgo.com',
|
|
63
|
-
|
|
63
|
+
// #31: 必须用 cn user-auth 里注册的 public client(开 password grant)。
|
|
64
|
+
// 注意不要换成同名的 confidential client `commerce-cli-cn-prod-*`,那个需要 client_secret,走不通 CLI 的 public ROPC flow。
|
|
65
|
+
clientId: 'dev-skill-cli-cn-pro-acvkmcuq',
|
|
64
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'
|
|
65
75
|
}
|
|
66
76
|
};
|
|
67
77
|
async function httpRequest(url, options = {}) {
|
|
@@ -90,7 +100,8 @@ async function registerMerchant(email, password, businessName, config, phone, ad
|
|
|
90
100
|
method: 'POST',
|
|
91
101
|
body: JSON.stringify(payload)
|
|
92
102
|
});
|
|
93
|
-
|
|
103
|
+
// #31: cn-prod register 返回体字段名是 `id`,AWS 是 `user_id`,两者兼容
|
|
104
|
+
console.log(`✓ Merchant registered successfully (ID: ${result.user_id ?? result.id})`);
|
|
94
105
|
return result;
|
|
95
106
|
}
|
|
96
107
|
catch (error) {
|
|
@@ -163,6 +174,7 @@ async function main() {
|
|
|
163
174
|
let phone;
|
|
164
175
|
let address;
|
|
165
176
|
let environment = 'ci';
|
|
177
|
+
let skipMerchant = false;
|
|
166
178
|
// 解析命令行参数
|
|
167
179
|
for (let i = 0; i < args.length; i++) {
|
|
168
180
|
const arg = args[i];
|
|
@@ -181,13 +193,16 @@ async function main() {
|
|
|
181
193
|
else if (arg === '--address' && args[i + 1]) {
|
|
182
194
|
address = args[++i];
|
|
183
195
|
}
|
|
196
|
+
else if (arg === '--skip-merchant') {
|
|
197
|
+
skipMerchant = true;
|
|
198
|
+
}
|
|
184
199
|
else if (arg === '--env' && args[i + 1]) {
|
|
185
200
|
const envArg = args[++i];
|
|
186
|
-
if (envArg === 'ci' || envArg === 'stage' || envArg === 'prod' || envArg === 'cn-prod') {
|
|
201
|
+
if (envArg === 'ci' || envArg === 'stage' || envArg === 'prod' || envArg === 'cn-prod' || envArg === 'cn-stage') {
|
|
187
202
|
environment = envArg;
|
|
188
203
|
}
|
|
189
204
|
else {
|
|
190
|
-
console.error(`❌ Invalid environment: ${envArg}. Must be 'ci', 'stage', 'prod', or 'cn-
|
|
205
|
+
console.error(`❌ Invalid environment: ${envArg}. Must be 'ci', 'stage', 'prod', 'cn-prod', or 'cn-stage'.`);
|
|
191
206
|
process.exit(1);
|
|
192
207
|
}
|
|
193
208
|
}
|
|
@@ -201,20 +216,24 @@ Options:
|
|
|
201
216
|
--business-name <name> Merchant business name (default: auto-generated)
|
|
202
217
|
--phone <phone> Merchant phone number (optional)
|
|
203
218
|
--address <address> Merchant address (optional)
|
|
204
|
-
--
|
|
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
|
|
205
222
|
--help, -h Show this help message
|
|
206
223
|
|
|
207
224
|
Environments:
|
|
208
|
-
ci
|
|
209
|
-
stage
|
|
210
|
-
prod
|
|
211
|
-
cn-prod
|
|
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)
|
|
212
230
|
|
|
213
231
|
Example:
|
|
214
232
|
optima-generate-test-token
|
|
215
233
|
optima-generate-test-token --env stage
|
|
216
234
|
optima-generate-test-token --env prod
|
|
217
235
|
optima-generate-test-token --env cn-prod
|
|
236
|
+
optima-generate-test-token --env cn-stage
|
|
218
237
|
optima-generate-test-token --business-name "My Test Shop" --env stage
|
|
219
238
|
`);
|
|
220
239
|
process.exit(0);
|
|
@@ -234,8 +253,12 @@ Example:
|
|
|
234
253
|
const user = await registerMerchant(email, password, businessName, config, phone, address);
|
|
235
254
|
// 2. 获取 token
|
|
236
255
|
const token = await getToken(email, password, config);
|
|
237
|
-
// 3. 设置 merchant profile(Commerce API)
|
|
238
|
-
const merchantProfile =
|
|
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)');
|
|
239
262
|
// 4. 保存 token 到临时文件
|
|
240
263
|
const tmpDir = os.tmpdir();
|
|
241
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
|
|
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
|
|
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
|
|
121
|
-
// and
|
|
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 (
|
|
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} (
|
|
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} (
|
|
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(
|
|
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
|
|
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 (!
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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.
|
|
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": {
|
|
@@ -19,8 +19,7 @@
|
|
|
19
19
|
"scripts": {
|
|
20
20
|
"postinstall": "node scripts/install.js",
|
|
21
21
|
"build": "tsc",
|
|
22
|
-
"prepare": "npm run build"
|
|
23
|
-
"test": "node --test tests/*.test.js"
|
|
22
|
+
"prepare": "npm run build"
|
|
24
23
|
},
|
|
25
24
|
"keywords": [
|
|
26
25
|
"claude-code",
|