@optima-chat/dev-skills 0.16.1 → 0.16.3
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 +60 -3
- package/.claude/commands/trace-user.md +18 -6
- package/.claude/skills/cn-deploy/SKILL.md +5 -3
- package/.claude/skills/logs/SKILL.md +10 -2
- package/.claude/skills/reset-onboarding/SKILL.md +116 -0
- package/.codex/skills/cn-deploy/SKILL.md +5 -3
- package/.codex/skills/reset-onboarding/SKILL.md +116 -0
- package/AGENTS.md +11 -6
- package/README.md +73 -61
- package/bin/helpers/cn-deploy.ts +44 -37
- package/bin/helpers/db-utils.ts +105 -6
- package/bin/helpers/logs.ts +464 -31
- package/dist/bin/helpers/cn-deploy.js +43 -37
- package/dist/bin/helpers/db-utils.js +105 -6
- package/dist/bin/helpers/logs.js +410 -31
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -25,24 +25,32 @@ npm install -g @optima-chat/dev-skills@latest
|
|
|
25
25
|
|
|
26
26
|
## 🎯 核心理念
|
|
27
27
|
|
|
28
|
-
Optima Dev Skills 让 Claude Code 能够直接在 **
|
|
28
|
+
Optima Dev Skills 让 Claude Code 能够直接在 **ci、stage、prod、cn-stage、cn-prod** 五个环境中执行开发任务(后两个是阿里云侧,与 AWS 侧完全独立;并非每个 skill 都覆盖全部五个,逐个见下)。
|
|
29
29
|
|
|
30
30
|
**核心价值**:
|
|
31
31
|
- **即时执行** - Claude 直接执行操作,开发者零手动操作
|
|
32
32
|
- **任务驱动** - 基于具体任务场景(查看日志、调用 API),不是抽象分类
|
|
33
|
-
- **跨环境协作** - 统一的命令在
|
|
34
|
-
|
|
35
|
-
## 📋
|
|
36
|
-
|
|
37
|
-
当 Claude Code 识别到以下任务时,会自动加载对应的 Skill
|
|
38
|
-
|
|
39
|
-
- **
|
|
40
|
-
- **
|
|
41
|
-
- **
|
|
42
|
-
- **
|
|
43
|
-
- **
|
|
33
|
+
- **跨环境协作** - 统一的命令在 AWS 侧(ci / stage / prod)与阿里云侧(cn-stage / cn-prod)通用
|
|
34
|
+
|
|
35
|
+
## 📋 任务场景
|
|
36
|
+
|
|
37
|
+
当 Claude Code 识别到以下任务时,会自动加载对应的 Skill。按目录字母序,与 `.claude/skills/` 一一对应(由 `tests/service-matrix-alignment.test.js` 校验,加 skill 漏更本清单会红):
|
|
38
|
+
|
|
39
|
+
- **account** - 查账号状态/订阅/权益,封禁与解封(stage / prod / cn-stage / cn-prod)
|
|
40
|
+
- **cn-deploy** - 把服务发布到 cn-stage,走云效流水线一条龙(cn-stage)
|
|
41
|
+
- **discount-codes** - 创建/生成/查看/停用 billing 优惠码(stage / prod)
|
|
42
|
+
- **entitlement** - 授予、撤销、查看产品权益(stage / prod / cn-stage / cn-prod)
|
|
43
|
+
- **gateway-admin** - gateway 管理面:COO kill、warm-pool、credits adjust、config 读写(cn-stage / cn-prod)
|
|
44
|
+
- **generate-test-token** - 生成测试 Access Token 并配好 merchant,用于 API 测试
|
|
45
|
+
- **grant-credits** - 赠送、发放积分(stage / prod / cn-stage / cn-prod)
|
|
46
|
+
- **grant-subscription** - 开通、赠送、升级订阅(stage / prod / cn-stage / cn-prod)
|
|
47
|
+
- **logs** - 查看服务日志(stage / prod 走 CloudWatch,cn-stage / cn-prod 走 SLS;CI 走 SSH + Docker Compose,不经 `optima-logs`)
|
|
48
|
+
- **query-db** - 查询数据库、执行 SQL(ci / stage / prod / cn-stage / cn-prod)
|
|
44
49
|
- **read-code** - 阅读 Optima-Chat 组织下任意仓库的代码
|
|
45
|
-
- **
|
|
50
|
+
- **reset-onboarding** - 重置账号的 onboarding 资格,让它重新触发新手引导问卷(cn-stage / cn-prod)
|
|
51
|
+
- **restart-ecs** - 重启 ECS 服务(stage / prod)
|
|
52
|
+
- **show-env** - 查看服务环境变量,从 Infisical 取(stage / prod / cn-stage / cn-prod)
|
|
53
|
+
- **use-commerce-cli** - 用 Commerce CLI 管理电商店铺(商品、订单、库存、运费、集合、首页、国际化)
|
|
46
54
|
|
|
47
55
|
## 👤 用户故事
|
|
48
56
|
|
|
@@ -71,29 +79,34 @@ Claude:
|
|
|
71
79
|
|
|
72
80
|
| 环境 | 部署方式 | 服务器 | 访问地址示例 |
|
|
73
81
|
|------|---------|--------|------------|
|
|
74
|
-
| **
|
|
75
|
-
| **
|
|
76
|
-
| **
|
|
82
|
+
| **ci** | Docker Compose | dev.optima.chat | api.optima.chat<br>auth.optima.chat<br>mcp.optima.chat |
|
|
83
|
+
| **stage** | AWS ECS | AWS ECS | api.stage.optima.onl<br>auth.stage.optima.onl |
|
|
84
|
+
| **prod** | AWS ECS | AWS ECS | api.optima.onl<br>auth.optima.onl |
|
|
85
|
+
| **cn-stage** | 阿里云 SAE(云效流水线发布) | 阿里云 SAE + RDS(经 buildbox ECS 跳板) | auth.stage.optima.chat<br>commerce.stage.optima.chat |
|
|
86
|
+
| **cn-prod** | 阿里云 SAE | 阿里云 SAE + RDS(经 buildbox ECS 跳板) | auth.yzsgo.com<br>commerce.yzsgo.com |
|
|
77
87
|
|
|
78
88
|
**说明**:
|
|
79
|
-
- **
|
|
80
|
-
- **
|
|
81
|
-
- **
|
|
89
|
+
- **ci** - 团队共享的持续集成测试环境,部署在 dev.optima.chat 服务器
|
|
90
|
+
- **stage** - 预发布环境,用于上线前的最终验证
|
|
91
|
+
- **prod** - 生产环境,服务真实用户
|
|
92
|
+
- **cn-stage / cn-prod** - 阿里云侧独立部署,与 AWS 侧完全无关(独立 Infisical 实例、独立 RDS 实例);cn-prod 服务境内真实用户
|
|
82
93
|
|
|
83
94
|
## 🚀 Claude Code 命令
|
|
84
95
|
|
|
85
96
|
| 命令 | 说明 | 示例 | 跨环境 |
|
|
86
97
|
|------|------|------|--------|
|
|
87
|
-
| `/
|
|
88
|
-
| `/
|
|
89
|
-
| `/
|
|
98
|
+
| `/generate-test-token` | 生成测试 token | `/generate-test-token` | ci / stage / prod / cn-stage / cn-prod |
|
|
99
|
+
| `/logs` | 查看服务日志 | `/logs commerce-backend 100` | ci / stage / prod / cn-stage / cn-prod |
|
|
100
|
+
| `/query-db` | 查询数据库 | `/query-db user-auth "SELECT COUNT(*) FROM users"` | ci / stage / prod / cn-stage / cn-prod |
|
|
90
101
|
| `/read-code` | 阅读代码 | `/read-code commerce-backend app/main.py` | - |
|
|
91
|
-
| `/
|
|
102
|
+
| `/restart-ecs` | 重启 ECS 服务 | `/restart-ecs user-auth stage` | stage / prod |
|
|
103
|
+
| `/trace-user` | 用户链路追踪:按账号把全链路行为拼成时间线 | `/trace-user user@example.com` | stage / prod / cn-stage / cn-prod |
|
|
92
104
|
|
|
93
105
|
**说明**:
|
|
94
|
-
-
|
|
95
|
-
-
|
|
96
|
-
-
|
|
106
|
+
- 本表与 `.claude/commands/` 一一对应(由 `tests/service-matrix-alignment.test.js` 校验,加命令漏更本表会红)。
|
|
107
|
+
- 上方「任务场景」里的 skill 同样可以用 `/<skill 名>` 直接唤起(例如 cn-deploy、gateway-admin),但它们是 skill、不在 `.claude/commands/` 里,因此不列进本表。
|
|
108
|
+
- 各命令支持的环境不同,见「跨环境」列。默认环境也不统一:`/logs`、`/query-db`、`/generate-test-token` 默认 `ci`,**`/restart-ecs`、`/trace-user` 默认 `stage`**(别当成 ci —— `/restart-ecs session-gateway` 不带环境重启的是 stage)。
|
|
109
|
+
- `/generate-test-token` 默认生成 `ci` 环境的账户(api.optima.chat);`--env` 可切到 stage / prod / cn-stage / cn-prod。**注意 `development` / `production` 不是合法取值**,CLI 只认上面五个(`bin/helpers/generate-test-token.ts:27`)
|
|
97
110
|
- Claude Code 会根据上下文自动选择环境和执行方式
|
|
98
111
|
|
|
99
112
|
## 🛠️ CLI 工具
|
|
@@ -111,13 +124,20 @@ Claude:
|
|
|
111
124
|
| `optima-entitlement` | 产品权益 grant/revoke/list | `optima-entitlement grant 18898654855 --product-key scout-gift --justification "..." --env cn-prod` |
|
|
112
125
|
| `optima-account` | 账号 status/ban/unban | `optima-account ban user@example.com --reason "abuse" --env prod` |
|
|
113
126
|
| `optima-cn-deploy` | 云效 Flow 发布到 cn-stage(构建→DB迁移→SAE 发布→sha 校验,20 服务) | `optima-cn-deploy billing` / `optima-cn-deploy user-auth --branch feat/xxx` |
|
|
127
|
+
| `optima-logs` | 查看服务日志(stage / prod 走 CloudWatch,cn 两侧直连 SLS;默认 `cn-prod`) | `optima-logs gateway-core --env cn-stage --since 2h` |
|
|
128
|
+
| `optima-verify-health` | 上线健康探针,L1 DNS → L5 依赖逐层探 | `optima-verify-health user-auth --env prod` |
|
|
129
|
+
| `optima-gateway-admin` | gateway-core `/admin/*` 直调(写操作需确认,默认 `cn-stage`) | `optima-gateway-admin GET /admin/llm-rates --env cn-stage` |
|
|
130
|
+
| `optima-plugin` | 插件市场态 show/set-paid/set-default/set-status | `optima-plugin show --slug <slug> --env stage` |
|
|
131
|
+
| `optima-product` | Product 的 create/update/add-channel/toggle-channel/show | `optima-product show --key <productKey> --env stage` |
|
|
132
|
+
|
|
133
|
+
本表与 `package.json` 的 `bin` 一一对应(同样由 `tests/service-matrix-alignment.test.js` 校验)。包自身的入口 `optima-dev-skills`(只提供 `version` / `help`)与 `optima-grant-credits` 的废弃别名 `optima-grant-balance` 有意不单独列行。
|
|
114
134
|
|
|
115
135
|
> **4 环境 + 标识符**:`grant-subscription` / `grant-credits` / `entitlement` / `account` 均支持 `stage` / `prod` / `cn-prod` / `cn-stage`。标识符 `<email\|phone\|userId>`——**cn-prod / cn-stage 用户多为手机号注册**,三种均可;AWS stage/prod 仅 email。`ban`/`unban` 及 `account status` 的禁用态读取需 admin-用户凭证(Infisical `/shared-secrets/credentials`;cn 另需 `INFISICAL_CN_EMAIL/PASSWORD`)。
|
|
116
136
|
|
|
117
137
|
**特点**:
|
|
118
|
-
- ✅ 支持
|
|
119
|
-
- ✅ 支持
|
|
120
|
-
- ✅ 自动管理 SSH
|
|
138
|
+
- ✅ 支持 ci / stage / prod / cn-stage / cn-prod(query-db,见 `bin/helpers/query-db.ts` 的 `VALID_ENVS`)
|
|
139
|
+
- ✅ 支持 stage / prod / cn-stage / cn-prod(show-env)
|
|
140
|
+
- ✅ 自动管理 DB 隧道与密钥(stage / prod 默认 AWS SSM 端口转发,cn 两侧经 buildbox ECS 跳板 SSH)
|
|
121
141
|
- ✅ 可在任何终端直接使用
|
|
122
142
|
- ✅ 自动注册账户、获取 token、设置 merchant profile(generate-test-token)
|
|
123
143
|
- ✅ Claude Code 的命令内部也使用这些工具
|
|
@@ -128,28 +148,21 @@ Claude:
|
|
|
128
148
|
```
|
|
129
149
|
optima-dev-skills/
|
|
130
150
|
├── .claude/
|
|
131
|
-
│ ├── commands/
|
|
132
|
-
│
|
|
133
|
-
│
|
|
134
|
-
|
|
135
|
-
│
|
|
136
|
-
│ │
|
|
137
|
-
│ └── skills/
|
|
138
|
-
│ ├── logs/ # 日志查看 skill
|
|
139
|
-
│ ├── query-db/ # 数据库查询 skill
|
|
140
|
-
│ ├── show-env/ # 环境变量查看 skill
|
|
141
|
-
│ ├── generate-test-token/ # 测试 token 生成 skill
|
|
142
|
-
│ ├── use-commerce-cli/ # Commerce CLI 使用 skill
|
|
143
|
-
│ └── read-code/ # 代码阅读 skill
|
|
151
|
+
│ ├── commands/ # /<name> 斜杠命令,一个命令一个 .md
|
|
152
|
+
│ └── skills/ # Claude Code skills,一个 skill 一个目录(清单见上方「任务场景」)
|
|
153
|
+
│
|
|
154
|
+
├── .codex/
|
|
155
|
+
│ └── skills/ # Codex skills,是 .claude/skills 的镜像子集(清单见 AGENTS.md)
|
|
144
156
|
│
|
|
145
157
|
├── bin/
|
|
146
|
-
│ └── helpers/
|
|
147
|
-
│
|
|
148
|
-
|
|
149
|
-
│
|
|
158
|
+
│ └── helpers/ # optima-* CLI 的 TypeScript 实现(清单见上方「CLI 工具」)
|
|
159
|
+
│
|
|
160
|
+
├── scripts/
|
|
161
|
+
│ └── install.js # postinstall:按 readdir 把上面各目录全量装到 ~/.claude 与 ~/.codex
|
|
162
|
+
│
|
|
163
|
+
├── tests/ # node --test;service-matrix-alignment 校验文档清单与实际目录一致
|
|
150
164
|
│
|
|
151
165
|
└── docs/
|
|
152
|
-
└── COMMANDS_DESIGN.md
|
|
153
166
|
```
|
|
154
167
|
|
|
155
168
|
## 💡 使用示例
|
|
@@ -171,17 +184,17 @@ Claude:
|
|
|
171
184
|
### 示例 2:生成测试 token 并管理店铺
|
|
172
185
|
|
|
173
186
|
```bash
|
|
174
|
-
# 1. 生成
|
|
175
|
-
$ optima-generate-test-token --env
|
|
187
|
+
# 1. 生成 prod 环境测试 token
|
|
188
|
+
$ optima-generate-test-token --env prod
|
|
176
189
|
|
|
177
|
-
Environment:
|
|
178
|
-
Auth API: https://auth.optima.
|
|
190
|
+
Environment: prod
|
|
191
|
+
Auth API: https://auth.optima.onl
|
|
179
192
|
✅ Test token generated successfully!
|
|
180
193
|
📁 Token File Path: /tmp/optima-test-token-xxx.txt
|
|
181
194
|
|
|
182
|
-
# 2. 使用 token
|
|
195
|
+
# 2. 使用 token 创建商品(OPTIMA_ENV 取 generate-test-token 回显的那个 envName)
|
|
183
196
|
$ OPTIMA_TOKEN=$(cat /tmp/optima-test-token-xxx.txt) \
|
|
184
|
-
OPTIMA_ENV=
|
|
197
|
+
OPTIMA_ENV=prod \
|
|
185
198
|
commerce product create --title "测试商品" --price 99.99 --stock 100
|
|
186
199
|
|
|
187
200
|
{
|
|
@@ -208,7 +221,7 @@ $ optima-query-db commerce-backend "SELECT id, title FROM products LIMIT 5" stag
|
|
|
208
221
|
|
|
209
222
|
### dev-skills 提供什么?
|
|
210
223
|
|
|
211
|
-
- ✅ **跨环境命令** - 在
|
|
224
|
+
- ✅ **跨环境命令** - 在 ci / stage / prod / cn-stage / cn-prod 统一执行
|
|
212
225
|
- ✅ **任务场景指导** - 完整的操作流程(不是零散命令)
|
|
213
226
|
- ✅ **团队协作工具** - 跨仓库、跨环境的共享知识
|
|
214
227
|
|
|
@@ -253,18 +266,17 @@ $ optima-query-db commerce-backend "SELECT id, title FROM products LIMIT 5" stag
|
|
|
253
266
|
|
|
254
267
|
## 🛠️ 开发状态
|
|
255
268
|
|
|
256
|
-
|
|
269
|
+
**当前版本**:见 [package.json](package.json),或 `npm view @optima-chat/dev-skills version`(不在此写死——写死的数字正是本节此前一路过期到 0.7.16 的原因)
|
|
257
270
|
|
|
258
271
|
**已完成**:
|
|
259
|
-
- ✅
|
|
260
|
-
- ✅
|
|
261
|
-
- ✅ 支持 CI、Stage、Prod 三个环境
|
|
272
|
+
- ✅ 命令、任务场景、CLI 工具三份清单见上方对应章节,此处不再重复计数(重复一次就多一处会漂的地方)
|
|
273
|
+
- ✅ 支持 ci、stage、prod、cn-stage、cn-prod 五个环境
|
|
262
274
|
- ✅ CI 环境通过 SSH + Docker 访问
|
|
263
|
-
- ✅
|
|
264
|
-
- ✅
|
|
275
|
+
- ✅ stage / prod 默认经共享 bastion 的 **AWS SSM 端口转发**访问 RDS(需 `session-manager-plugin`;`OPTIMA_DB_TUNNEL=ssh` 可回退 legacy SSH 隧道)
|
|
276
|
+
- ✅ cn-stage / cn-prod 经 buildbox ECS 跳板的 **SSH 隧道**访问内网 RDS
|
|
265
277
|
- ✅ 通过 Infisical 动态获取密钥和环境变量
|
|
266
278
|
- ✅ 自动生成测试 token 并设置 merchant profile
|
|
267
|
-
- ✅ `generate-test-token` 支持
|
|
279
|
+
- ✅ `generate-test-token` 支持 ci / stage / prod / cn-stage / cn-prod 五个环境
|
|
268
280
|
|
|
269
281
|
**设计原则**:
|
|
270
282
|
- 命令提供信息(URL、路径、凭证位置),不实现复杂逻辑
|
package/bin/helpers/cn-deploy.ts
CHANGED
|
@@ -20,11 +20,17 @@
|
|
|
20
20
|
* optima-cn-deploy --list # 列出全部可发服务
|
|
21
21
|
* optima-cn-deploy billing --vtag cn-v1.2.3 # stage 按版本 tag 构建(发版前验证)
|
|
22
22
|
* optima-cn-deploy billing --env prod --vtag cn-v1.2.3
|
|
23
|
-
* # cn-prod vtag
|
|
23
|
+
* # cn-prod vtag 发版:构建→迁移→digest 钉死部署。🔴 **中间没有人工卡点**——
|
|
24
|
+
* # 触发即一路跑到 cn-prod 部署完,没有"到云效控制台再审批一次"这一步。
|
|
25
|
+
* # 实测 2026-08-09:gateway-core / billing / user-auth / commerce-backend /
|
|
26
|
+
* # optima-scout 五条 *-cn-prod 流水线 stages 均为 build→migrate→deploy,
|
|
27
|
+
* # 无任何审批任务(aliyun devops GetPipeline 查 pipelineConfig.flow 可复核)。
|
|
28
|
+
* # ⇒ 唯一的决策点是敲下这条命令之前。
|
|
24
29
|
*
|
|
25
30
|
* 前置: aliyun CLI(profile 默认 aliyun-optima,可用 OPTIMA_ALIYUN_PROFILE 覆盖)+ gh 已登录。
|
|
26
|
-
* 流水线定义的单一信源在 optima-terraform yunxiao/(gen-pipelines.py)
|
|
27
|
-
*
|
|
31
|
+
* 流水线定义的单一信源在 optima-terraform yunxiao/(gen-pipelines.py)。本表只快照
|
|
32
|
+
* repo / SAE appId;pipelineId 不在此表 —— stage/prod 均按流水线名 ${svc}-cn-${env}
|
|
33
|
+
* 从云效 ListPipelines 实时解析,不再手工同步(见 #84)。
|
|
28
34
|
*/
|
|
29
35
|
import { execFileSync } from 'node:child_process';
|
|
30
36
|
|
|
@@ -33,33 +39,34 @@ const ENDPOINT = 'devops.cn-hangzhou.aliyuncs.com';
|
|
|
33
39
|
const PROFILE = process.env.OPTIMA_ALIYUN_PROFILE || 'aliyun-optima';
|
|
34
40
|
const CODEUP_BASE = `https://codeup.aliyun.com/${ORG}`;
|
|
35
41
|
|
|
36
|
-
//
|
|
37
|
-
|
|
42
|
+
// 服务注册表:repo / SAE appId 快照自 optima-terraform services.stage.env(2026-07-13)。
|
|
43
|
+
// pipelineId 不再硬编码 —— 按名 ${svc}-cn-${env} 从云效实时解析,根除与 cn-run.py 手工表漂移(#84)。
|
|
44
|
+
interface Svc { repo: string; saeAppId?: string; prodSaeAppId?: string; buildOnly?: boolean; }
|
|
38
45
|
const SERVICES: Record<string, Svc> = {
|
|
39
|
-
'agent-portal': {
|
|
46
|
+
'agent-portal': { repo: 'optima-portals', saeAppId: 'fe757f78-d18d-4480-9402-fe59d4721055', prodSaeAppId: '9211db38-a255-4393-ae92-7a7bb41b583d' },
|
|
40
47
|
// build-only:非 SAE 常驻(gateway-core 按 session 拉起的镜像)。release 段=解析 ACR digest
|
|
41
48
|
// → 回写 Infisical /services/gateway-core/ALIYUN_AGENT_RUNTIME_IMAGE(#807)→ 滚动重启 gateway-core。
|
|
42
|
-
// 无自身 saeAppId
|
|
43
|
-
'agent-runtime': {
|
|
44
|
-
'agentic-chat': {
|
|
45
|
-
'billing': {
|
|
46
|
-
'browser-backend': {
|
|
47
|
-
'commerce-backend': {
|
|
48
|
-
'commerce-rq-scheduler': {
|
|
49
|
-
'commerce-rq-worker': {
|
|
50
|
-
'gateway-core': {
|
|
51
|
-
'gw-admin': {
|
|
52
|
-
'kb-backend': {
|
|
53
|
-
'ops-portal': {
|
|
54
|
-
'optima-generation': {
|
|
55
|
-
'optima-generation-worker': {
|
|
56
|
-
'optima-scout': {
|
|
57
|
-
'optima-sentinel': {
|
|
58
|
-
'optima-sentinel-worker': {
|
|
59
|
-
'optima-skills': {
|
|
60
|
-
'user-auth': {
|
|
61
|
-
'user-auth-admin': {
|
|
62
|
-
'yzsgo-api': {
|
|
49
|
+
// 无自身 saeAppId(build-only 不跑 SAE ImageUrl 校验)。
|
|
50
|
+
'agent-runtime': { repo: 'optima-gateway', buildOnly: true },
|
|
51
|
+
'agentic-chat': { repo: 'agentic-chat', saeAppId: '6aea1ce1-f813-4e1c-8e97-d1ecb5398e37', prodSaeAppId: '6e290c73-a646-43ef-9da5-ad0b2e7eff73' },
|
|
52
|
+
'billing': { repo: 'optima-billing', saeAppId: '09d8e292-dc64-4af8-bce5-0a56cb666921', prodSaeAppId: '6c31cf82-8802-4d45-b6a2-e9d7c83ccce9' },
|
|
53
|
+
'browser-backend': { repo: 'optima-browser-use', saeAppId: '1fced3f6-a80a-41a4-8f23-e4d5a467f8eb', prodSaeAppId: 'eb782ed9-c468-4e81-a0fe-87e5b4264192' },
|
|
54
|
+
'commerce-backend': { repo: 'commerce-backend', saeAppId: '49d09808-508c-471a-9560-553c49a67f72', prodSaeAppId: 'd40597b6-a98c-4063-9297-8b9fdfa8add4' },
|
|
55
|
+
'commerce-rq-scheduler': { repo: 'commerce-backend', saeAppId: '1d2810ef-889e-4675-b6e6-a299e4722e68', prodSaeAppId: '762dd7a5-7fe8-41ed-9332-cfd31b0e087e' },
|
|
56
|
+
'commerce-rq-worker': { repo: 'commerce-backend', saeAppId: 'd661da87-6d24-40b8-93ed-e1c967d8abe2', prodSaeAppId: '2c9ee19a-5062-4205-9f5b-6ae3f8062a23' },
|
|
57
|
+
'gateway-core': { repo: 'optima-gateway', saeAppId: '9326b7ff-da52-48d6-86db-9c4a884be108', prodSaeAppId: 'a08ce23f-3d3e-4d89-a2cf-53c8adba614e' },
|
|
58
|
+
'gw-admin': { repo: 'optima-gateway', saeAppId: '90e0daf7-7910-46b8-b5bf-a0b8bcc60859', prodSaeAppId: 'c6bc5a78-b27f-46e2-825a-eaa338c23645' },
|
|
59
|
+
'kb-backend': { repo: 'kb-skills', saeAppId: 'c7f65160-9d9e-416e-9e36-5439010d2b2d', prodSaeAppId: '732dfc8c-ed78-43f7-a722-c614359ff1a1' },
|
|
60
|
+
'ops-portal': { repo: 'optima-portals', saeAppId: '41d0ee66-8402-4e72-bfda-8eba75d1270c', prodSaeAppId: '69e513fd-29bf-4355-a275-5eba82b21136' },
|
|
61
|
+
'optima-generation': { repo: 'optima-gen', saeAppId: '327856d5-8b18-4e15-bfd8-b8bd3b807ffa', prodSaeAppId: '868d1a69-12c9-4bb7-868b-3e89c26784b5' },
|
|
62
|
+
'optima-generation-worker': { repo: 'optima-gen', saeAppId: 'ad75b3b0-9ff4-443d-b0ef-81886bc7aa60', prodSaeAppId: '2adc941e-374b-406a-9863-e53d40bc9500' },
|
|
63
|
+
'optima-scout': { repo: 'optima-scout', saeAppId: 'bac2c3a4-90ce-49a6-81d5-caa38cb5c807', prodSaeAppId: 'f5bb7e82-e57c-4bd4-83ae-2c25f8d38647' },
|
|
64
|
+
'optima-sentinel': { repo: 'optima-sentinel', saeAppId: '3759db7b-4640-4a04-8436-4f241f0ec9d9', prodSaeAppId: 'e120e70c-b2c1-49ef-a66c-9016fd2d807d' },
|
|
65
|
+
'optima-sentinel-worker': { repo: 'optima-sentinel', saeAppId: '7c975caf-ebfb-4bb3-91c4-2a8c83d1d2e5', prodSaeAppId: 'e0f9b92c-00af-4ade-9b07-15bba48c14cc' },
|
|
66
|
+
'optima-skills': { repo: 'optima-skills', saeAppId: '90457be3-5eb3-4efc-a362-1788dcc51921', prodSaeAppId: '55a63ee7-fb75-46e4-99f3-20854a699237' },
|
|
67
|
+
'user-auth': { repo: 'user-auth', saeAppId: '36efc3a6-5b42-49fa-8db2-0865aa0c25d2', prodSaeAppId: 'd6fbf9de-fed6-4165-978f-7b3a0a456acc' },
|
|
68
|
+
'user-auth-admin': { repo: 'user-auth', saeAppId: '7feb9cc3-4731-4ea8-964c-4870a9e63afb', prodSaeAppId: '54093f99-37bf-4354-abd7-f6e76abdbcb6' },
|
|
69
|
+
'yzsgo-api': { repo: 'yzsgo', saeAppId: 'd0af7f66-ef66-4587-9e93-250d01ee3cf6', prodSaeAppId: '5a820992-e11b-404a-9e58-8f607d8e0c5f' },
|
|
63
70
|
};
|
|
64
71
|
|
|
65
72
|
function sh(cmd: string, args: string[]): string {
|
|
@@ -104,7 +111,8 @@ async function main() {
|
|
|
104
111
|
console.error(`✗ vtag 格式必须 cn-vX.Y.Z(得到 ${vtag});裸 v* 会误触 AWS prod,绝不放行`); process.exit(1);
|
|
105
112
|
}
|
|
106
113
|
if (envName === 'prod') {
|
|
107
|
-
// prod = vtag 制:先 stage 同 tag 验证,再发 prod
|
|
114
|
+
// prod = vtag 制:先 stage 同 tag 验证,再发 prod。🔴 流水线内**没有**人工卡点,
|
|
115
|
+
// 下面这道 vtag 校验就是最后一道闸(实测见文件头注)。
|
|
108
116
|
if (!vtag) { console.error('✗ --env prod 必须带 --vtag cn-vX.Y.Z(先 stage 同 tag 验证)'); process.exit(1); }
|
|
109
117
|
if (branch !== 'main') { console.error('✗ prod 发布按 vtag,不接受 --branch'); process.exit(1); }
|
|
110
118
|
}
|
|
@@ -130,14 +138,12 @@ async function main() {
|
|
|
130
138
|
if (!synced) { console.error('✗ 300s 内 Codeup mirror 未追平 GitHub,中止(检查 mirror 凭证)'); process.exit(1); }
|
|
131
139
|
console.log(`✓ mirror 已追平 ${svc.repo}@${ref} = ${ghSha.slice(0, 10)}`);
|
|
132
140
|
|
|
133
|
-
// 2. 触发(凭证由云效变量组供给,无需注入)。prod 流水线 id
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
pipelineId = hit.pipelineId;
|
|
140
|
-
}
|
|
141
|
+
// 2. 触发(凭证由云效变量组供给,无需注入)。stage/prod 流水线 id 均按名 `${svc}-cn-${env}`
|
|
142
|
+
// 从云效实时解析、不硬编码,根除与 optima-terraform cn-run.py 手工 PIPELINES 表漂移(#84)。
|
|
143
|
+
const lp = devops('ListPipelines', { maxResults: '100' });
|
|
144
|
+
const hit = (lp.pipelines || []).find((p: any) => p.pipelineName === `${svcName}-cn-${envName}`);
|
|
145
|
+
if (!hit) { console.error(`✗ 云效无 ${svcName}-cn-${envName} 流水线`); process.exit(1); }
|
|
146
|
+
const pipelineId = hit.pipelineId;
|
|
141
147
|
const kv: Record<string, string> = { pipelineId: String(pipelineId) };
|
|
142
148
|
if (vtag) {
|
|
143
149
|
kv.params = JSON.stringify({
|
|
@@ -153,7 +159,8 @@ async function main() {
|
|
|
153
159
|
console.log(`▶ ${svcName} run#${runId} 已启动 (ref=${ref}, env=${envName})`);
|
|
154
160
|
console.log(` https://flow.aliyun.com/pipelines/${pipelineId}/current`);
|
|
155
161
|
if (envName === 'prod') {
|
|
156
|
-
console.log('
|
|
162
|
+
console.log('🔴 cn-prod:流水线**没有**人工卡点 —— 构建→迁移→digest 部署会一路自动走完,');
|
|
163
|
+
console.log(' 不要去云效控制台等『发布审批』(那一步不存在)。要中止只能在上面的链接里手动取消 run。');
|
|
157
164
|
}
|
|
158
165
|
if (noWait) return;
|
|
159
166
|
|
package/bin/helpers/db-utils.ts
CHANGED
|
@@ -299,6 +299,79 @@ function isTunnelProcessOnPort(localPort: number): boolean {
|
|
|
299
299
|
});
|
|
300
300
|
}
|
|
301
301
|
|
|
302
|
+
/**
|
|
303
|
+
* 隧道进程 cmdline 指向的 RDS host 与期望的 `dbHost` 是否一致(#76)。
|
|
304
|
+
*
|
|
305
|
+
* `isTunnelProcessOnPort` + `isTunnelHealthy` 只能回答「端口上是我们的隧道且 postgres
|
|
306
|
+
* 应答」—— **答不了「通向哪个库」**:连到错误实例的隧道同样 ssh + 同样 pg_isready 通。
|
|
307
|
+
* 注册表一旦出现两个 host 指向同一端口(见 pickTunnelPort 注释),warm reuse 就会把
|
|
308
|
+
* cn-prod 的查询送进 cn-stage 的隧道;两库账号密码不同才会报 auth failed 暴露出来,
|
|
309
|
+
* 同名账号则是**静默连错环境**。故复用前解析 cmdline 里的转发目标。
|
|
310
|
+
*
|
|
311
|
+
* 两种隧道形态的 cmdline(**均已在本机实测**,勿凭文档猜):
|
|
312
|
+
* ssh / cn-ssh:`ssh … -L [bind:]<localPort>:<dbHost>:5432 root@<bastion>`
|
|
313
|
+
* SSM :持端口的是 `session-manager-plugin`(不是 `aws` CLI),argv 里是
|
|
314
|
+
* StartSession 请求 **JSON**:
|
|
315
|
+
* `… StartSession {"Target":"i-…","DocumentName":"AWS-StartPortForwarding…",
|
|
316
|
+
* "Parameters": {"host": ["<dbHost>"], "portNumber": ["5432"],
|
|
317
|
+
* "localPortNumber": ["<localPort>"]}} https://ssm.…`
|
|
318
|
+
*
|
|
319
|
+
* **按 localPort 锚定**:一个 ssh 进程可以带多条 `-L`(工程师自建的多路转发也会被
|
|
320
|
+
* `isTunnelProcessOnPort` 认作「我们的隧道」)。只要 cmdline 里任一条 `-L` 命中 dbHost
|
|
321
|
+
* 就判 match,会把「25432 实际通向 stage、25433 才通向 prod」的进程误判为可复用 →
|
|
322
|
+
* 正好造成本 issue 要防的连错库。故只认**转发自本端口**的那一条。
|
|
323
|
+
*
|
|
324
|
+
* 返回 `unknown`(读不到 cmdline / 格式不认识)时**保守回退**:宁可偶尔多复用一次,
|
|
325
|
+
* 也不因误判杀掉正常 warm reuse;`mismatch` 才是确凿证据,一律拒绝复用。注意
|
|
326
|
+
* `unknown` 单独不足以防「注册表已存在重复端口」,那由 ensureTunnel 的 dup 判据兜底。
|
|
327
|
+
*/
|
|
328
|
+
export function classifyTunnelTarget(
|
|
329
|
+
args: string,
|
|
330
|
+
dbHost: string,
|
|
331
|
+
localPort?: number,
|
|
332
|
+
): 'match' | 'mismatch' | 'unknown' {
|
|
333
|
+
const lp = localPort === undefined ? '\\d+' : String(localPort);
|
|
334
|
+
const targets: string[] = [];
|
|
335
|
+
// ssh:`-L [bind_address:]<localPort>:<host>:<remotePort>`(bind_address 可选是 ssh 合法写法)
|
|
336
|
+
for (const m of args.matchAll(new RegExp(`-L\\s*(?:[A-Za-z0-9._-]+:)?${lp}:([A-Za-z0-9._-]+):\\d+`, 'g'))) {
|
|
337
|
+
targets.push(m[1]);
|
|
338
|
+
}
|
|
339
|
+
// SSM:请求 JSON 里的 host;带 localPortNumber 时同样按本端口锚定
|
|
340
|
+
const ssmHost = args.match(/"host"\s*:\s*\[\s*"([A-Za-z0-9._-]+)"/);
|
|
341
|
+
if (ssmHost) {
|
|
342
|
+
const ssmLocal = args.match(/"localPortNumber"\s*:\s*\[\s*"(\d+)"/);
|
|
343
|
+
if (localPort === undefined || !ssmLocal || ssmLocal[1] === String(localPort)) targets.push(ssmHost[1]);
|
|
344
|
+
}
|
|
345
|
+
if (targets.length === 0) return 'unknown';
|
|
346
|
+
const want = dbHost.toLowerCase();
|
|
347
|
+
return targets.some(t => t.toLowerCase() === want) ? 'match' : 'mismatch';
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* 端口上的隧道进程是否通向 `dbHost`(聚合多 pid:任一 match 即 match)。
|
|
352
|
+
* 只读**隧道进程**(comm=ssh / session-manager)的 argv —— `lsof -ti:PORT` 同时会列出
|
|
353
|
+
* 连到该端口的**客户端**(psql / pg_isready),其 argv 里若恰好含 `host=…` 会造成假 mismatch。
|
|
354
|
+
*/
|
|
355
|
+
function tunnelTargetOnPort(localPort: number, dbHost: string): 'match' | 'mismatch' | 'unknown' {
|
|
356
|
+
let sawMismatch = false;
|
|
357
|
+
for (const pid of portPids(localPort)) {
|
|
358
|
+
let comm = '';
|
|
359
|
+
let args = '';
|
|
360
|
+
try {
|
|
361
|
+
const out = execSync(`ps -o comm=,args= -p ${pid}`, { encoding: 'utf-8' }).trim();
|
|
362
|
+
comm = out.split(/\s+/)[0] ?? '';
|
|
363
|
+
args = out;
|
|
364
|
+
} catch {
|
|
365
|
+
continue; // 进程刚退出
|
|
366
|
+
}
|
|
367
|
+
if (comm !== 'ssh' && !comm.includes('session-manager')) continue; // 客户端进程,跳过
|
|
368
|
+
const verdict = classifyTunnelTarget(args, dbHost, localPort);
|
|
369
|
+
if (verdict === 'match') return 'match';
|
|
370
|
+
if (verdict === 'mismatch') sawMismatch = true;
|
|
371
|
+
}
|
|
372
|
+
return sawMismatch ? 'mismatch' : 'unknown';
|
|
373
|
+
}
|
|
374
|
+
|
|
302
375
|
function killOrphanTunnel(localPort: number): void {
|
|
303
376
|
// 只杀我们的隧道进程 —— 端口上若是别人(docker-proxy/postgres),kill -9 是破坏性的。
|
|
304
377
|
for (const pid of portPids(localPort)) {
|
|
@@ -429,13 +502,21 @@ function writeTunnelRegistry(reg: Record<string, number>): void {
|
|
|
429
502
|
fs.writeFileSync(TUNNEL_REGISTRY, JSON.stringify(reg, null, 2));
|
|
430
503
|
}
|
|
431
504
|
|
|
432
|
-
/**
|
|
433
|
-
|
|
505
|
+
/**
|
|
506
|
+
* First free local port, preferring the registry's previous assignment.
|
|
507
|
+
*
|
|
508
|
+
* `reserved` = 注册表里**其它 host** 已占用的端口(#76)。原实现只看「此刻空闲」:
|
|
509
|
+
* 隧道进程死掉后端口释放,下一个 host 扫描到同一个 25432 就写进注册表 → 多个 host
|
|
510
|
+
* 指向同一端口(实遇 4 个 RDS 里 3 个都是 25432),之后任一条隧道活着都会被其它
|
|
511
|
+
* host 误复用 → 连错库。排除其它 host 已认领的端口,注册表由构造保持单射。
|
|
512
|
+
*/
|
|
513
|
+
function pickTunnelPort(preferred: number | undefined, reserved: ReadonlySet<number> = new Set()): number {
|
|
434
514
|
const candidates = preferred ? [preferred] : [];
|
|
435
515
|
for (let p = TUNNEL_PORT_SCAN_BASE; p < TUNNEL_PORT_SCAN_BASE + TUNNEL_PORT_SCAN_LIMIT; p++) {
|
|
436
516
|
if (p !== preferred) candidates.push(p);
|
|
437
517
|
}
|
|
438
518
|
for (const p of candidates) {
|
|
519
|
+
if (reserved.has(p)) continue;
|
|
439
520
|
if (portPids(p).length === 0 && !isPortResponding(p)) return p;
|
|
440
521
|
}
|
|
441
522
|
throw new Error(`No free local port in ${TUNNEL_PORT_SCAN_BASE}-${TUNNEL_PORT_SCAN_BASE + TUNNEL_PORT_SCAN_LIMIT - 1} for the DB tunnel`);
|
|
@@ -454,15 +535,33 @@ export function ensureTunnel(dbHost: string, via: 'aws' | 'cn-buildbox' = 'aws')
|
|
|
454
535
|
const reg = readTunnelRegistry();
|
|
455
536
|
const known = reg[dbHost];
|
|
456
537
|
|
|
538
|
+
// #76:端口同时被注册表里其它 host 认领 = 注册表已损坏(历史遗留缓存就是这个状态)。
|
|
539
|
+
// 此时若目标又验不出来(unknown,如 argv 格式不认识),不能乐观复用——否则那条路径
|
|
540
|
+
// 永远早返回、不写注册表,重复项永不自愈(AWS/SSM 默认路径尤其吃这个亏)。
|
|
541
|
+
const dupClaimed = known !== undefined
|
|
542
|
+
&& Object.entries(reg).some(([h, p]) => h !== dbHost && p === known);
|
|
543
|
+
|
|
457
544
|
if (known !== undefined && isTunnelProcessOnPort(known)) {
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
545
|
+
// #76:健康 ≠ 通向正确的库 —— 先验转发目标,确凿指向别的 RDS 就别复用(也别杀,
|
|
546
|
+
// 那是另一个 host 正在用的隧道),换个端口自己建。
|
|
547
|
+
const verdict = tunnelTargetOnPort(known, dbHost);
|
|
548
|
+
if (verdict === 'mismatch' || (verdict === 'unknown' && dupClaimed)) {
|
|
549
|
+
console.log(`! Tunnel on port ${known} may forward to a different RDS host, not reusing (#76)`);
|
|
550
|
+
} else if (isTunnelHealthy(known)) {
|
|
551
|
+
return known; // warm reuse — no cold start
|
|
552
|
+
} else {
|
|
553
|
+
console.log(`! Tunnel on port ${known} not responding (zombie), replacing...`);
|
|
554
|
+
killOrphanTunnel(known);
|
|
555
|
+
}
|
|
461
556
|
}
|
|
462
557
|
|
|
463
558
|
// Foreign process on the recorded port (e.g. a local Docker PG) → pick a
|
|
464
559
|
// different port instead of talking to whatever squats there.
|
|
465
|
-
|
|
560
|
+
// #76:同时排除注册表里其它 host 认领的端口,避免两个 host 收敛到同一端口。
|
|
561
|
+
const reserved = new Set(
|
|
562
|
+
Object.entries(reg).filter(([h]) => h !== dbHost).map(([, p]) => p),
|
|
563
|
+
);
|
|
564
|
+
const port = pickTunnelPort(known, reserved);
|
|
466
565
|
if (via === 'cn-buildbox') {
|
|
467
566
|
setupCnSSHTunnel(dbHost, port);
|
|
468
567
|
} else if ((process.env.OPTIMA_DB_TUNNEL || 'ssm').toLowerCase() === 'ssh') {
|