@optima-chat/dev-skills 0.7.38 → 0.7.40

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,12 +1,12 @@
1
1
  ---
2
2
  name: "grant-balance"
3
- description: "当用户请求赠送余额、充值 USD 余额、grant balance、加余额、奖励、补偿、推荐奖励、运营发放时,使用此技能。支持 Stage、Prod 两个环境。"
3
+ description: "当用户请求赠送余额、充值 USD 余额、grant balance、加余额、奖励、补偿、推荐奖励、运营发放时,使用此技能。支持 Stage、Prod、cn-prod 三个环境。"
4
4
  allowed-tools: ["Bash"]
5
5
  ---
6
6
 
7
7
  # 赠送 USD 余额(Grant Balance)
8
8
 
9
- 当你需要为用户赠送 wallet USD 余额时,使用这个场景。金额会加到 `usd_wallets.granted_balance_micros`,billing 服务在扣费时会优先消费 granted balance。
9
+ 当你需要为用户赠送 wallet USD 余额时,使用这个场景。金额按 $1=700 积分换算,经 billing API 入 **bonus 积分桶(30 天有效期)**(P15 钱包退役后语义),billing 服务在扣费时会优先消费 granted balance。
10
10
 
11
11
  ## 执行方式:使用 CLI 工具
12
12
 
@@ -17,8 +17,8 @@ optima-grant-balance <email> --amount <usd> [options]
17
17
  **为什么使用 CLI 工具**:
18
18
  - 自动通过 email 查找 userId(跨 user-auth 数据库)
19
19
  - 自动处理 SSH 隧道和数据库连接
20
- - 不会影响现有订阅和已有余额(纯追加到 granted balance)
21
- - 自动写入 audit trail(`usd_wallet_topups` source=admin_grant)
20
+ - 不会影响现有订阅和已有余额(发放 bonus 积分(30 天有效期,重复执行会叠加发放))
21
+ - 自动留审计痕迹(credit_lot 行,幂等键前缀 `dev-skills-grant:`)
22
22
 
23
23
  ## 适用情况
24
24
 
@@ -41,7 +41,7 @@ optima-grant-balance user@example.com --amount 20 --description "服务中断补
41
41
  ```
42
42
 
43
43
  > **单位是美元(USD)**。`--amount 5` 即赠送 $5.00 到 granted balance。
44
- > 数据库底层用 micro-USD 精度(1 USD = 1,000,000 micros),CLI 自动换算。
44
+ > 数据库底层用 micro-USD 精度(1 USD = 700 积分(P15 统一账本口径)
45
45
 
46
46
  ### 参数说明
47
47
 
@@ -50,7 +50,9 @@ 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 | stage |
53
+ | `--env <env>` | 环境:stage, prod, cn-prod | stage |
54
+
55
+ > **cn-prod(国内环境)**:全程 HTTPS(auth-cn/billing-cn.optima.chat),email 查找走 user-auth internal lookup API(无 SSH 隧道)。金额输入仍是 USD($1 = 700 积分,与 CN ¥1 = 100 积分同一账本单位)。例:`optima-grant-balance user@example.com --amount 1 --env cn-prod`
54
56
 
55
57
  ## 与 grant-subscription 的区别
56
58
 
@@ -93,10 +95,10 @@ optima-grant-balance xxx@gmail.com --amount 20 --env stage
93
95
  1. **Stage 优先**:默认操作 Stage 环境
94
96
  2. **Prod 谨慎**:操作 Prod 前确认邮箱和金额
95
97
  3. **纯追加**:不会影响现有余额和订阅(累加到 granted balance)
96
- 4. **Audit trail**:每次赠送会插入一条 `usd_wallet_topups` 记录(source=`admin_grant`)
98
+ 4. **Audit trail**:每次赠送对应一个 credit_lot(幂等键前缀 `dev-skills-grant:`,type=bonus)
97
99
 
98
100
  ## 相关命令
99
101
 
100
102
  - `optima-grant-balance` - 赠送 USD 余额(主要方式)
101
103
  - `optima-grant-subscription` - 开通订阅计划
102
- - `optima-query-db` - 查询数据库验证结果(`SELECT granted_balance_micros FROM usd_wallets WHERE user_id=...`)
104
+ - `optima-query-db` - 查询数据库验证结果(`GET /api/billing/balance(credits.byType.bonus)或 query-db credit_lot
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: "grant-subscription"
3
- description: "当用户请求开通会员、赠送订阅、grant subscription、开通 Pro/Starter/Enterprise、升级计划、给用户开会员时,使用此技能。支持 Stage、Prod 两个环境。"
3
+ description: "当用户请求开通会员、赠送订阅、grant subscription、开通 Pro/Starter/Enterprise、升级计划、给用户开会员时,使用此技能。支持 Stage、Prod、cn-prod 三个环境。"
4
4
  allowed-tools: ["Bash"]
5
5
  ---
6
6
 
@@ -19,8 +19,8 @@ optima-grant-subscription <email> [options]
19
19
  **为什么使用 CLI 工具**:
20
20
  - 自动通过 email 查找 userId(跨 user-auth 数据库)
21
21
  - 自动处理 SSH 隧道和数据库连接
22
- - 自动取消旧订阅、重置 wallet granted balance
23
- - 自动按 plan 配置授予 USD wallet 余额和 token quota
22
+ - 自动取消旧订阅、经 billing API supersede 旧授予(void 旧 subscription lot + 发新)
23
+ - 自动按 plan 配置发放 subscription 积分(期末过期)和 token quota
24
24
  - 一条命令完成所有操作
25
25
 
26
26
  ## 适用情况
@@ -46,6 +46,10 @@ optima-grant-subscription user@example.com --plan starter --months 3
46
46
 
47
47
  # 开通 Enterprise,Prod 环境
48
48
  optima-grant-subscription user@example.com --plan enterprise --env prod
49
+
50
+ # cn-prod(国内环境):plan 用 -cn 档,默认 pro-cn
51
+ optima-grant-subscription user@example.com --env cn-prod
52
+ optima-grant-subscription user@example.com --plan starter-cn --env cn-prod
49
53
  ```
50
54
 
51
55
  ### 参数说明
@@ -55,7 +59,9 @@ optima-grant-subscription user@example.com --plan enterprise --env prod
55
59
  | `<email>` | 用户邮箱(必填) | - |
56
60
  | `--plan <id>` | 计划:trial, starter, pro, enterprise | pro |
57
61
  | `--months <n>` | 时长(月) | 1 |
58
- | `--env <env>` | 环境:stage, prod | stage |
62
+ | `--env <env>` | 环境:stage, prod, cn-prod | stage |
63
+
64
+ > **cn-prod(国内环境)**:plan 用 CNY 定价的 `-cn` 档(trial / starter-cn / pro-cn / enterprise-cn,默认 pro-cn);裸 USD 档 id 会被客户端拒绝(防止给 CN 用户误赠 USD 档积分量)。email 查找走 user-auth internal lookup API(无 SSH 隧道)。
59
65
 
60
66
  ### 计划配置
61
67
 
@@ -66,7 +72,7 @@ optima-grant-subscription user@example.com --plan enterprise --env prod
66
72
  | pro | $20.00 | 2,000 | 8M | 40M |
67
73
  | enterprise | $100.00 | 10,000 | 16M | 80M |
68
74
 
69
- > 1 credit = $0.01 = 10,000 micros。授予额存入 `usd_wallets.granted_balance_micros`。
75
+ > 积分经 billing API 发放(subscription 桶,期末过期;P15 后无 wallet)
70
76
 
71
77
  ## 常见使用场景
72
78
 
@@ -102,7 +108,7 @@ optima-grant-subscription user@example.com --plan starter --env prod
102
108
  2. 加载对应 plan 的配置(月授予额、token 限额等)
103
109
  3. 取消该用户的所有活跃订阅
104
110
  4. 创建新订阅(设置到期时间)
105
- 5. 重置 wallet granted balance 并授予新额度
111
+ 5. billing API supersede 旧授予(void 旧 subscription lot + 发新) 并授予新额度
106
112
  6. 记录 topup 审计记录(source: subscription_grant)
107
113
  7. 更新 token quota 限额
108
114
 
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  name: "grant-balance"
3
- description: "Use when the user wants to grant USD wallet balance to an Optima user — for promotional grants, compensation, referral rewards, etc. Adds to granted_balance_micros without affecting subscriptions."
3
+ description: "Use when the user wants to grant credits (bonus, 30-day expiry) to an Optima user — for promotional grants, compensation, referral rewards, etc. $1 = 700 credits via billing API; does not affect subscriptions."
4
4
  ---
5
5
 
6
6
  # 赠送 USD 余额(Grant Balance)
7
7
 
8
- 当你需要为用户赠送 wallet USD 余额时,使用这个场景。金额会加到 `usd_wallets.granted_balance_micros`,billing 服务在扣费时会优先消费 granted balance。
8
+ 当你需要为用户赠送 wallet USD 余额时,使用这个场景。金额按 $1=700 积分换算,经 billing API 入 **bonus 积分桶(30 天有效期)**(P15 钱包退役后语义),billing 服务在扣费时会优先消费 granted balance。
9
9
 
10
10
  ## 执行方式:使用 CLI 工具
11
11
 
@@ -16,8 +16,8 @@ optima-grant-balance <email> --amount <usd> [options]
16
16
  **为什么使用 CLI 工具**:
17
17
  - 自动通过 email 查找 userId(跨 user-auth 数据库)
18
18
  - 自动处理 SSH 隧道和数据库连接
19
- - 不会影响现有订阅和已有余额(纯追加到 granted balance)
20
- - 自动写入 audit trail(`usd_wallet_topups` source=admin_grant)
19
+ - 不会影响现有订阅和已有余额(发放 bonus 积分(30 天有效期,重复执行会叠加发放))
20
+ - 自动留审计痕迹(credit_lot 行,幂等键前缀 `dev-skills-grant:`)
21
21
 
22
22
  ## 适用情况
23
23
 
@@ -40,7 +40,7 @@ optima-grant-balance user@example.com --amount 20 --description "服务中断补
40
40
  ```
41
41
 
42
42
  > **单位是美元(USD)**。`--amount 5` 即赠送 $5.00 到 granted balance。
43
- > 数据库底层用 micro-USD 精度(1 USD = 1,000,000 micros),CLI 自动换算。
43
+ > 数据库底层用 micro-USD 精度(1 USD = 700 积分(P15 统一账本口径)
44
44
 
45
45
  ### 参数说明
46
46
 
@@ -49,7 +49,9 @@ 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 | stage |
52
+ | `--env <env>` | 环境:stage, prod, cn-prod | stage |
53
+
54
+ > **cn-prod**:走 HTTPS(auth-cn/billing-cn.optima.chat),email 查找经 user-auth internal lookup API(无 SSH 隧道)。金额输入仍是 USD($1 = 700 积分 = ¥7 档积分口径一致)。
53
55
 
54
56
  ## 与 grant-subscription 的区别
55
57
 
@@ -92,10 +94,10 @@ optima-grant-balance xxx@gmail.com --amount 20 --env stage
92
94
  1. **Stage 优先**:默认操作 Stage 环境
93
95
  2. **Prod 谨慎**:操作 Prod 前确认邮箱和金额
94
96
  3. **纯追加**:不会影响现有余额和订阅(累加到 granted balance)
95
- 4. **Audit trail**:每次赠送会插入一条 `usd_wallet_topups` 记录(source=`admin_grant`)
97
+ 4. **Audit trail**:每次赠送对应一个 credit_lot(幂等键前缀 `dev-skills-grant:`,type=bonus)
96
98
 
97
99
  ## 相关命令
98
100
 
99
101
  - `optima-grant-balance` - 赠送 USD 余额(主要方式)
100
102
  - `optima-grant-subscription` - 开通订阅计划
101
- - `optima-query-db` - 查询数据库验证结果(`SELECT granted_balance_micros FROM usd_wallets WHERE user_id=...`)
103
+ - `optima-query-db` - 查询数据库验证结果(`GET /api/billing/balance(credits.byType.bonus)或 query-db credit_lot
@@ -21,9 +21,16 @@ optima-grant-subscription user@example.com --plan starter --months 3
21
21
  optima-grant-subscription user@example.com --plan enterprise --env prod
22
22
  ```
23
23
 
24
+ ```bash
25
+ # cn-prod(国内环境):plan 用 -cn 档,默认 pro-cn
26
+ optima-grant-subscription user@example.com --env cn-prod
27
+ optima-grant-subscription user@example.com --plan starter-cn --env cn-prod
28
+ ```
29
+
24
30
  ## Guidance
25
31
 
26
32
  - Default to `stage`.
27
- - Confirm the user email before running on `prod`.
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.
28
35
  - This operation replaces existing subscription state and resets the wallet granted balance according to the selected plan.
29
36
  - Use `optima-query-db` afterward if the user asks for verification.
@@ -5,8 +5,14 @@ import { getInfisicalConfig, getInfisicalToken } from './db-utils';
5
5
  const USER_AUTH_URLS: Record<string, string> = {
6
6
  stage: 'https://auth.stage.optima.onl',
7
7
  prod: 'https://auth.optima.onl',
8
+ 'cn-prod': 'https://auth-cn.optima.chat',
8
9
  };
9
10
 
11
+ // cn-prod URLs are hardcoded: cn Infisical (secrets-cn.optima.chat) is a
12
+ // separate instance dev-skills has no machine identity for, and these domains
13
+ // are stable. AWS envs keep reading /shared-secrets/domain-urls.
14
+ const CN_PROD_BILLING_URL = 'https://billing-cn.optima.chat';
15
+
10
16
  /**
11
17
  * Validate the --env flag value at command entry, before any I/O.
12
18
  *
@@ -24,6 +30,20 @@ export function validateEnv(env: string): 'stage' | 'prod' {
24
30
  return env;
25
31
  }
26
32
 
33
+ /**
34
+ * Variant for commands that also support cn-prod — currently grant-balance /
35
+ * grant-subscription, which reach billing + user-auth over HTTPS only.
36
+ * Other commands resolve users via the AWS RDS SSH tunnel, which does not
37
+ * exist for cn-prod (Aliyun VPC-internal RDS) — keep them on validateEnv so a
38
+ * cn-prod typo fails fast instead of dying inside the tunnel setup.
39
+ */
40
+ export function validateEnvCnProd(env: string): 'stage' | 'prod' | 'cn-prod' {
41
+ if (env !== 'stage' && env !== 'prod' && env !== 'cn-prod') {
42
+ throw new Error(`--env must be "stage", "prod" or "cn-prod" (got: ${env})`);
43
+ }
44
+ return env;
45
+ }
46
+
27
47
  // T1 discovered: client_id differs per env (stage=dev-skills-ubd3qz6n,
28
48
  // prod=dev-skills-hinxa0rs). Both stored in Infisical alongside the
29
49
  // secret at /shared-secrets/oauth-clients/.
@@ -31,6 +51,21 @@ const DEV_SKILLS_OAUTH_PATH = '/shared-secrets/oauth-clients';
31
51
  const DEV_SKILLS_CLIENT_ID_KEY = 'DEV_SKILLS_OAUTH_CLIENT_ID';
32
52
  const DEV_SKILLS_CLIENT_SECRET_KEY = 'DEV_SKILLS_OAUTH_CLIENT_SECRET';
33
53
 
54
+ // cn-prod: the client (dev-skills-ecee51qo) lives in cn user-auth, but its
55
+ // credentials are mirrored into AWS Infisical (prod environment, same path)
56
+ // under CN_PROD-prefixed keys so we reuse the existing Infisical access —
57
+ // zero new credential chain. Canonical copy lives in cn Infisical
58
+ // /shared-secrets/oauth-clients (DEV_SKILLS_OAUTH_CLIENT_ID/SECRET).
59
+ const DEV_SKILLS_CN_CLIENT_ID_KEY = 'DEV_SKILLS_CN_PROD_OAUTH_CLIENT_ID';
60
+ const DEV_SKILLS_CN_CLIENT_SECRET_KEY = 'DEV_SKILLS_CN_PROD_OAUTH_CLIENT_SECRET';
61
+
62
+ // cn-prod tokens must carry this scope: resolveUserIdByEmail calls cn
63
+ // user-auth POST /api/v1/internal/users/lookup, whose guard
64
+ // (verify_internal_service_token) requires it. user-auth issues
65
+ // request∩allowed_scopes and an unscoped request yields scope="" (verified
66
+ // against cn-prod 2026-06-12), so the request must name it explicitly.
67
+ const CN_PROD_TOKEN_SCOPE = 'internal:users:write';
68
+
34
69
  // ───── Cache (process-lifetime) ─────────────────────────────────────────────
35
70
  // One CLI invocation does at most a handful of HTTP calls. We mint the M2M
36
71
  // token once and reuse it. Cross-invocation re-mint is fine — JWT TTL is
@@ -46,6 +81,7 @@ const billingUrlCache: Record<string, string> = {};
46
81
  const skillsUrlCache: Record<string, string> = {};
47
82
 
48
83
  function getBillingUrl(env: string): string {
84
+ if (env === 'cn-prod') return CN_PROD_BILLING_URL;
49
85
  if (billingUrlCache[env]) return billingUrlCache[env];
50
86
  const url = fetchInfisicalSecret(env, '/shared-secrets/domain-urls', 'BILLING_URL');
51
87
  billingUrlCache[env] = url;
@@ -65,13 +101,20 @@ export function getServiceToken(env: string): string {
65
101
  const cfg = getInfisicalConfig();
66
102
  const tok = getInfisicalToken(cfg);
67
103
  // Fetch BOTH client_id and client_secret from Infisical — they differ per env.
68
- const clientId = fetchInfisicalSecret(env, DEV_SKILLS_OAUTH_PATH, DEV_SKILLS_CLIENT_ID_KEY, cfg, tok);
69
- const clientSecret = fetchInfisicalSecret(env, DEV_SKILLS_OAUTH_PATH, DEV_SKILLS_CLIENT_SECRET_KEY, cfg, tok);
104
+ // cn-prod credentials are mirrored in the AWS Infisical *prod* environment
105
+ // (fetchInfisicalSecret has no cn-prod env slug), under CN-specific keys.
106
+ const isCn = env === 'cn-prod';
107
+ const infisicalEnv = isCn ? 'prod' : env;
108
+ const idKey = isCn ? DEV_SKILLS_CN_CLIENT_ID_KEY : DEV_SKILLS_CLIENT_ID_KEY;
109
+ const secretKey = isCn ? DEV_SKILLS_CN_CLIENT_SECRET_KEY : DEV_SKILLS_CLIENT_SECRET_KEY;
110
+ const clientId = fetchInfisicalSecret(infisicalEnv, DEV_SKILLS_OAUTH_PATH, idKey, cfg, tok);
111
+ const clientSecret = fetchInfisicalSecret(infisicalEnv, DEV_SKILLS_OAUTH_PATH, secretKey, cfg, tok);
70
112
 
71
113
  const authUrl = USER_AUTH_URLS[env];
72
114
  if (!authUrl) throw new Error(`Unknown env: ${env}`);
73
115
 
74
- const body = `grant_type=client_credentials&client_id=${encodeURIComponent(clientId)}&client_secret=${encodeURIComponent(clientSecret)}`;
116
+ const scopeParam = isCn ? `&scope=${encodeURIComponent(CN_PROD_TOKEN_SCOPE)}` : '';
117
+ const body = `grant_type=client_credentials&client_id=${encodeURIComponent(clientId)}&client_secret=${encodeURIComponent(clientSecret)}${scopeParam}`;
75
118
  const response = execSync(
76
119
  `curl -s -X POST '${authUrl}/api/v1/oauth/token' -H 'Content-Type: application/x-www-form-urlencoded' -d '${body}'`,
77
120
  { encoding: 'utf-8' },
@@ -189,3 +232,40 @@ export async function callSkills<T = unknown>(
189
232
  ): Promise<ServiceResponse<T>> {
190
233
  return callService<T>(getSkillsUrl(env), env, method, path, body);
191
234
  }
235
+
236
+ /**
237
+ * Resolve a user's id by email via user-auth's internal lookup endpoint
238
+ * (POST /api/v1/internal/users/lookup). cn-prod only: AWS envs resolve via
239
+ * the RDS SSH tunnel (db-utils resolveUserId) and their dev-skills clients
240
+ * don't carry the internal:users:write scope this endpoint requires.
241
+ */
242
+ export async function resolveUserIdByEmail(env: string, email: string): Promise<string> {
243
+ console.log(`Looking up user by email: ${email}`);
244
+ const token = getServiceToken(env);
245
+ const authUrl = USER_AUTH_URLS[env];
246
+ if (!authUrl) throw new Error(`Unknown env: ${env}`);
247
+
248
+ const res = await fetch(`${authUrl}/api/v1/internal/users/lookup`, {
249
+ method: 'POST',
250
+ headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
251
+ body: JSON.stringify({ email }),
252
+ });
253
+ const text = await res.text();
254
+ if (res.status === 404) {
255
+ throw new Error(`User not found (${env}): ${email}`);
256
+ }
257
+ if (!res.ok) {
258
+ throw new Error(formatServiceError(res.status, res.statusText, text));
259
+ }
260
+ let parsed: { user_id?: string };
261
+ try {
262
+ parsed = JSON.parse(text);
263
+ } catch {
264
+ throw new Error(`user-auth lookup returned non-JSON 2xx body: ${text.slice(0, 200)}`);
265
+ }
266
+ if (!parsed.user_id) {
267
+ throw new Error(`user-auth lookup response missing user_id: ${text.slice(0, 200)}`);
268
+ }
269
+ console.log(`✓ Found user: ${parsed.user_id}`);
270
+ return parsed.user_id;
271
+ }