@optima-chat/dev-skills 0.7.37 → 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.
- package/.claude/commands/logs.md +55 -0
- package/.claude/skills/grant-balance/SKILL.md +10 -8
- package/.claude/skills/grant-subscription/SKILL.md +12 -6
- package/.claude/skills/logs/SKILL.md +14 -1
- package/.codex/skills/grant-balance/SKILL.md +10 -8
- package/.codex/skills/grant-subscription/SKILL.md +8 -1
- package/bin/cli.js +1 -0
- package/bin/helpers/billing-http.ts +83 -3
- package/bin/helpers/db-utils.ts +333 -23
- package/bin/helpers/grant-balance.ts +39 -46
- package/bin/helpers/grant-subscription.ts +47 -89
- package/bin/helpers/query-db.ts +25 -88
- package/bin/helpers/show-env.ts +3 -28
- package/bin/helpers/verify-health.ts +208 -0
- package/dist/bin/helpers/billing-http.js +82 -3
- package/dist/bin/helpers/db-utils.js +356 -27
- package/dist/bin/helpers/discount.js +0 -0
- package/dist/bin/helpers/entitlement.js +0 -0
- package/dist/bin/helpers/generate-test-token.js +0 -0
- package/dist/bin/helpers/grant-balance.js +35 -43
- package/dist/bin/helpers/grant-subscription.js +36 -86
- package/dist/bin/helpers/plugin.js +0 -0
- package/dist/bin/helpers/product.js +0 -0
- package/dist/bin/helpers/query-db.js +34 -71
- package/dist/bin/helpers/show-env.js +5 -17
- package/dist/bin/helpers/verify-health.js +253 -0
- package/package.json +3 -2
package/.claude/commands/logs.md
CHANGED
|
@@ -50,6 +50,7 @@
|
|
|
50
50
|
- `ci` - CI 持续集成环境(开发环境,默认)
|
|
51
51
|
- `stage` - Stage 预发布环境(ECS Fargate)
|
|
52
52
|
- `prod` - 生产环境(ECS Fargate)
|
|
53
|
+
- `cn-prod`(别名 `cn`)- 阿里云 cn-prod 环境(SAE,cn-beijing,经 buildbox 取日志)
|
|
53
54
|
|
|
54
55
|
## 示例
|
|
55
56
|
|
|
@@ -71,6 +72,7 @@
|
|
|
71
72
|
- `ci` 或未指定 → 使用 SSH + Docker Compose(第 0 节,默认)
|
|
72
73
|
- `stage` → 使用 AWS CloudWatch Logs - ECS(第 1 节)
|
|
73
74
|
- `prod` → 使用 AWS CloudWatch Logs - ECS(第 2 节)
|
|
75
|
+
- `cn-prod` / `cn` → 阿里云 SAE,经 buildbox 调 `DescribeInstanceLog`(第 3 节)
|
|
74
76
|
|
|
75
77
|
### 0. CI 环境(environment = "ci" 或默认)
|
|
76
78
|
|
|
@@ -209,6 +211,59 @@ aws logs get-log-events --log-group-name /ecs/commerce-backend-prod --log-stream
|
|
|
209
211
|
|
|
210
212
|
**注意**: `optima-store` 仅在 Stage 环境部署
|
|
211
213
|
|
|
214
|
+
### 3. cn-prod 环境(environment = "cn-prod" 或 "cn")
|
|
215
|
+
|
|
216
|
+
**部署方式**: 阿里云 **SAE**(Serverless App Engine,cn-beijing),不是 ECS/CloudWatch。
|
|
217
|
+
**访问方式**: 经 buildbox(`root@47.94.105.163`,唯一装了 `aliyun-optima` profile + VPC 可达的机器)调 SAE OpenAPI。
|
|
218
|
+
|
|
219
|
+
> ⚠️ SAE **未配 SLS**,stdout 日志只能用 `DescribeInstanceLog` 取实例**当前缓冲**(非历史检索;重启/滚动后旧日志丢)。要历史检索需给 SAE 配 SLS。
|
|
220
|
+
|
|
221
|
+
**日志链**: service → AppId → GroupId → InstanceId → `DescribeInstanceLog`
|
|
222
|
+
|
|
223
|
+
**关键坑**(实测):
|
|
224
|
+
- API 是 RPC-style `aliyun sae DescribeInstanceLog`,ROA 路径是 `/pop/v1/sam/**instance**/describeInstanceLog`(不是 `/sam/app/...`,写错报 "can not find api by path")。
|
|
225
|
+
- buildbox 密码在 1P `op://Optima/gdmixcizjci5bvuu3nuvgg4ooe/password`(标题带括号必须用 item ID)。
|
|
226
|
+
|
|
227
|
+
**service → AppId 映射**(cn-prod,2026-06-01;新增/变动用 `aliyun sae ListApplications` 重查):
|
|
228
|
+
|
|
229
|
+
| service | AppId |
|
|
230
|
+
|---|---|
|
|
231
|
+
| `gateway-core` | `a08ce23f-3d3e-4d89-a2cf-53c8adba614e` |
|
|
232
|
+
| `agentic-chat` | `6e290c73-a646-43ef-9da5-ad0b2e7eff73` |
|
|
233
|
+
| `gw-admin` | `c6bc5a78-b27f-46e2-825a-eaa338c23645` |
|
|
234
|
+
| `user-auth` | `d6fbf9de-fed6-4165-978f-7b3a0a456acc` |
|
|
235
|
+
| `user-auth-admin` | `54093f99-37bf-4354-abd7-f6e76abdbcb6` |
|
|
236
|
+
| `optima-scout` | `f5bb7e82-e57c-4bd4-83ae-2c25f8d38647` |
|
|
237
|
+
| `optima-skills` | `55a63ee7-fb75-46e4-99f3-20854a699237` |
|
|
238
|
+
| `infisical` | `52b33fc6-b26d-495a-8885-225d38b3d42f` |
|
|
239
|
+
|
|
240
|
+
**完整脚本**(本地跑、经 buildbox 中转;改 `SERVICE`/`LINES`):
|
|
241
|
+
```bash
|
|
242
|
+
SERVICE=gateway-core; LINES=80
|
|
243
|
+
BBPW=$(op-win read "op://Optima/gdmixcizjci5bvuu3nuvgg4ooe/password")
|
|
244
|
+
declare -A APPID=(
|
|
245
|
+
[gateway-core]=a08ce23f-3d3e-4d89-a2cf-53c8adba614e
|
|
246
|
+
[agentic-chat]=6e290c73-a646-43ef-9da5-ad0b2e7eff73
|
|
247
|
+
[gw-admin]=c6bc5a78-b27f-46e2-825a-eaa338c23645
|
|
248
|
+
[user-auth]=d6fbf9de-fed6-4165-978f-7b3a0a456acc
|
|
249
|
+
[user-auth-admin]=54093f99-37bf-4354-abd7-f6e76abdbcb6
|
|
250
|
+
[optima-scout]=f5bb7e82-e57c-4bd4-83ae-2c25f8d38647
|
|
251
|
+
[optima-skills]=55a63ee7-fb75-46e4-99f3-20854a699237
|
|
252
|
+
[infisical]=52b33fc6-b26d-495a-8885-225d38b3d42f )
|
|
253
|
+
AID=${APPID[$SERVICE]}
|
|
254
|
+
sshpass -p "$BBPW" ssh -o StrictHostKeyChecking=no root@47.94.105.163 "bash -s" <<REMOTE | tail -n $LINES
|
|
255
|
+
P="--region cn-beijing --profile aliyun-optima"
|
|
256
|
+
GID=\$(aliyun sae GET /pop/v1/sam/app/describeApplicationGroups --AppId $AID \$P 2>/dev/null | python3 -c "import sys,json;print(json.load(sys.stdin)['Data'][0]['GroupId'])")
|
|
257
|
+
IID=\$(aliyun sae GET /pop/v1/sam/app/describeApplicationInstances --AppId $AID --GroupId \$GID \$P 2>/dev/null | python3 -c "import sys,json;d=json.load(sys.stdin)['Data'];print((d if isinstance(d,list) else d['Instances'])[0]['InstanceId'])")
|
|
258
|
+
aliyun sae DescribeInstanceLog --InstanceId "\$IID" \$P 2>/dev/null | python3 -c "import sys,json;print(json.load(sys.stdin).get('Data',''))"
|
|
259
|
+
REMOTE
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
**局限 / 提示**:
|
|
263
|
+
- 只取第 1 个 InstanceId;多副本要遍历 `Data` 里全部 instance。
|
|
264
|
+
- `agent-runtime` 不在 SAE(是 ECI 动态容器)——它的日志看 `gateway-core` 里 `eci-bridge` 的转发/错误行,或单独查对应 ECI。
|
|
265
|
+
- 过滤:把上面输出 `| grep -iE 'error|eci|fail'` 即可(例:排 agentic-chat 链路 ECI 失败就 grep `gateway-core` 日志的 `eci-bridge`)。
|
|
266
|
+
|
|
212
267
|
## 完整示例脚本
|
|
213
268
|
|
|
214
269
|
### Stage 环境
|
|
@@ -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
|
|
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
|
-
-
|
|
21
|
-
-
|
|
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 =
|
|
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
|
|
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` - 查询数据库验证结果(`
|
|
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
|
-
-
|
|
23
|
-
- 自动按 plan
|
|
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
|
-
>
|
|
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.
|
|
111
|
+
5. 经 billing API supersede 旧授予(void 旧 subscription lot + 发新) 并授予新额度
|
|
106
112
|
6. 记录 topup 审计记录(source: subscription_grant)
|
|
107
113
|
7. 更新 token quota 限额
|
|
108
114
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: "logs"
|
|
3
|
-
description: "当用户请求查看日志、查看服务日志、排查问题、看看日志、检查日志、商品服务日志、后端日志、API日志、正式环境日志、生产环境日志、CI
|
|
3
|
+
description: "当用户请求查看日志、查看服务日志、排查问题、看看日志、检查日志、商品服务日志、后端日志、API日志、正式环境日志、生产环境日志、CI环境日志、开发环境日志、阿里云日志、cn-prod 日志、SAE 日志时,使用此技能。支持 CI、Stage、Prod(AWS ECS/CloudWatch)以及 cn-prod(阿里云 SAE,经 buildbox)四个环境的 commerce-backend、user-auth、agentic-chat、gateway-core、optima-scout、optima-skills 等服务。"
|
|
4
4
|
allowed-tools: ["Bash", "SlashCommand"]
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -175,6 +175,19 @@ INFO - Database query took 3200ms: SELECT * FROM products WHERE...
|
|
|
175
175
|
- 通过 CloudWatch Logs 查看
|
|
176
176
|
- 日志保留 7 天
|
|
177
177
|
|
|
178
|
+
### cn-prod 环境(阿里云)
|
|
179
|
+
|
|
180
|
+
```
|
|
181
|
+
/logs gateway-core 100 cn-prod
|
|
182
|
+
/logs agentic-chat 80 cn
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
**特点**:
|
|
186
|
+
- 阿里云 **SAE**(cn-beijing),不是 AWS
|
|
187
|
+
- 经 buildbox(`root@47.94.105.163`)调 SAE `DescribeInstanceLog`
|
|
188
|
+
- SAE 未配 SLS → 只取实例**当前 stdout 缓冲**(非历史检索)
|
|
189
|
+
- 技术细节 + service→AppId 映射见 `/logs --help`(第 3 节)
|
|
190
|
+
|
|
178
191
|
## 支持的服务列表
|
|
179
192
|
|
|
180
193
|
| 服务 | 说明 | CI | Stage | Prod |
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: "grant-balance"
|
|
3
|
-
description: "Use when the user wants to grant
|
|
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
|
|
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
|
-
-
|
|
20
|
-
-
|
|
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 =
|
|
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
|
|
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` - 查询数据库验证结果(`
|
|
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.
|
package/bin/cli.js
CHANGED
|
@@ -32,6 +32,7 @@ switch (command) {
|
|
|
32
32
|
log('Available Commands:', 'yellow');
|
|
33
33
|
log(' optima-query-db <service> "<sql>" [env] Query database', 'cyan');
|
|
34
34
|
log(' optima-show-env <service> [env] Show service env vars', 'cyan');
|
|
35
|
+
log(' optima-verify-health <service> [--env cn|prod|all] Probe L1-L5 上线健康', 'cyan');
|
|
35
36
|
log(' optima-generate-test-token [--env production] Generate test token', 'cyan');
|
|
36
37
|
log(' optima-grant-balance <email> --amount <usd> [--env] Grant USD wallet balance', 'cyan');
|
|
37
38
|
log(' optima-grant-subscription <email> --plan <p> [--env] Grant subscription', 'cyan');
|
|
@@ -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
|
-
|
|
69
|
-
|
|
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
|
|
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
|
+
}
|