@optima-chat/dev-skills 0.7.37 → 0.7.40
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/logs.md +55 -0
- package/.claude/skills/grant-balance/SKILL.md +10 -8
- package/.claude/skills/grant-subscription/SKILL.md +12 -6
- package/.claude/skills/logs/SKILL.md +14 -1
- package/.codex/skills/grant-balance/SKILL.md +10 -8
- package/.codex/skills/grant-subscription/SKILL.md +8 -1
- package/bin/cli.js +1 -0
- package/bin/helpers/billing-http.ts +83 -3
- package/bin/helpers/db-utils.ts +333 -23
- package/bin/helpers/grant-balance.ts +39 -46
- package/bin/helpers/grant-subscription.ts +47 -89
- package/bin/helpers/query-db.ts +25 -88
- package/bin/helpers/show-env.ts +3 -28
- package/bin/helpers/verify-health.ts +208 -0
- package/dist/bin/helpers/billing-http.js +82 -3
- package/dist/bin/helpers/db-utils.js +356 -27
- package/dist/bin/helpers/discount.js +0 -0
- package/dist/bin/helpers/entitlement.js +0 -0
- package/dist/bin/helpers/generate-test-token.js +0 -0
- package/dist/bin/helpers/grant-balance.js +35 -43
- package/dist/bin/helpers/grant-subscription.js +36 -86
- package/dist/bin/helpers/plugin.js +0 -0
- package/dist/bin/helpers/product.js +0 -0
- package/dist/bin/helpers/query-db.js +34 -71
- package/dist/bin/helpers/show-env.js +5 -17
- package/dist/bin/helpers/verify-health.js +253 -0
- package/package.json +3 -2
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
4
|
+
if (k2 === undefined) k2 = k;
|
|
5
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
6
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
7
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
8
|
+
}
|
|
9
|
+
Object.defineProperty(o, k2, desc);
|
|
10
|
+
}) : (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
o[k2] = m[k];
|
|
13
|
+
}));
|
|
14
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
15
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
16
|
+
}) : function(o, v) {
|
|
17
|
+
o["default"] = v;
|
|
18
|
+
});
|
|
19
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
20
|
+
var ownKeys = function(o) {
|
|
21
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
22
|
+
var ar = [];
|
|
23
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
24
|
+
return ar;
|
|
25
|
+
};
|
|
26
|
+
return ownKeys(o);
|
|
27
|
+
};
|
|
28
|
+
return function (mod) {
|
|
29
|
+
if (mod && mod.__esModule) return mod;
|
|
30
|
+
var result = {};
|
|
31
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
32
|
+
__setModuleDefault(result, mod);
|
|
33
|
+
return result;
|
|
34
|
+
};
|
|
35
|
+
})();
|
|
36
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
|
+
/**
|
|
38
|
+
* optima-verify-health —— 服务上线健康探针(L1 DNS / L2 TLS / L3-L5 /health)。
|
|
39
|
+
*
|
|
40
|
+
* 逐层探、逐层报,逐层 pass 才算上线成功。零依赖(纯 Node 内置)。
|
|
41
|
+
* 配套静态合规审计 audit-probe.py 在 optima-terraform 仓 docs/cn-prod/probes/(读 terraform 文件,绑仓不搬)。
|
|
42
|
+
*
|
|
43
|
+
* L1 DNS : FQDN 能解析到 IP
|
|
44
|
+
* L2 TLS : 443 握手 + 证书链/有效期/SNI(SAN)匹配
|
|
45
|
+
* L3 /health : optima-core 风格 /health,顶层 status 健康
|
|
46
|
+
* L4 真部署 : 解析 gitCommit;传 --expect-commit <sha> 时比对(证明跑的是这次镜像)
|
|
47
|
+
* L5 依赖 : checks[*] 全绿(DB/Redis/上游由服务在 health handler 自注册)
|
|
48
|
+
*
|
|
49
|
+
* ⚠️ optima-core 的 JS 与 Py 两套 /health schema 不一致,都兼容:
|
|
50
|
+
* JS : gitCommit(camel) / status∈{healthy,unhealthy} / 不健康返 503 / 有 gitBranch
|
|
51
|
+
* Py : git_commit(snake) / status∈{healthy,degraded} / 永远 200 / check 可 timeout|error / 无 git_branch
|
|
52
|
+
*
|
|
53
|
+
* 用法:
|
|
54
|
+
* optima-verify-health user-auth # 默认 cn-prod
|
|
55
|
+
* optima-verify-health user-auth --env prod # 环境 stage|prod|cn|all
|
|
56
|
+
* optima-verify-health --all --env all # stage/prod/cn 三环境矩阵
|
|
57
|
+
* optima-verify-health gateway-core --expect-commit a1b2c3d
|
|
58
|
+
* optima-verify-health --url https://auth-cn.optima.chat/health
|
|
59
|
+
* optima-verify-health --all --json # 机器可读,接 CI
|
|
60
|
+
* optima-verify-health --all --strict # warn 也算不通过
|
|
61
|
+
* 退出码:fail → 非 0;warn 默认放行(0),--strict 让 warn 也非 0。
|
|
62
|
+
*/
|
|
63
|
+
const node_dns_1 = require("node:dns");
|
|
64
|
+
const tls = __importStar(require("node:tls"));
|
|
65
|
+
const https = __importStar(require("node:https"));
|
|
66
|
+
// 服务 × 环境 FQDN 表。某服务某环境没部署 → 该 env 键缺省,探时跳过。
|
|
67
|
+
// cn-prod 真实 subdomain 抄自 optima-terraform 的 stack main.tf(非文档表,如 agentic-chat 真名是 agentic-chat-cn 不是 ai-cn)。
|
|
68
|
+
const SERVICES = {
|
|
69
|
+
'user-auth': { path: '/health', stage: 'auth-stage.optima.onl', prod: 'auth.optima.onl', cn: 'auth-cn.optima.chat' },
|
|
70
|
+
'agentic-chat': { path: '/api/health', stage: 'ai-stage.optima.onl', prod: 'ai.optima.onl', cn: 'agentic-chat-cn.optima.chat' },
|
|
71
|
+
'commerce-backend': { path: '/health', stage: 'api-stage.optima.onl', prod: 'api.optima.onl', cn: 'api-cn.optima.chat', cn_path: '/health/live' },
|
|
72
|
+
'mcp-host': { path: '/health', stage: 'mcp-stage.optima.onl', prod: 'mcp.optima.onl' },
|
|
73
|
+
'gateway-core': { path: '/health', cn: 'gw-cn.optima.chat' },
|
|
74
|
+
'optima-scout': { path: '/health', cn: 'scout-cn.optima.chat' },
|
|
75
|
+
'optima-skills': { path: '/health', cn: 'skills-cn.optima.chat' },
|
|
76
|
+
};
|
|
77
|
+
const ENVS = ['stage', 'prod', 'cn'];
|
|
78
|
+
const G = '\x1b[32m', R = '\x1b[31m', Y = '\x1b[33m', B = '\x1b[34m', N = '\x1b[0m';
|
|
79
|
+
const MARK = { ok: `${G}✅${N}`, fail: `${R}❌${N}`, warn: `${Y}⚠️ ${N}`, na: `${B}··${N}` };
|
|
80
|
+
const verdict = (ls) => ls.some((l) => l.result === 'fail') ? 'fail' : ls.some((l) => l.result === 'warn') ? 'warn' : 'pass';
|
|
81
|
+
const VTXT = {
|
|
82
|
+
pass: `${G}上线成功(L1-L5 全绿)${N}`,
|
|
83
|
+
warn: `${Y}存活但有告警(见上 ⚠️;非阻塞)${N}`,
|
|
84
|
+
fail: `${R}未通过 — 见上 ❌${N}`,
|
|
85
|
+
};
|
|
86
|
+
function tlsCheck(host, timeout = 10000) {
|
|
87
|
+
return new Promise((resolve) => {
|
|
88
|
+
const sock = tls.connect({ host, port: 443, servername: host, timeout }, () => {
|
|
89
|
+
const cert = sock.getPeerCertificate();
|
|
90
|
+
const days = Math.floor((new Date(cert.valid_to).getTime() - Date.now()) / 86400000);
|
|
91
|
+
const sans = (cert.subjectaltname || '').split(',').map((s) => s.trim().replace(/^DNS:/, '')).slice(0, 3);
|
|
92
|
+
sock.end();
|
|
93
|
+
resolve({ ok: true, result: days > 7 ? 'ok' : 'warn',
|
|
94
|
+
detail: `证书 ${days}d 后过期, SAN=${sans.join(',')}` + (days > 7 ? '' : ` (剩 ${days}d!)`) });
|
|
95
|
+
});
|
|
96
|
+
sock.on('error', (e) => resolve({ ok: false, result: 'fail', detail: `握手/证书失败: ${e.message}` }));
|
|
97
|
+
sock.on('timeout', () => { sock.destroy(); resolve({ ok: false, result: 'fail', detail: '握手超时' }); });
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
function httpGet(url, timeout = 10000) {
|
|
101
|
+
return new Promise((resolve, reject) => {
|
|
102
|
+
const req = https.get(url, { headers: { 'User-Agent': 'optima-verify-health' }, timeout }, (res) => {
|
|
103
|
+
let body = '';
|
|
104
|
+
res.on('data', (c) => (body += c));
|
|
105
|
+
res.on('end', () => resolve({ code: res.statusCode || 0, body, location: res.headers.location }));
|
|
106
|
+
});
|
|
107
|
+
req.on('error', reject);
|
|
108
|
+
req.on('timeout', () => { req.destroy(new Error('请求超时')); });
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
async function probe(host, path, expect) {
|
|
112
|
+
const layers = [];
|
|
113
|
+
// L1 DNS
|
|
114
|
+
try {
|
|
115
|
+
const addrs = await node_dns_1.promises.lookup(host, { all: true });
|
|
116
|
+
const ips = [...new Set(addrs.map((a) => a.address))];
|
|
117
|
+
layers.push({ layer: 'L1 DNS', result: 'ok', detail: `${host} → ${ips.join(', ')}` });
|
|
118
|
+
}
|
|
119
|
+
catch (e) {
|
|
120
|
+
layers.push({ layer: 'L1 DNS', result: 'fail', detail: `${host} 无法解析: ${e.message}` });
|
|
121
|
+
return layers;
|
|
122
|
+
}
|
|
123
|
+
// L2 TLS
|
|
124
|
+
const t = await tlsCheck(host);
|
|
125
|
+
layers.push({ layer: 'L2 TLS', result: t.result, detail: t.detail });
|
|
126
|
+
if (!t.ok)
|
|
127
|
+
return layers;
|
|
128
|
+
// L3-L5 /health
|
|
129
|
+
const url = `https://${host}${path}`;
|
|
130
|
+
let r;
|
|
131
|
+
try {
|
|
132
|
+
r = await httpGet(url);
|
|
133
|
+
}
|
|
134
|
+
catch (e) {
|
|
135
|
+
layers.push({ layer: 'L3 /health', result: 'fail', detail: `${url} 无法连接: ${e.message}` });
|
|
136
|
+
return layers;
|
|
137
|
+
}
|
|
138
|
+
if (r.code >= 300 && r.code < 400) {
|
|
139
|
+
layers.push({ layer: 'L3 /health', result: 'fail', detail: `HTTP ${r.code} 重定向到 ${r.location || '?'}(没路由到服务,疑似未部署/listener 缺失)` });
|
|
140
|
+
return layers;
|
|
141
|
+
}
|
|
142
|
+
let d;
|
|
143
|
+
try {
|
|
144
|
+
d = JSON.parse(r.body || '{}');
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
layers.push({ layer: 'L3 /health', result: 'fail', detail: `HTTP ${r.code} 但非 JSON(可能不是 optima-core /health): ${JSON.stringify(r.body.slice(0, 80))}` });
|
|
148
|
+
return layers;
|
|
149
|
+
}
|
|
150
|
+
const status = d.status ?? '?';
|
|
151
|
+
if (status === 'healthy')
|
|
152
|
+
layers.push({ layer: 'L3 /health', result: 'ok', detail: `HTTP ${r.code} status=healthy svc=${d.service ?? '?'} ver=${d.version ?? '?'}` });
|
|
153
|
+
else if (['ok', 'up', 'pass'].includes(status))
|
|
154
|
+
layers.push({ layer: 'L3 /health', result: 'warn', detail: `HTTP ${r.code} status=${status}(存活,但非 optima-core 标准 health,L4/L5 无从验证)` });
|
|
155
|
+
else if (status === 'degraded')
|
|
156
|
+
layers.push({ layer: 'L3 /health', result: 'warn', detail: `HTTP ${r.code} status=degraded(Py:部分依赖挂但服务起着,见 L5)` });
|
|
157
|
+
else
|
|
158
|
+
layers.push({ layer: 'L3 /health', result: 'fail', detail: `HTTP ${r.code} status=${status}` });
|
|
159
|
+
// L4 真部署 / 非裸起
|
|
160
|
+
const commit = d.gitCommit || d.git_commit || '';
|
|
161
|
+
const branch = d.gitBranch || '';
|
|
162
|
+
const bt = `commit=${commit || '∅'}` + (branch ? ` branch=${branch}` : '');
|
|
163
|
+
if (!commit)
|
|
164
|
+
layers.push({ layer: 'L4 部署', result: 'warn', detail: '无 gitCommit(没用 optima-core health,或 build-info 没烘进去)' });
|
|
165
|
+
else if (expect) {
|
|
166
|
+
const short = expect.slice(0, commit.length);
|
|
167
|
+
layers.push(commit === short
|
|
168
|
+
? { layer: 'L4 部署', result: 'ok', detail: `${bt} == 期望 ${short} ✓ 跑的是这次镜像` }
|
|
169
|
+
: { layer: 'L4 部署', result: 'fail', detail: `${bt} != 期望 ${short} ✗ 旧镜像/部署没生效` });
|
|
170
|
+
}
|
|
171
|
+
else
|
|
172
|
+
layers.push({ layer: 'L4 部署', result: 'ok', detail: `${bt}(未传 --expect-commit,仅展示)` });
|
|
173
|
+
// L5 依赖 checks 全绿
|
|
174
|
+
const checks = d.checks || {};
|
|
175
|
+
const keys = Object.keys(checks);
|
|
176
|
+
if (keys.length === 0)
|
|
177
|
+
layers.push({ layer: 'L5 依赖', result: 'warn', detail: 'health 未注册任何 checks(服务没在 handler 里挂 DB/Redis/上游)' });
|
|
178
|
+
else {
|
|
179
|
+
const bad = keys.filter((k) => checks[k].status !== 'healthy');
|
|
180
|
+
const summary = keys.map((k) => `${k}=${checks[k].status}(${checks[k].latencyMs ?? checks[k].latency_ms ?? '?'}ms)`).join(', ');
|
|
181
|
+
layers.push(bad.length
|
|
182
|
+
? { layer: 'L5 依赖', result: 'fail', detail: `${bad.length} 个不健康: ${bad.map((k) => `${k}=${checks[k].status}`).join(',')} | 全部: ${summary}` }
|
|
183
|
+
: { layer: 'L5 依赖', result: 'ok', detail: `${keys.length} 个依赖全绿: ${summary}` });
|
|
184
|
+
}
|
|
185
|
+
return layers;
|
|
186
|
+
}
|
|
187
|
+
function resolve(svc, e) {
|
|
188
|
+
const cfg = SERVICES[svc];
|
|
189
|
+
const host = cfg[e];
|
|
190
|
+
if (!host)
|
|
191
|
+
return null;
|
|
192
|
+
const path = cfg[`${e}_path`] || cfg.path;
|
|
193
|
+
return [`${svc} [${e}]`, host, path];
|
|
194
|
+
}
|
|
195
|
+
async function main() {
|
|
196
|
+
const argv = process.argv.slice(2);
|
|
197
|
+
const has = (f) => argv.includes(f);
|
|
198
|
+
const val = (f) => { const i = argv.indexOf(f); return i >= 0 ? argv[i + 1] : undefined; };
|
|
199
|
+
const asJson = has('--json'), strict = has('--strict');
|
|
200
|
+
const expect = val('--expect-commit');
|
|
201
|
+
const env = (val('--env') || 'cn');
|
|
202
|
+
const envs = env === 'all' ? ENVS : [env];
|
|
203
|
+
const positional = argv.filter((x, i) => !x.startsWith('--') && !(i > 0 && argv[i - 1].startsWith('--') && ['--env', '--expect-commit', '--url'].includes(argv[i - 1])));
|
|
204
|
+
let targets = [];
|
|
205
|
+
if (has('--url')) {
|
|
206
|
+
const u = new URL(val('--url'));
|
|
207
|
+
targets = [[u.hostname, u.hostname, u.pathname || '/health']];
|
|
208
|
+
}
|
|
209
|
+
else if (has('--all')) {
|
|
210
|
+
for (const e of envs)
|
|
211
|
+
for (const s of Object.keys(SERVICES)) {
|
|
212
|
+
const t = resolve(s, e);
|
|
213
|
+
if (t)
|
|
214
|
+
targets.push(t);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
else if (positional[0] && SERVICES[positional[0]]) {
|
|
218
|
+
for (const e of envs) {
|
|
219
|
+
const t = resolve(positional[0], e);
|
|
220
|
+
if (t)
|
|
221
|
+
targets.push(t);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
console.log((require('fs').readFileSync(__filename, 'utf-8').match(/\/\*\*[\s\S]*?\*\//)?.[0] || '').replace(/^\s*\*?/gm, ''));
|
|
226
|
+
process.exit(2);
|
|
227
|
+
}
|
|
228
|
+
if (targets.length === 0) {
|
|
229
|
+
console.log(`(无目标:${env} 环境下该服务无部署)`);
|
|
230
|
+
process.exit(2);
|
|
231
|
+
}
|
|
232
|
+
const results = [];
|
|
233
|
+
let worstOk = true;
|
|
234
|
+
for (const [name, host, path] of targets) {
|
|
235
|
+
const layers = await probe(host, path, expect);
|
|
236
|
+
const v = verdict(layers);
|
|
237
|
+
if (v === 'fail' || (strict && v === 'warn'))
|
|
238
|
+
worstOk = false;
|
|
239
|
+
if (asJson)
|
|
240
|
+
results.push({ service: name, url: `https://${host}${path}`, verdict: v, layers });
|
|
241
|
+
else {
|
|
242
|
+
console.log(`\n${'='.repeat(60)}\n ${name} https://${host}${path}\n${'='.repeat(60)}`);
|
|
243
|
+
for (const l of layers)
|
|
244
|
+
console.log(` ${MARK[l.result] || '?'} ${l.layer.padEnd(11)} ${l.detail}`);
|
|
245
|
+
console.log(` ${'—'.repeat(56)}\n ${'结论'.padEnd(10)} ${VTXT[v]}`);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
if (asJson)
|
|
249
|
+
console.log(JSON.stringify(results, null, 2));
|
|
250
|
+
// 用 exitCode 而非 process.exit():后者会在管道(--json | jq)未 flush 完就退出而截断输出
|
|
251
|
+
process.exitCode = worstOk ? 0 : 1;
|
|
252
|
+
}
|
|
253
|
+
main();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@optima-chat/dev-skills",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.40",
|
|
4
4
|
"description": "Claude Code Skills for Optima development team - cross-environment collaboration tools",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
"optima-discount": "dist/bin/helpers/discount.js",
|
|
14
14
|
"optima-product": "dist/bin/helpers/product.js",
|
|
15
15
|
"optima-query-db": "dist/bin/helpers/query-db.js",
|
|
16
|
-
"optima-show-env": "dist/bin/helpers/show-env.js"
|
|
16
|
+
"optima-show-env": "dist/bin/helpers/show-env.js",
|
|
17
|
+
"optima-verify-health": "dist/bin/helpers/verify-health.js"
|
|
17
18
|
},
|
|
18
19
|
"scripts": {
|
|
19
20
|
"postinstall": "node scripts/install.js",
|