@optima-chat/dev-skills 0.14.0 → 0.16.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/cn-deploy/SKILL.md +3 -2
- package/.claude/skills/gateway-admin/SKILL.md +38 -0
- package/.codex/skills/cn-deploy/SKILL.md +3 -2
- package/bin/cli.js +1 -0
- package/bin/helpers/billing-http.ts +10 -4
- package/bin/helpers/cn-deploy.ts +20 -3
- package/bin/helpers/gateway-admin.ts +186 -0
- package/dist/bin/helpers/billing-http.js +11 -5
- package/dist/bin/helpers/cn-deploy.js +18 -2
- package/dist/bin/helpers/gateway-admin.js +202 -0
- package/package.json +3 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: cn-deploy
|
|
3
|
-
description: 当用户请求把服务发布/部署/构建到 cn-stage、跑云效流水线、cn-stage 发版、部署某个分支到 cn-stage、触发 cn 构建、yunxiao 部署、"把 xx 发到 cn"时,使用此技能。覆盖 cn-stage 全部
|
|
3
|
+
description: 当用户请求把服务发布/部署/构建到 cn-stage、跑云效流水线、cn-stage 发版、部署某个分支到 cn-stage、触发 cn 构建、yunxiao 部署、"把 xx 发到 cn"时,使用此技能。覆盖 cn-stage 全部 21 个服务(构建→DB迁移→SAE 发布一条龙;含 build-only 的 agent-runtime)。
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# cn-deploy - 云效 cn-stage 流水线发布
|
|
@@ -21,12 +21,13 @@ optima-cn-deploy --list # 列出全部可发服务
|
|
|
21
21
|
|
|
22
22
|
## 使用要点
|
|
23
23
|
|
|
24
|
-
1. **先 `--list` 确认服务名**(
|
|
24
|
+
1. **先 `--list` 确认服务名**(21 个服务,含 commerce-backend / optima-scout / build-only 的 agent-runtime)。
|
|
25
25
|
2. 全链约 3-8 分钟(agentic-chat 最重)。触发后会输出云效 run 链接,可发给用户自行围观。
|
|
26
26
|
3. **凭证零配置**:构建凭证由云效变量组 `cn-stage-build-credentials`(41970) 供给,不需要注入任何 secret。
|
|
27
27
|
4. 分支构建会把该分支代码部署到 cn-stage(替换现运行版本)——用户明确要求部署分支时才用。
|
|
28
28
|
5. 失败排查:命令输出的 flow.aliyun.com 链接里看具体 step 日志;常见原因是该分支/仓的 Codeup mirror 凭证问题(命令会在 mirror 步就报错并中止,不会构建旧代码)。
|
|
29
29
|
6. 成功标准:最后一行 `✓ SAE ImageUrl tag=<sha>` —— 流水线绿且 SAE 真的钉到了目标 commit。只看流水线绿不算完成。
|
|
30
|
+
7. **build-only 服务(`agent-runtime`)**:非 SAE 常驻(gateway-core 按 session 拉起的镜像)。成功标准不是 SAE ImageUrl,而是流水线绿即可——release 段自动把 ACR digest 回写 Infisical `/services/gateway-core/ALIYUN_AGENT_RUNTIME_IMAGE`(#807) 并滚动重启 gateway-core(任一步失败即 exit 非 0)。工具最后一行为 `✓ 流水线 SUCCESS(build-only)…` —— CLI 以流水线终态为准,未独立校验 Infisical/gateway-core(digest 与重启变更单见 run『发版』段日志)。
|
|
30
31
|
|
|
31
32
|
|
|
32
33
|
## cn-prod vtag 发版(⚠️ 待 svenyang 首次验证通过后再日常使用)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: gateway-admin
|
|
3
|
+
description: 当用户请求操作 gateway 管理面时使用——COO kill/软杀某用户的协调官、查看或调整 warm-pool、credits adjust、provider key 管理、gateway config/llm-rates/models 的读写,或任何 gateway-core /admin/* 端点调用。支持 cn-stage、cn-prod 两个环境。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Gateway Admin — gateway-core /admin/* 操作
|
|
7
|
+
|
|
8
|
+
当用户要求「杀掉某用户的 COO / 软杀容器 / drain warm pool / 查 gateway 配置 / 调 llm 费率 / 管理 provider key」等 gateway 管理面操作时,使用 `optima-gateway-admin` CLI。
|
|
9
|
+
|
|
10
|
+
## 用法
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
optima-gateway-admin <METHOD> </admin/...> [jsonBody] [--env cn-stage|cn-prod] [--yes]
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
- 默认环境 `cn-stage`;`cn-prod` 是生产,谨慎。
|
|
17
|
+
- GET/HEAD 直通;**POST/PUT/PATCH/DELETE 会回显完整请求并要求输入 yes 确认**(所有环境,不只 prod)。脚本化传 `--yes`。
|
|
18
|
+
- path 必须以 `/admin/` 开头(硬约束,防 token 误用于其它面)。
|
|
19
|
+
- 凭证自动走 dev-skills OAuth client(client_credentials,scope=gateway:admin,#70 方向 1);cn-stage 需要 `INFISICAL_CN_EMAIL/PASSWORD` 环境变量(与 query-db cn 同一前提)。
|
|
20
|
+
|
|
21
|
+
## 常用例子
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
# 查 llm 费率表
|
|
25
|
+
optima-gateway-admin GET /admin/llm-rates --env cn-stage
|
|
26
|
+
|
|
27
|
+
# 软杀某用户的 COO(#1681 止血这类场景——不再手写 DB UPDATE)
|
|
28
|
+
optima-gateway-admin POST /admin/coo/users/<userId>/kill --env cn-stage
|
|
29
|
+
|
|
30
|
+
# 改 gateway 动态配置
|
|
31
|
+
optima-gateway-admin PUT /admin/config/<KEY> '{"value":"..."}' --env cn-stage
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## 注意
|
|
35
|
+
|
|
36
|
+
- 403 且提示 scope 为空 → 该环境 dev-skills client 的 `allowed_scopes` 缺 `gateway:admin`(见 optima-dev-skills#70)。
|
|
37
|
+
- 写操作打的是运行中的 gateway 管理面,stage 上也可能打断在跑会话——确认提示不要盲敲 yes。
|
|
38
|
+
- AWS stage 未接(AWS prod gateway 已关停);需要时见 #70。
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: cn-deploy
|
|
3
|
-
description: 当用户请求把服务发布/部署/构建到 cn-stage、跑云效流水线、cn-stage 发版、部署某个分支到 cn-stage、触发 cn 构建、yunxiao 部署、"把 xx 发到 cn"时,使用此技能。覆盖 cn-stage 全部
|
|
3
|
+
description: 当用户请求把服务发布/部署/构建到 cn-stage、跑云效流水线、cn-stage 发版、部署某个分支到 cn-stage、触发 cn 构建、yunxiao 部署、"把 xx 发到 cn"时,使用此技能。覆盖 cn-stage 全部 21 个服务(构建→DB迁移→SAE 发布一条龙;含 build-only 的 agent-runtime)。
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# cn-deploy - 云效 cn-stage 流水线发布
|
|
@@ -21,12 +21,13 @@ optima-cn-deploy --list # 列出全部可发服务
|
|
|
21
21
|
|
|
22
22
|
## 使用要点
|
|
23
23
|
|
|
24
|
-
1. **先 `--list` 确认服务名**(
|
|
24
|
+
1. **先 `--list` 确认服务名**(21 个服务,含 commerce-backend / optima-scout / build-only 的 agent-runtime)。
|
|
25
25
|
2. 全链约 3-8 分钟(agentic-chat 最重)。触发后会输出云效 run 链接,可发给用户自行围观。
|
|
26
26
|
3. **凭证零配置**:构建凭证由云效变量组 `cn-stage-build-credentials`(41970) 供给,不需要注入任何 secret。
|
|
27
27
|
4. 分支构建会把该分支代码部署到 cn-stage(替换现运行版本)——用户明确要求部署分支时才用。
|
|
28
28
|
5. 失败排查:命令输出的 flow.aliyun.com 链接里看具体 step 日志;常见原因是该分支/仓的 Codeup mirror 凭证问题(命令会在 mirror 步就报错并中止,不会构建旧代码)。
|
|
29
29
|
6. 成功标准:最后一行 `✓ SAE ImageUrl tag=<sha>` —— 流水线绿且 SAE 真的钉到了目标 commit。只看流水线绿不算完成。
|
|
30
|
+
7. **build-only 服务(`agent-runtime`)**:非 SAE 常驻(gateway-core 按 session 拉起的镜像)。成功标准不是 SAE ImageUrl,而是流水线绿即可——release 段自动把 ACR digest 回写 Infisical `/services/gateway-core/ALIYUN_AGENT_RUNTIME_IMAGE`(#807) 并滚动重启 gateway-core(任一步失败即 exit 非 0)。工具最后一行为 `✓ 流水线 SUCCESS(build-only)…` —— CLI 以流水线终态为准,未独立校验 Infisical/gateway-core(digest 与重启变更单见 run『发版』段日志)。
|
|
30
31
|
|
|
31
32
|
|
|
32
33
|
## cn-prod vtag 发版(⚠️ 待 svenyang 首次验证通过后再日常使用)
|
package/bin/cli.js
CHANGED
|
@@ -37,6 +37,7 @@ switch (command) {
|
|
|
37
37
|
log(' optima-grant-credits <email|phone|userId> --credits <n> [--env] Grant credits (bonus, 30d)', 'cyan');
|
|
38
38
|
log(' optima-grant-subscription <email|phone|userId> --plan <p> [--env] Grant subscription', 'cyan');
|
|
39
39
|
log(' optima-logs <service> [--env] [--since] [--grep] [-n] View logs (cn=SLS 直连/aws=CloudWatch)', 'cyan');
|
|
40
|
+
log(' optima-gateway-admin <METHOD> </admin/path> [body] [--env cn-stage|cn-prod] Gateway /admin/* passthrough (#70)', 'cyan');
|
|
40
41
|
log(' /restart-ecs <service> [env] Restart ECS service (skill)', 'cyan');
|
|
41
42
|
|
|
42
43
|
log('\nSupported Services:', 'yellow');
|
|
@@ -107,8 +107,13 @@ function getSkillsUrl(env: string): string {
|
|
|
107
107
|
return url;
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
export function getServiceToken(env: string): string {
|
|
111
|
-
|
|
110
|
+
export function getServiceToken(env: string, scope?: string): string {
|
|
111
|
+
// Cache key includes scope: the same env can mint tokens with different
|
|
112
|
+
// scopes (default internal:users:write vs gateway:admin for gateway-admin,
|
|
113
|
+
// #70) and user-auth issues request∩allowed_scopes — mixing them up would
|
|
114
|
+
// hand a wrongly-scoped token to the caller.
|
|
115
|
+
const cacheKey = scope ? `${env}:${scope}` : env;
|
|
116
|
+
if (tokenCache[cacheKey]) return tokenCache[cacheKey];
|
|
112
117
|
|
|
113
118
|
// cn-prod 与 cn-stage 都用 client_credentials M2M token,scope=internal:users:write。
|
|
114
119
|
// 凭证来源不同:
|
|
@@ -141,7 +146,8 @@ export function getServiceToken(env: string): string {
|
|
|
141
146
|
const authUrl = USER_AUTH_URLS[env];
|
|
142
147
|
if (!authUrl) throw new Error(`Unknown env: ${env}`);
|
|
143
148
|
|
|
144
|
-
const
|
|
149
|
+
const effectiveScope = scope ?? (isCn ? CN_PROD_TOKEN_SCOPE : undefined);
|
|
150
|
+
const scopeParam = effectiveScope ? `&scope=${encodeURIComponent(effectiveScope)}` : '';
|
|
145
151
|
const body = `grant_type=client_credentials&client_id=${encodeURIComponent(clientId)}&client_secret=${encodeURIComponent(clientSecret)}${scopeParam}`;
|
|
146
152
|
const response = execSync(
|
|
147
153
|
`curl -s -X POST '${authUrl}/api/v1/oauth/token' -H 'Content-Type: application/x-www-form-urlencoded' -d '${body}'`,
|
|
@@ -157,7 +163,7 @@ export function getServiceToken(env: string): string {
|
|
|
157
163
|
if (!parsed.access_token) {
|
|
158
164
|
throw new Error(`user-auth token mint failed (${env}): ${response.slice(0, 200)}`);
|
|
159
165
|
}
|
|
160
|
-
tokenCache[
|
|
166
|
+
tokenCache[cacheKey] = parsed.access_token;
|
|
161
167
|
return parsed.access_token;
|
|
162
168
|
}
|
|
163
169
|
|
package/bin/helpers/cn-deploy.ts
CHANGED
|
@@ -34,9 +34,13 @@ const PROFILE = process.env.OPTIMA_ALIYUN_PROFILE || 'aliyun-optima';
|
|
|
34
34
|
const CODEUP_BASE = `https://codeup.aliyun.com/${ORG}`;
|
|
35
35
|
|
|
36
36
|
// 服务注册表(快照自 optima-terraform services.stage.env + gen-pipelines.py,2026-07-13)
|
|
37
|
-
interface Svc { pipelineId: number; repo: string; saeAppId?: string; prodSaeAppId?: string; }
|
|
37
|
+
interface Svc { pipelineId: number; repo: string; saeAppId?: string; prodSaeAppId?: string; buildOnly?: boolean; }
|
|
38
38
|
const SERVICES: Record<string, Svc> = {
|
|
39
39
|
'agent-portal': { pipelineId: 5118519, repo: 'optima-portals', saeAppId: 'fe757f78-d18d-4480-9402-fe59d4721055', prodSaeAppId: '9211db38-a255-4393-ae92-7a7bb41b583d' },
|
|
40
|
+
// build-only:非 SAE 常驻(gateway-core 按 session 拉起的镜像)。release 段=解析 ACR digest
|
|
41
|
+
// → 回写 Infisical /services/gateway-core/ALIYUN_AGENT_RUNTIME_IMAGE(#807)→ 滚动重启 gateway-core。
|
|
42
|
+
// 无自身 saeAppId;prod 流水线(agent-runtime-cn-prod,5124970)按名实时解析(见下方 ListPipelines 逻辑)。
|
|
43
|
+
'agent-runtime': { pipelineId: 5124962, repo: 'optima-gateway', buildOnly: true },
|
|
40
44
|
'agentic-chat': { pipelineId: 5118520, repo: 'agentic-chat', saeAppId: '6aea1ce1-f813-4e1c-8e97-d1ecb5398e37', prodSaeAppId: '6e290c73-a646-43ef-9da5-ad0b2e7eff73' },
|
|
41
45
|
'billing': { pipelineId: 5118521, repo: 'optima-billing', saeAppId: '09d8e292-dc64-4af8-bce5-0a56cb666921', prodSaeAppId: '6c31cf82-8802-4d45-b6a2-e9d7c83ccce9' },
|
|
42
46
|
'browser-backend': { pipelineId: 5118522, repo: 'optima-browser-use', saeAppId: '1fced3f6-a80a-41a4-8f23-e4d5a467f8eb', prodSaeAppId: 'eb782ed9-c468-4e81-a0fe-87e5b4264192' },
|
|
@@ -116,7 +120,10 @@ async function main() {
|
|
|
116
120
|
devops('TriggerRepositoryMirrorSync', { repositoryId: String(repoId), account: 'xbfool', token: ghTok });
|
|
117
121
|
let synced = false;
|
|
118
122
|
for (let i = 0; i < 30; i++) {
|
|
119
|
-
|
|
123
|
+
// vtag 发版时 ref 是 tag,不是 branch —— GetBranchInfo 查不到 tag 会恒超时,改用 tag 接口。
|
|
124
|
+
const b = vtag
|
|
125
|
+
? devops('GetRepositoryTag', { repositoryId: String(repoId), tagName: ref })
|
|
126
|
+
: devops('GetBranchInfo', { repositoryId: String(repoId), branchName: ref });
|
|
120
127
|
if (b?.result?.commit?.id === ghSha) { synced = true; break; }
|
|
121
128
|
await sleep(10000);
|
|
122
129
|
}
|
|
@@ -161,7 +168,17 @@ async function main() {
|
|
|
161
168
|
console.log(`${status === 'SUCCESS' ? '✅' : '❌'} ${svcName} run#${runId}: ${status}`);
|
|
162
169
|
if (status !== 'SUCCESS') process.exit(1);
|
|
163
170
|
|
|
164
|
-
// 4.
|
|
171
|
+
// 4. 成功标准。build-only(agent-runtime)无自身 SAE app 可查:digest 回写 Infisical +
|
|
172
|
+
// 滚动重启 gateway-core 都在流水线 release 段内完成(任一步失败即 exit 非 0)。CLI 不碰
|
|
173
|
+
// Infisical 凭证,只以流水线终态为准、不独立校验回写是否真落地——故措辞用「应已」而非「已」;
|
|
174
|
+
// 真实 digest / 重启变更单在 run『发版』段日志里(见上方链接)。
|
|
175
|
+
if (svc.buildOnly) {
|
|
176
|
+
const infEnv = envName === 'prod' ? 'prod' : 'staging';
|
|
177
|
+
console.log(`✓ 流水线 SUCCESS(build-only)。release 段应已回写 ${infEnv} Infisical ALIYUN_AGENT_RUNTIME_IMAGE(@sha256 digest)并滚动重启 gateway-core;CLI 未独立校验,digest 见上方 run『发版』段日志`);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// SAE ImageUrl 校验(空返回重试守卫)。prod 是 digest 寻址,校验含 @sha256 即部署成功。
|
|
165
182
|
const appId = envName === 'prod' ? svc.prodSaeAppId : svc.saeAppId;
|
|
166
183
|
if (appId) {
|
|
167
184
|
let img = '';
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// optima-gateway-admin — 通用透传型 gateway-core /admin/* 操作 CLI(#70)。
|
|
4
|
+
//
|
|
5
|
+
// 定位:dev-skills 现有 admin 技能(account/grant-credits/entitlement)打的都是
|
|
6
|
+
// user-auth 与 billing;gateway 自身的 /admin/*(COO kill、warm-pool drain、
|
|
7
|
+
// credits adjust、provider key CRUD、config CRUD、llm-rates、models 等)此前只能
|
|
8
|
+
// 手写 curl + 手拼 token(#1681 止血即被迫手写 DB UPDATE + aliyun CLI)。本命令
|
|
9
|
+
// 把「铸 gateway:admin token → 调端点 → 展示结果」收敛成一条命令。
|
|
10
|
+
//
|
|
11
|
+
// 凭证(#70 方向 1):复用 dev-skills 各环境既有 OAuth client(client_credentials),
|
|
12
|
+
// 其 allowed_scopes 已加 gateway:admin(cn-stage/cn-prod 2026-07-23 提权);
|
|
13
|
+
// token 铸造显式带 scope=gateway:admin(user-auth 发 request∩allowed_scopes,
|
|
14
|
+
// 不显式带则 scope 为空 → gateway 403,见 #70 技术现状)。
|
|
15
|
+
//
|
|
16
|
+
// 安全门:GET/HEAD 直通;写方法(POST/PUT/PATCH/DELETE)一律回显完整
|
|
17
|
+
// method+URL+body 后要求交互确认(所有环境,非仅 prod——admin 面写操作在
|
|
18
|
+
// stage 也可能打断在跑会话),--yes 跳过(脚本化用)。
|
|
19
|
+
//
|
|
20
|
+
// v1 支持 cn-stage / cn-prod(AWS prod gateway 已关停 2026-06-26;AWS stage
|
|
21
|
+
// 需要时再接 domain-urls,见 #70 讨论)。
|
|
22
|
+
|
|
23
|
+
import * as readline from 'readline';
|
|
24
|
+
import { getServiceToken } from './billing-http';
|
|
25
|
+
|
|
26
|
+
const GATEWAY_ADMIN_SCOPE = 'gateway:admin';
|
|
27
|
+
|
|
28
|
+
// 与 verify-health.ts 的 gateway-core 域名同源(cn 域名稳定、硬编码,与
|
|
29
|
+
// billing-http 的 cn URL 处理同一 rationale)。
|
|
30
|
+
const GATEWAY_URLS: Record<string, string> = {
|
|
31
|
+
'cn-prod': 'https://gw.yzsgo.com',
|
|
32
|
+
'cn-stage': 'https://gw.stage.optima.chat',
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const WRITE_METHODS = new Set(['POST', 'PUT', 'PATCH', 'DELETE']);
|
|
36
|
+
|
|
37
|
+
interface Parsed {
|
|
38
|
+
method: string;
|
|
39
|
+
path: string;
|
|
40
|
+
body: string | null;
|
|
41
|
+
env: string;
|
|
42
|
+
yes: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function usage(): never {
|
|
46
|
+
console.log(`Usage: optima-gateway-admin <METHOD> <path> [jsonBody] [options]
|
|
47
|
+
|
|
48
|
+
Call a gateway-core /admin/* endpoint with a gateway:admin service token (#70).
|
|
49
|
+
|
|
50
|
+
Arguments:
|
|
51
|
+
METHOD GET | POST | PUT | PATCH | DELETE
|
|
52
|
+
path Endpoint path, must start with /admin/
|
|
53
|
+
jsonBody JSON request body (write methods only)
|
|
54
|
+
|
|
55
|
+
Options:
|
|
56
|
+
--env <env> cn-stage (default) | cn-prod
|
|
57
|
+
--yes Skip the write-confirmation prompt (scripting)
|
|
58
|
+
-h, --help Show this help
|
|
59
|
+
|
|
60
|
+
Safety:
|
|
61
|
+
GET/HEAD run directly. POST/PUT/PATCH/DELETE echo the full request and
|
|
62
|
+
require typing "yes" — on EVERY env, not just prod (admin writes can
|
|
63
|
+
disrupt live sessions on stage too). Use --yes to skip.
|
|
64
|
+
|
|
65
|
+
Examples:
|
|
66
|
+
optima-gateway-admin GET /admin/llm-rates --env cn-stage
|
|
67
|
+
optima-gateway-admin GET /admin/coo/instances --env cn-prod
|
|
68
|
+
optima-gateway-admin POST /admin/coo/users/<userId>/kill --env cn-stage
|
|
69
|
+
optima-gateway-admin PUT /admin/config/SOME_KEY '{"value":"x"}' --env cn-stage`);
|
|
70
|
+
process.exit(0);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function parseArgs(args: string[]): Parsed {
|
|
74
|
+
if (args.length === 0 || args[0] === '--help' || args[0] === '-h') usage();
|
|
75
|
+
|
|
76
|
+
let env = 'cn-stage';
|
|
77
|
+
let yes = false;
|
|
78
|
+
const positional: string[] = [];
|
|
79
|
+
for (let i = 0; i < args.length; i++) {
|
|
80
|
+
const a = args[i];
|
|
81
|
+
if (a === '--env' || a === '-e') {
|
|
82
|
+
env = args[++i] ?? '';
|
|
83
|
+
} else if (a.startsWith('--env=')) {
|
|
84
|
+
env = a.slice('--env='.length);
|
|
85
|
+
} else if (a === '--yes' || a === '-y') {
|
|
86
|
+
yes = true;
|
|
87
|
+
} else if (a.startsWith('--')) {
|
|
88
|
+
console.error(`❌ Unknown flag: ${a}`);
|
|
89
|
+
process.exit(1);
|
|
90
|
+
} else {
|
|
91
|
+
positional.push(a);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (!GATEWAY_URLS[env]) {
|
|
96
|
+
console.error(`❌ --env must be "cn-stage" or "cn-prod" (got: ${env}). AWS stage 未接(prod gateway 已关停),需要时见 #70。`);
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const method = (positional[0] ?? '').toUpperCase();
|
|
101
|
+
if (!['GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE'].includes(method)) {
|
|
102
|
+
console.error(`❌ METHOD must be GET/HEAD/POST/PUT/PATCH/DELETE (got: ${positional[0] ?? '(missing)'})`);
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const path = positional[1] ?? '';
|
|
107
|
+
// 硬约束 /admin/ 前缀:本命令的 token 只该用于 admin 面;防拼错路径把
|
|
108
|
+
// gateway:admin token 打到任意端点(最小暴露面)。
|
|
109
|
+
if (!path.startsWith('/admin/')) {
|
|
110
|
+
console.error(`❌ path must start with /admin/ (got: ${path || '(missing)'})`);
|
|
111
|
+
process.exit(1);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
let body: string | null = null;
|
|
115
|
+
if (positional[2] !== undefined) {
|
|
116
|
+
if (!WRITE_METHODS.has(method)) {
|
|
117
|
+
console.error(`❌ ${method} does not take a body`);
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
|
120
|
+
try {
|
|
121
|
+
JSON.parse(positional[2]);
|
|
122
|
+
} catch {
|
|
123
|
+
console.error(`❌ jsonBody is not valid JSON: ${positional[2].slice(0, 120)}`);
|
|
124
|
+
process.exit(1);
|
|
125
|
+
}
|
|
126
|
+
body = positional[2];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return { method, path, body, env, yes };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function confirmWrite(p: Parsed, url: string): Promise<void> {
|
|
133
|
+
if (!WRITE_METHODS.has(p.method) || p.yes) return;
|
|
134
|
+
const prodMark = p.env === 'cn-prod' ? ' 🔴 PRODUCTION' : '';
|
|
135
|
+
console.log(`\n⚠️ About to ${p.method} on ${p.env.toUpperCase()}${prodMark}:\n ${p.method} ${url}${p.body ? `\n body: ${p.body}` : ''}\n`);
|
|
136
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
137
|
+
const answer = await new Promise<string>((resolve) => {
|
|
138
|
+
rl.question('Type "yes" to confirm: ', (a) => { rl.close(); resolve(a.trim()); });
|
|
139
|
+
});
|
|
140
|
+
if (answer !== 'yes') {
|
|
141
|
+
console.error('❌ Aborted by user.');
|
|
142
|
+
process.exit(1);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function main(): Promise<void> {
|
|
147
|
+
const p = parseArgs(process.argv.slice(2));
|
|
148
|
+
const url = `${GATEWAY_URLS[p.env]}${p.path}`;
|
|
149
|
+
|
|
150
|
+
await confirmWrite(p, url);
|
|
151
|
+
|
|
152
|
+
const token = getServiceToken(p.env, GATEWAY_ADMIN_SCOPE);
|
|
153
|
+
|
|
154
|
+
const res = await fetch(url, {
|
|
155
|
+
method: p.method,
|
|
156
|
+
headers: {
|
|
157
|
+
Authorization: `Bearer ${token}`,
|
|
158
|
+
...(p.body ? { 'Content-Type': 'application/json' } : {}),
|
|
159
|
+
},
|
|
160
|
+
...(p.body ? { body: p.body } : {}),
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
const text = await res.text();
|
|
164
|
+
const statusLine = `${res.status} ${res.statusText}`;
|
|
165
|
+
if (!res.ok) {
|
|
166
|
+
console.error(`❌ [${statusLine}] ${p.method} ${p.path} (${p.env})`);
|
|
167
|
+
if (res.status === 403) {
|
|
168
|
+
console.error(' 403 提示:token scope 可能为空——确认该环境 dev-skills client 的 allowed_scopes 已含 gateway:admin(#70)。');
|
|
169
|
+
}
|
|
170
|
+
} else {
|
|
171
|
+
console.log(`✓ [${statusLine}] ${p.method} ${p.path} (${p.env})`);
|
|
172
|
+
}
|
|
173
|
+
if (text) {
|
|
174
|
+
try {
|
|
175
|
+
console.log(JSON.stringify(JSON.parse(text), null, 2));
|
|
176
|
+
} catch {
|
|
177
|
+
console.log(text.slice(0, 4000));
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
process.exit(res.ok ? 0 : 1);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
main().catch((err) => {
|
|
184
|
+
console.error(`\n❌ Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
185
|
+
process.exit(1);
|
|
186
|
+
});
|
|
@@ -114,9 +114,14 @@ function getSkillsUrl(env) {
|
|
|
114
114
|
skillsUrlCache[env] = url;
|
|
115
115
|
return url;
|
|
116
116
|
}
|
|
117
|
-
function getServiceToken(env) {
|
|
118
|
-
|
|
119
|
-
|
|
117
|
+
function getServiceToken(env, scope) {
|
|
118
|
+
// Cache key includes scope: the same env can mint tokens with different
|
|
119
|
+
// scopes (default internal:users:write vs gateway:admin for gateway-admin,
|
|
120
|
+
// #70) and user-auth issues request∩allowed_scopes — mixing them up would
|
|
121
|
+
// hand a wrongly-scoped token to the caller.
|
|
122
|
+
const cacheKey = scope ? `${env}:${scope}` : env;
|
|
123
|
+
if (tokenCache[cacheKey])
|
|
124
|
+
return tokenCache[cacheKey];
|
|
120
125
|
// cn-prod 与 cn-stage 都用 client_credentials M2M token,scope=internal:users:write。
|
|
121
126
|
// 凭证来源不同:
|
|
122
127
|
// · cn-prod —— 镜像在 AWS Infisical prod 环境(CN_PROD_ 前缀键),复用既有 AWS 访问。
|
|
@@ -148,7 +153,8 @@ function getServiceToken(env) {
|
|
|
148
153
|
const authUrl = USER_AUTH_URLS[env];
|
|
149
154
|
if (!authUrl)
|
|
150
155
|
throw new Error(`Unknown env: ${env}`);
|
|
151
|
-
const
|
|
156
|
+
const effectiveScope = scope ?? (isCn ? CN_PROD_TOKEN_SCOPE : undefined);
|
|
157
|
+
const scopeParam = effectiveScope ? `&scope=${encodeURIComponent(effectiveScope)}` : '';
|
|
152
158
|
const body = `grant_type=client_credentials&client_id=${encodeURIComponent(clientId)}&client_secret=${encodeURIComponent(clientSecret)}${scopeParam}`;
|
|
153
159
|
const response = (0, child_process_1.execSync)(`curl -s -X POST '${authUrl}/api/v1/oauth/token' -H 'Content-Type: application/x-www-form-urlencoded' -d '${body}'`, { encoding: 'utf-8' });
|
|
154
160
|
let parsed;
|
|
@@ -161,7 +167,7 @@ function getServiceToken(env) {
|
|
|
161
167
|
if (!parsed.access_token) {
|
|
162
168
|
throw new Error(`user-auth token mint failed (${env}): ${response.slice(0, 200)}`);
|
|
163
169
|
}
|
|
164
|
-
tokenCache[
|
|
170
|
+
tokenCache[cacheKey] = parsed.access_token;
|
|
165
171
|
return parsed.access_token;
|
|
166
172
|
}
|
|
167
173
|
function formatServiceError(status, statusText, body) {
|
|
@@ -35,6 +35,10 @@ const PROFILE = process.env.OPTIMA_ALIYUN_PROFILE || 'aliyun-optima';
|
|
|
35
35
|
const CODEUP_BASE = `https://codeup.aliyun.com/${ORG}`;
|
|
36
36
|
const SERVICES = {
|
|
37
37
|
'agent-portal': { pipelineId: 5118519, repo: 'optima-portals', saeAppId: 'fe757f78-d18d-4480-9402-fe59d4721055', prodSaeAppId: '9211db38-a255-4393-ae92-7a7bb41b583d' },
|
|
38
|
+
// build-only:非 SAE 常驻(gateway-core 按 session 拉起的镜像)。release 段=解析 ACR digest
|
|
39
|
+
// → 回写 Infisical /services/gateway-core/ALIYUN_AGENT_RUNTIME_IMAGE(#807)→ 滚动重启 gateway-core。
|
|
40
|
+
// 无自身 saeAppId;prod 流水线(agent-runtime-cn-prod,5124970)按名实时解析(见下方 ListPipelines 逻辑)。
|
|
41
|
+
'agent-runtime': { pipelineId: 5124962, repo: 'optima-gateway', buildOnly: true },
|
|
38
42
|
'agentic-chat': { pipelineId: 5118520, repo: 'agentic-chat', saeAppId: '6aea1ce1-f813-4e1c-8e97-d1ecb5398e37', prodSaeAppId: '6e290c73-a646-43ef-9da5-ad0b2e7eff73' },
|
|
39
43
|
'billing': { pipelineId: 5118521, repo: 'optima-billing', saeAppId: '09d8e292-dc64-4af8-bce5-0a56cb666921', prodSaeAppId: '6c31cf82-8802-4d45-b6a2-e9d7c83ccce9' },
|
|
40
44
|
'browser-backend': { pipelineId: 5118522, repo: 'optima-browser-use', saeAppId: '1fced3f6-a80a-41a4-8f23-e4d5a467f8eb', prodSaeAppId: 'eb782ed9-c468-4e81-a0fe-87e5b4264192' },
|
|
@@ -133,7 +137,10 @@ async function main() {
|
|
|
133
137
|
devops('TriggerRepositoryMirrorSync', { repositoryId: String(repoId), account: 'xbfool', token: ghTok });
|
|
134
138
|
let synced = false;
|
|
135
139
|
for (let i = 0; i < 30; i++) {
|
|
136
|
-
|
|
140
|
+
// vtag 发版时 ref 是 tag,不是 branch —— GetBranchInfo 查不到 tag 会恒超时,改用 tag 接口。
|
|
141
|
+
const b = vtag
|
|
142
|
+
? devops('GetRepositoryTag', { repositoryId: String(repoId), tagName: ref })
|
|
143
|
+
: devops('GetBranchInfo', { repositoryId: String(repoId), branchName: ref });
|
|
137
144
|
if (b?.result?.commit?.id === ghSha) {
|
|
138
145
|
synced = true;
|
|
139
146
|
break;
|
|
@@ -191,7 +198,16 @@ async function main() {
|
|
|
191
198
|
console.log(`${status === 'SUCCESS' ? '✅' : '❌'} ${svcName} run#${runId}: ${status}`);
|
|
192
199
|
if (status !== 'SUCCESS')
|
|
193
200
|
process.exit(1);
|
|
194
|
-
// 4.
|
|
201
|
+
// 4. 成功标准。build-only(agent-runtime)无自身 SAE app 可查:digest 回写 Infisical +
|
|
202
|
+
// 滚动重启 gateway-core 都在流水线 release 段内完成(任一步失败即 exit 非 0)。CLI 不碰
|
|
203
|
+
// Infisical 凭证,只以流水线终态为准、不独立校验回写是否真落地——故措辞用「应已」而非「已」;
|
|
204
|
+
// 真实 digest / 重启变更单在 run『发版』段日志里(见上方链接)。
|
|
205
|
+
if (svc.buildOnly) {
|
|
206
|
+
const infEnv = envName === 'prod' ? 'prod' : 'staging';
|
|
207
|
+
console.log(`✓ 流水线 SUCCESS(build-only)。release 段应已回写 ${infEnv} Infisical ALIYUN_AGENT_RUNTIME_IMAGE(@sha256 digest)并滚动重启 gateway-core;CLI 未独立校验,digest 见上方 run『发版』段日志`);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
// SAE ImageUrl 校验(空返回重试守卫)。prod 是 digest 寻址,校验含 @sha256 即部署成功。
|
|
195
211
|
const appId = envName === 'prod' ? svc.prodSaeAppId : svc.saeAppId;
|
|
196
212
|
if (appId) {
|
|
197
213
|
let img = '';
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
// optima-gateway-admin — 通用透传型 gateway-core /admin/* 操作 CLI(#70)。
|
|
4
|
+
//
|
|
5
|
+
// 定位:dev-skills 现有 admin 技能(account/grant-credits/entitlement)打的都是
|
|
6
|
+
// user-auth 与 billing;gateway 自身的 /admin/*(COO kill、warm-pool drain、
|
|
7
|
+
// credits adjust、provider key CRUD、config CRUD、llm-rates、models 等)此前只能
|
|
8
|
+
// 手写 curl + 手拼 token(#1681 止血即被迫手写 DB UPDATE + aliyun CLI)。本命令
|
|
9
|
+
// 把「铸 gateway:admin token → 调端点 → 展示结果」收敛成一条命令。
|
|
10
|
+
//
|
|
11
|
+
// 凭证(#70 方向 1):复用 dev-skills 各环境既有 OAuth client(client_credentials),
|
|
12
|
+
// 其 allowed_scopes 已加 gateway:admin(cn-stage/cn-prod 2026-07-23 提权);
|
|
13
|
+
// token 铸造显式带 scope=gateway:admin(user-auth 发 request∩allowed_scopes,
|
|
14
|
+
// 不显式带则 scope 为空 → gateway 403,见 #70 技术现状)。
|
|
15
|
+
//
|
|
16
|
+
// 安全门:GET/HEAD 直通;写方法(POST/PUT/PATCH/DELETE)一律回显完整
|
|
17
|
+
// method+URL+body 后要求交互确认(所有环境,非仅 prod——admin 面写操作在
|
|
18
|
+
// stage 也可能打断在跑会话),--yes 跳过(脚本化用)。
|
|
19
|
+
//
|
|
20
|
+
// v1 支持 cn-stage / cn-prod(AWS prod gateway 已关停 2026-06-26;AWS stage
|
|
21
|
+
// 需要时再接 domain-urls,见 #70 讨论)。
|
|
22
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
23
|
+
if (k2 === undefined) k2 = k;
|
|
24
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
25
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
26
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
27
|
+
}
|
|
28
|
+
Object.defineProperty(o, k2, desc);
|
|
29
|
+
}) : (function(o, m, k, k2) {
|
|
30
|
+
if (k2 === undefined) k2 = k;
|
|
31
|
+
o[k2] = m[k];
|
|
32
|
+
}));
|
|
33
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
34
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
35
|
+
}) : function(o, v) {
|
|
36
|
+
o["default"] = v;
|
|
37
|
+
});
|
|
38
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
39
|
+
var ownKeys = function(o) {
|
|
40
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
41
|
+
var ar = [];
|
|
42
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
43
|
+
return ar;
|
|
44
|
+
};
|
|
45
|
+
return ownKeys(o);
|
|
46
|
+
};
|
|
47
|
+
return function (mod) {
|
|
48
|
+
if (mod && mod.__esModule) return mod;
|
|
49
|
+
var result = {};
|
|
50
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
51
|
+
__setModuleDefault(result, mod);
|
|
52
|
+
return result;
|
|
53
|
+
};
|
|
54
|
+
})();
|
|
55
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
56
|
+
const readline = __importStar(require("readline"));
|
|
57
|
+
const billing_http_1 = require("./billing-http");
|
|
58
|
+
const GATEWAY_ADMIN_SCOPE = 'gateway:admin';
|
|
59
|
+
// 与 verify-health.ts 的 gateway-core 域名同源(cn 域名稳定、硬编码,与
|
|
60
|
+
// billing-http 的 cn URL 处理同一 rationale)。
|
|
61
|
+
const GATEWAY_URLS = {
|
|
62
|
+
'cn-prod': 'https://gw.yzsgo.com',
|
|
63
|
+
'cn-stage': 'https://gw.stage.optima.chat',
|
|
64
|
+
};
|
|
65
|
+
const WRITE_METHODS = new Set(['POST', 'PUT', 'PATCH', 'DELETE']);
|
|
66
|
+
function usage() {
|
|
67
|
+
console.log(`Usage: optima-gateway-admin <METHOD> <path> [jsonBody] [options]
|
|
68
|
+
|
|
69
|
+
Call a gateway-core /admin/* endpoint with a gateway:admin service token (#70).
|
|
70
|
+
|
|
71
|
+
Arguments:
|
|
72
|
+
METHOD GET | POST | PUT | PATCH | DELETE
|
|
73
|
+
path Endpoint path, must start with /admin/
|
|
74
|
+
jsonBody JSON request body (write methods only)
|
|
75
|
+
|
|
76
|
+
Options:
|
|
77
|
+
--env <env> cn-stage (default) | cn-prod
|
|
78
|
+
--yes Skip the write-confirmation prompt (scripting)
|
|
79
|
+
-h, --help Show this help
|
|
80
|
+
|
|
81
|
+
Safety:
|
|
82
|
+
GET/HEAD run directly. POST/PUT/PATCH/DELETE echo the full request and
|
|
83
|
+
require typing "yes" — on EVERY env, not just prod (admin writes can
|
|
84
|
+
disrupt live sessions on stage too). Use --yes to skip.
|
|
85
|
+
|
|
86
|
+
Examples:
|
|
87
|
+
optima-gateway-admin GET /admin/llm-rates --env cn-stage
|
|
88
|
+
optima-gateway-admin GET /admin/coo/instances --env cn-prod
|
|
89
|
+
optima-gateway-admin POST /admin/coo/users/<userId>/kill --env cn-stage
|
|
90
|
+
optima-gateway-admin PUT /admin/config/SOME_KEY '{"value":"x"}' --env cn-stage`);
|
|
91
|
+
process.exit(0);
|
|
92
|
+
}
|
|
93
|
+
function parseArgs(args) {
|
|
94
|
+
if (args.length === 0 || args[0] === '--help' || args[0] === '-h')
|
|
95
|
+
usage();
|
|
96
|
+
let env = 'cn-stage';
|
|
97
|
+
let yes = false;
|
|
98
|
+
const positional = [];
|
|
99
|
+
for (let i = 0; i < args.length; i++) {
|
|
100
|
+
const a = args[i];
|
|
101
|
+
if (a === '--env' || a === '-e') {
|
|
102
|
+
env = args[++i] ?? '';
|
|
103
|
+
}
|
|
104
|
+
else if (a.startsWith('--env=')) {
|
|
105
|
+
env = a.slice('--env='.length);
|
|
106
|
+
}
|
|
107
|
+
else if (a === '--yes' || a === '-y') {
|
|
108
|
+
yes = true;
|
|
109
|
+
}
|
|
110
|
+
else if (a.startsWith('--')) {
|
|
111
|
+
console.error(`❌ Unknown flag: ${a}`);
|
|
112
|
+
process.exit(1);
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
positional.push(a);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (!GATEWAY_URLS[env]) {
|
|
119
|
+
console.error(`❌ --env must be "cn-stage" or "cn-prod" (got: ${env}). AWS stage 未接(prod gateway 已关停),需要时见 #70。`);
|
|
120
|
+
process.exit(1);
|
|
121
|
+
}
|
|
122
|
+
const method = (positional[0] ?? '').toUpperCase();
|
|
123
|
+
if (!['GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE'].includes(method)) {
|
|
124
|
+
console.error(`❌ METHOD must be GET/HEAD/POST/PUT/PATCH/DELETE (got: ${positional[0] ?? '(missing)'})`);
|
|
125
|
+
process.exit(1);
|
|
126
|
+
}
|
|
127
|
+
const path = positional[1] ?? '';
|
|
128
|
+
// 硬约束 /admin/ 前缀:本命令的 token 只该用于 admin 面;防拼错路径把
|
|
129
|
+
// gateway:admin token 打到任意端点(最小暴露面)。
|
|
130
|
+
if (!path.startsWith('/admin/')) {
|
|
131
|
+
console.error(`❌ path must start with /admin/ (got: ${path || '(missing)'})`);
|
|
132
|
+
process.exit(1);
|
|
133
|
+
}
|
|
134
|
+
let body = null;
|
|
135
|
+
if (positional[2] !== undefined) {
|
|
136
|
+
if (!WRITE_METHODS.has(method)) {
|
|
137
|
+
console.error(`❌ ${method} does not take a body`);
|
|
138
|
+
process.exit(1);
|
|
139
|
+
}
|
|
140
|
+
try {
|
|
141
|
+
JSON.parse(positional[2]);
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
console.error(`❌ jsonBody is not valid JSON: ${positional[2].slice(0, 120)}`);
|
|
145
|
+
process.exit(1);
|
|
146
|
+
}
|
|
147
|
+
body = positional[2];
|
|
148
|
+
}
|
|
149
|
+
return { method, path, body, env, yes };
|
|
150
|
+
}
|
|
151
|
+
async function confirmWrite(p, url) {
|
|
152
|
+
if (!WRITE_METHODS.has(p.method) || p.yes)
|
|
153
|
+
return;
|
|
154
|
+
const prodMark = p.env === 'cn-prod' ? ' 🔴 PRODUCTION' : '';
|
|
155
|
+
console.log(`\n⚠️ About to ${p.method} on ${p.env.toUpperCase()}${prodMark}:\n ${p.method} ${url}${p.body ? `\n body: ${p.body}` : ''}\n`);
|
|
156
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
157
|
+
const answer = await new Promise((resolve) => {
|
|
158
|
+
rl.question('Type "yes" to confirm: ', (a) => { rl.close(); resolve(a.trim()); });
|
|
159
|
+
});
|
|
160
|
+
if (answer !== 'yes') {
|
|
161
|
+
console.error('❌ Aborted by user.');
|
|
162
|
+
process.exit(1);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
async function main() {
|
|
166
|
+
const p = parseArgs(process.argv.slice(2));
|
|
167
|
+
const url = `${GATEWAY_URLS[p.env]}${p.path}`;
|
|
168
|
+
await confirmWrite(p, url);
|
|
169
|
+
const token = (0, billing_http_1.getServiceToken)(p.env, GATEWAY_ADMIN_SCOPE);
|
|
170
|
+
const res = await fetch(url, {
|
|
171
|
+
method: p.method,
|
|
172
|
+
headers: {
|
|
173
|
+
Authorization: `Bearer ${token}`,
|
|
174
|
+
...(p.body ? { 'Content-Type': 'application/json' } : {}),
|
|
175
|
+
},
|
|
176
|
+
...(p.body ? { body: p.body } : {}),
|
|
177
|
+
});
|
|
178
|
+
const text = await res.text();
|
|
179
|
+
const statusLine = `${res.status} ${res.statusText}`;
|
|
180
|
+
if (!res.ok) {
|
|
181
|
+
console.error(`❌ [${statusLine}] ${p.method} ${p.path} (${p.env})`);
|
|
182
|
+
if (res.status === 403) {
|
|
183
|
+
console.error(' 403 提示:token scope 可能为空——确认该环境 dev-skills client 的 allowed_scopes 已含 gateway:admin(#70)。');
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
console.log(`✓ [${statusLine}] ${p.method} ${p.path} (${p.env})`);
|
|
188
|
+
}
|
|
189
|
+
if (text) {
|
|
190
|
+
try {
|
|
191
|
+
console.log(JSON.stringify(JSON.parse(text), null, 2));
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
console.log(text.slice(0, 4000));
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
process.exit(res.ok ? 0 : 1);
|
|
198
|
+
}
|
|
199
|
+
main().catch((err) => {
|
|
200
|
+
console.error(`\n❌ Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
201
|
+
process.exit(1);
|
|
202
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@optima-chat/dev-skills",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"description": "Claude Code Skills for Optima development team - cross-environment collaboration tools",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
"optima-query-db": "dist/bin/helpers/query-db.js",
|
|
19
19
|
"optima-show-env": "dist/bin/helpers/show-env.js",
|
|
20
20
|
"optima-verify-health": "dist/bin/helpers/verify-health.js",
|
|
21
|
-
"optima-cn-deploy": "dist/bin/helpers/cn-deploy.js"
|
|
21
|
+
"optima-cn-deploy": "dist/bin/helpers/cn-deploy.js",
|
|
22
|
+
"optima-gateway-admin": "dist/bin/helpers/gateway-admin.js"
|
|
22
23
|
},
|
|
23
24
|
"scripts": {
|
|
24
25
|
"postinstall": "node scripts/install.js",
|