@optima-chat/dev-skills 0.7.43 → 0.8.1
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 +55 -16
- package/bin/helpers/confirm-prompt.ts +8 -4
- package/bin/helpers/db-utils.ts +34 -12
- package/bin/helpers/discount/create.ts +3 -3
- package/bin/helpers/discount/disable.ts +3 -3
- package/bin/helpers/discount/generate.ts +3 -3
- package/bin/helpers/discount/list.ts +3 -3
- package/bin/helpers/entitlement/grant.ts +4 -7
- package/bin/helpers/entitlement/list.ts +4 -7
- package/bin/helpers/entitlement/revoke.ts +4 -7
- package/bin/helpers/generate-test-token.ts +28 -10
- 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 +55 -14
- package/dist/bin/helpers/confirm-prompt.js +7 -4
- package/dist/bin/helpers/db-utils.js +34 -12
- package/dist/bin/helpers/discount/create.js +2 -2
- package/dist/bin/helpers/discount/disable.js +2 -2
- package/dist/bin/helpers/discount/generate.js +2 -2
- package/dist/bin/helpers/discount/list.js +2 -2
- package/dist/bin/helpers/entitlement/grant.js +3 -6
- package/dist/bin/helpers/entitlement/list.js +3 -6
- package/dist/bin/helpers/entitlement/revoke.js +3 -6
- package/dist/bin/helpers/generate-test-token.js +29 -9
- 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 +1 -1
|
@@ -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, resolveUserId } 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}`);
|
|
@@ -272,6 +292,25 @@ export async function resolveUserIdByEmail(env: string, email: string): Promise<
|
|
|
272
292
|
return parsed.user_id;
|
|
273
293
|
}
|
|
274
294
|
|
|
295
|
+
/**
|
|
296
|
+
* Resolve email → userId across all envs, picking the path that exists for
|
|
297
|
+
* each cloud. AWS (stage/prod) reaches the database over the RDS SSH tunnel
|
|
298
|
+
* (db-utils.resolveUserId). cn-prod / cn-stage have no tunnel into the Aliyun
|
|
299
|
+
* VPC-internal RDS, so they resolve via user-auth's internal HTTP lookup
|
|
300
|
+
* (resolveUserIdByEmail) — the cn dev-skills token carries the
|
|
301
|
+
* internal:users:write scope that endpoint requires. Lets the entitlement
|
|
302
|
+
* subcommands support cn the same way grant-subscription already does, without
|
|
303
|
+
* each call site re-implementing the branch.
|
|
304
|
+
*/
|
|
305
|
+
export async function resolveUserIdByEmailAnyEnv(env: string, email: string): Promise<string> {
|
|
306
|
+
if (env === 'cn-prod' || env === 'cn-stage') {
|
|
307
|
+
return resolveUserIdByEmail(env, email);
|
|
308
|
+
}
|
|
309
|
+
const cfg = getInfisicalConfig();
|
|
310
|
+
const token = getInfisicalToken(cfg);
|
|
311
|
+
return resolveUserId(email, env, cfg, token);
|
|
312
|
+
}
|
|
313
|
+
|
|
275
314
|
/**
|
|
276
315
|
* Resolve a user's id by phone via user-auth's internal lookup endpoint
|
|
277
316
|
* (POST /api/v1/internal/users/lookup with {phone}). Mirrors
|
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
import * as readline from 'readline';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* On
|
|
5
|
-
*
|
|
4
|
+
* On a production env, print the resolved action and require typing "yes" to
|
|
5
|
+
* proceed. Both AWS `prod` and Aliyun `cn-prod` are production — cn-prod must
|
|
6
|
+
* gate identically or the cn rollout silently loses the prod safety prompt.
|
|
7
|
+
* No-op on stage / cn-stage or when --yes was passed. Exits 1 if user declines.
|
|
6
8
|
*/
|
|
9
|
+
const PROD_ENVS = new Set(['prod', 'cn-prod']);
|
|
10
|
+
|
|
7
11
|
export async function confirmIfProd(
|
|
8
12
|
env: string,
|
|
9
13
|
actionDescription: string,
|
|
10
14
|
skipFlag: boolean,
|
|
11
15
|
): Promise<void> {
|
|
12
|
-
if (env
|
|
16
|
+
if (!PROD_ENVS.has(env) || skipFlag) return;
|
|
13
17
|
|
|
14
|
-
console.log(`\n⚠️ About to perform on
|
|
18
|
+
console.log(`\n⚠️ About to perform on ${env.toUpperCase()}:\n${actionDescription}\n`);
|
|
15
19
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
16
20
|
const answer = await new Promise<string>((resolve) => {
|
|
17
21
|
rl.question('Type "yes" to confirm: ', (a) => { rl.close(); resolve(a.trim()); });
|
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 含凭证,报错只说事实,不回显值
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { callBilling,
|
|
1
|
+
import { callBilling, validateEnvCnProd } from '../billing-http';
|
|
2
2
|
import { confirmIfProd } from '../confirm-prompt';
|
|
3
3
|
|
|
4
4
|
interface CreateArgs {
|
|
@@ -34,7 +34,7 @@ Optional:
|
|
|
34
34
|
--ends <date> Valid-until
|
|
35
35
|
--max <N> Max total redemptions (default: unlimited; 1 = single-use)
|
|
36
36
|
--campaign <label> Grouping label
|
|
37
|
-
--env stage|prod
|
|
37
|
+
--env stage|prod|cn-prod|cn-stage (default: stage)
|
|
38
38
|
--yes Skip prod confirmation`);
|
|
39
39
|
process.exit(0);
|
|
40
40
|
}
|
|
@@ -62,7 +62,7 @@ Optional:
|
|
|
62
62
|
|
|
63
63
|
export async function runCreate(argv: string[]): Promise<void> {
|
|
64
64
|
const args = parseArgs(argv);
|
|
65
|
-
|
|
65
|
+
validateEnvCnProd(args.env);
|
|
66
66
|
await confirmIfProd(args.env, `Create discount code ${args.code.toUpperCase()} (${args.percentOff}% off)`, args.yes);
|
|
67
67
|
|
|
68
68
|
const body: Record<string, unknown> = { code: args.code, percentOff: args.percentOff };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { callBilling,
|
|
1
|
+
import { callBilling, validateEnvCnProd } from '../billing-http';
|
|
2
2
|
import { confirmIfProd } from '../confirm-prompt';
|
|
3
3
|
|
|
4
4
|
interface DisableArgs {
|
|
@@ -15,7 +15,7 @@ Required:
|
|
|
15
15
|
--code <CODE>
|
|
16
16
|
|
|
17
17
|
Optional:
|
|
18
|
-
--env stage|prod
|
|
18
|
+
--env stage|prod|cn-prod|cn-stage (default: stage)
|
|
19
19
|
--yes Skip prod confirmation`);
|
|
20
20
|
process.exit(0);
|
|
21
21
|
}
|
|
@@ -36,7 +36,7 @@ Optional:
|
|
|
36
36
|
|
|
37
37
|
export async function runDisable(argv: string[]): Promise<void> {
|
|
38
38
|
const args = parseArgs(argv);
|
|
39
|
-
|
|
39
|
+
validateEnvCnProd(args.env);
|
|
40
40
|
await confirmIfProd(args.env, `Disable discount code ${args.code.toUpperCase()}`, args.yes);
|
|
41
41
|
const res = await callBilling(args.env, 'PATCH', `/api/billing/admin/discount-codes/${encodeURIComponent(args.code)}`, { status: 'DISABLED' });
|
|
42
42
|
console.log(`✓ Disabled (HTTP ${res.status}):`);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as fs from 'fs';
|
|
2
|
-
import { callBilling,
|
|
2
|
+
import { callBilling, validateEnvCnProd } from '../billing-http';
|
|
3
3
|
import { confirmIfProd } from '../confirm-prompt';
|
|
4
4
|
|
|
5
5
|
interface GenArgs {
|
|
@@ -35,7 +35,7 @@ Optional:
|
|
|
35
35
|
--products <a,b,...> Limit to these productKeys
|
|
36
36
|
--starts <date> Valid-from (YYYY-MM-DD or ISO)
|
|
37
37
|
--ends <date> Valid-until
|
|
38
|
-
--env stage|prod
|
|
38
|
+
--env stage|prod|cn-prod|cn-stage (default: stage)
|
|
39
39
|
--yes Skip prod confirmation`);
|
|
40
40
|
process.exit(0);
|
|
41
41
|
}
|
|
@@ -63,7 +63,7 @@ Optional:
|
|
|
63
63
|
|
|
64
64
|
export async function runGenerate(argv: string[]): Promise<void> {
|
|
65
65
|
const args = parseArgs(argv);
|
|
66
|
-
|
|
66
|
+
validateEnvCnProd(args.env);
|
|
67
67
|
await confirmIfProd(args.env, `Generate ${args.count} unique discount codes (${args.percentOff}% off, campaign ${args.campaign})`, args.yes);
|
|
68
68
|
|
|
69
69
|
const body: Record<string, unknown> = { count: args.count, percentOff: args.percentOff, campaign: args.campaign };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { callBilling,
|
|
1
|
+
import { callBilling, validateEnvCnProd } from '../billing-http';
|
|
2
2
|
|
|
3
3
|
interface ListArgs {
|
|
4
4
|
campaign?: string;
|
|
@@ -15,7 +15,7 @@ Optional:
|
|
|
15
15
|
--campaign <label> Filter by campaign
|
|
16
16
|
--code <CODE> Filter by exact code
|
|
17
17
|
--limit <N> Max rows (default 500, max 1000)
|
|
18
|
-
--env stage|prod
|
|
18
|
+
--env stage|prod|cn-prod|cn-stage (default: stage)`);
|
|
19
19
|
process.exit(0);
|
|
20
20
|
}
|
|
21
21
|
const out: Partial<ListArgs> = { env: 'stage' };
|
|
@@ -35,7 +35,7 @@ Optional:
|
|
|
35
35
|
|
|
36
36
|
export async function runList(argv: string[]): Promise<void> {
|
|
37
37
|
const args = parseArgs(argv);
|
|
38
|
-
|
|
38
|
+
validateEnvCnProd(args.env);
|
|
39
39
|
const qs = new URLSearchParams();
|
|
40
40
|
if (args.campaign) qs.set('campaign', args.campaign);
|
|
41
41
|
if (args.code) qs.set('code', args.code);
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { callBilling,
|
|
1
|
+
import { callBilling, validateEnvCnProd, resolveUserIdByEmailAnyEnv } from '../billing-http';
|
|
2
2
|
import { confirmIfProd } from '../confirm-prompt';
|
|
3
|
-
import { getInfisicalConfig, getInfisicalToken, resolveUserId } from '../db-utils';
|
|
4
3
|
|
|
5
4
|
interface GrantArgs {
|
|
6
5
|
email: string;
|
|
@@ -21,7 +20,7 @@ Required:
|
|
|
21
20
|
|
|
22
21
|
Optional:
|
|
23
22
|
--yes Skip prod confirmation prompt (no-op on stage)
|
|
24
|
-
--env stage|prod
|
|
23
|
+
--env stage|prod|cn-prod|cn-stage (default: stage)
|
|
25
24
|
|
|
26
25
|
Hardcoded server-side: source=ADMIN_GRANT, priceCents=0, currency=USD, grantedBy=<clientId>.`);
|
|
27
26
|
process.exit(0);
|
|
@@ -47,11 +46,9 @@ Hardcoded server-side: source=ADMIN_GRANT, priceCents=0, currency=USD, grantedBy
|
|
|
47
46
|
|
|
48
47
|
export async function runGrant(argv: string[]): Promise<void> {
|
|
49
48
|
const args = parseArgs(argv);
|
|
50
|
-
|
|
49
|
+
validateEnvCnProd(args.env);
|
|
51
50
|
|
|
52
|
-
const
|
|
53
|
-
const token = getInfisicalToken(cfg);
|
|
54
|
-
const userId = await resolveUserId(args.email, args.env, cfg, token);
|
|
51
|
+
const userId = await resolveUserIdByEmailAnyEnv(args.env, args.email);
|
|
55
52
|
|
|
56
53
|
await confirmIfProd(
|
|
57
54
|
args.env,
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { callBilling,
|
|
2
|
-
import { getInfisicalConfig, getInfisicalToken, resolveUserId } from '../db-utils';
|
|
1
|
+
import { callBilling, validateEnvCnProd, resolveUserIdByEmailAnyEnv } from '../billing-http';
|
|
3
2
|
|
|
4
3
|
interface ListArgs {
|
|
5
4
|
email: string;
|
|
@@ -14,7 +13,7 @@ Required:
|
|
|
14
13
|
--email <user-email> Resolved to userId via user-auth DB
|
|
15
14
|
|
|
16
15
|
Optional:
|
|
17
|
-
--env stage|prod
|
|
16
|
+
--env stage|prod|cn-prod|cn-stage (default: stage)`);
|
|
18
17
|
process.exit(0);
|
|
19
18
|
}
|
|
20
19
|
const out: Partial<ListArgs> = { env: 'stage' };
|
|
@@ -42,11 +41,9 @@ interface EntitlementRow {
|
|
|
42
41
|
|
|
43
42
|
export async function runList(argv: string[]): Promise<void> {
|
|
44
43
|
const args = parseArgs(argv);
|
|
45
|
-
|
|
44
|
+
validateEnvCnProd(args.env);
|
|
46
45
|
|
|
47
|
-
const
|
|
48
|
-
const token = getInfisicalToken(cfg);
|
|
49
|
-
const userId = await resolveUserId(args.email, args.env, cfg, token);
|
|
46
|
+
const userId = await resolveUserIdByEmailAnyEnv(args.env, args.email);
|
|
50
47
|
|
|
51
48
|
const res = await callBilling<{ entitlements: EntitlementRow[] }>(
|
|
52
49
|
args.env,
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { callBilling,
|
|
1
|
+
import { callBilling, validateEnvCnProd, resolveUserIdByEmailAnyEnv } from '../billing-http';
|
|
2
2
|
import { confirmIfProd } from '../confirm-prompt';
|
|
3
|
-
import { getInfisicalConfig, getInfisicalToken, resolveUserId } from '../db-utils';
|
|
4
3
|
|
|
5
4
|
interface RevokeArgs {
|
|
6
5
|
email: string;
|
|
@@ -28,7 +27,7 @@ Required:
|
|
|
28
27
|
|
|
29
28
|
Optional:
|
|
30
29
|
--yes Skip prod confirmation prompt (no-op on stage)
|
|
31
|
-
--env stage|prod
|
|
30
|
+
--env stage|prod|cn-prod|cn-stage (default: stage)
|
|
32
31
|
|
|
33
32
|
Refuses non-ADMIN_GRANT sources (PAYMENT, PARTNER) with source-specific
|
|
34
33
|
error pointing to the right reversal flow.`);
|
|
@@ -59,11 +58,9 @@ const PARTNER_REFUSAL = `refusing to revoke a PARTNER-source entitlement via CLI
|
|
|
59
58
|
|
|
60
59
|
export async function runRevoke(argv: string[]): Promise<void> {
|
|
61
60
|
const args = parseArgs(argv);
|
|
62
|
-
|
|
61
|
+
validateEnvCnProd(args.env);
|
|
63
62
|
|
|
64
|
-
const
|
|
65
|
-
const token = getInfisicalToken(cfg);
|
|
66
|
-
const userId = await resolveUserId(args.email, args.env, cfg, token);
|
|
63
|
+
const userId = await resolveUserIdByEmailAnyEnv(args.env, args.email);
|
|
67
64
|
|
|
68
65
|
// Step 1: Fetch user's entitlements
|
|
69
66
|
const listRes = await callBilling<{ entitlements: EntitlementRow[] }>(
|
|
@@ -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-
|
|
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
|
-
--
|
|
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
|
|
224
|
-
stage
|
|
225
|
-
prod
|
|
226
|
-
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)
|
|
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 =
|
|
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
|
|
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();
|