@optima-chat/dev-skills 0.8.0 → 0.8.2
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 +27 -47
- package/bin/cli.js +1 -1
- package/bin/helpers/billing-http.ts +20 -1
- package/bin/helpers/confirm-prompt.ts +8 -4
- 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/logs.ts +200 -0
- package/dist/bin/helpers/billing-http.js +19 -0
- package/dist/bin/helpers/confirm-prompt.js +7 -4
- 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/logs.js +207 -0
- package/package.json +2 -1
package/.claude/commands/logs.md
CHANGED
|
@@ -50,7 +50,8 @@
|
|
|
50
50
|
- `ci` - CI 持续集成环境(开发环境,默认)
|
|
51
51
|
- `stage` - Stage 预发布环境(ECS Fargate)
|
|
52
52
|
- `prod` - 生产环境(ECS Fargate)
|
|
53
|
-
- `cn-prod`(别名 `cn`)- 阿里云 cn-prod 环境(SAE,cn-beijing
|
|
53
|
+
- `cn-prod`(别名 `cn`)- 阿里云 cn-prod 环境(SAE,cn-beijing,`optima-logs` 直连 SLS)
|
|
54
|
+
- `cn-stage` - 阿里云 cn-stage 预发环境(SAE,cn-beijing,`optima-logs` 直连 SLS)
|
|
54
55
|
|
|
55
56
|
## 示例
|
|
56
57
|
|
|
@@ -72,7 +73,7 @@
|
|
|
72
73
|
- `ci` 或未指定 → 使用 SSH + Docker Compose(第 0 节,默认)
|
|
73
74
|
- `stage` → 使用 AWS CloudWatch Logs - ECS(第 1 节)
|
|
74
75
|
- `prod` → 使用 AWS CloudWatch Logs - ECS(第 2 节)
|
|
75
|
-
- `cn-prod` / `cn` → 阿里云
|
|
76
|
+
- `cn-prod` / `cn` / `cn-stage` → 阿里云 SLS,`optima-logs` 直连(第 3 节,免 buildbox)
|
|
76
77
|
|
|
77
78
|
### 0. CI 环境(environment = "ci" 或默认)
|
|
78
79
|
|
|
@@ -211,59 +212,38 @@ aws logs get-log-events --log-group-name /ecs/commerce-backend-prod --log-stream
|
|
|
211
212
|
|
|
212
213
|
**注意**: `optima-store` 仅在 Stage 环境部署
|
|
213
214
|
|
|
214
|
-
### 3. cn-prod 环境(environment = "cn-prod" 或 "cn")
|
|
215
|
+
### 3. cn-prod / cn-stage 环境(environment = "cn-prod"/"cn" 或 "cn-stage")
|
|
215
216
|
|
|
216
|
-
**部署方式**: 阿里云 **SAE**(
|
|
217
|
-
**访问方式**:
|
|
217
|
+
**部署方式**: 阿里云 **SAE**(cn-beijing),不是 ECS/CloudWatch。
|
|
218
|
+
**访问方式**: ✅ **用 `optima-logs` CLI 直连阿里云 SLS**,不再经 buildbox。
|
|
218
219
|
|
|
219
|
-
>
|
|
220
|
+
> ✅ 现状更新(2026-06-16):cn-prod / cn-stage 全部服务**已接 SLS**(含 ECI 的 `agent-runtime`)。
|
|
221
|
+
> SLS `GetLogs` 是公网控制面 API,本机 `aliyun-optima` profile 直连即可,支持**历史检索 + 时间窗 + 关键词**——
|
|
222
|
+
> 旧的「SSH buildbox → SAE `DescribeInstanceLog` 取当前缓冲」流程已弃用(缓冲式、重启即丢、不能检索)。
|
|
220
223
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
-
|
|
225
|
-
-
|
|
226
|
-
|
|
227
|
-
|
|
224
|
+
**推荐用法(首选)**:
|
|
225
|
+
```bash
|
|
226
|
+
optima-logs gateway-core # 默认 cn-prod,最近 1h,100 行
|
|
227
|
+
optima-logs gateway-core --env cn-stage
|
|
228
|
+
optima-logs agent-runtime --env cn-prod --since 2h # ECI 容器也直接可见
|
|
229
|
+
optima-logs user-auth --grep error -n 200
|
|
230
|
+
optima-logs gateway-core --since 30m --json | jq . # 机器可读
|
|
231
|
+
```
|
|
228
232
|
|
|
229
|
-
|
|
|
230
|
-
|
|
231
|
-
|
|
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` |
|
|
233
|
+
参数:`--env stage|prod|cn-prod|cn-stage`、`--since 30m|2h|1d`、`--grep <kw>`、`--lines/-n <N>`、`--json`。
|
|
234
|
+
`service` == SLS logstore 名(同名)。列全部 logstore:
|
|
235
|
+
`aliyun sls ListLogStores --project optima-cn-prod-1911493506120573 --region cn-beijing --profile aliyun-optima`
|
|
239
236
|
|
|
240
|
-
|
|
237
|
+
**底层(仅参考,正常用 `optima-logs` 即可)**:
|
|
238
|
+
- SLS project:`optima-cn-prod-1911493506120573` / `optima-cn-stage-1911493506120573`(`optima-<env>-<accountId>`)。
|
|
239
|
+
- logstore = service 名;日志正文在 `content` 字段。
|
|
241
240
|
```bash
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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
|
|
241
|
+
NOW=$(date +%s); FROM=$((NOW-3600))
|
|
242
|
+
aliyun sls GetLogs --project optima-cn-prod-1911493506120573 --logstore gateway-core \
|
|
243
|
+
--from $FROM --to $NOW --line 100 --reverse true --query error \
|
|
244
|
+
--region cn-beijing --profile aliyun-optima
|
|
260
245
|
```
|
|
261
246
|
|
|
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
|
-
|
|
267
247
|
## 完整示例脚本
|
|
268
248
|
|
|
269
249
|
### Stage 环境
|
package/bin/cli.js
CHANGED
|
@@ -36,7 +36,7 @@ switch (command) {
|
|
|
36
36
|
log(' optima-generate-test-token [--env production] Generate test token', 'cyan');
|
|
37
37
|
log(' optima-grant-balance <email> --amount <usd> [--env] Grant USD wallet balance', 'cyan');
|
|
38
38
|
log(' optima-grant-subscription <email|phone|userId> --plan <p> [--env] Grant subscription', 'cyan');
|
|
39
|
-
log('
|
|
39
|
+
log(' optima-logs <service> [--env] [--since] [--grep] [-n] View logs (cn=SLS 直连/aws=CloudWatch)', 'cyan');
|
|
40
40
|
log(' /restart-ecs <service> [env] Restart ECS service (skill)', 'cyan');
|
|
41
41
|
|
|
42
42
|
log('\nSupported Services:', 'yellow');
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { execSync } from 'child_process';
|
|
2
2
|
import { fetchInfisicalSecret } from './infisical-secrets';
|
|
3
|
-
import { getInfisicalConfig, getInfisicalToken, getCnInfisicalToken, getCnSecrets } 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',
|
|
@@ -292,6 +292,25 @@ export async function resolveUserIdByEmail(env: string, email: string): Promise<
|
|
|
292
292
|
return parsed.user_id;
|
|
293
293
|
}
|
|
294
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
|
+
|
|
295
314
|
/**
|
|
296
315
|
* Resolve a user's id by phone via user-auth's internal lookup endpoint
|
|
297
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()); });
|
|
@@ -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[] }>(
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* optima-logs —— 一条命令直取服务日志,四环境统一,用 --env 区分。
|
|
4
|
+
*
|
|
5
|
+
* stage / prod : AWS CloudWatch(`aws logs tail /ecs/<svc>-<env>`)
|
|
6
|
+
* cn-prod / cn-stage: 阿里云 SLS 直连(`aliyun sls GetLogs`)
|
|
7
|
+
*
|
|
8
|
+
* cn 的关键改进:旧流程要 SSH 进 buildbox 再调 SAE `DescribeInstanceLog`,
|
|
9
|
+
* 只能看实例**当前缓冲**(重启即丢、不能检索)。现在 cn-prod/cn-stage 全部
|
|
10
|
+
* 服务已接 SLS,GetLogs 是公网控制面 API,本机 `aliyun-optima` profile 直连即可:
|
|
11
|
+
* - 免 buildbox 跳板
|
|
12
|
+
* - 支持时间窗(--since)+ 关键词检索(--grep)+ 历史(重启不丢)
|
|
13
|
+
*
|
|
14
|
+
* 前置:
|
|
15
|
+
* AWS → 已配 aws CLI 凭证(ap-southeast-1)
|
|
16
|
+
* cn → 已配 aliyun CLI profile `aliyun-optima`(cn-beijing)
|
|
17
|
+
*
|
|
18
|
+
* 用法:
|
|
19
|
+
* optima-logs gateway-core # 默认 cn-prod,最近 1h,100 行
|
|
20
|
+
* optima-logs gateway-core --env cn-stage
|
|
21
|
+
* optima-logs user-auth --env prod --since 2h
|
|
22
|
+
* optima-logs commerce-backend --grep error -n 200
|
|
23
|
+
* optima-logs gateway-core --since 30m --json # 机器可读
|
|
24
|
+
*
|
|
25
|
+
* service == SLS logstore == AWS 日志组 `/ecs/<svc>-<env>` 的 <svc>(同名)。
|
|
26
|
+
*/
|
|
27
|
+
import { execFileSync } from 'child_process';
|
|
28
|
+
|
|
29
|
+
const ALIYUN_ACCOUNT = '1911493506120573';
|
|
30
|
+
const ALIYUN_PROFILE = 'aliyun-optima';
|
|
31
|
+
const ALIYUN_REGION = 'cn-beijing';
|
|
32
|
+
const AWS_REGION = 'ap-southeast-1';
|
|
33
|
+
|
|
34
|
+
const AWS_ENVS = ['stage', 'prod'] as const;
|
|
35
|
+
const CN_ENVS = ['cn-prod', 'cn-stage'] as const;
|
|
36
|
+
type Env = (typeof AWS_ENVS)[number] | (typeof CN_ENVS)[number];
|
|
37
|
+
const ALL_ENVS: Env[] = [...AWS_ENVS, ...CN_ENVS];
|
|
38
|
+
|
|
39
|
+
const C = { g: '\x1b[32m', y: '\x1b[33m', b: '\x1b[34m', d: '\x1b[2m', n: '\x1b[0m' };
|
|
40
|
+
|
|
41
|
+
interface Args {
|
|
42
|
+
service: string;
|
|
43
|
+
env: Env;
|
|
44
|
+
lines: number;
|
|
45
|
+
since: string; // 原样字符串,如 1h / 30m / 2d
|
|
46
|
+
grep?: string;
|
|
47
|
+
json: boolean;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const HELP = `Usage: optima-logs <service> [options]
|
|
51
|
+
|
|
52
|
+
四环境统一查日志,cn 直连阿里云 SLS(免 buildbox),AWS 走 CloudWatch。
|
|
53
|
+
|
|
54
|
+
Required:
|
|
55
|
+
<service> 服务名(== SLS logstore == /ecs/<svc>-<env> 的 <svc>)
|
|
56
|
+
|
|
57
|
+
Optional:
|
|
58
|
+
--env <e> stage | prod | cn-prod | cn-stage (default: cn-prod)
|
|
59
|
+
--lines, -n <N> 返回行数 (default: 100)
|
|
60
|
+
--since <dur> 时间窗,如 30m / 2h / 1d / 3600(秒) (default: 1h)
|
|
61
|
+
--grep <kw> 关键词检索(cn=SLS query / aws=filter-pattern)
|
|
62
|
+
--json 原始 JSON 输出(接管道)
|
|
63
|
+
--help, -h 显示本帮助
|
|
64
|
+
|
|
65
|
+
Examples:
|
|
66
|
+
optima-logs gateway-core
|
|
67
|
+
optima-logs user-auth --env prod --since 2h
|
|
68
|
+
optima-logs commerce-backend --env cn-prod --grep error -n 200`;
|
|
69
|
+
|
|
70
|
+
/** 把 30m / 2h / 1d / 纯秒 解析成秒数;同时回填给 aws 用的带单位字符串。 */
|
|
71
|
+
function parseSince(raw: string): { sec: number; awsStr: string } {
|
|
72
|
+
const m = raw.match(/^(\d+)([smhdw]?)$/);
|
|
73
|
+
if (!m) throw new Error(`--since 格式非法: ${raw}(用 30m / 2h / 1d / 纯秒)`);
|
|
74
|
+
const n = parseInt(m[1], 10);
|
|
75
|
+
const unit = m[2] || 's';
|
|
76
|
+
const mult: Record<string, number> = { s: 1, m: 60, h: 3600, d: 86400, w: 604800 };
|
|
77
|
+
return { sec: n * mult[unit], awsStr: `${n}${unit}` };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function parseArgs(argv: string[]): Args {
|
|
81
|
+
if (argv.length === 0 || argv[0] === '-h' || argv[0] === '--help') {
|
|
82
|
+
console.log(HELP);
|
|
83
|
+
process.exit(0);
|
|
84
|
+
}
|
|
85
|
+
const out: Partial<Args> = { env: 'cn-prod', lines: 100, since: '1h', json: false };
|
|
86
|
+
const positional: string[] = [];
|
|
87
|
+
for (let i = 0; i < argv.length; i++) {
|
|
88
|
+
const a = argv[i];
|
|
89
|
+
const next = argv[i + 1];
|
|
90
|
+
switch (a) {
|
|
91
|
+
case '--env': out.env = next as Env; i++; break;
|
|
92
|
+
case '--lines': case '-n': {
|
|
93
|
+
const v = parseInt(next, 10);
|
|
94
|
+
if (isNaN(v) || v <= 0) throw new Error('--lines 需要正整数');
|
|
95
|
+
out.lines = v; i++; break;
|
|
96
|
+
}
|
|
97
|
+
case '--since': out.since = next; i++; break;
|
|
98
|
+
case '--grep': out.grep = next; i++; break;
|
|
99
|
+
case '--json': out.json = true; break;
|
|
100
|
+
default:
|
|
101
|
+
if (a.startsWith('-')) throw new Error(`未知参数: ${a}`);
|
|
102
|
+
positional.push(a);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (positional.length === 0) throw new Error('缺少 <service>(见 --help)');
|
|
106
|
+
if (positional.length > 1) throw new Error(`多余参数: ${positional.slice(1).join(' ')}`);
|
|
107
|
+
out.service = positional[0];
|
|
108
|
+
if (!ALL_ENVS.includes(out.env as Env)) {
|
|
109
|
+
throw new Error(`--env 非法: ${out.env}(可选 ${ALL_ENVS.join(' | ')})`);
|
|
110
|
+
}
|
|
111
|
+
return out as Args;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function run(cmd: string, args: string[]): string {
|
|
115
|
+
return execFileSync(cmd, args, { encoding: 'utf-8', maxBuffer: 128 * 1024 * 1024 });
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** AWS CloudWatch:aws logs tail /ecs/<svc>-<env>。 */
|
|
119
|
+
function fetchAws(args: Args): void {
|
|
120
|
+
const group = `/ecs/${args.service}-${args.env}`;
|
|
121
|
+
const { awsStr } = parseSince(args.since);
|
|
122
|
+
const cmd = ['logs', 'tail', group, '--since', awsStr, '--region', AWS_REGION, '--format', args.json ? 'json' : 'short'];
|
|
123
|
+
if (args.grep) cmd.push('--filter-pattern', args.grep);
|
|
124
|
+
process.stderr.write(`${C.d}# AWS CloudWatch ${group} (since ${awsStr}${args.grep ? `, grep "${args.grep}"` : ''})${C.n}\n`);
|
|
125
|
+
try {
|
|
126
|
+
process.stdout.write(run('aws', cmd));
|
|
127
|
+
} catch (e: any) {
|
|
128
|
+
if (/ResourceNotFoundException/.test(e.stderr || e.message || '')) {
|
|
129
|
+
throw new Error(`日志组不存在: ${group}\n 确认服务名/环境对,或该服务未部署在 ${args.env}。`);
|
|
130
|
+
}
|
|
131
|
+
throw new Error(e.stderr || e.message);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** 阿里云 SLS:aliyun sls GetLogs,project=optima-<env>-<account>,logstore=service。 */
|
|
136
|
+
function fetchCn(args: Args): void {
|
|
137
|
+
const project = `optima-${args.env}-${ALIYUN_ACCOUNT}`;
|
|
138
|
+
const { sec } = parseSince(args.since);
|
|
139
|
+
const now = Math.floor(Date.now() / 1000);
|
|
140
|
+
const from = now - sec;
|
|
141
|
+
const cmd = [
|
|
142
|
+
'sls', 'GetLogs',
|
|
143
|
+
'--project', project,
|
|
144
|
+
'--logstore', args.service,
|
|
145
|
+
'--from', String(from),
|
|
146
|
+
'--to', String(now),
|
|
147
|
+
'--line', String(args.lines),
|
|
148
|
+
'--reverse', 'true', // 先拿最新 N 条
|
|
149
|
+
'--region', ALIYUN_REGION,
|
|
150
|
+
'--profile', ALIYUN_PROFILE,
|
|
151
|
+
];
|
|
152
|
+
if (args.grep) { cmd.push('--query', args.grep); }
|
|
153
|
+
process.stderr.write(`${C.d}# 阿里云 SLS ${project}/${args.service} (since ${args.since}${args.grep ? `, query "${args.grep}"` : ''})${C.n}\n`);
|
|
154
|
+
|
|
155
|
+
let raw: string;
|
|
156
|
+
try {
|
|
157
|
+
raw = run('aliyun', cmd);
|
|
158
|
+
} catch (e: any) {
|
|
159
|
+
const msg = e.stderr || e.message || '';
|
|
160
|
+
if (/LogStoreNotExist|ProjectNotExist/.test(msg)) {
|
|
161
|
+
throw new Error(`SLS logstore 不存在: ${project}/${args.service}\n 确认服务名对,或该服务未接 SLS。列全部:\n aliyun sls ListLogStores --project ${project} --region ${ALIYUN_REGION} --profile ${ALIYUN_PROFILE}`);
|
|
162
|
+
}
|
|
163
|
+
throw new Error(msg);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const rows: Array<Record<string, string>> = JSON.parse(raw || '[]');
|
|
167
|
+
// reverse=true 取到的是新→旧,翻回旧→新便于阅读
|
|
168
|
+
rows.reverse();
|
|
169
|
+
if (args.json) {
|
|
170
|
+
console.log(JSON.stringify(rows, null, 2));
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
if (rows.length === 0) {
|
|
174
|
+
process.stderr.write(`${C.y}(无日志:该时间窗内 ${args.service} 无输出,或 --grep 没命中)${C.n}\n`);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
for (const r of rows) {
|
|
178
|
+
// SLS 存的 content 已含容器时间戳 + stream(stdout/stderr)前缀,直接打印即可
|
|
179
|
+
process.stdout.write((r.content ?? JSON.stringify(r)) + '\n');
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function main(): void {
|
|
184
|
+
let args: Args;
|
|
185
|
+
try {
|
|
186
|
+
args = parseArgs(process.argv.slice(2));
|
|
187
|
+
} catch (e: any) {
|
|
188
|
+
console.error(`${C.y}✗ ${e.message}${C.n}`);
|
|
189
|
+
process.exit(1);
|
|
190
|
+
}
|
|
191
|
+
try {
|
|
192
|
+
if (CN_ENVS.includes(args.env as any)) fetchCn(args);
|
|
193
|
+
else fetchAws(args);
|
|
194
|
+
} catch (e: any) {
|
|
195
|
+
console.error(`${C.y}✗ ${e.message}${C.n}`);
|
|
196
|
+
process.exit(1);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
main();
|
|
@@ -6,6 +6,7 @@ exports.getServiceToken = getServiceToken;
|
|
|
6
6
|
exports.callBilling = callBilling;
|
|
7
7
|
exports.callSkills = callSkills;
|
|
8
8
|
exports.resolveUserIdByEmail = resolveUserIdByEmail;
|
|
9
|
+
exports.resolveUserIdByEmailAnyEnv = resolveUserIdByEmailAnyEnv;
|
|
9
10
|
exports.resolveUserIdByPhone = resolveUserIdByPhone;
|
|
10
11
|
exports.getUserById = getUserById;
|
|
11
12
|
const child_process_1 = require("child_process");
|
|
@@ -256,6 +257,24 @@ async function resolveUserIdByEmail(env, email) {
|
|
|
256
257
|
console.log(`✓ Found user: ${parsed.user_id}`);
|
|
257
258
|
return parsed.user_id;
|
|
258
259
|
}
|
|
260
|
+
/**
|
|
261
|
+
* Resolve email → userId across all envs, picking the path that exists for
|
|
262
|
+
* each cloud. AWS (stage/prod) reaches the database over the RDS SSH tunnel
|
|
263
|
+
* (db-utils.resolveUserId). cn-prod / cn-stage have no tunnel into the Aliyun
|
|
264
|
+
* VPC-internal RDS, so they resolve via user-auth's internal HTTP lookup
|
|
265
|
+
* (resolveUserIdByEmail) — the cn dev-skills token carries the
|
|
266
|
+
* internal:users:write scope that endpoint requires. Lets the entitlement
|
|
267
|
+
* subcommands support cn the same way grant-subscription already does, without
|
|
268
|
+
* each call site re-implementing the branch.
|
|
269
|
+
*/
|
|
270
|
+
async function resolveUserIdByEmailAnyEnv(env, email) {
|
|
271
|
+
if (env === 'cn-prod' || env === 'cn-stage') {
|
|
272
|
+
return resolveUserIdByEmail(env, email);
|
|
273
|
+
}
|
|
274
|
+
const cfg = (0, db_utils_1.getInfisicalConfig)();
|
|
275
|
+
const token = (0, db_utils_1.getInfisicalToken)(cfg);
|
|
276
|
+
return (0, db_utils_1.resolveUserId)(email, env, cfg, token);
|
|
277
|
+
}
|
|
259
278
|
/**
|
|
260
279
|
* Resolve a user's id by phone via user-auth's internal lookup endpoint
|
|
261
280
|
* (POST /api/v1/internal/users/lookup with {phone}). Mirrors
|
|
@@ -36,13 +36,16 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
exports.confirmIfProd = confirmIfProd;
|
|
37
37
|
const readline = __importStar(require("readline"));
|
|
38
38
|
/**
|
|
39
|
-
* On
|
|
40
|
-
*
|
|
39
|
+
* On a production env, print the resolved action and require typing "yes" to
|
|
40
|
+
* proceed. Both AWS `prod` and Aliyun `cn-prod` are production — cn-prod must
|
|
41
|
+
* gate identically or the cn rollout silently loses the prod safety prompt.
|
|
42
|
+
* No-op on stage / cn-stage or when --yes was passed. Exits 1 if user declines.
|
|
41
43
|
*/
|
|
44
|
+
const PROD_ENVS = new Set(['prod', 'cn-prod']);
|
|
42
45
|
async function confirmIfProd(env, actionDescription, skipFlag) {
|
|
43
|
-
if (env
|
|
46
|
+
if (!PROD_ENVS.has(env) || skipFlag)
|
|
44
47
|
return;
|
|
45
|
-
console.log(`\n⚠️ About to perform on
|
|
48
|
+
console.log(`\n⚠️ About to perform on ${env.toUpperCase()}:\n${actionDescription}\n`);
|
|
46
49
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
47
50
|
const answer = await new Promise((resolve) => {
|
|
48
51
|
rl.question('Type "yes" to confirm: ', (a) => { rl.close(); resolve(a.trim()); });
|
|
@@ -24,7 +24,7 @@ Optional:
|
|
|
24
24
|
--ends <date> Valid-until
|
|
25
25
|
--max <N> Max total redemptions (default: unlimited; 1 = single-use)
|
|
26
26
|
--campaign <label> Grouping label
|
|
27
|
-
--env stage|prod
|
|
27
|
+
--env stage|prod|cn-prod|cn-stage (default: stage)
|
|
28
28
|
--yes Skip prod confirmation`);
|
|
29
29
|
process.exit(0);
|
|
30
30
|
}
|
|
@@ -81,7 +81,7 @@ Optional:
|
|
|
81
81
|
}
|
|
82
82
|
async function runCreate(argv) {
|
|
83
83
|
const args = parseArgs(argv);
|
|
84
|
-
(0, billing_http_1.
|
|
84
|
+
(0, billing_http_1.validateEnvCnProd)(args.env);
|
|
85
85
|
await (0, confirm_prompt_1.confirmIfProd)(args.env, `Create discount code ${args.code.toUpperCase()} (${args.percentOff}% off)`, args.yes);
|
|
86
86
|
const body = { code: args.code, percentOff: args.percentOff };
|
|
87
87
|
if (args.productKeys)
|
|
@@ -11,7 +11,7 @@ Required:
|
|
|
11
11
|
--code <CODE>
|
|
12
12
|
|
|
13
13
|
Optional:
|
|
14
|
-
--env stage|prod
|
|
14
|
+
--env stage|prod|cn-prod|cn-stage (default: stage)
|
|
15
15
|
--yes Skip prod confirmation`);
|
|
16
16
|
process.exit(0);
|
|
17
17
|
}
|
|
@@ -40,7 +40,7 @@ Optional:
|
|
|
40
40
|
}
|
|
41
41
|
async function runDisable(argv) {
|
|
42
42
|
const args = parseArgs(argv);
|
|
43
|
-
(0, billing_http_1.
|
|
43
|
+
(0, billing_http_1.validateEnvCnProd)(args.env);
|
|
44
44
|
await (0, confirm_prompt_1.confirmIfProd)(args.env, `Disable discount code ${args.code.toUpperCase()}`, args.yes);
|
|
45
45
|
const res = await (0, billing_http_1.callBilling)(args.env, 'PATCH', `/api/billing/admin/discount-codes/${encodeURIComponent(args.code)}`, { status: 'DISABLED' });
|
|
46
46
|
console.log(`✓ Disabled (HTTP ${res.status}):`);
|
|
@@ -59,7 +59,7 @@ Optional:
|
|
|
59
59
|
--products <a,b,...> Limit to these productKeys
|
|
60
60
|
--starts <date> Valid-from (YYYY-MM-DD or ISO)
|
|
61
61
|
--ends <date> Valid-until
|
|
62
|
-
--env stage|prod
|
|
62
|
+
--env stage|prod|cn-prod|cn-stage (default: stage)
|
|
63
63
|
--yes Skip prod confirmation`);
|
|
64
64
|
process.exit(0);
|
|
65
65
|
}
|
|
@@ -114,7 +114,7 @@ Optional:
|
|
|
114
114
|
}
|
|
115
115
|
async function runGenerate(argv) {
|
|
116
116
|
const args = parseArgs(argv);
|
|
117
|
-
(0, billing_http_1.
|
|
117
|
+
(0, billing_http_1.validateEnvCnProd)(args.env);
|
|
118
118
|
await (0, confirm_prompt_1.confirmIfProd)(args.env, `Generate ${args.count} unique discount codes (${args.percentOff}% off, campaign ${args.campaign})`, args.yes);
|
|
119
119
|
const body = { count: args.count, percentOff: args.percentOff, campaign: args.campaign };
|
|
120
120
|
if (args.productKeys)
|
|
@@ -10,7 +10,7 @@ Optional:
|
|
|
10
10
|
--campaign <label> Filter by campaign
|
|
11
11
|
--code <CODE> Filter by exact code
|
|
12
12
|
--limit <N> Max rows (default 500, max 1000)
|
|
13
|
-
--env stage|prod
|
|
13
|
+
--env stage|prod|cn-prod|cn-stage (default: stage)`);
|
|
14
14
|
process.exit(0);
|
|
15
15
|
}
|
|
16
16
|
const out = { env: 'stage' };
|
|
@@ -45,7 +45,7 @@ Optional:
|
|
|
45
45
|
}
|
|
46
46
|
async function runList(argv) {
|
|
47
47
|
const args = parseArgs(argv);
|
|
48
|
-
(0, billing_http_1.
|
|
48
|
+
(0, billing_http_1.validateEnvCnProd)(args.env);
|
|
49
49
|
const qs = new URLSearchParams();
|
|
50
50
|
if (args.campaign)
|
|
51
51
|
qs.set('campaign', args.campaign);
|
|
@@ -3,7 +3,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.runGrant = runGrant;
|
|
4
4
|
const billing_http_1 = require("../billing-http");
|
|
5
5
|
const confirm_prompt_1 = require("../confirm-prompt");
|
|
6
|
-
const db_utils_1 = require("../db-utils");
|
|
7
6
|
function parseArgs(argv) {
|
|
8
7
|
if (argv.length === 0 || argv[0] === '-h' || argv[0] === '--help') {
|
|
9
8
|
console.log(`Usage: optima-entitlement grant --email <user> --product-key <slug> --justification "..." [options]
|
|
@@ -15,7 +14,7 @@ Required:
|
|
|
15
14
|
|
|
16
15
|
Optional:
|
|
17
16
|
--yes Skip prod confirmation prompt (no-op on stage)
|
|
18
|
-
--env stage|prod
|
|
17
|
+
--env stage|prod|cn-prod|cn-stage (default: stage)
|
|
19
18
|
|
|
20
19
|
Hardcoded server-side: source=ADMIN_GRANT, priceCents=0, currency=USD, grantedBy=<clientId>.`);
|
|
21
20
|
process.exit(0);
|
|
@@ -57,10 +56,8 @@ Hardcoded server-side: source=ADMIN_GRANT, priceCents=0, currency=USD, grantedBy
|
|
|
57
56
|
}
|
|
58
57
|
async function runGrant(argv) {
|
|
59
58
|
const args = parseArgs(argv);
|
|
60
|
-
(0, billing_http_1.
|
|
61
|
-
const
|
|
62
|
-
const token = (0, db_utils_1.getInfisicalToken)(cfg);
|
|
63
|
-
const userId = await (0, db_utils_1.resolveUserId)(args.email, args.env, cfg, token);
|
|
59
|
+
(0, billing_http_1.validateEnvCnProd)(args.env);
|
|
60
|
+
const userId = await (0, billing_http_1.resolveUserIdByEmailAnyEnv)(args.env, args.email);
|
|
64
61
|
await (0, confirm_prompt_1.confirmIfProd)(args.env, `Action: GRANT product '${args.productKey}' to user ${args.email} (userId=${userId}) on ${args.env.toUpperCase()}\nJustification: ${args.justification}`, args.yes);
|
|
65
62
|
console.log(`\n🎁 Granting ${args.productKey} to ${args.email}...`);
|
|
66
63
|
const res = await (0, billing_http_1.callBilling)(args.env, 'POST', '/api/billing/admin/grant-entitlement', {
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.runList = runList;
|
|
4
4
|
const billing_http_1 = require("../billing-http");
|
|
5
|
-
const db_utils_1 = require("../db-utils");
|
|
6
5
|
function parseArgs(argv) {
|
|
7
6
|
if (argv.length === 0 || argv[0] === '-h' || argv[0] === '--help') {
|
|
8
7
|
console.log(`Usage: optima-entitlement list --email <user-email> [options]
|
|
@@ -11,7 +10,7 @@ Required:
|
|
|
11
10
|
--email <user-email> Resolved to userId via user-auth DB
|
|
12
11
|
|
|
13
12
|
Optional:
|
|
14
|
-
--env stage|prod
|
|
13
|
+
--env stage|prod|cn-prod|cn-stage (default: stage)`);
|
|
15
14
|
process.exit(0);
|
|
16
15
|
}
|
|
17
16
|
const out = { env: 'stage' };
|
|
@@ -36,10 +35,8 @@ Optional:
|
|
|
36
35
|
}
|
|
37
36
|
async function runList(argv) {
|
|
38
37
|
const args = parseArgs(argv);
|
|
39
|
-
(0, billing_http_1.
|
|
40
|
-
const
|
|
41
|
-
const token = (0, db_utils_1.getInfisicalToken)(cfg);
|
|
42
|
-
const userId = await (0, db_utils_1.resolveUserId)(args.email, args.env, cfg, token);
|
|
38
|
+
(0, billing_http_1.validateEnvCnProd)(args.env);
|
|
39
|
+
const userId = await (0, billing_http_1.resolveUserIdByEmailAnyEnv)(args.env, args.email);
|
|
43
40
|
const res = await (0, billing_http_1.callBilling)(args.env, 'GET', `/api/billing/admin/entitlements?userId=${encodeURIComponent(userId)}`);
|
|
44
41
|
const rows = res.body.entitlements ?? [];
|
|
45
42
|
if (rows.length === 0) {
|
|
@@ -3,7 +3,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.runRevoke = runRevoke;
|
|
4
4
|
const billing_http_1 = require("../billing-http");
|
|
5
5
|
const confirm_prompt_1 = require("../confirm-prompt");
|
|
6
|
-
const db_utils_1 = require("../db-utils");
|
|
7
6
|
function parseArgs(argv) {
|
|
8
7
|
if (argv.length === 0 || argv[0] === '-h' || argv[0] === '--help') {
|
|
9
8
|
console.log(`Usage: optima-entitlement revoke --email <user> --product-key <slug> --reason "..." [options]
|
|
@@ -15,7 +14,7 @@ Required:
|
|
|
15
14
|
|
|
16
15
|
Optional:
|
|
17
16
|
--yes Skip prod confirmation prompt (no-op on stage)
|
|
18
|
-
--env stage|prod
|
|
17
|
+
--env stage|prod|cn-prod|cn-stage (default: stage)
|
|
19
18
|
|
|
20
19
|
Refuses non-ADMIN_GRANT sources (PAYMENT, PARTNER) with source-specific
|
|
21
20
|
error pointing to the right reversal flow.`);
|
|
@@ -60,10 +59,8 @@ const PAYMENT_REFUSAL = `refusing to revoke a PAYMENT-source entitlement via CLI
|
|
|
60
59
|
const PARTNER_REFUSAL = `refusing to revoke a PARTNER-source entitlement via CLI; PARTNER grants are issued out-of-band and must be reversed via the partner contract / process that issued them. Manual psql is the escape hatch if absolutely necessary.`;
|
|
61
60
|
async function runRevoke(argv) {
|
|
62
61
|
const args = parseArgs(argv);
|
|
63
|
-
(0, billing_http_1.
|
|
64
|
-
const
|
|
65
|
-
const token = (0, db_utils_1.getInfisicalToken)(cfg);
|
|
66
|
-
const userId = await (0, db_utils_1.resolveUserId)(args.email, args.env, cfg, token);
|
|
62
|
+
(0, billing_http_1.validateEnvCnProd)(args.env);
|
|
63
|
+
const userId = await (0, billing_http_1.resolveUserIdByEmailAnyEnv)(args.env, args.email);
|
|
67
64
|
// Step 1: Fetch user's entitlements
|
|
68
65
|
const listRes = await (0, billing_http_1.callBilling)(args.env, 'GET', `/api/billing/admin/entitlements?userId=${encodeURIComponent(userId)}`);
|
|
69
66
|
const all = listRes.body.entitlements ?? [];
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
/**
|
|
5
|
+
* optima-logs —— 一条命令直取服务日志,四环境统一,用 --env 区分。
|
|
6
|
+
*
|
|
7
|
+
* stage / prod : AWS CloudWatch(`aws logs tail /ecs/<svc>-<env>`)
|
|
8
|
+
* cn-prod / cn-stage: 阿里云 SLS 直连(`aliyun sls GetLogs`)
|
|
9
|
+
*
|
|
10
|
+
* cn 的关键改进:旧流程要 SSH 进 buildbox 再调 SAE `DescribeInstanceLog`,
|
|
11
|
+
* 只能看实例**当前缓冲**(重启即丢、不能检索)。现在 cn-prod/cn-stage 全部
|
|
12
|
+
* 服务已接 SLS,GetLogs 是公网控制面 API,本机 `aliyun-optima` profile 直连即可:
|
|
13
|
+
* - 免 buildbox 跳板
|
|
14
|
+
* - 支持时间窗(--since)+ 关键词检索(--grep)+ 历史(重启不丢)
|
|
15
|
+
*
|
|
16
|
+
* 前置:
|
|
17
|
+
* AWS → 已配 aws CLI 凭证(ap-southeast-1)
|
|
18
|
+
* cn → 已配 aliyun CLI profile `aliyun-optima`(cn-beijing)
|
|
19
|
+
*
|
|
20
|
+
* 用法:
|
|
21
|
+
* optima-logs gateway-core # 默认 cn-prod,最近 1h,100 行
|
|
22
|
+
* optima-logs gateway-core --env cn-stage
|
|
23
|
+
* optima-logs user-auth --env prod --since 2h
|
|
24
|
+
* optima-logs commerce-backend --grep error -n 200
|
|
25
|
+
* optima-logs gateway-core --since 30m --json # 机器可读
|
|
26
|
+
*
|
|
27
|
+
* service == SLS logstore == AWS 日志组 `/ecs/<svc>-<env>` 的 <svc>(同名)。
|
|
28
|
+
*/
|
|
29
|
+
const child_process_1 = require("child_process");
|
|
30
|
+
const ALIYUN_ACCOUNT = '1911493506120573';
|
|
31
|
+
const ALIYUN_PROFILE = 'aliyun-optima';
|
|
32
|
+
const ALIYUN_REGION = 'cn-beijing';
|
|
33
|
+
const AWS_REGION = 'ap-southeast-1';
|
|
34
|
+
const AWS_ENVS = ['stage', 'prod'];
|
|
35
|
+
const CN_ENVS = ['cn-prod', 'cn-stage'];
|
|
36
|
+
const ALL_ENVS = [...AWS_ENVS, ...CN_ENVS];
|
|
37
|
+
const C = { g: '\x1b[32m', y: '\x1b[33m', b: '\x1b[34m', d: '\x1b[2m', n: '\x1b[0m' };
|
|
38
|
+
const HELP = `Usage: optima-logs <service> [options]
|
|
39
|
+
|
|
40
|
+
四环境统一查日志,cn 直连阿里云 SLS(免 buildbox),AWS 走 CloudWatch。
|
|
41
|
+
|
|
42
|
+
Required:
|
|
43
|
+
<service> 服务名(== SLS logstore == /ecs/<svc>-<env> 的 <svc>)
|
|
44
|
+
|
|
45
|
+
Optional:
|
|
46
|
+
--env <e> stage | prod | cn-prod | cn-stage (default: cn-prod)
|
|
47
|
+
--lines, -n <N> 返回行数 (default: 100)
|
|
48
|
+
--since <dur> 时间窗,如 30m / 2h / 1d / 3600(秒) (default: 1h)
|
|
49
|
+
--grep <kw> 关键词检索(cn=SLS query / aws=filter-pattern)
|
|
50
|
+
--json 原始 JSON 输出(接管道)
|
|
51
|
+
--help, -h 显示本帮助
|
|
52
|
+
|
|
53
|
+
Examples:
|
|
54
|
+
optima-logs gateway-core
|
|
55
|
+
optima-logs user-auth --env prod --since 2h
|
|
56
|
+
optima-logs commerce-backend --env cn-prod --grep error -n 200`;
|
|
57
|
+
/** 把 30m / 2h / 1d / 纯秒 解析成秒数;同时回填给 aws 用的带单位字符串。 */
|
|
58
|
+
function parseSince(raw) {
|
|
59
|
+
const m = raw.match(/^(\d+)([smhdw]?)$/);
|
|
60
|
+
if (!m)
|
|
61
|
+
throw new Error(`--since 格式非法: ${raw}(用 30m / 2h / 1d / 纯秒)`);
|
|
62
|
+
const n = parseInt(m[1], 10);
|
|
63
|
+
const unit = m[2] || 's';
|
|
64
|
+
const mult = { s: 1, m: 60, h: 3600, d: 86400, w: 604800 };
|
|
65
|
+
return { sec: n * mult[unit], awsStr: `${n}${unit}` };
|
|
66
|
+
}
|
|
67
|
+
function parseArgs(argv) {
|
|
68
|
+
if (argv.length === 0 || argv[0] === '-h' || argv[0] === '--help') {
|
|
69
|
+
console.log(HELP);
|
|
70
|
+
process.exit(0);
|
|
71
|
+
}
|
|
72
|
+
const out = { env: 'cn-prod', lines: 100, since: '1h', json: false };
|
|
73
|
+
const positional = [];
|
|
74
|
+
for (let i = 0; i < argv.length; i++) {
|
|
75
|
+
const a = argv[i];
|
|
76
|
+
const next = argv[i + 1];
|
|
77
|
+
switch (a) {
|
|
78
|
+
case '--env':
|
|
79
|
+
out.env = next;
|
|
80
|
+
i++;
|
|
81
|
+
break;
|
|
82
|
+
case '--lines':
|
|
83
|
+
case '-n': {
|
|
84
|
+
const v = parseInt(next, 10);
|
|
85
|
+
if (isNaN(v) || v <= 0)
|
|
86
|
+
throw new Error('--lines 需要正整数');
|
|
87
|
+
out.lines = v;
|
|
88
|
+
i++;
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
case '--since':
|
|
92
|
+
out.since = next;
|
|
93
|
+
i++;
|
|
94
|
+
break;
|
|
95
|
+
case '--grep':
|
|
96
|
+
out.grep = next;
|
|
97
|
+
i++;
|
|
98
|
+
break;
|
|
99
|
+
case '--json':
|
|
100
|
+
out.json = true;
|
|
101
|
+
break;
|
|
102
|
+
default:
|
|
103
|
+
if (a.startsWith('-'))
|
|
104
|
+
throw new Error(`未知参数: ${a}`);
|
|
105
|
+
positional.push(a);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (positional.length === 0)
|
|
109
|
+
throw new Error('缺少 <service>(见 --help)');
|
|
110
|
+
if (positional.length > 1)
|
|
111
|
+
throw new Error(`多余参数: ${positional.slice(1).join(' ')}`);
|
|
112
|
+
out.service = positional[0];
|
|
113
|
+
if (!ALL_ENVS.includes(out.env)) {
|
|
114
|
+
throw new Error(`--env 非法: ${out.env}(可选 ${ALL_ENVS.join(' | ')})`);
|
|
115
|
+
}
|
|
116
|
+
return out;
|
|
117
|
+
}
|
|
118
|
+
function run(cmd, args) {
|
|
119
|
+
return (0, child_process_1.execFileSync)(cmd, args, { encoding: 'utf-8', maxBuffer: 128 * 1024 * 1024 });
|
|
120
|
+
}
|
|
121
|
+
/** AWS CloudWatch:aws logs tail /ecs/<svc>-<env>。 */
|
|
122
|
+
function fetchAws(args) {
|
|
123
|
+
const group = `/ecs/${args.service}-${args.env}`;
|
|
124
|
+
const { awsStr } = parseSince(args.since);
|
|
125
|
+
const cmd = ['logs', 'tail', group, '--since', awsStr, '--region', AWS_REGION, '--format', args.json ? 'json' : 'short'];
|
|
126
|
+
if (args.grep)
|
|
127
|
+
cmd.push('--filter-pattern', args.grep);
|
|
128
|
+
process.stderr.write(`${C.d}# AWS CloudWatch ${group} (since ${awsStr}${args.grep ? `, grep "${args.grep}"` : ''})${C.n}\n`);
|
|
129
|
+
try {
|
|
130
|
+
process.stdout.write(run('aws', cmd));
|
|
131
|
+
}
|
|
132
|
+
catch (e) {
|
|
133
|
+
if (/ResourceNotFoundException/.test(e.stderr || e.message || '')) {
|
|
134
|
+
throw new Error(`日志组不存在: ${group}\n 确认服务名/环境对,或该服务未部署在 ${args.env}。`);
|
|
135
|
+
}
|
|
136
|
+
throw new Error(e.stderr || e.message);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
/** 阿里云 SLS:aliyun sls GetLogs,project=optima-<env>-<account>,logstore=service。 */
|
|
140
|
+
function fetchCn(args) {
|
|
141
|
+
const project = `optima-${args.env}-${ALIYUN_ACCOUNT}`;
|
|
142
|
+
const { sec } = parseSince(args.since);
|
|
143
|
+
const now = Math.floor(Date.now() / 1000);
|
|
144
|
+
const from = now - sec;
|
|
145
|
+
const cmd = [
|
|
146
|
+
'sls', 'GetLogs',
|
|
147
|
+
'--project', project,
|
|
148
|
+
'--logstore', args.service,
|
|
149
|
+
'--from', String(from),
|
|
150
|
+
'--to', String(now),
|
|
151
|
+
'--line', String(args.lines),
|
|
152
|
+
'--reverse', 'true', // 先拿最新 N 条
|
|
153
|
+
'--region', ALIYUN_REGION,
|
|
154
|
+
'--profile', ALIYUN_PROFILE,
|
|
155
|
+
];
|
|
156
|
+
if (args.grep) {
|
|
157
|
+
cmd.push('--query', args.grep);
|
|
158
|
+
}
|
|
159
|
+
process.stderr.write(`${C.d}# 阿里云 SLS ${project}/${args.service} (since ${args.since}${args.grep ? `, query "${args.grep}"` : ''})${C.n}\n`);
|
|
160
|
+
let raw;
|
|
161
|
+
try {
|
|
162
|
+
raw = run('aliyun', cmd);
|
|
163
|
+
}
|
|
164
|
+
catch (e) {
|
|
165
|
+
const msg = e.stderr || e.message || '';
|
|
166
|
+
if (/LogStoreNotExist|ProjectNotExist/.test(msg)) {
|
|
167
|
+
throw new Error(`SLS logstore 不存在: ${project}/${args.service}\n 确认服务名对,或该服务未接 SLS。列全部:\n aliyun sls ListLogStores --project ${project} --region ${ALIYUN_REGION} --profile ${ALIYUN_PROFILE}`);
|
|
168
|
+
}
|
|
169
|
+
throw new Error(msg);
|
|
170
|
+
}
|
|
171
|
+
const rows = JSON.parse(raw || '[]');
|
|
172
|
+
// reverse=true 取到的是新→旧,翻回旧→新便于阅读
|
|
173
|
+
rows.reverse();
|
|
174
|
+
if (args.json) {
|
|
175
|
+
console.log(JSON.stringify(rows, null, 2));
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
if (rows.length === 0) {
|
|
179
|
+
process.stderr.write(`${C.y}(无日志:该时间窗内 ${args.service} 无输出,或 --grep 没命中)${C.n}\n`);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
for (const r of rows) {
|
|
183
|
+
// SLS 存的 content 已含容器时间戳 + stream(stdout/stderr)前缀,直接打印即可
|
|
184
|
+
process.stdout.write((r.content ?? JSON.stringify(r)) + '\n');
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
function main() {
|
|
188
|
+
let args;
|
|
189
|
+
try {
|
|
190
|
+
args = parseArgs(process.argv.slice(2));
|
|
191
|
+
}
|
|
192
|
+
catch (e) {
|
|
193
|
+
console.error(`${C.y}✗ ${e.message}${C.n}`);
|
|
194
|
+
process.exit(1);
|
|
195
|
+
}
|
|
196
|
+
try {
|
|
197
|
+
if (CN_ENVS.includes(args.env))
|
|
198
|
+
fetchCn(args);
|
|
199
|
+
else
|
|
200
|
+
fetchAws(args);
|
|
201
|
+
}
|
|
202
|
+
catch (e) {
|
|
203
|
+
console.error(`${C.y}✗ ${e.message}${C.n}`);
|
|
204
|
+
process.exit(1);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
main();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@optima-chat/dev-skills",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.2",
|
|
4
4
|
"description": "Claude Code Skills for Optima development team - cross-environment collaboration tools",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"optima-generate-test-token": "dist/bin/helpers/generate-test-token.js",
|
|
10
10
|
"optima-grant-balance": "dist/bin/helpers/grant-balance.js",
|
|
11
11
|
"optima-grant-subscription": "dist/bin/helpers/grant-subscription.js",
|
|
12
|
+
"optima-logs": "dist/bin/helpers/logs.js",
|
|
12
13
|
"optima-plugin": "dist/bin/helpers/plugin.js",
|
|
13
14
|
"optima-discount": "dist/bin/helpers/discount.js",
|
|
14
15
|
"optima-product": "dist/bin/helpers/product.js",
|