@optima-chat/dev-skills 0.8.1 → 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/logs.ts +200 -0
- 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');
|
|
@@ -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();
|
|
@@ -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",
|