@optima-chat/dev-skills 0.8.2 → 0.9.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/account/SKILL.md +61 -0
- package/.claude/skills/entitlement/SKILL.md +55 -0
- package/.claude/skills/grant-balance/SKILL.md +3 -3
- package/.claude/skills/grant-subscription/SKILL.md +4 -4
- package/.codex/skills/account/SKILL.md +61 -0
- package/.codex/skills/entitlement/SKILL.md +55 -0
- package/.codex/skills/grant-balance/SKILL.md +3 -3
- package/.codex/skills/grant-subscription/SKILL.md +1 -1
- package/README.md +6 -0
- package/bin/helpers/account.ts +146 -0
- package/bin/helpers/billing-http.ts +105 -19
- package/bin/helpers/entitlement/grant.ts +18 -12
- package/bin/helpers/entitlement/list.ts +15 -11
- package/bin/helpers/entitlement/revoke.ts +18 -14
- package/bin/helpers/grant-balance.ts +17 -21
- package/bin/helpers/grant-subscription.ts +47 -21
- package/dist/bin/helpers/account.js +141 -0
- package/dist/bin/helpers/billing-http.js +99 -19
- package/dist/bin/helpers/entitlement/grant.js +20 -12
- package/dist/bin/helpers/entitlement/list.js +17 -11
- package/dist/bin/helpers/entitlement/revoke.js +20 -14
- package/dist/bin/helpers/grant-balance.js +15 -20
- package/dist/bin/helpers/grant-subscription.js +37 -20
- package/docs/.admin-cli-4env/spec.md +109 -0
- package/package.json +2 -1
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: "account"
|
|
3
|
+
description: "当用户请求查看账号状态、查询某用户的订阅/权益、禁用账号、封禁用户、ban、停用账号、解封、恢复账号、unban 时,使用此技能。支持 stage、prod、cn-prod、cn-stage 四个环境;标识符支持邮箱/手机号/userId。"
|
|
4
|
+
allowed-tools: ["Bash"]
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# 账号运营(status / ban / unban)
|
|
8
|
+
|
|
9
|
+
以用户为中心的运营 admin 操作:查状态、禁用、恢复。CLI:`optima-account`。
|
|
10
|
+
|
|
11
|
+
标识符 `<email|phone|userId>`:**cn-prod / cn-stage 用户多为手机号注册**,三种都支持;AWS stage/prod 仅 email(经 RDS SSH 隧道解析)。执行前都会打印 `🎯 目标账号` 反查回显,防止发错账号。
|
|
12
|
+
|
|
13
|
+
## 执行方式
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
optima-account status <email|phone|userId> --env <env>
|
|
17
|
+
optima-account ban <email|phone|userId> --reason "..." --env <env> [--yes]
|
|
18
|
+
optima-account unban <email|phone|userId> --env <env> [--yes]
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
| 参数 | 说明 | 默认 |
|
|
22
|
+
|------|------|------|
|
|
23
|
+
| `<email\|phone\|userId>` | 用户标识(必填);cn 支持手机号/userId | - |
|
|
24
|
+
| `--reason "..."` | 禁用原因(**ban 必填**) | - |
|
|
25
|
+
| `--env <env>` | stage / prod / cn-prod / cn-stage | stage |
|
|
26
|
+
| `--yes` | 跳过 prod / cn-prod 的确认提示 | - |
|
|
27
|
+
|
|
28
|
+
## status(只读聚合)
|
|
29
|
+
|
|
30
|
+
显示:订阅(membership-status)+ 权益(entitlements)。纯 M2M,无需 admin 凭证。
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
optima-account status 18898654855 --env cn-prod
|
|
34
|
+
```
|
|
35
|
+
> ⚠️ 账号禁用状态(banned_at/原因)与 credits 余额**暂不显示**——无 M2M 读取端点,见 optima-dev-skills#36。
|
|
36
|
+
|
|
37
|
+
## ban / unban(需 admin-用户 token)
|
|
38
|
+
|
|
39
|
+
走 user-auth `/api/v1/admin/users/{id}/{ban,unban}`,鉴权用 **admin 用户 token**(ROPC password grant,凭证读 Infisical `/shared-secrets/credentials`)。
|
|
40
|
+
|
|
41
|
+
- **cn-prod / cn-stage** 需运行时设 `INFISICAL_CN_EMAIL` / `INFISICAL_CN_PASSWORD`(读 cn Infisical)。
|
|
42
|
+
- **prod / cn-prod**(两个生产环境)执行前要求输入 `yes` 确认;`--yes` 可跳过。
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
optima-account ban user@example.com --reason "abuse" --env prod
|
|
46
|
+
optima-account unban 18898654855 --env cn-prod
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
> ⚠️ **ban 不是即时踢会话**:仅置 `is_active=false`,挡新登录/刷新,但**已签发的 access token 仍有效到过期**。需要立即失效在线会话的能力是 user-auth 后续支持项。
|
|
50
|
+
|
|
51
|
+
## 安全提醒
|
|
52
|
+
|
|
53
|
+
1. ban/unban 前看清打印的 `🎯 目标账号`(手机/email/userId)。
|
|
54
|
+
2. prod / cn-prod 是生产,确认无误再 `yes`。
|
|
55
|
+
3. ban 是名义禁用,活跃会话不会立刻断——别误以为已立即封死。
|
|
56
|
+
|
|
57
|
+
## 相关命令
|
|
58
|
+
|
|
59
|
+
- `optima-grant-subscription` / `optima-grant-balance` - 开通会员 / 赠送 credits
|
|
60
|
+
- `optima-entitlement` - 产品权益授予/撤销/查询
|
|
61
|
+
- `optima-query-db` - 直查数据库核对(如 `is_active` / `banned_at`)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: "entitlement"
|
|
3
|
+
description: "当用户请求授予产品权益、发放 entitlement、发 scout-gift、给用户开某个 product、撤销权益、退权益、refund entitlement、查看某用户的权益列表时,使用此技能。支持 stage、prod、cn-prod、cn-stage 四个环境;标识符支持邮箱/手机号/userId。"
|
|
4
|
+
allowed-tools: ["Bash"]
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# 产品权益(Entitlement:grant / revoke / list)
|
|
8
|
+
|
|
9
|
+
管理 billing 的产品权益(如 `scout-gift` 这类 `ONE_SHOT_SKILL` product)。CLI:`optima-entitlement`。
|
|
10
|
+
|
|
11
|
+
标识符 `<email|phone|userId>`:**cn-prod / cn-stage 用户多为手机号**,三种都支持;AWS stage/prod 仅 email。授予/撤销前打印 `🎯 目标账号` 反查回显。
|
|
12
|
+
|
|
13
|
+
## 执行方式
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
optima-entitlement grant <email|phone|userId> --product-key <slug> --justification "..." --env <env> [--yes]
|
|
17
|
+
optima-entitlement revoke <email|phone|userId> --product-key <slug> --reason "..." --env <env> [--yes]
|
|
18
|
+
optima-entitlement list <email|phone|userId> --env <env>
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
| 参数 | 说明 | 默认 |
|
|
22
|
+
|------|------|------|
|
|
23
|
+
| `<email\|phone\|userId>` | 用户标识(必填;`--email` 为向后兼容别名) | - |
|
|
24
|
+
| `--product-key <slug>` | 产品 key,如 `scout-gift`(grant/revoke 必填) | - |
|
|
25
|
+
| `--justification "..."` | 授予理由(grant 必填,billing 否则 400) | - |
|
|
26
|
+
| `--reason "..."` | 撤销理由(revoke 必填) | - |
|
|
27
|
+
| `--env <env>` | stage / prod / cn-prod / cn-stage | stage |
|
|
28
|
+
| `--yes` | 跳过 prod / cn-prod 确认 | - |
|
|
29
|
+
|
|
30
|
+
服务端硬编码:`source=ADMIN_GRANT, priceCents=0, currency=USD, grantedBy=<dev-skills client>`。revoke 拒绝非 ADMIN_GRANT 来源(PAYMENT/PARTNER)。
|
|
31
|
+
|
|
32
|
+
## 快速操作
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
# 给 cn-prod 手机号用户发 scout-gift
|
|
36
|
+
optima-entitlement grant 18898654855 --product-key scout-gift --justification "运营赠送" --env cn-prod
|
|
37
|
+
|
|
38
|
+
# 查权益
|
|
39
|
+
optima-entitlement list 18898654855 --env cn-prod
|
|
40
|
+
|
|
41
|
+
# 撤销
|
|
42
|
+
optima-entitlement revoke user@example.com --product-key scout-gift --reason "误发" --env prod
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## 环境说明
|
|
46
|
+
|
|
47
|
+
- **stage / prod(AWS)**:仅 email;经 RDS SSH 隧道解析 userId。
|
|
48
|
+
- **cn-prod / cn-stage(阿里云)**:email/手机号/userId;经 user-auth internal lookup(M2M,无 SSH 隧道)。走 HTTPS billing API(billing-api.yzsgo.com / billing-api.stage.optima.chat)。
|
|
49
|
+
|
|
50
|
+
> prod / cn-prod 为生产环境,grant/revoke 执行前要求 `yes` 确认(`--yes` 跳过)。
|
|
51
|
+
|
|
52
|
+
## 相关命令
|
|
53
|
+
|
|
54
|
+
- `optima-account status` - 查看用户订阅+权益汇总
|
|
55
|
+
- `optima-grant-subscription` / `optima-grant-balance` - 开通会员 / 赠送 credits
|
|
@@ -11,11 +11,11 @@ allowed-tools: ["Bash"]
|
|
|
11
11
|
## 执行方式:使用 CLI 工具
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
|
-
optima-grant-balance <email> --amount <usd> [options]
|
|
14
|
+
optima-grant-balance <email|phone|userId> --amount <usd> [options]
|
|
15
15
|
```
|
|
16
16
|
|
|
17
17
|
**为什么使用 CLI 工具**:
|
|
18
|
-
- 自动通过 email 查找 userId
|
|
18
|
+
- 自动通过 email/手机号/userId 查找 userId(cn 用户多为手机号;执行前打印 🎯 目标账号反查)
|
|
19
19
|
- 自动处理 SSH 隧道和数据库连接
|
|
20
20
|
- 不会影响现有订阅和已有余额(发放 bonus 积分(30 天有效期,重复执行会叠加发放))
|
|
21
21
|
- 自动留审计痕迹(credit_lot 行,幂等键前缀 `dev-skills-grant:`)
|
|
@@ -47,7 +47,7 @@ optima-grant-balance user@example.com --amount 20 --description "服务中断补
|
|
|
47
47
|
|
|
48
48
|
| 参数 | 说明 | 默认值 |
|
|
49
49
|
|------|------|--------|
|
|
50
|
-
| `<email>` |
|
|
50
|
+
| `<email|phone|userId>` | 用户标识(必填;cn 支持手机号/userId) | - |
|
|
51
51
|
| `--amount <usd>` | USD 金额(必填,> 0) | - |
|
|
52
52
|
| `--description <text>` | 描述/原因(仅 console 输出) | - |
|
|
53
53
|
| `--env <env>` | 环境:stage, prod, cn-prod, cn-stage | stage |
|
|
@@ -13,11 +13,11 @@ allowed-tools: ["Bash"]
|
|
|
13
13
|
**重要**:无论用户使用 `/grant-subscription` 命令还是直接请求开通会员,都应该使用 `optima-grant-subscription` CLI 工具:
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
|
-
optima-grant-subscription <email> [options]
|
|
16
|
+
optima-grant-subscription <email|phone|userId> [options]
|
|
17
17
|
```
|
|
18
18
|
|
|
19
19
|
**为什么使用 CLI 工具**:
|
|
20
|
-
- 自动通过 email 查找 userId
|
|
20
|
+
- 自动通过 email/手机号/userId 查找 userId(cn 用户多为手机号;执行前打印 🎯 目标账号反查)
|
|
21
21
|
- 自动处理 SSH 隧道和数据库连接
|
|
22
22
|
- 自动取消旧订阅、经 billing API supersede 旧授予(void 旧 subscription lot + 发新)
|
|
23
23
|
- 自动按 plan 配置发放 subscription 积分(期末过期)和 token quota
|
|
@@ -60,10 +60,10 @@ optima-grant-subscription user@example.com --plan pro-cn --env cn-stage
|
|
|
60
60
|
|
|
61
61
|
| 参数 | 说明 | 默认值 |
|
|
62
62
|
|------|------|--------|
|
|
63
|
-
| `<email>` |
|
|
63
|
+
| `<email|phone|userId>` | 用户标识(必填;cn 支持手机号/userId) | - |
|
|
64
64
|
| `--plan <id>` | 计划:trial, starter, pro, enterprise | pro |
|
|
65
65
|
| `--months <n>` | 时长(月) | 1 |
|
|
66
|
-
| `--env <env>` | 环境:stage, prod, cn-prod | stage |
|
|
66
|
+
| `--env <env>` | 环境:stage, prod, cn-prod, cn-stage | stage |
|
|
67
67
|
|
|
68
68
|
> **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 隧道)。
|
|
69
69
|
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: "account"
|
|
3
|
+
description: "当用户请求查看账号状态、查询某用户的订阅/权益、禁用账号、封禁用户、ban、停用账号、解封、恢复账号、unban 时,使用此技能。支持 stage、prod、cn-prod、cn-stage 四个环境;标识符支持邮箱/手机号/userId。"
|
|
4
|
+
allowed-tools: ["Bash"]
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# 账号运营(status / ban / unban)
|
|
8
|
+
|
|
9
|
+
以用户为中心的运营 admin 操作:查状态、禁用、恢复。CLI:`optima-account`。
|
|
10
|
+
|
|
11
|
+
标识符 `<email|phone|userId>`:**cn-prod / cn-stage 用户多为手机号注册**,三种都支持;AWS stage/prod 仅 email(经 RDS SSH 隧道解析)。执行前都会打印 `🎯 目标账号` 反查回显,防止发错账号。
|
|
12
|
+
|
|
13
|
+
## 执行方式
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
optima-account status <email|phone|userId> --env <env>
|
|
17
|
+
optima-account ban <email|phone|userId> --reason "..." --env <env> [--yes]
|
|
18
|
+
optima-account unban <email|phone|userId> --env <env> [--yes]
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
| 参数 | 说明 | 默认 |
|
|
22
|
+
|------|------|------|
|
|
23
|
+
| `<email\|phone\|userId>` | 用户标识(必填);cn 支持手机号/userId | - |
|
|
24
|
+
| `--reason "..."` | 禁用原因(**ban 必填**) | - |
|
|
25
|
+
| `--env <env>` | stage / prod / cn-prod / cn-stage | stage |
|
|
26
|
+
| `--yes` | 跳过 prod / cn-prod 的确认提示 | - |
|
|
27
|
+
|
|
28
|
+
## status(只读聚合)
|
|
29
|
+
|
|
30
|
+
显示:订阅(membership-status)+ 权益(entitlements)。纯 M2M,无需 admin 凭证。
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
optima-account status 18898654855 --env cn-prod
|
|
34
|
+
```
|
|
35
|
+
> ⚠️ 账号禁用状态(banned_at/原因)与 credits 余额**暂不显示**——无 M2M 读取端点,见 optima-dev-skills#36。
|
|
36
|
+
|
|
37
|
+
## ban / unban(需 admin-用户 token)
|
|
38
|
+
|
|
39
|
+
走 user-auth `/api/v1/admin/users/{id}/{ban,unban}`,鉴权用 **admin 用户 token**(ROPC password grant,凭证读 Infisical `/shared-secrets/credentials`)。
|
|
40
|
+
|
|
41
|
+
- **cn-prod / cn-stage** 需运行时设 `INFISICAL_CN_EMAIL` / `INFISICAL_CN_PASSWORD`(读 cn Infisical)。
|
|
42
|
+
- **prod / cn-prod**(两个生产环境)执行前要求输入 `yes` 确认;`--yes` 可跳过。
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
optima-account ban user@example.com --reason "abuse" --env prod
|
|
46
|
+
optima-account unban 18898654855 --env cn-prod
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
> ⚠️ **ban 不是即时踢会话**:仅置 `is_active=false`,挡新登录/刷新,但**已签发的 access token 仍有效到过期**。需要立即失效在线会话的能力是 user-auth 后续支持项。
|
|
50
|
+
|
|
51
|
+
## 安全提醒
|
|
52
|
+
|
|
53
|
+
1. ban/unban 前看清打印的 `🎯 目标账号`(手机/email/userId)。
|
|
54
|
+
2. prod / cn-prod 是生产,确认无误再 `yes`。
|
|
55
|
+
3. ban 是名义禁用,活跃会话不会立刻断——别误以为已立即封死。
|
|
56
|
+
|
|
57
|
+
## 相关命令
|
|
58
|
+
|
|
59
|
+
- `optima-grant-subscription` / `optima-grant-balance` - 开通会员 / 赠送 credits
|
|
60
|
+
- `optima-entitlement` - 产品权益授予/撤销/查询
|
|
61
|
+
- `optima-query-db` - 直查数据库核对(如 `is_active` / `banned_at`)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: "entitlement"
|
|
3
|
+
description: "当用户请求授予产品权益、发放 entitlement、发 scout-gift、给用户开某个 product、撤销权益、退权益、refund entitlement、查看某用户的权益列表时,使用此技能。支持 stage、prod、cn-prod、cn-stage 四个环境;标识符支持邮箱/手机号/userId。"
|
|
4
|
+
allowed-tools: ["Bash"]
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# 产品权益(Entitlement:grant / revoke / list)
|
|
8
|
+
|
|
9
|
+
管理 billing 的产品权益(如 `scout-gift` 这类 `ONE_SHOT_SKILL` product)。CLI:`optima-entitlement`。
|
|
10
|
+
|
|
11
|
+
标识符 `<email|phone|userId>`:**cn-prod / cn-stage 用户多为手机号**,三种都支持;AWS stage/prod 仅 email。授予/撤销前打印 `🎯 目标账号` 反查回显。
|
|
12
|
+
|
|
13
|
+
## 执行方式
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
optima-entitlement grant <email|phone|userId> --product-key <slug> --justification "..." --env <env> [--yes]
|
|
17
|
+
optima-entitlement revoke <email|phone|userId> --product-key <slug> --reason "..." --env <env> [--yes]
|
|
18
|
+
optima-entitlement list <email|phone|userId> --env <env>
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
| 参数 | 说明 | 默认 |
|
|
22
|
+
|------|------|------|
|
|
23
|
+
| `<email\|phone\|userId>` | 用户标识(必填;`--email` 为向后兼容别名) | - |
|
|
24
|
+
| `--product-key <slug>` | 产品 key,如 `scout-gift`(grant/revoke 必填) | - |
|
|
25
|
+
| `--justification "..."` | 授予理由(grant 必填,billing 否则 400) | - |
|
|
26
|
+
| `--reason "..."` | 撤销理由(revoke 必填) | - |
|
|
27
|
+
| `--env <env>` | stage / prod / cn-prod / cn-stage | stage |
|
|
28
|
+
| `--yes` | 跳过 prod / cn-prod 确认 | - |
|
|
29
|
+
|
|
30
|
+
服务端硬编码:`source=ADMIN_GRANT, priceCents=0, currency=USD, grantedBy=<dev-skills client>`。revoke 拒绝非 ADMIN_GRANT 来源(PAYMENT/PARTNER)。
|
|
31
|
+
|
|
32
|
+
## 快速操作
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
# 给 cn-prod 手机号用户发 scout-gift
|
|
36
|
+
optima-entitlement grant 18898654855 --product-key scout-gift --justification "运营赠送" --env cn-prod
|
|
37
|
+
|
|
38
|
+
# 查权益
|
|
39
|
+
optima-entitlement list 18898654855 --env cn-prod
|
|
40
|
+
|
|
41
|
+
# 撤销
|
|
42
|
+
optima-entitlement revoke user@example.com --product-key scout-gift --reason "误发" --env prod
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## 环境说明
|
|
46
|
+
|
|
47
|
+
- **stage / prod(AWS)**:仅 email;经 RDS SSH 隧道解析 userId。
|
|
48
|
+
- **cn-prod / cn-stage(阿里云)**:email/手机号/userId;经 user-auth internal lookup(M2M,无 SSH 隧道)。走 HTTPS billing API(billing-api.yzsgo.com / billing-api.stage.optima.chat)。
|
|
49
|
+
|
|
50
|
+
> prod / cn-prod 为生产环境,grant/revoke 执行前要求 `yes` 确认(`--yes` 跳过)。
|
|
51
|
+
|
|
52
|
+
## 相关命令
|
|
53
|
+
|
|
54
|
+
- `optima-account status` - 查看用户订阅+权益汇总
|
|
55
|
+
- `optima-grant-subscription` / `optima-grant-balance` - 开通会员 / 赠送 credits
|
|
@@ -10,11 +10,11 @@ description: "Use when the user wants to grant credits (bonus, 30-day expiry) to
|
|
|
10
10
|
## 执行方式:使用 CLI 工具
|
|
11
11
|
|
|
12
12
|
```bash
|
|
13
|
-
optima-grant-balance <email> --amount <usd> [options]
|
|
13
|
+
optima-grant-balance <email|phone|userId> --amount <usd> [options]
|
|
14
14
|
```
|
|
15
15
|
|
|
16
16
|
**为什么使用 CLI 工具**:
|
|
17
|
-
- 自动通过 email 查找 userId
|
|
17
|
+
- 自动通过 email/手机号/userId 查找 userId(cn 用户多为手机号;执行前打印 🎯 目标账号反查)
|
|
18
18
|
- 自动处理 SSH 隧道和数据库连接
|
|
19
19
|
- 不会影响现有订阅和已有余额(发放 bonus 积分(30 天有效期,重复执行会叠加发放))
|
|
20
20
|
- 自动留审计痕迹(credit_lot 行,幂等键前缀 `dev-skills-grant:`)
|
|
@@ -46,7 +46,7 @@ optima-grant-balance user@example.com --amount 20 --description "服务中断补
|
|
|
46
46
|
|
|
47
47
|
| 参数 | 说明 | 默认值 |
|
|
48
48
|
|------|------|--------|
|
|
49
|
-
| `<email>` |
|
|
49
|
+
| `<email|phone|userId>` | 用户标识(必填;cn 支持手机号/userId) | - |
|
|
50
50
|
| `--amount <usd>` | USD 金额(必填,> 0) | - |
|
|
51
51
|
| `--description <text>` | 描述/原因(仅 console 输出) | - |
|
|
52
52
|
| `--env <env>` | 环境:stage, prod, cn-prod, cn-stage | stage |
|
package/README.md
CHANGED
|
@@ -105,6 +105,12 @@ Claude:
|
|
|
105
105
|
| `optima-show-env` | 查看服务环境变量 | `optima-show-env commerce-backend stage --filter DATABASE` |
|
|
106
106
|
| `optima-generate-test-token` | 生成测试 token | `optima-generate-test-token --business-name "测试店铺"` |
|
|
107
107
|
| `optima-discount` | 优惠码管理 | `optima-discount create --code LAUNCH20 --percent 20 --env stage` |
|
|
108
|
+
| `optima-grant-subscription` | 开通/切换订阅 | `optima-grant-subscription 18898654855 --plan pro-cn --env cn-prod` |
|
|
109
|
+
| `optima-grant-balance` | 赠送 credits | `optima-grant-balance user@example.com --amount 5 --env prod` |
|
|
110
|
+
| `optima-entitlement` | 产品权益 grant/revoke/list | `optima-entitlement grant 18898654855 --product-key scout-gift --justification "..." --env cn-prod` |
|
|
111
|
+
| `optima-account` | 账号 status/ban/unban | `optima-account ban user@example.com --reason "abuse" --env prod` |
|
|
112
|
+
|
|
113
|
+
> **4 环境 + 标识符**:`grant-subscription` / `grant-balance` / `entitlement` / `account` 均支持 `stage` / `prod` / `cn-prod` / `cn-stage`。标识符 `<email\|phone\|userId>`——**cn-prod / cn-stage 用户多为手机号注册**,三种均可;AWS stage/prod 仅 email。`ban`/`unban` 及 `account status` 的禁用态读取需 admin-用户凭证(Infisical `/shared-secrets/credentials`;cn 另需 `INFISICAL_CN_EMAIL/PASSWORD`)。
|
|
108
114
|
|
|
109
115
|
**特点**:
|
|
110
116
|
- ✅ 支持 CI、Stage、Prod 三个环境(query-db)
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// optima-account:以用户为中心的运营 admin 操作。
|
|
4
|
+
// · status — 只读聚合(订阅 + 权益)。M2M。
|
|
5
|
+
// · ban / unban — user-auth 账号禁用/恢复。需 admin-用户 token(非 M2M)。
|
|
6
|
+
//
|
|
7
|
+
// 标识符 <email|phone|userId>:cn-prod/cn-stage 支持三种,AWS stage/prod 仅 email
|
|
8
|
+
// (见 resolveTargetUser)。两个生产环境(prod/cn-prod)ban/unban 前会要求确认。
|
|
9
|
+
import { resolveTargetUser } from './grant-subscription';
|
|
10
|
+
import { callBilling, callUserAuthAsAdmin, validateEnvCnProd } from './billing-http';
|
|
11
|
+
import { confirmIfProd } from './confirm-prompt';
|
|
12
|
+
|
|
13
|
+
interface CommonArgs {
|
|
14
|
+
identifier: string;
|
|
15
|
+
env: string;
|
|
16
|
+
reason: string | null;
|
|
17
|
+
yes: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function parseArgs(argv: string[], opts: { reason?: boolean } = {}): CommonArgs {
|
|
21
|
+
const out: Partial<CommonArgs> = { env: 'stage', reason: null, yes: false };
|
|
22
|
+
for (let i = 0; i < argv.length; i++) {
|
|
23
|
+
const a = argv[i];
|
|
24
|
+
const next = argv[i + 1];
|
|
25
|
+
if (a === '--env') { out.env = next; i++; }
|
|
26
|
+
else if (a === '--reason') { out.reason = next; i++; }
|
|
27
|
+
else if (a === '--yes') { out.yes = true; }
|
|
28
|
+
else if (a.startsWith('--')) { throw new Error(`Unknown arg: ${a}`); }
|
|
29
|
+
else if (out.identifier) { throw new Error(`Unexpected positional arg: ${a} (identifier already set to ${out.identifier})`); }
|
|
30
|
+
else { out.identifier = a; }
|
|
31
|
+
}
|
|
32
|
+
if (!out.identifier) throw new Error('target user required: <email|phone|userId>');
|
|
33
|
+
validateEnvCnProd(out.env as string);
|
|
34
|
+
if (opts.reason && !out.reason) throw new Error('--reason required for ban');
|
|
35
|
+
return out as CommonArgs;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function runStatus(argv: string[]): Promise<void> {
|
|
39
|
+
const { identifier, env } = parseArgs(argv);
|
|
40
|
+
const { userId, identity } = await resolveTargetUser(env, identifier);
|
|
41
|
+
|
|
42
|
+
// 订阅(membership-status,M2M;路径前缀 /api/internal 经 billing base URL)
|
|
43
|
+
let sub = '(读取失败)';
|
|
44
|
+
try {
|
|
45
|
+
const { body } = await callBilling<{ active: boolean; planId: string; status: string }>(
|
|
46
|
+
env, 'GET', `/api/internal/users/${encodeURIComponent(userId)}/membership-status`,
|
|
47
|
+
);
|
|
48
|
+
sub = `active=${body.active} plan=${body.planId} status=${body.status}`;
|
|
49
|
+
} catch (e) {
|
|
50
|
+
sub = `(读取失败: ${(e as Error).message})`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 权益(admin/entitlements,M2M)
|
|
54
|
+
let ents = '(读取失败)';
|
|
55
|
+
try {
|
|
56
|
+
const { body } = await callBilling<{ entitlements: Array<{ productKey: string; status: string; expiresAt?: string | null }> }>(
|
|
57
|
+
env, 'GET', `/api/billing/admin/entitlements?userId=${encodeURIComponent(userId)}`,
|
|
58
|
+
);
|
|
59
|
+
const rows = body.entitlements ?? [];
|
|
60
|
+
ents = rows.length ? rows.map((r) => `${r.productKey}(${r.status})`).join(', ') : '(无)';
|
|
61
|
+
} catch (e) {
|
|
62
|
+
ents = `(读取失败: ${(e as Error).message})`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
console.log(`\n=== 账号状态 [${env.toUpperCase()}] ===`);
|
|
66
|
+
console.log(`userId : ${userId}`);
|
|
67
|
+
console.log(`手机 : ${identity.phone || '(无)'}`);
|
|
68
|
+
console.log(`email : ${identity.email || '(无)'}`);
|
|
69
|
+
console.log(`订阅 : ${sub}`);
|
|
70
|
+
console.log(`权益 : ${ents}`);
|
|
71
|
+
console.log(`禁用态/credits 余额: 暂未纳入(banned_at 与 credits 无 M2M 读取端点,见 optima-dev-skills#36)\n`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function runBan(argv: string[]): Promise<void> {
|
|
75
|
+
const { identifier, env, reason, yes } = parseArgs(argv, { reason: true });
|
|
76
|
+
const { userId, identity } = await resolveTargetUser(env, identifier);
|
|
77
|
+
|
|
78
|
+
await confirmIfProd(
|
|
79
|
+
env,
|
|
80
|
+
`Action: BAN userId=${userId} (手机=${identity.phone || '(无)'} email=${identity.email || '(无)'}) on ${env.toUpperCase()}\nReason: ${reason}`,
|
|
81
|
+
yes,
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
console.log(`\n🚫 Banning ${identifier} (userId=${userId})...`);
|
|
85
|
+
const res = await callUserAuthAsAdmin(env, 'POST', `/api/v1/admin/users/${encodeURIComponent(userId)}/ban`, { reason });
|
|
86
|
+
console.log(`✓ Banned (HTTP ${res.status})`);
|
|
87
|
+
console.log('⚠️ 注意:ban 仅置 is_active=false——挡新登录/刷新,但**不会立即失效已签发的 access token**(活跃会话到 token 过期才失效)。即时踢会话需 user-auth 后续支持。\n');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function runUnban(argv: string[]): Promise<void> {
|
|
91
|
+
const { identifier, env, yes } = parseArgs(argv);
|
|
92
|
+
const { userId, identity } = await resolveTargetUser(env, identifier);
|
|
93
|
+
|
|
94
|
+
await confirmIfProd(
|
|
95
|
+
env,
|
|
96
|
+
`Action: UNBAN userId=${userId} (手机=${identity.phone || '(无)'} email=${identity.email || '(无)'}) on ${env.toUpperCase()}`,
|
|
97
|
+
yes,
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
console.log(`\n♻️ Unbanning ${identifier} (userId=${userId})...`);
|
|
101
|
+
const res = await callUserAuthAsAdmin(env, 'POST', `/api/v1/admin/users/${encodeURIComponent(userId)}/unban`);
|
|
102
|
+
console.log(`✓ Unbanned (HTTP ${res.status})\n`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function printHelp(): void {
|
|
106
|
+
console.log(`Usage: optima-account <subcommand> <email|phone|userId> [options]
|
|
107
|
+
|
|
108
|
+
Subcommands:
|
|
109
|
+
status 只读聚合:订阅(membership) + 权益(entitlements)
|
|
110
|
+
ban 禁用账号(user-auth is_active=false)。需 --reason
|
|
111
|
+
unban 恢复账号
|
|
112
|
+
|
|
113
|
+
Target user: <email|phone|userId> (positional). phone/userId only on
|
|
114
|
+
cn-prod / cn-stage; AWS stage/prod resolve email only.
|
|
115
|
+
|
|
116
|
+
Options:
|
|
117
|
+
--reason "..." Ban reason (required for ban; stored on the user)
|
|
118
|
+
--yes Skip prod/cn-prod confirmation prompt
|
|
119
|
+
--env <env> Environment: stage, prod, cn-prod, cn-stage (default: stage)
|
|
120
|
+
|
|
121
|
+
Notes:
|
|
122
|
+
· ban/unban 用 admin-用户 token(Infisical /shared-secrets/credentials)。
|
|
123
|
+
· ban 非即时踢会话:仅挡新登录/刷新,活跃 token 过期后失效。
|
|
124
|
+
· 禁用原因/banned_at 与 credits 余额暂不在 status 显示(见 #36)。
|
|
125
|
+
|
|
126
|
+
Examples:
|
|
127
|
+
optima-account status 18898654855 --env cn-prod
|
|
128
|
+
optima-account ban user@example.com --reason "abuse" --env prod
|
|
129
|
+
optima-account unban 18898654855 --env cn-prod`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function main(): Promise<void> {
|
|
133
|
+
const [, , subcommand, ...rest] = process.argv;
|
|
134
|
+
if (!subcommand || subcommand === '-h' || subcommand === '--help') { printHelp(); process.exit(0); }
|
|
135
|
+
switch (subcommand) {
|
|
136
|
+
case 'status': await runStatus(rest); break;
|
|
137
|
+
case 'ban': await runBan(rest); break;
|
|
138
|
+
case 'unban': await runUnban(rest); break;
|
|
139
|
+
default:
|
|
140
|
+
console.error(`Unknown subcommand: ${subcommand}`);
|
|
141
|
+
printHelp();
|
|
142
|
+
process.exit(1);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
main().catch((err) => { console.error('\n❌ Error:', err.message); process.exit(1); });
|
|
@@ -255,6 +255,111 @@ export async function callSkills<T = unknown>(
|
|
|
255
255
|
return callService<T>(getSkillsUrl(env), env, method, path, body);
|
|
256
256
|
}
|
|
257
257
|
|
|
258
|
+
// ───── Admin-USER token (ROPC password grant) ───────────────────────────────
|
|
259
|
+
// Distinct from getServiceToken (M2M client_credentials): user-auth's /admin/*
|
|
260
|
+
// endpoints gate on get_current_admin_user — a real role=ADMIN *user* — which an
|
|
261
|
+
// M2M token (no user_id) can't satisfy. So ban/unban mint a password-grant token
|
|
262
|
+
// for the seeded admin account. Per-env ROPC client = the same public client
|
|
263
|
+
// generate-test-token uses; admin email/password live in Infisical
|
|
264
|
+
// /shared-secrets/credentials. Cached separately from the M2M tokenCache.
|
|
265
|
+
const adminTokenCache: Record<string, string> = {};
|
|
266
|
+
|
|
267
|
+
const ADMIN_ROPC_CLIENT: Record<string, string> = {
|
|
268
|
+
stage: 'commerce-cli-stage-ihbbwplz',
|
|
269
|
+
prod: 'commerce-cli-ecs-pro-i2r5of1h',
|
|
270
|
+
'cn-prod': 'dev-skill-cli-cn-pro-acvkmcuq',
|
|
271
|
+
'cn-stage': 'dev-skill-cli-cn-sta-3dvsxzdo',
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
const ADMIN_CREDS_PATH = '/shared-secrets/credentials';
|
|
275
|
+
|
|
276
|
+
export async function getAdminUserToken(env: string): Promise<string> {
|
|
277
|
+
if (adminTokenCache[env]) return adminTokenCache[env];
|
|
278
|
+
|
|
279
|
+
let email: string;
|
|
280
|
+
let password: string;
|
|
281
|
+
if (env === 'cn-prod' || env === 'cn-stage') {
|
|
282
|
+
// cn Infisical (separate instance; needs INFISICAL_CN_EMAIL/PASSWORD at runtime).
|
|
283
|
+
const cnTok = getCnInfisicalToken();
|
|
284
|
+
const creds = getCnSecrets(cnTok, ADMIN_CREDS_PATH, false, env === 'cn-stage' ? 'staging' : 'prod');
|
|
285
|
+
email = creds['USER_AUTH_ADMIN_EMAIL'];
|
|
286
|
+
password = creds['USER_AUTH_ADMIN_PASSWORD'];
|
|
287
|
+
} else {
|
|
288
|
+
const cfg = getInfisicalConfig();
|
|
289
|
+
const tok = getInfisicalToken(cfg);
|
|
290
|
+
email = fetchInfisicalSecret(env, ADMIN_CREDS_PATH, 'USER_AUTH_ADMIN_EMAIL', cfg, tok);
|
|
291
|
+
password = fetchInfisicalSecret(env, ADMIN_CREDS_PATH, 'USER_AUTH_ADMIN_PASSWORD', cfg, tok);
|
|
292
|
+
}
|
|
293
|
+
if (!email || !password) {
|
|
294
|
+
throw new Error(`admin 凭证缺失(Infisical ${ADMIN_CREDS_PATH} 的 USER_AUTH_ADMIN_EMAIL/PASSWORD,env=${env})`);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const clientId = ADMIN_ROPC_CLIENT[env];
|
|
298
|
+
const authUrl = USER_AUTH_URLS[env];
|
|
299
|
+
if (!clientId || !authUrl) throw new Error(`Unknown env: ${env}`);
|
|
300
|
+
|
|
301
|
+
// fetch (not execSync curl) so the password never lands in a shell command line.
|
|
302
|
+
const form = new URLSearchParams({
|
|
303
|
+
grant_type: 'password',
|
|
304
|
+
username: email,
|
|
305
|
+
password,
|
|
306
|
+
client_id: clientId,
|
|
307
|
+
});
|
|
308
|
+
const res = await fetch(`${authUrl}/api/v1/oauth/token`, {
|
|
309
|
+
method: 'POST',
|
|
310
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
311
|
+
body: form,
|
|
312
|
+
});
|
|
313
|
+
const text = await res.text();
|
|
314
|
+
let parsed: { access_token?: string };
|
|
315
|
+
try {
|
|
316
|
+
parsed = JSON.parse(text);
|
|
317
|
+
} catch {
|
|
318
|
+
throw new Error(`user-auth admin token endpoint returned non-JSON (${env}): ${text.slice(0, 200)}`);
|
|
319
|
+
}
|
|
320
|
+
if (!parsed.access_token) {
|
|
321
|
+
throw new Error(`admin-user token mint failed (${env}): ${text.slice(0, 200)}`);
|
|
322
|
+
}
|
|
323
|
+
adminTokenCache[env] = parsed.access_token;
|
|
324
|
+
return parsed.access_token;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Authenticated call to user-auth as the admin USER (role=ADMIN), for the
|
|
329
|
+
* /api/v1/admin/* endpoints that getServiceToken's M2M token can't reach
|
|
330
|
+
* (ban/unban). Mirrors callService's shape; single retry on 5xx.
|
|
331
|
+
*/
|
|
332
|
+
export async function callUserAuthAsAdmin<T = unknown>(
|
|
333
|
+
env: string,
|
|
334
|
+
method: 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE',
|
|
335
|
+
path: string,
|
|
336
|
+
body?: object,
|
|
337
|
+
): Promise<ServiceResponse<T>> {
|
|
338
|
+
const authUrl = USER_AUTH_URLS[env];
|
|
339
|
+
if (!authUrl) throw new Error(`Unknown env: ${env}`);
|
|
340
|
+
const token = await getAdminUserToken(env);
|
|
341
|
+
|
|
342
|
+
const doFetch = async () => fetch(`${authUrl}${path}`, {
|
|
343
|
+
method,
|
|
344
|
+
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
|
|
345
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
let res = await doFetch();
|
|
349
|
+
if (res.status >= 500) res = await doFetch();
|
|
350
|
+
const text = await res.text();
|
|
351
|
+
if (!res.ok) {
|
|
352
|
+
throw new Error(formatServiceError(res.status, res.statusText, text));
|
|
353
|
+
}
|
|
354
|
+
let parsed: T;
|
|
355
|
+
try {
|
|
356
|
+
parsed = text ? (JSON.parse(text) as T) : (undefined as unknown as T);
|
|
357
|
+
} catch {
|
|
358
|
+
throw new Error(`user-auth admin returned non-JSON 2xx body: ${text.slice(0, 200)}`);
|
|
359
|
+
}
|
|
360
|
+
return { status: res.status, body: parsed };
|
|
361
|
+
}
|
|
362
|
+
|
|
258
363
|
/**
|
|
259
364
|
* Resolve a user's id by email via user-auth's internal lookup endpoint
|
|
260
365
|
* (POST /api/v1/internal/users/lookup). cn-prod only: AWS envs resolve via
|
|
@@ -292,25 +397,6 @@ export async function resolveUserIdByEmail(env: string, email: string): Promise<
|
|
|
292
397
|
return parsed.user_id;
|
|
293
398
|
}
|
|
294
399
|
|
|
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
|
-
|
|
314
400
|
/**
|
|
315
401
|
* Resolve a user's id by phone via user-auth's internal lookup endpoint
|
|
316
402
|
* (POST /api/v1/internal/users/lookup with {phone}). Mirrors
|