@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/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 能够直接在 **CIStageProd** 三个环境中执行开发任务。
28
+ Optima Dev Skills 让 Claude Code 能够直接在 **cistageprod、cn-stage、cn-prod** 五个环境中执行开发任务(后两个是阿里云侧,与 AWS 侧完全独立;并非每个 skill 都覆盖全部五个,逐个见下)。
29
29
 
30
30
  **核心价值**:
31
31
  - **即时执行** - Claude 直接执行操作,开发者零手动操作
32
32
  - **任务驱动** - 基于具体任务场景(查看日志、调用 API),不是抽象分类
33
- - **跨环境协作** - 统一的命令在 CI、Stage、Prod 三个环境中使用
34
-
35
- ## 📋 任务场景(6 个)
36
-
37
- 当 Claude Code 识别到以下任务时,会自动加载对应的 Skill
38
-
39
- - **logs** - 查看 CI/Stage/Prod 的服务器日志
40
- - **query-db** - 查询 CI/Stage/Prod 的数据库
41
- - **show-env** - 查看 Stage/Prod 的服务环境变量(从 Infisical
42
- - **generate-test-token** - 生成测试 Access Token 用于 API 测试
43
- - **use-commerce-cli** - 使用 Commerce CLI 管理电商店铺
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
- - **discount-codes** - 创建/生成/查看/停用 billing 优惠码(Stage/Prod
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
- | **CI** | Docker Compose | dev.optima.chat | api.optima.chat<br>auth.optima.chat<br>mcp.optima.chat |
75
- | **Stage** | AWS ECS | AWS ECS | api.stage.optima.onl<br>auth.stage.optima.onl<br>mcp.stage.optima.onl |
76
- | **Prod** | EC2 + Docker | AWS EC2 | api.optima.shop<br>auth.optima.shop<br>mcp.optima.shop |
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
- - **CI** - 团队共享的持续集成测试环境,部署在 dev.optima.chat 服务器
80
- - **Stage** - 预发布环境,用于上线前的最终验证
81
- - **Prod** - 生产环境,服务真实用户
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
- | `/logs` | 查看服务日志 | `/logs commerce-backend 100` | |
88
- | `/query-db` | 查询数据库 | `/query-db user-auth "SELECT COUNT(*) FROM users"` | |
89
- | `/generate-test-token` | 生成测试 token | `/generate-test-token` | 🔧 Development |
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
- | `/cn-deploy` | 发布服务到 cn-stage(云效流水线一条龙) | 「把 billing 发到 cn-stage | cn-stage |
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
- - 命令支持 CI、Stage、Prod 三个环境
95
- - 默认使用 CI 环境,适合日常开发
96
- - `/generate-test-token` 生成的账户用于 development 环境(api.optima.chat)
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
- - ✅ 支持 CI、Stage、Prod 三个环境(query-db
119
- - ✅ 支持 Stage、Prod 环境(show-env)
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
- │ ├── logs.md # /logs - 查看服务日志
133
- │ ├── query-db.md # /query-db - 查询数据库
134
- │ │ ├── generate-test-token.md # /generate-test-token - 生成测试 token
135
- └── read-code.md # /read-code - 阅读代码
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
- ├── query-db.ts # CLI: 数据库查询
148
- ├── show-env.ts # CLI: 查看环境变量
149
- └── generate-test-token.ts # CLI: 生成测试 token
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. 生成 production 环境测试 token
175
- $ optima-generate-test-token --env production
187
+ # 1. 生成 prod 环境测试 token
188
+ $ optima-generate-test-token --env prod
176
189
 
177
- Environment: production
178
- Auth API: https://auth.optima.shop
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=production \
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
- - ✅ **跨环境命令** - 在 CI/Stage/Prod 统一执行
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
- **当前版本**: 0.7.16
269
+ **当前版本**:见 [package.json](package.json),或 `npm view @optima-chat/dev-skills version`(不在此写死——写死的数字正是本节此前一路过期到 0.7.16 的原因)
257
270
 
258
271
  **已完成**:
259
- - ✅ 4 个命令:`/logs`、`/query-db`、`/generate-test-token`、`/read-code`
260
- - ✅ 6 个任务场景:`logs`、`query-db`、`show-env`、`generate-test-token`、`use-commerce-cli`、`read-code`
261
- - ✅ 支持 CI、Stage、Prod 三个环境
272
+ - ✅ 命令、任务场景、CLI 工具三份清单见上方对应章节,此处不再重复计数(重复一次就多一处会漂的地方)
273
+ - ✅ 支持 ci、stage、prod、cn-stage、cn-prod 五个环境
262
274
  - ✅ CI 环境通过 SSH + Docker 访问
263
- - ✅ Stage/Prod 通过 SSH 隧道访问 RDS
264
- - ✅ TypeScript CLI 工具:`optima-query-db`、`optima-show-env`、`optima-generate-test-token`
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` 支持 development production 环境
279
+ - ✅ `generate-test-token` 支持 ci / stage / prod / cn-stage / cn-prod 五个环境
268
280
 
269
281
  **设计原则**:
270
282
  - 命令提供信息(URL、路径、凭证位置),不实现复杂逻辑
@@ -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 发版:构建→停在人工卡点(云效控制台审批)→迁移→digest 钉死部署
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
- * 新增服务后同步(pipelineId 稳定,不常变)。
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
- // 服务注册表(快照自 optima-terraform services.stage.env + gen-pipelines.py,2026-07-13)
37
- interface Svc { pipelineId: number; repo: string; saeAppId?: string; prodSaeAppId?: string; buildOnly?: boolean; }
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': { pipelineId: 5118519, repo: 'optima-portals', saeAppId: 'fe757f78-d18d-4480-9402-fe59d4721055', prodSaeAppId: '9211db38-a255-4393-ae92-7a7bb41b583d' },
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;prod 流水线(agent-runtime-cn-prod,5124970)按名实时解析(见下方 ListPipelines 逻辑)。
43
- 'agent-runtime': { pipelineId: 5124962, repo: 'optima-gateway', buildOnly: true },
44
- 'agentic-chat': { pipelineId: 5118520, repo: 'agentic-chat', saeAppId: '6aea1ce1-f813-4e1c-8e97-d1ecb5398e37', prodSaeAppId: '6e290c73-a646-43ef-9da5-ad0b2e7eff73' },
45
- 'billing': { pipelineId: 5118521, repo: 'optima-billing', saeAppId: '09d8e292-dc64-4af8-bce5-0a56cb666921', prodSaeAppId: '6c31cf82-8802-4d45-b6a2-e9d7c83ccce9' },
46
- 'browser-backend': { pipelineId: 5118522, repo: 'optima-browser-use', saeAppId: '1fced3f6-a80a-41a4-8f23-e4d5a467f8eb', prodSaeAppId: 'eb782ed9-c468-4e81-a0fe-87e5b4264192' },
47
- 'commerce-backend': { pipelineId: 5107005, repo: 'commerce-backend', saeAppId: '49d09808-508c-471a-9560-553c49a67f72', prodSaeAppId: 'd40597b6-a98c-4063-9297-8b9fdfa8add4' },
48
- 'commerce-rq-scheduler': { pipelineId: 5118523, repo: 'commerce-backend', saeAppId: '1d2810ef-889e-4675-b6e6-a299e4722e68', prodSaeAppId: '762dd7a5-7fe8-41ed-9332-cfd31b0e087e' },
49
- 'commerce-rq-worker': { pipelineId: 5118524, repo: 'commerce-backend', saeAppId: 'd661da87-6d24-40b8-93ed-e1c967d8abe2', prodSaeAppId: '2c9ee19a-5062-4205-9f5b-6ae3f8062a23' },
50
- 'gateway-core': { pipelineId: 5118525, repo: 'optima-gateway', saeAppId: '9326b7ff-da52-48d6-86db-9c4a884be108', prodSaeAppId: 'a08ce23f-3d3e-4d89-a2cf-53c8adba614e' },
51
- 'gw-admin': { pipelineId: 5118526, repo: 'optima-gateway', saeAppId: '90e0daf7-7910-46b8-b5bf-a0b8bcc60859', prodSaeAppId: 'c6bc5a78-b27f-46e2-825a-eaa338c23645' },
52
- 'kb-backend': { pipelineId: 5118527, repo: 'kb-skills', saeAppId: 'c7f65160-9d9e-416e-9e36-5439010d2b2d', prodSaeAppId: '732dfc8c-ed78-43f7-a722-c614359ff1a1' },
53
- 'ops-portal': { pipelineId: 5118529, repo: 'optima-portals', saeAppId: '41d0ee66-8402-4e72-bfda-8eba75d1270c', prodSaeAppId: '69e513fd-29bf-4355-a275-5eba82b21136' },
54
- 'optima-generation': { pipelineId: 5118530, repo: 'optima-gen', saeAppId: '327856d5-8b18-4e15-bfd8-b8bd3b807ffa', prodSaeAppId: '868d1a69-12c9-4bb7-868b-3e89c26784b5' },
55
- 'optima-generation-worker': { pipelineId: 5118531, repo: 'optima-gen', saeAppId: 'ad75b3b0-9ff4-443d-b0ef-81886bc7aa60', prodSaeAppId: '2adc941e-374b-406a-9863-e53d40bc9500' },
56
- 'optima-scout': { pipelineId: 5117336, repo: 'optima-scout', saeAppId: 'bac2c3a4-90ce-49a6-81d5-caa38cb5c807', prodSaeAppId: 'f5bb7e82-e57c-4bd4-83ae-2c25f8d38647' },
57
- 'optima-sentinel': { pipelineId: 5118532, repo: 'optima-sentinel', saeAppId: '3759db7b-4640-4a04-8436-4f241f0ec9d9', prodSaeAppId: 'e120e70c-b2c1-49ef-a66c-9016fd2d807d' },
58
- 'optima-sentinel-worker': { pipelineId: 5118533, repo: 'optima-sentinel', saeAppId: '7c975caf-ebfb-4bb3-91c4-2a8c83d1d2e5', prodSaeAppId: 'e0f9b92c-00af-4ade-9b07-15bba48c14cc' },
59
- 'optima-skills': { pipelineId: 5118534, repo: 'optima-skills', saeAppId: '90457be3-5eb3-4efc-a362-1788dcc51921', prodSaeAppId: '55a63ee7-fb75-46e4-99f3-20854a699237' },
60
- 'user-auth': { pipelineId: 5118510, repo: 'user-auth', saeAppId: '36efc3a6-5b42-49fa-8db2-0865aa0c25d2', prodSaeAppId: 'd6fbf9de-fed6-4165-978f-7b3a0a456acc' },
61
- 'user-auth-admin': { pipelineId: 5118535, repo: 'user-auth', saeAppId: '7feb9cc3-4731-4ea8-964c-4870a9e63afb', prodSaeAppId: '54093f99-37bf-4354-abd7-f6e76abdbcb6' },
62
- 'yzsgo-api': { pipelineId: 5118536, repo: 'yzsgo', saeAppId: 'd0af7f66-ef66-4587-9e93-250d01ee3cf6', prodSaeAppId: '5a820992-e11b-404a-9e58-8f607d8e0c5f' },
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
- let pipelineId = svc.pipelineId;
135
- if (envName === 'prod') {
136
- const lp = devops('ListPipelines', { maxResults: '100' });
137
- const hit = (lp.pipelines || []).find((p: any) => p.pipelineName === `${svcName}-cn-prod`);
138
- if (!hit) { console.error(`✗ 云效无 ${svcName}-cn-prod 流水线`); process.exit(1); }
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(' 构建完成后会停在『发布审批』人工卡点 —— 需 xbfool/svenyang 在云效控制台通过,之后迁移+digest 部署自动走完');
162
+ console.log('🔴 cn-prod:流水线**没有**人工卡点 —— 构建→迁移→digest 部署会一路自动走完,');
163
+ console.log(' 不要去云效控制台等『发布审批』(那一步不存在)。要中止只能在上面的链接里手动取消 run。');
157
164
  }
158
165
  if (noWait) return;
159
166
 
@@ -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
- /** First free local port, preferring the registry's previous assignment. */
433
- function pickTunnelPort(preferred?: number): number {
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
- if (isTunnelHealthy(known)) return known; // warm reuse — no cold start
459
- console.log(`! Tunnel on port ${known} not responding (zombie), replacing...`);
460
- killOrphanTunnel(known);
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
- const port = pickTunnelPort(known);
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') {