@optima-chat/dev-skills 0.10.0 → 0.12.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.
@@ -1,14 +1,24 @@
1
- # /trace-user - 用户链路追踪
2
-
3
- 查看指定用户在 agentic-chat -> optima-gateway -> agent-runtime 全链路的日志,分析耗时、成功/失败、返回值等。
4
-
5
- **版本**: v0.1.0
6
-
7
- ## 使用场景
8
-
9
- **前端开发者**: 用户反馈 "聊天没反应",快速定位是前端、网关还是 Agent 的问题
10
- **后端开发者**: 追踪特定用户的请求链路,分析 LLM 调用耗时和工具执行
11
- **DevOps**: 排查线上用户问题,查看完整的跨服务日志关联
1
+ # /trace-user - 用户链路追踪(一条龙)
2
+
3
+ 按账号把一个用户在 agentic-chat gateway-core agent-runtime 全链路的行为**从结构化日志**拼成一条时间线:每次 LLM 调用(模型 / token / 延迟 / 缓存命中 / stop reason)、每个工具执行、子代理 spawn、报错,按 `userId → session → conversation → turn` 聚合。
4
+
5
+ **版本**: v0.4.0
6
+
7
+ > **v0.4 新增(最高频需求)**:**错误视图**——①给一个用户查他出过哪些错;②一段时间内每个用户出了哪些错(面上视图)。`--grep` 服务端过滤全窗 error/warn + benign 噪声降噪 + 按 userId 聚合。并补**日志↔message 缝合**(§4 B2,conversationId 这根线把"内部事件"和"对话内容"串成一条龙)。cn-prod 实测挖出 `LLM stream interrupted` 命中多用户。
8
+ > **v0.3 新增**:**反向追踪**(一段内容 → 是谁 → 手机号,§6),cn-prod 真实账号双向实测通过。
9
+ >
10
+ > **v0.2 做了什么**(相对 v0.1):
11
+ > - **数据源 = 结构化日志**(不是 OTel trace)。一条龙所需的全部字段(model / inputTokens / outputTokens / cacheHitRate / stopReason / toolCount / durationMs / 子代理 spawn / 报错)本就逐条落在 agent-runtime 的结构化日志里,并可靠流入日志后端;按关联键拼装即可。
12
+ > - **新增 cn-stage / cn-prod 分支**(阿里云 SLS,`optima-logs` 直连),与 AWS(CloudWatch)并列。
13
+ > - **关联键标准化**:`userId`(=enduser) → `sessionId`(`ses_*`) → `conversationId` → `turnId`。
14
+ >
15
+ > **双向**:
16
+ > - **正向**(手机/userId → 一条龙时间线)—— §1–§5,结构化日志 + messages 表。
17
+ > - **反向**(一段内容 → 是谁 → 手机号)—— §6,`messages` 表 `content ILIKE` 检索 → user_id → user-auth 解手机号。**已实测可用**(cn-prod 真实账号双向验证)。
18
+ >
19
+ > **不在本工具范围(已知、刻意不做)**:
20
+ > - OTel **原生跨服务 trace 树**(browser→gateway→agent 一个 traceId 的可视化)—— 那条线在 cn 仍有未通的缺陷(业务 span 采样/context 抑制 + SLS ingest 限流),单独跟踪(gateway#1219),不阻塞本工具。本工具用日志/DB 关联键做"逻辑一条龙",覆盖同样的信息维度。
21
+ > - **可扩展的中文全文索引**(zhparser/分词,给高 QPS 内容搜索用)—— cn RDS 装不了扩展,单独排(#1201)。一次性反查用 `ILIKE` 已够,不依赖它。
12
22
 
13
23
  ## 用法
14
24
 
@@ -19,275 +29,327 @@
19
29
  ## 参数
20
30
 
21
31
  - `user-id-or-email` (必需): 用户标识
22
- - 包含 `@` → email(用 `userEmail` 字段过滤)
23
- - UUID 格式 → userId(用 `userId` 字段过滤)
24
- - `environment` (可选): `stage` `prod`,默认 `stage`
25
- - `time-range` (可选): `10m`, `30m`, `1h`, `2h`, `1d`,默认 `30m`
32
+ - 包含 `@` → email(AWS `userEmail` 字段;cn 多数日志只带 `userId`,email 需先换 userId)
33
+ - UUID 格式 → `userId`(= span 里的 enduser.id)
34
+ - `environment` (可选): `stage` | `prod` | `cn-stage` | `cn-prod`,默认 `stage`
35
+ - `time-range` (可选): `10m` `30m` `1h` `2h` `1d`,默认 `30m`
26
36
  - 选项:
27
37
  - `--errors` — 只看 error/warn 级别
28
- - `--session SESSION_ID` — 限定某个 session
29
- - `--trace TRACE_ID` — 限定某个 traceId
30
- - `--raw` — 显示原始 JSON
38
+ - `--session SESSION_ID` — 限定某个 session(`ses_*`)
39
+ - `--turn TURN_ID` — 限定某个 turn
40
+ - `--raw` — 显示原始 JSON 日志行
41
+ - `--summary` — 只输出统计聚合(不逐条打时间线)
31
42
 
32
43
  ## 示例
33
44
 
34
45
  ```bash
35
- /trace-user alice@example.com # Stage,最近 30 分钟
36
- /trace-user 37c03a9f-0b47-409c-81a3-5634eaab1a6c prod 2h # Prod,最近 2 小时
37
- /trace-user alice@example.com --errors # 只看错误
38
- /trace-user alice@example.com --session sess-xxx # 指定 session
46
+ /trace-user 9781cf19-3d9a-451f-af5b-0a4f8b3397d9 cn-stage 1h # cn-stage,最近 1 小时
47
+ /trace-user alice@example.com prod 2h # AWS prod
48
+ /trace-user 9781cf19-... cn-prod --session ses_azwDo9estPhg17Axb8q7q
49
+ /trace-user 9781cf19-... cn-stage --errors # 只看错误
39
50
  ```
40
51
 
41
52
  如果用户输入 `/trace-user` 或 `/trace-user --help`,显示此帮助文档,不执行查询。
42
53
 
43
54
  ARGUMENTS: $ARGUMENTS
44
55
 
45
- ## Claude Code 执行步骤
56
+ ---
46
57
 
47
- ### 1. 解析参数
58
+ ## 核心概念:关联键 + 数据源
48
59
 
49
- `$ARGUMENTS` 中解析用户标识、环境、时间范围和选项。
60
+ **一条龙不依赖 OTel trace,靠日志里的关联键自然串起来:**
50
61
 
51
- **用户标识判断**:
52
- - 包含 `@` email → 使用 `userEmail` 字段
53
- - UUID 格式(`xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`)→ 使用 `userId` 字段
54
- - 其他 当作 userId 尝试
55
-
56
- ### 2. 日志组映射
62
+ ```
63
+ userId (账号 UUID, = enduser.id)
64
+ └─ sessionId ses_xxxxxxxxxxxx 一次 WS 会话 / 一个 agent 容器生命周期
65
+ └─ conversationId cmqxxxx / <sess>-conv 一段对话
66
+ └─ turnId turn-xxxx-xxxx 一个 agentic turn(= 一轮用户消息触发的
67
+ LLM↔tool 循环,通常含多次 LLM 调用)
68
+ ```
57
69
 
58
- 三层服务对应的 CloudWatch Log Group:
70
+ 每条结构化日志都带 `userId / sessionId / conversationId / turnId`,所以按这些键 group 即可重建任意层级的时间线。
59
71
 
60
- | 层 | 服务 | Stage 日志组 | Prod 日志组 |
61
- |----|------|-------------|------------|
62
- | L1 | agentic-chat | `/ecs/agentic-chat-stage` | `/ecs/agentic-chat-prod` |
63
- | L2 | gateway-core | `/ecs/gateway-core-stage` | `/ecs/gateway-core-prod` |
64
- | L3 | agent-runtime | `/ecs/gw-agent-runtime-stage` | `/ecs/gw-agent-runtime-prod` |
72
+ **各服务承载的信息(哪层查哪个 logstore):**
65
73
 
66
- **AWS 区域**: `ap-southeast-1`
74
+ | | 服务 (logstore/log group) | 这条龙里给什么 |
75
+ |----|--------------------------|---------------|
76
+ | L1 | agentic-chat | 前端/BFF(SSR)。聊天链路主要走浏览器 WS 直连 gateway,前端日志辅助看连接/报错 |
77
+ | L2 | gateway-core | WS 连接、鉴权(OIDC)、session 创建、消息转发、`traceId`、`duration_ms` |
78
+ | L3 | **agent-runtime** | **一条龙核心**:每次 LLM 调用(model/token/延迟/cache/stopReason)、工具执行、子代理 spawn、turn 生命周期 |
67
79
 
68
- ### 3. 搜索策略
80
+ > 经验:L3 (agent-runtime) 承载一条龙 90% 的价值。L2 补会话/连接/鉴权上下文。
69
81
 
70
- #### 第一步:在 gateway-core 找到用户的 session
82
+ ---
71
83
 
72
- ```bash
73
- # 按 email 搜索
74
- aws logs filter-log-events \
75
- --log-group-name /ecs/gateway-core-{ENV} \
76
- --filter-pattern '{ $.userEmail = "USER_EMAIL" }' \
77
- --start-time $(date -d 'TIME_RANGE ago' +%s)000 \
78
- --region ap-southeast-1 \
79
- --output json | head -100
84
+ ## 执行步骤
80
85
 
81
- # userId 搜索
82
- aws logs filter-log-events \
83
- --log-group-name /ecs/gateway-core-{ENV} \
84
- --filter-pattern '{ $.userId = "USER_ID" }' \
85
- --start-time $(date -d 'TIME_RANGE ago' +%s)000 \
86
- --region ap-southeast-1 \
87
- --output json | head -100
88
- ```
86
+ ### A. 选择执行方式(按 environment)
89
87
 
90
- 从结果中提取 `sessionId` `traceId`。
88
+ | environment | 部署 | 取日志方式 |
89
+ |---|---|---|
90
+ | `stage` / `prod` | AWS ECS Fargate | AWS CloudWatch Logs(第 1 节) |
91
+ | `cn-stage` / `cn-prod` | 阿里云 SAE / ECI | `optima-logs` 直连阿里云 SLS(第 2 节,**首选**) |
91
92
 
92
- #### 第二步:跨服务关联查询(Logs Insights
93
+ ### 1. AWS(environment = stage / prod
93
94
 
94
- 优先使用 CloudWatch Logs Insights 跨多个 log group 查询:
95
+ 日志组:`/ecs/<service>-<env>`(区域 `ap-southeast-1`,prod 必须带 `--region ap-southeast-1`)。
95
96
 
96
97
  ```bash
97
- # userId/email 跨服务查询
98
+ # 第一步:在 agent-runtime 找该用户的所有 turn(一条龙核心数据都在这)
99
+ aws logs filter-log-events \
100
+ --log-group-name /ecs/gw-agent-runtime-{ENV} \
101
+ --filter-pattern '{ $.userId = "USER_ID" }' \
102
+ --start-time $(date -d 'TIME_RANGE ago' +%s)000 \
103
+ --region ap-southeast-1 --output json | jq -r '.events[].message'
104
+
105
+ # 第二步:跨服务关联(Logs Insights,gateway-core + agent-runtime)
98
106
  aws logs start-query \
99
- --log-group-names \
100
- /ecs/gateway-core-{ENV} \
101
- /ecs/gw-agent-runtime-{ENV} \
102
- --start-time $(date -d 'TIME_RANGE ago' +%s) \
103
- --end-time $(date +%s) \
104
- --query-string '
105
- fields @timestamp, @logStream, @message
107
+ --log-group-names /ecs/gateway-core-{ENV} /ecs/gw-agent-runtime-{ENV} \
108
+ --start-time $(date -d 'TIME_RANGE ago' +%s) --end-time $(date +%s) \
109
+ --query-string 'fields @timestamp, @logStream, message, userId, sessionId, conversationId, turnId, model, inputTokens, outputTokens, elapsedMs, stopReason, duration_ms
106
110
  | filter userId = "USER_ID" or userEmail = "USER_EMAIL"
107
- | sort @timestamp asc
108
- | limit 500
109
- ' \
111
+ | sort @timestamp asc | limit 1000' \
110
112
  --region ap-southeast-1
111
-
112
- # 等几秒后获取结果
113
- aws logs get-query-results --query-id QUERY_ID --region ap-southeast-1
113
+ # 等几秒:aws logs get-query-results --query-id QUERY_ID --region ap-southeast-1
114
114
  ```
115
115
 
116
- 如果已知 sessionId:
116
+ 服务映射(AWS):`agentic-chat`→`/ecs/agentic-chat-<env>`,`gateway-core`→`/ecs/gateway-core-<env>`,`agent-runtime`→`/ecs/gw-agent-runtime-<env>`。
117
+
118
+ ### 2. cn-stage / cn-prod(阿里云 SLS,首选 `optima-logs`)
119
+
120
+ > SLS `GetLogs` 是公网控制面 API,本机 `aliyun-optima` profile 直连即可,支持历史检索+时间窗+关键词。
121
+ > SLS project:`optima-cn-stage-1911493506120573` / `optima-cn-prod-1911493506120573`;logstore == service 名;正文在 `content`。
117
122
 
118
123
  ```bash
119
- aws logs start-query \
120
- --log-group-names \
121
- /ecs/gateway-core-{ENV} \
122
- /ecs/gw-agent-runtime-{ENV} \
123
- --start-time $(date -d 'TIME_RANGE ago' +%s) \
124
- --end-time $(date +%s) \
125
- --query-string '
126
- fields @timestamp, @logStream, level, message, sessionId, traceId, duration_ms
127
- | filter sessionId = "SESSION_ID"
128
- | sort @timestamp asc
129
- | limit 500
130
- ' \
131
- --region ap-southeast-1
132
- ```
124
+ # 第一步(核心):agent-runtime —— 拉该用户全部 LLM/tool/turn 结构化日志
125
+ optima-logs agent-runtime --env {cn-stage|cn-prod} --since {TIME_RANGE} --grep "USER_ID" -n 500 --json
133
126
 
134
- #### 第三步:agentic-chat 日志(补充)
127
+ # 第二步:gateway-core —— 会话/连接/鉴权上下文
128
+ optima-logs gateway-core --env {cn-stage|cn-prod} --since {TIME_RANGE} --grep "USER_ID" -n 200 --json
135
129
 
136
- agentic-chat 前端日志可能不含 userId/sessionId 字段,用时间窗口 + 关键词辅助:
130
+ # 限定某个 session / turn(缩小范围)
131
+ optima-logs agent-runtime --env cn-stage --since 1h --grep "ses_xxxxx" -n 500 --json
132
+ optima-logs agent-runtime --env cn-stage --since 1h --grep "turn-xxxx-xxxx" -n 200 --json
137
133
 
138
- ```bash
139
- aws logs tail /ecs/agentic-chat-{ENV} --since {TIME_RANGE} \
140
- --region ap-southeast-1 | grep -i "USER_EMAIL_OR_ID"
134
+ # 只看错误
135
+ optima-logs agent-runtime --env cn-stage --since 1h --grep "USER_ID" -n 500 --json \
136
+ | jq '.[] | (.content|fromjson?) | select(.level=="error" or .level=="warn")'
137
+
138
+ # 底层(一般不用):aliyun sls GetLogs --project optima-cn-stage-1911493506120573 \
139
+ # --logstore agent-runtime --from $FROM --to $NOW --line 500 --query "USER_ID" \
140
+ # --region cn-beijing --profile aliyun-optima
141
141
  ```
142
142
 
143
- #### 第四步:只看错误(--errors)
143
+ > **解析 `--json` 输出(重要)**:`optima-logs ... --json` 返回**一个 JSON 数组**,每个元素是 SLS 记录(带 `__source__`/`eci_id`/`_image_name_` 等元字段),**结构化日志正文是 `.content` 字段里的 JSON 字符串**。所以解析模式固定是 `.[] | (.content|fromjson?)`,再 select 业务字段。元字段 `_image_name_` 还能顺带看是哪个镜像 digest 的容器在服务。
144
144
 
145
+ **一条龙拼装 + 统计的 canonical 配方(已实测可用):**
145
146
  ```bash
146
- aws logs start-query \
147
- --log-group-names \
148
- /ecs/gateway-core-{ENV} \
149
- /ecs/gw-agent-runtime-{ENV} \
150
- --start-time $(date -d 'TIME_RANGE ago' +%s) \
151
- --end-time $(date +%s) \
152
- --query-string '
153
- fields @timestamp, @logStream, level, message, sessionId, error
154
- | filter (userId = "USER_ID" or userEmail = "USER_EMAIL")
155
- and (level = "error" or level = "warn")
156
- | sort @timestamp asc
157
- | limit 200
158
- ' \
159
- --region ap-southeast-1
147
+ U=<USER_ID>
148
+ # 时间线:每次 LLM 调用一行(model / token / 延迟 / cache / stopReason),按 turn
149
+ optima-logs agent-runtime --env cn-stage --since 1h --grep "$U" -n 1200 --json \
150
+ | jq -r '[.[] | (.content|fromjson?)] | map(select(.message=="LLM stream completed"))
151
+ | sort_by(.timestamp) | .[]
152
+ | "\(.timestamp[11:19]) sess=\(.sessionId[-6:]) turn=\(.turnId[-8:]) \(.model) in\(.inputTokens)/out\(.outputTokens) \(.elapsedMs)ms cache\((.cacheHitRate*1000|floor)/10)% →\(.stopReason)"'
153
+
154
+ # 统计:调用次数 / token 合计 / 延迟 / stop 分布 / 子代理 spawn
155
+ optima-logs agent-runtime --env cn-stage --since 1h --grep "$U" -n 2000 --json \
156
+ | jq '[.[] | (.content|fromjson?)] as $all | ($all|map(select(.message=="LLM stream completed"))) as $llm
157
+ | { "LLM调用次数":($llm|length), "不同session":([$llm[].sessionId]|unique|length), "不同turn":([$llm[].turnId]|unique|length),
158
+ "输入token":($llm|map(.inputTokens)|add), "输出token":($llm|map(.outputTokens)|add),
159
+ "平均延迟ms":(($llm|map(.elapsedMs)|add)/($llm|length)|floor), "最慢ms":($llm|map(.elapsedMs)|max),
160
+ "慢调用>5s":($llm|map(select(.elapsedMs>5000))|length),
161
+ "stop分布":($llm|group_by(.stopReason)|map({(.[0].stopReason//"?"):length})|add),
162
+ "子代理spawn":($all|map(select(.event_key=="spawn.lifecycle" and .phase=="completed"))|length),
163
+ "模型":([$llm[].model]|unique) }'
160
164
  ```
161
165
 
162
- ### 4. 结构化日志字段参考
166
+ > ⚠️ cn 多数 agent-runtime 日志只带 `userId` 不带 `userEmail`。若输入 email,先去 user-auth 换 userId(见 query-db / account 技能),再用 userId 查。
167
+ > ⚠️ `--grep` 是 SLS 全文检索;userId/sessionId/turnId 都是高区分度 token,直接 grep 即可精确命中。
163
168
 
164
- optima-gateway 的日志通过 AsyncLocalStorage 自动注入:
169
+ ---
170
+
171
+ ## 3. 结构化日志字段参考(真实 schema)
172
+
173
+ **LLM 调用**(`service` = `openai-compat-provider` 或具体 provider,message = `LLM request sending` / `LLM response headers received` / `LLM first chunk received` / `LLM stream completed`):
165
174
 
166
175
  ```json
167
- {
168
- "timestamp": "2026-04-16T10:30:00.000Z",
169
- "level": "info",
170
- "service": "gateway-core",
171
- "userId": "37c03a9f-...",
172
- "userEmail": "alice@example.com",
173
- "sessionId": "sess-xxx",
174
- "traceId": "gw-abc123",
175
- "message": "Session created",
176
- "duration_ms": 123
177
- }
176
+ { "userId","sessionId","conversationId","turnId",
177
+ "model":"deepseek-v4-pro", "baseUrl":"https://api.deepseek.com/v1",
178
+ "messageCount":320, "toolCount":14, "status":200,
179
+ "ttfbMs":408, "firstChunkMs":8, "chunksReceived":384, "elapsedMs":6753,
180
+ "inputTokens":653, "outputTokens":422, "cacheReadTokens":105344, "cacheHitRate":0.994,
181
+ "stopReason":"tool_use" }
178
182
  ```
183
+ > 一个 `turnId` 下通常有**多次** `LLM stream completed`(agentic 循环:LLM→tool_use→LLM→…)。把它们按时间排好就是这个 turn 的"思考过程"。
179
184
 
180
- **关键字段**:
181
- - `userId` / `userEmail` — 用户标识
182
- - `sessionId` 会话 ID
183
- - `traceId` 跨服务链路 ID
184
- - `duration_ms` — 操作耗时
185
- - `level` — 日志级别(info/warn/error)
185
+ **子代理 spawn**(`service` = `agent-runtime:spawn`,`event_key` = `spawn.lifecycle`):
186
+ ```json
187
+ { "message":"spawn completed", "agentId":"subagent-3-9cni", "agentType":"general-purpose",
188
+ "phase":"completed", "durationMs":4883, "tokensIn":338, "tokensOut":398,
189
+ "success":true, "runInBackground":false }
190
+ ```
191
+
192
+ **turn / session 生命周期**(`service` = `agent-runtime:optima-runtime`):`sendMessage handleMessage done` / `Destroying Optima session`。
193
+
194
+ **工具执行**:grep `tool` / `execute` / tool 名;tool 相关行带同样的 `turnId` 关联键。
195
+
196
+ **gateway-core**:`userId / userEmail / sessionId / traceId / duration_ms / message`(WS 连接、OIDC、session 状态)。
186
197
 
187
- ### 5. 输出格式
198
+ **关键关联字段**:`userId`(账号) · `sessionId`(`ses_*`) · `conversationId` · `turnId` · `model` · `inputTokens`/`outputTokens` · `elapsedMs`/`ttfbMs` · `stopReason` · `duration_ms` · `level`。
188
199
 
189
- #### A. 概览信息
200
+ ---
190
201
 
202
+ ## 4. 输出格式(一条龙)
203
+
204
+ ### A. 概览
191
205
  ```
192
- 用户: alice@example.com (37c03a9f-...)
193
- 环境: Stage
194
- 时间范围: 最近 30 分钟
195
- 找到 session 数: 3
206
+ 用户: 9781cf19-3d9a-451f-af5b-0a4f8b3397d9 环境: cn-stage 时间范围: 最近 1h
207
+ 找到 session: 2 | conversation: 3 | turn: 14 | LLM 调用: 51 | 工具: 23 | 错误: 1
196
208
  ```
197
209
 
198
- #### B. Session 列表
199
-
200
- | Session ID | 创建时间 | 状态 | 持续时间 | 消息数 | 错误数 |
201
- |-----------|---------|------|---------|-------|-------|
202
- | sess-001 | 10:30:00 | running | 5m | 12 | 0 |
203
- | sess-002 | 10:20:00 | terminated | 8m | 24 | 1 |
210
+ ### B. 一条龙时间线(按 session → conversation → turn)
211
+ ```
212
+ session ses_azwDo9estPhg17Axb8q7q (conv cmqrg6dua…)
213
+ └ turn …o0b78la3 06:34:33 → 06:34:50 (17s, 5 LLM calls, 4 tools)
214
+ 06:34:33 LLM deepseek-v4-pro in195/out125 1915ms cache99.8% →tool_use
215
+ 06:34:35 LLM deepseek-v4-pro in653/out422 6753ms cache99.4% →tool_use (14 tools avail)
216
+ ├ 06:34:42 LLM deepseek-v4-pro in116/out78 1727ms cache99.9% →tool_use
217
+ ├ 06:34:44 LLM deepseek-v4-pro in126/out92 1930ms cache99.9% →tool_use
218
+ └ 06:34:47 LLM deepseek-v4-pro in150/out147 2257ms cache99.9% →stop
219
+ ```
204
220
 
205
- #### C. 请求链路时间线
221
+ ### B2. 缝合 message + 日志(同一根线 conversationId/turnId)
206
222
 
223
+ message 给「说了什么 / 哪个模型 / 多少 token」,日志给「内部调了什么、缓存/历史恢复、工具、耗时、报错」。用 `conversationId` 把两边缝成一条龙:
224
+ ```bash
225
+ # 1) 从 messages 拿这个用户最近的 conversation_id
226
+ CONV=$(optima-query-db gateway-core "SELECT conversation_id FROM messages WHERE user_id='$U' ORDER BY created_at DESC LIMIT 1" cn-prod | grep -oE 'cm[a-z0-9]{20,}' | head -1)
227
+ # 2) 用 conversationId grep 日志 → 这段对话的全部内部事件(建session/缓存/历史恢复/LLM/工具/错/销毁)
228
+ optima-logs agent-runtime --env cn-prod --since 24h --grep "$CONV" -n 100 --json \
229
+ | jq -r '[.[]|(.content|fromjson?)] | sort_by(.timestamp) | .[]
230
+ | "\(.timestamp[11:19]) [\(.service)] \(.message)\(if .model then " ("+.model+" in"+(.inputTokens|tostring)+"/out"+(.outputTokens|tostring)+" "+(.elapsedMs|tostring)+"ms)" else "" end)"'
231
+ # 3) 同一个 conversation 的 message 内容
232
+ optima-query-db gateway-core "SELECT to_char(created_at,'HH24:MI:SS') t, role, left(content,60) c FROM messages WHERE conversation_id='$CONV' ORDER BY created_at" cn-prod
233
+ ```
234
+ 缝出来的样子(内部事件 + 内容并排):
207
235
  ```
208
- Session: sess-001
209
- TraceId: gw-abc123
210
-
211
- 时间线:
212
- ┌──────────────────────────────────────────────────────────────────────┐
213
- │ 10:30:00.000 [gateway-core] WS connected, authenticating... │
214
- │ 10:30:00.150 [gateway-core] OIDC verified, userId=37c03a9f │ +150ms
215
- │ 10:30:00.200 [gateway-core] Session creating │ +50ms
216
- │ 10:30:00.800 [gateway-core] Agent task started (ECS RunTask) │ +600ms
217
- │ 10:30:03.200 [agent-runtime] Container ready, WS connected │ +2400ms
218
- │ 10:30:03.250 [gateway-core] Session running │ +50ms
219
- │ 10:30:05.000 [gateway-core] Client message received │
220
- │ 10:30:05.100 [agent-runtime] LLM request started (openai) │ +100ms
221
- │ 10:30:08.500 [agent-runtime] LLM response complete │ +3400ms
222
- │ 10:30:08.600 [agent-runtime] Tool call: read_file │
223
- │ 10:30:08.800 [agent-runtime] Tool result returned │ +200ms
224
- │ 10:30:09.000 [gateway-core] Message forwarded to client │
225
- └──────────────────────────────────────────────────────────────────────┘
236
+ 03:50:38 [ws-bridge] Creating agent session
237
+ 03:50:44 [conv-cache] Multi-source cache all-miss → 从 gateway 恢复历史
238
+ 03:50:44 user "hi"
239
+ 03:50:44 [openai-provider] LLM request sending (deepseek-v4-pro)
240
+ 03:50:50 [openai-provider] LLM stream completed (in17696/out244 5307ms)
241
+ 03:50:50 assistant "你好!我是 鸭嘴兽 AI…"
242
+ 04:11:28 [optima-runtime] Destroying Optima session
226
243
  ```
227
244
 
228
- #### D. 耗时分析
245
+ ### C. 耗时 / token 统计
246
+ ```
247
+ 本窗口合计: LLM 调用 51 次 | 平均延迟 2.6s | P95 6.8s | 慢调用(>5s) 4 次
248
+ token: 输入 12,840 输出 4,210 缓存命中率 99.3%
249
+ 工具: bash×9 read×6 web_search×3 …
250
+ 子代理: spawn 2 (general-purpose, 平均 4.9s, 全 success)
251
+ ```
229
252
 
253
+ ### D. 错误 / 告警(`--errors`)
230
254
  ```
231
- 关键耗时:
232
- OIDC 认证: 150ms
233
- Session 创建: 50ms
234
- ECS 容器启动: 2400ms (>2s 时标记 warning)
235
- WS 回连: 50ms
236
- 首次 LLM 调用: 3400ms
237
- 工具执行: 200ms
238
-
239
- 统计:
240
- 总消息数: 12 (user: 5, assistant: 7)
241
- LLM 调用次数: 7
242
- 平均 LLM 延迟: 2800ms
243
- 工具调用次数: 3
255
+ 06:41:12 [openai-compat-provider] WARN LLM request retry turnId=turn-… reason=429
256
+ CircuitBreaker half-open,已重试
244
257
  ```
245
258
 
246
- #### E. 错误/告警
259
+ ---
260
+
261
+ ## 5. 常用诊断场景
262
+
263
+ | 用户报告 | 策略 |
264
+ |---------|------|
265
+ | "没反应/卡住" | agent-runtime 查该 turn 是否有 `LLM request sending` 却无 `stream completed`(卡在 LLM);或有 `tool_use` 却无后续工具完成 |
266
+ | "回复慢" | 看 `elapsedMs`/`ttfbMs`/`firstChunkMs`;`slow:true` 标记;P95 |
267
+ | "出错" | `--errors`,看 LLM `status!=200` / retry / CircuitBreaker |
268
+ | "连不上" | gateway-core 的 WS 连接 + OIDC 日志 |
269
+ | "token 烧太快/账单" | 按 turn 汇总 `inputTokens`/`outputTokens`,看 `cacheHitRate` 是否异常低 |
270
+ | "子代理炸了" | grep `spawn.lifecycle`,看 `success:false` / `durationMs` |
271
+
272
+ ---
247
273
 
274
+ ## ⭐ 错误视图(最高频需求)
275
+
276
+ > **核心技术**:`optima-logs --grep <kw>` 是 **SLS 服务端过滤**,覆盖整个 `--since` 窗口(不受"无过滤时只回最近 ~100 条"的截断)。日志正文是 `"level":"error"` 这样的 JSON,所以 `--grep error` / `--grep warn` 正好命中 level 字段 → **全窗 error/warn 全抓得到**。
277
+ > **降噪**:以下属基础设施噪声,默认过滤掉(除非排查它们):`CLI binary name collision`、`Multi-source cache all-miss`、`Runtime other than "optima" is deprecated`、`cache.load miss: ENOENT`。
278
+
279
+ ### 查询 A:给一个用户 → 他出过哪些错
280
+ ```bash
281
+ U=<userId> # 手机先换 userId(见 §6)
282
+ optima-logs agent-runtime --env cn-prod --since 24h --grep "$U" -n 300 --json \
283
+ | jq -r '[.[]|(.content|fromjson?)]
284
+ | map(select((.level=="error" or .level=="warn" or (.status//200)>=400 or .success==false)
285
+ and ((.message|test("CLI binary name collision|Multi-source cache all-miss|deprecated|cache.load miss"))|not)))
286
+ | sort_by(.timestamp) | .[]
287
+ | "\(.timestamp[11:19]) [\(.level//"-")] \(.service) \(.message)\(if .error then " err="+(.error|tostring) else "" end)\(if .turnId then " turn="+(.turnId[-8:]) else "" end)"'
248
288
  ```
249
- 发现 1 个错误:
250
- 10:35:12 [agent-runtime] ERROR: LLM request failed
251
- provider: openai
252
- error: "rate_limit_exceeded"
253
- sessionId: sess-002
254
- 已自动重试(CircuitBreaker half-open)
289
+
290
+ ### 查询 B:一段时间 每个用户出了哪些错(面上视图)
291
+ ```bash
292
+ # grep "error"/"warn" 服务端抓全窗 error/warn;+ 语义错关键词;按 userId 聚合
293
+ for kw in error warn fail timeout 429 500 ECONN abort; do
294
+ optima-logs agent-runtime --env cn-prod --since 24h --grep "$kw" -n 300 --json 2>/dev/null \
295
+ | jq -c '[.[]|(.content|fromjson?)] | .[]
296
+ | select((.level=="error" or .level=="warn" or (.status//200)>=400 or .success==false)
297
+ and ((.message|test("CLI binary name collision|Multi-source cache all-miss|deprecated|cache.load miss"))|not))
298
+ | {uid:((.userId//"-")[0:8]), lvl:(.level//"-"), msg:.message}'
299
+ done | jq -s 'unique | group_by(.uid)
300
+ | map({user:.[0].uid, n:length, detail:(group_by(.msg)|map("\(.[0].lvl):\(.[0].msg) ×\(length)"))})
301
+ | sort_by(-.n) | .[] | "用户 \(.user)… \(.n)类:\n " + (.detail|join("\n "))' -r
255
302
  ```
303
+ > 重点看 `error:` 级(如 `LLM stream interrupted` / `LLM fetch error`)——若同一个错命中**多个用户**,多半是面上问题不是个例。多服务都要查时,把 `agent-runtime` 换成 `gateway-core` 等再跑一遍合并。
304
+ > AWS(stage/prod)同需求用 CloudWatch Logs Insights 原生聚合:`filter level in ["error","warn"] | stats count() by userId, message`,更省事。
305
+
306
+ ### 串到具体一条对话(错 + 上下文)
307
+ 拿到出错的 `turnId`/`conversationId`,用它 grep 把那一刻**前后的内部事件 + 这条对话的 message**都拉出来(见 §4 缝合),就知道错发生在"调哪个工具 / 哪次 LLM / 说了什么"的什么位置。
256
308
 
257
- ### 6. 常用诊断场景
309
+ ---
258
310
 
259
- | 用户报告 | 推荐策略 |
260
- |---------|---------|
261
- | "连不上" / "打不开" | 查 gateway-core 连接日志 + OIDC 认证 |
262
- | "没反应" / "卡住了" | 查 agent-runtime 是否收到消息 + LLM 是否响应 |
263
- | "回复慢" | 查 duration_ms 字段,分析 ECS 启动 + LLM 延迟 |
264
- | "出错了" / "报错" | `--errors` 模式,重点看 error 级别 |
265
- | "用了一半断了" | 查 WS 断连事件 + session 状态变化 |
266
- | "总是失败" | 查 CircuitBreaker 状态 + provider 切换日志 |
311
+ ## 6. 反向追踪:一段内容 是谁 → 手机号(content → person)
267
312
 
268
- ### 7. 快捷跟进命令
313
+ 与正向(手机→一条龙)对称的另一方向:拿到一段聊天内容/截图文字,反查是哪个用户、解出手机号。**对话正文存在 gateway-core 的 `messages` 表(Postgres),直接 `content ILIKE` 检索即可——不需要中文分词**(分词只为大规模高效索引,一次性查找用不上)。
269
314
 
315
+ **前置:cn DB 访问凭证(query-db 走 buildbox 隧道)**
270
316
  ```bash
271
- # 查看服务健康状态
272
- curl -s https://gw.stage.optima.onl/health | jq .
273
- curl -s https://ai.stage.optima.onl/api/health | jq .
274
-
275
- # 查看服务 debug 信息
276
- curl -s -H "X-Debug-Key: 7eede5747b6c50f1c8f2358b98462f74696cdef9bfeab85eaf7ea41166788b5c" \
277
- https://gw.stage.optima.onl/debug/info | jq .
278
-
279
- # 查看 ECS 服务状态
280
- aws ecs describe-services --cluster optima-stage-cluster \
281
- --services gateway-core-stage --region ap-southeast-1 \
282
- --query 'services[0].{status:status,running:runningCount,desired:desiredCount}'
317
+ # cn Infisical admin(本地缓存)
318
+ CREDS=~/.config/optima/cn-infisical-creds.json
319
+ export INFISICAL_CN_EMAIL="$(python3 -c "import json;print(json.load(open('$CREDS'))['email'])")"
320
+ export INFISICAL_CN_PASSWORD="$(python3 -c "import json;print(json.load(open('$CREDS'))['password'])")"
321
+ # buildbox root 密码(1P "Aliyun cn-prod buildbox ECS (root)")
322
+ OP='/mnt/c/Users/xbfoo/AppData/Local/Microsoft/WinGet/Links/op.exe'
323
+ export OPTIMA_CN_BUILDBOX_PASSWORD="$("$OP" item get gdmixcizjci5bvuu3nuvgg4ooe --reveal --field password)"
283
324
  ```
284
325
 
326
+ **步骤 1:内容 → user_id(messages 表,role='user' 只看用户发言)**
327
+ ```bash
328
+ optima-query-db gateway-core "SELECT user_id, conversation_id, to_char(created_at,'MM-DD HH24:MI') t, left(content,80) c
329
+ FROM messages WHERE role='user' AND content ILIKE '%你那句话%' ORDER BY created_at DESC LIMIT 20" cn-prod
330
+ ```
331
+ - 命中唯一 user_id → 直接定位到人。
332
+ - 命中多个(如 `hi` 这种 194 人都说过的高频词)→ 不唯一;用「说得最多」排名缩小:
333
+ `... GROUP BY user_id ORDER BY count(*) DESC`,或要求更有特征的片段。
334
+
335
+ **步骤 2:user_id → 手机号(user-auth 表)**
336
+ ```bash
337
+ optima-query-db user-auth "SELECT id, phone, email, created_at FROM users WHERE id='<USER_ID>'" cn-prod
338
+ ```
339
+
340
+ **反过来 手机→userId(正向第一跳)也在这张表**:`WHERE phone='<手机号>'`。
341
+
342
+ > `messages` 表关键列:`id / conversation_id / user_id / role / content / content_parts(jsonb) / tool_calls(jsonb) / model / input_tokens / output_tokens / created_at`。
343
+ > ⚠️ 反查到人后只用于运营/排障,遵守数据合规;正文含用户隐私,别外泄。
344
+
345
+ ---
346
+
285
347
  ## 注意事项
286
348
 
287
- 1. **CloudWatch 延迟**: 最新日志可能需要等 10-30 秒才出现
288
- 2. **速率限制**: `filter-log-events` 有限流,大范围查询优先用 `start-query` (Logs Insights)
289
- 3. **日志保留**: CloudWatch 保留 14
290
- 4. **结构化日志**: JSON 日志输出到 stderr,在 CloudWatch 中显示为普通文本行
291
- 5. **Insights 限制**: 最多返回 10000 条,一般够用
292
- 6. **权限要求**: 需要 AWS CLI 配置了正确的凭证(`ap-southeast-1` 区域)
293
- 7. 如果用户没有任何日志,可能是该用户在指定时间范围内没有活动、标识输入有误、或服务未部署
349
+ 1. **数据源是结构化日志**,不是 OTel trace。一条龙的准确性取决于这些字段,已验证 cn-stage/cn-prod + AWS 均稳定输出。
350
+ 2. **cn 日志走 stdout→SLS**,不受 OTel traces logstore 的 ingest 限流影响。
351
+ 3. **关联键优先 userId/sessionId/turnId**(高区分度,全文 grep 精确命中)。email 在 cn 需先换 userId。
352
+ 4. **日志延迟**:CloudWatch 1-2s;SLS 数秒。最新几秒的可能要等。
353
+ 5. **保留期**:CloudWatch 7-14d;cn SLS logstore 按配置(agent-runtime stdout 通常数天起)。
354
+ 6. 如果查不到:时间窗内无活动 / 标识写错 / 用了 email 但 cn 日志只认 userId / 该容器已回收(warm 池轮换后旧容器日志仍在 SLS,按时间窗能查到)。
355
+ 7. **OTel 原生 trace 树**是独立增强项(见顶部说明),不在本工具;需要 span 级火焰图时另走 trace 后端,且 cn 侧待修复后才有业务 span。
@@ -0,0 +1,39 @@
1
+ ---
2
+ name: cn-deploy
3
+ description: 当用户请求把服务发布/部署/构建到 cn-stage、跑云效流水线、cn-stage 发版、部署某个分支到 cn-stage、触发 cn 构建、yunxiao 部署、"把 xx 发到 cn"时,使用此技能。覆盖 cn-stage 全部 20 个服务(构建→DB迁移→SAE 发布一条龙)。
4
+ ---
5
+
6
+ # cn-deploy - 云效 cn-stage 流水线发布
7
+
8
+ ## 用途
9
+
10
+ 把服务的 GitHub 代码经云效 Flow 流水线发布到阿里云 cn-stage(SAE)。一条命令完成:
11
+ Codeup mirror 同步 → 构建(含 NEXT_PUBLIC 自动烤/境内镜像源改写)→ DB 迁移 → SAE 滚动发布 → 部署 sha 校验。
12
+
13
+ ## 命令
14
+
15
+ ```bash
16
+ optima-cn-deploy <service> # main 全链发布
17
+ optima-cn-deploy <service> --branch feat/xxx # 非 main 分支构建+部署
18
+ optima-cn-deploy <service> --no-wait # 只触发不等待
19
+ optima-cn-deploy --list # 列出全部可发服务
20
+ ```
21
+
22
+ ## 使用要点
23
+
24
+ 1. **先 `--list` 确认服务名**(20 个服务,含 commerce-backend / optima-scout)。
25
+ 2. 全链约 3-8 分钟(agentic-chat 最重)。触发后会输出云效 run 链接,可发给用户自行围观。
26
+ 3. **凭证零配置**:构建凭证由云效变量组 `cn-stage-build-credentials`(41970) 供给,不需要注入任何 secret。
27
+ 4. 分支构建会把该分支代码部署到 cn-stage(替换现运行版本)——用户明确要求部署分支时才用。
28
+ 5. 失败排查:命令输出的 flow.aliyun.com 链接里看具体 step 日志;常见原因是该分支/仓的 Codeup mirror 凭证问题(命令会在 mirror 步就报错并中止,不会构建旧代码)。
29
+ 6. 成功标准:最后一行 `✓ SAE ImageUrl tag=<sha>` —— 流水线绿且 SAE 真的钉到了目标 commit。只看流水线绿不算完成。
30
+
31
+ ## 前置依赖
32
+
33
+ - `aliyun` CLI 已配置(profile 默认 `aliyun-optima`,可用环境变量 `OPTIMA_ALIYUN_PROFILE` 覆盖为自己的 RAM 用户 profile)
34
+ - `gh` 已登录(mirror 同步需要 GitHub token)
35
+
36
+ ## 边界
37
+
38
+ - 只发 **cn-stage**,不发 cn-prod(cn-prod 发布另走人工卡点流程)。
39
+ - 服务注册表是 optima-terraform `alicloud/stacks/cn-prod-buildbox/yunxiao/` 的快照;新增服务先在那边 gen-pipelines 建好流水线,再同步本工具的 SERVICES 表。
@@ -0,0 +1,39 @@
1
+ ---
2
+ name: cn-deploy
3
+ description: 当用户请求把服务发布/部署/构建到 cn-stage、跑云效流水线、cn-stage 发版、部署某个分支到 cn-stage、触发 cn 构建、yunxiao 部署、"把 xx 发到 cn"时,使用此技能。覆盖 cn-stage 全部 20 个服务(构建→DB迁移→SAE 发布一条龙)。
4
+ ---
5
+
6
+ # cn-deploy - 云效 cn-stage 流水线发布
7
+
8
+ ## 用途
9
+
10
+ 把服务的 GitHub 代码经云效 Flow 流水线发布到阿里云 cn-stage(SAE)。一条命令完成:
11
+ Codeup mirror 同步 → 构建(含 NEXT_PUBLIC 自动烤/境内镜像源改写)→ DB 迁移 → SAE 滚动发布 → 部署 sha 校验。
12
+
13
+ ## 命令
14
+
15
+ ```bash
16
+ optima-cn-deploy <service> # main 全链发布
17
+ optima-cn-deploy <service> --branch feat/xxx # 非 main 分支构建+部署
18
+ optima-cn-deploy <service> --no-wait # 只触发不等待
19
+ optima-cn-deploy --list # 列出全部可发服务
20
+ ```
21
+
22
+ ## 使用要点
23
+
24
+ 1. **先 `--list` 确认服务名**(20 个服务,含 commerce-backend / optima-scout)。
25
+ 2. 全链约 3-8 分钟(agentic-chat 最重)。触发后会输出云效 run 链接,可发给用户自行围观。
26
+ 3. **凭证零配置**:构建凭证由云效变量组 `cn-stage-build-credentials`(41970) 供给,不需要注入任何 secret。
27
+ 4. 分支构建会把该分支代码部署到 cn-stage(替换现运行版本)——用户明确要求部署分支时才用。
28
+ 5. 失败排查:命令输出的 flow.aliyun.com 链接里看具体 step 日志;常见原因是该分支/仓的 Codeup mirror 凭证问题(命令会在 mirror 步就报错并中止,不会构建旧代码)。
29
+ 6. 成功标准:最后一行 `✓ SAE ImageUrl tag=<sha>` —— 流水线绿且 SAE 真的钉到了目标 commit。只看流水线绿不算完成。
30
+
31
+ ## 前置依赖
32
+
33
+ - `aliyun` CLI 已配置(profile 默认 `aliyun-optima`,可用环境变量 `OPTIMA_ALIYUN_PROFILE` 覆盖为自己的 RAM 用户 profile)
34
+ - `gh` 已登录(mirror 同步需要 GitHub token)
35
+
36
+ ## 边界
37
+
38
+ - 只发 **cn-stage**,不发 cn-prod(cn-prod 发布另走人工卡点流程)。
39
+ - 服务注册表是 optima-terraform `alicloud/stacks/cn-prod-buildbox/yunxiao/` 的快照;新增服务先在那边 gen-pipelines 建好流水线,再同步本工具的 SERVICES 表。
package/AGENTS.md CHANGED
@@ -13,6 +13,7 @@ Prefer the installed CLI tools over reimplementing long shell workflows:
13
13
  - `optima-grant-credits <email|phone|userId> --credits <n> [options]`
14
14
  - `optima-product <create|update|add-channel|toggle-channel|show> [options]` — manage paid-plugin marketplace Products + Stripe channels (Wave 1.5 admin endpoints; stage default)
15
15
  - `optima-entitlement <grant|revoke|list> [options]` — admin-grant / revoke / list paid-plugin entitlements (refuses revoke of PAYMENT / PARTNER source)
16
+ - `optima-cn-deploy <service> [--branch feat/xxx] [--no-wait]` — 云效 Flow 发布到 cn-stage(mirror 同步→构建→DB 迁移→SAE 发布→sha 校验;20 服务,凭证由云效变量组供给零配置)
16
17
  - `optima-plugin <show|set-paid|set-default> [options]` — flip a plugin's skills-side paid/free state (isPaid) + defaultForUser (the user-facing gate; pairs with optima-product for the billing side)
17
18
 
18
19
  For code-reading tasks across Optima repositories, use `gh` commands against `Optima-Chat/<repo>`.
@@ -30,6 +31,7 @@ After `npm install -g @optima-chat/dev-skills`, this package installs skills und
30
31
  - `grant-credits`
31
32
  - `restart-ecs`
32
33
  - `use-commerce-cli`
34
+ - `cn-deploy`
33
35
 
34
36
  ## Tooling Assumptions
35
37
 
package/README.md CHANGED
@@ -88,6 +88,7 @@ Claude:
88
88
  | `/query-db` | 查询数据库 | `/query-db user-auth "SELECT COUNT(*) FROM users"` | ✅ |
89
89
  | `/generate-test-token` | 生成测试 token | `/generate-test-token` | 🔧 Development |
90
90
  | `/read-code` | 阅读代码 | `/read-code commerce-backend app/main.py` | - |
91
+ | `/cn-deploy` | 发布服务到 cn-stage(云效流水线一条龙) | 「把 billing 发到 cn-stage」 | cn-stage |
91
92
 
92
93
  **说明**:
93
94
  - 命令支持 CI、Stage、Prod 三个环境
@@ -109,6 +110,7 @@ Claude:
109
110
  | `optima-grant-credits` | 赠送积分 | `optima-grant-credits user@example.com --credits 10000 --env prod`(`optima-grant-balance` 为废弃别名)|
110
111
  | `optima-entitlement` | 产品权益 grant/revoke/list | `optima-entitlement grant 18898654855 --product-key scout-gift --justification "..." --env cn-prod` |
111
112
  | `optima-account` | 账号 status/ban/unban | `optima-account ban user@example.com --reason "abuse" --env prod` |
113
+ | `optima-cn-deploy` | 云效 Flow 发布到 cn-stage(构建→DB迁移→SAE 发布→sha 校验,20 服务) | `optima-cn-deploy billing` / `optima-cn-deploy user-auth --branch feat/xxx` |
112
114
 
113
115
  > **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`)。
114
116
 
@@ -0,0 +1,148 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * optima-cn-deploy —— 云效 Flow cn-stage 流水线一条龙触发器(团队版)。
4
+ *
5
+ * 对应 optima-terraform alicloud/stacks/cn-prod-buildbox/yunxiao/ 的 cn-run.py,
6
+ * 去掉 buildbox SSH 依赖(凭证由云效变量组 41970 供给,2026-07-13 起控制台/API 裸跑均可)。
7
+ *
8
+ * 做四件事(每件都是踩过的坑):
9
+ * 1. 触发服务仓 Codeup mirror 同步并等目标分支追平 GitHub —— mirror 不新鲜=构建旧代码。
10
+ * GitHub token 用 `gh auth token`(需本机 gh 已登录)。
11
+ * 2. StartPipelineRun;--branch 时带 runningBranchs 覆盖服务仓 source 分支。
12
+ * 3. 轮询到终态,给出云效 run 链接。
13
+ * 4. SUCCESS 后校验 SAE ImageUrl tag == 目标分支 HEAD short-sha(防"流水线绿但没部上");
14
+ * aliyun API 偶发空返回,带重试守卫。
15
+ *
16
+ * 用法:
17
+ * optima-cn-deploy billing # main 全链(构建→迁移→SAE 发布)
18
+ * optima-cn-deploy user-auth --branch feat/xxx # 非 main 分支构建+部署到 cn-stage
19
+ * optima-cn-deploy gateway-core --no-wait # 只触发不等待
20
+ * optima-cn-deploy --list # 列出全部可发服务
21
+ *
22
+ * 前置: aliyun CLI(profile 默认 aliyun-optima,可用 OPTIMA_ALIYUN_PROFILE 覆盖)+ gh 已登录。
23
+ * 流水线定义的单一信源在 optima-terraform yunxiao/(gen-pipelines.py);本表为其快照,
24
+ * 新增服务后同步(pipelineId 稳定,不常变)。
25
+ */
26
+ import { execFileSync } from 'node:child_process';
27
+
28
+ const ORG = '6a17b6282bf8b1184fc0e0c6';
29
+ const ENDPOINT = 'devops.cn-hangzhou.aliyuncs.com';
30
+ const PROFILE = process.env.OPTIMA_ALIYUN_PROFILE || 'aliyun-optima';
31
+ const CODEUP_BASE = `https://codeup.aliyun.com/${ORG}`;
32
+
33
+ // 服务注册表(快照自 optima-terraform services.stage.env + gen-pipelines.py,2026-07-13)
34
+ interface Svc { pipelineId: number; repo: string; saeAppId?: string; }
35
+ const SERVICES: Record<string, Svc> = {
36
+ 'agent-portal': { pipelineId: 5118519, repo: 'optima-portals', saeAppId: 'fe757f78-d18d-4480-9402-fe59d4721055' },
37
+ 'agentic-chat': { pipelineId: 5118520, repo: 'agentic-chat', saeAppId: '6aea1ce1-f813-4e1c-8e97-d1ecb5398e37' },
38
+ 'billing': { pipelineId: 5118521, repo: 'optima-billing', saeAppId: '09d8e292-dc64-4af8-bce5-0a56cb666921' },
39
+ 'browser-backend': { pipelineId: 5118522, repo: 'optima-browser-use', saeAppId: '1fced3f6-a80a-41a4-8f23-e4d5a467f8eb' },
40
+ 'commerce-backend': { pipelineId: 5107005, repo: 'commerce-backend', saeAppId: '49d09808-508c-471a-9560-553c49a67f72' },
41
+ 'commerce-rq-scheduler': { pipelineId: 5118523, repo: 'commerce-backend', saeAppId: '1d2810ef-889e-4675-b6e6-a299e4722e68' },
42
+ 'commerce-rq-worker': { pipelineId: 5118524, repo: 'commerce-backend', saeAppId: 'd661da87-6d24-40b8-93ed-e1c967d8abe2' },
43
+ 'gateway-core': { pipelineId: 5118525, repo: 'optima-gateway', saeAppId: '9326b7ff-da52-48d6-86db-9c4a884be108' },
44
+ 'gw-admin': { pipelineId: 5118526, repo: 'optima-gateway', saeAppId: '90e0daf7-7910-46b8-b5bf-a0b8bcc60859' },
45
+ 'kb-backend': { pipelineId: 5118527, repo: 'kb-skills', saeAppId: 'c7f65160-9d9e-416e-9e36-5439010d2b2d' },
46
+ 'ops-portal': { pipelineId: 5118529, repo: 'optima-portals', saeAppId: '41d0ee66-8402-4e72-bfda-8eba75d1270c' },
47
+ 'optima-generation': { pipelineId: 5118530, repo: 'optima-gen', saeAppId: '327856d5-8b18-4e15-bfd8-b8bd3b807ffa' },
48
+ 'optima-generation-worker': { pipelineId: 5118531, repo: 'optima-gen', saeAppId: 'ad75b3b0-9ff4-443d-b0ef-81886bc7aa60' },
49
+ 'optima-scout': { pipelineId: 5117336, repo: 'optima-scout', saeAppId: 'bac2c3a4-90ce-49a6-81d5-caa38cb5c807' },
50
+ 'optima-sentinel': { pipelineId: 5118532, repo: 'optima-sentinel', saeAppId: '3759db7b-4640-4a04-8436-4f241f0ec9d9' },
51
+ 'optima-sentinel-worker': { pipelineId: 5118533, repo: 'optima-sentinel', saeAppId: '7c975caf-ebfb-4bb3-91c4-2a8c83d1d2e5' },
52
+ 'optima-skills': { pipelineId: 5118534, repo: 'optima-skills', saeAppId: '90457be3-5eb3-4efc-a362-1788dcc51921' },
53
+ 'user-auth': { pipelineId: 5118510, repo: 'user-auth', saeAppId: '36efc3a6-5b42-49fa-8db2-0865aa0c25d2' },
54
+ 'user-auth-admin': { pipelineId: 5118535, repo: 'user-auth', saeAppId: '7feb9cc3-4731-4ea8-964c-4870a9e63afb' },
55
+ 'yzsgo-api': { pipelineId: 5118536, repo: 'yzsgo', saeAppId: 'd0af7f66-ef66-4587-9e93-250d01ee3cf6' },
56
+ };
57
+
58
+ function sh(cmd: string, args: string[]): string {
59
+ try {
60
+ return execFileSync(cmd, args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim();
61
+ } catch (e: any) {
62
+ return '';
63
+ }
64
+ }
65
+
66
+ function devops(action: string, kv: Record<string, string>, retries = 3): any {
67
+ const args = ['devops', action, '--organizationId', ORG, '--endpoint', ENDPOINT, '--profile', PROFILE];
68
+ for (const [k, v] of Object.entries(kv)) args.push(`--${k}`, v);
69
+ for (let i = 0; i < retries; i++) {
70
+ const out = sh('aliyun', args);
71
+ try { return JSON.parse(out); } catch { /* aliyun 偶发空返回/超时,重试 */ }
72
+ }
73
+ return {};
74
+ }
75
+
76
+ const sleep = (ms: number) => new Promise(r => setTimeout(r, ms));
77
+
78
+ async function main() {
79
+ const argv = process.argv.slice(2);
80
+ if (argv.includes('--list') || argv.length === 0) {
81
+ console.log('可发服务(cn-stage):\n ' + Object.keys(SERVICES).sort().join('\n '));
82
+ console.log('\n用法: optima-cn-deploy <service> [--branch feat/xxx] [--no-wait]');
83
+ process.exit(argv.includes('--list') ? 0 : 1);
84
+ }
85
+ const svcName = argv[0];
86
+ const svc = SERVICES[svcName];
87
+ if (!svc) { console.error(`✗ 未知服务 ${svcName}(--list 看全量)`); process.exit(1); }
88
+ const bIdx = argv.indexOf('--branch');
89
+ const branch = bIdx >= 0 ? argv[bIdx + 1] : 'main';
90
+ const noWait = argv.includes('--no-wait');
91
+
92
+ // 1. GitHub HEAD + mirror 同步
93
+ const ghSha = sh('gh', ['api', `repos/Optima-Chat/${svc.repo}/commits/${branch}`, '--jq', '.sha']);
94
+ if (!ghSha) { console.error(`✗ GitHub 上无 ${svc.repo}@${branch}(或 gh 未登录)`); process.exit(1); }
95
+ const repos = devops('ListRepositories', { perPage: '100' });
96
+ const repoId = (repos.result || []).find((r: any) => r.name === svc.repo)?.Id;
97
+ if (!repoId) { console.error(`✗ Codeup 无 mirror 仓 ${svc.repo}`); process.exit(1); }
98
+ const ghTok = sh('gh', ['auth', 'token']);
99
+ devops('TriggerRepositoryMirrorSync', { repositoryId: String(repoId), account: 'xbfool', token: ghTok });
100
+ let synced = false;
101
+ for (let i = 0; i < 30; i++) {
102
+ const b = devops('GetBranchInfo', { repositoryId: String(repoId), branchName: branch });
103
+ if (b?.result?.commit?.id === ghSha) { synced = true; break; }
104
+ await sleep(10000);
105
+ }
106
+ if (!synced) { console.error('✗ 300s 内 Codeup mirror 未追平 GitHub,中止(检查 mirror 凭证)'); process.exit(1); }
107
+ console.log(`✓ mirror 已追平 ${svc.repo}@${branch} = ${ghSha.slice(0, 10)}`);
108
+
109
+ // 2. 触发(凭证由云效变量组 41970 供给,无需注入)
110
+ const kv: Record<string, string> = { pipelineId: String(svc.pipelineId) };
111
+ if (branch !== 'main') {
112
+ kv.params = JSON.stringify({ runningBranchs: { [`${CODEUP_BASE}/${svc.repo}.git`]: branch } });
113
+ }
114
+ const start = devops('StartPipelineRun', kv);
115
+ const runId = start.pipelineRunId;
116
+ if (!runId) { console.error(`✗ 启动失败: ${JSON.stringify(start).slice(0, 150)}`); process.exit(1); }
117
+ console.log(`▶ ${svcName} run#${runId} 已启动 (branch=${branch})`);
118
+ console.log(` https://flow.aliyun.com/pipelines/${svc.pipelineId}/current`);
119
+ if (noWait) return;
120
+
121
+ // 3. 轮询
122
+ let status = '';
123
+ for (;;) {
124
+ const d = devops('GetPipelineRun', { pipelineId: String(svc.pipelineId), pipelineRunId: String(runId) });
125
+ status = d?.pipelineRun?.status || status;
126
+ if (['SUCCESS', 'FAIL', 'CANCELED', 'ERROR'].includes(status)) break;
127
+ await sleep(45000);
128
+ }
129
+ console.log(`${status === 'SUCCESS' ? '✅' : '❌'} ${svcName} run#${runId}: ${status}`);
130
+ if (status !== 'SUCCESS') process.exit(1);
131
+
132
+ // 4. SAE ImageUrl 校验(空返回重试守卫)
133
+ if (svc.saeAppId) {
134
+ let img = '';
135
+ for (let i = 0; i < 3; i++) {
136
+ const raw = sh('aliyun', ['sae', '--profile', PROFILE, '--region', 'cn-beijing',
137
+ 'DescribeApplicationConfig', '--AppId', svc.saeAppId]);
138
+ try { img = JSON.parse(raw)?.Data?.ImageUrl || ''; break; } catch { await sleep(3000); }
139
+ }
140
+ if (!img) { console.log('⚠️ SAE 配置查询连续空返回,跳过 ImageUrl 校验(流水线本身已 SUCCESS)'); return; }
141
+ const tag = img.split(':').pop() || '';
142
+ const ok = ghSha.startsWith(tag);
143
+ console.log(`${ok ? '✓' : '✗'} SAE ImageUrl tag=${tag} vs 目标 ${ghSha.slice(0, 10)}`);
144
+ if (!ok) process.exit(1);
145
+ }
146
+ }
147
+
148
+ main().catch(e => { console.error('✗', e?.message || e); process.exit(1); });
@@ -0,0 +1,173 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ /**
5
+ * optima-cn-deploy —— 云效 Flow cn-stage 流水线一条龙触发器(团队版)。
6
+ *
7
+ * 对应 optima-terraform alicloud/stacks/cn-prod-buildbox/yunxiao/ 的 cn-run.py,
8
+ * 去掉 buildbox SSH 依赖(凭证由云效变量组 41970 供给,2026-07-13 起控制台/API 裸跑均可)。
9
+ *
10
+ * 做四件事(每件都是踩过的坑):
11
+ * 1. 触发服务仓 Codeup mirror 同步并等目标分支追平 GitHub —— mirror 不新鲜=构建旧代码。
12
+ * GitHub token 用 `gh auth token`(需本机 gh 已登录)。
13
+ * 2. StartPipelineRun;--branch 时带 runningBranchs 覆盖服务仓 source 分支。
14
+ * 3. 轮询到终态,给出云效 run 链接。
15
+ * 4. SUCCESS 后校验 SAE ImageUrl tag == 目标分支 HEAD short-sha(防"流水线绿但没部上");
16
+ * aliyun API 偶发空返回,带重试守卫。
17
+ *
18
+ * 用法:
19
+ * optima-cn-deploy billing # main 全链(构建→迁移→SAE 发布)
20
+ * optima-cn-deploy user-auth --branch feat/xxx # 非 main 分支构建+部署到 cn-stage
21
+ * optima-cn-deploy gateway-core --no-wait # 只触发不等待
22
+ * optima-cn-deploy --list # 列出全部可发服务
23
+ *
24
+ * 前置: aliyun CLI(profile 默认 aliyun-optima,可用 OPTIMA_ALIYUN_PROFILE 覆盖)+ gh 已登录。
25
+ * 流水线定义的单一信源在 optima-terraform yunxiao/(gen-pipelines.py);本表为其快照,
26
+ * 新增服务后同步(pipelineId 稳定,不常变)。
27
+ */
28
+ const node_child_process_1 = require("node:child_process");
29
+ const ORG = '6a17b6282bf8b1184fc0e0c6';
30
+ const ENDPOINT = 'devops.cn-hangzhou.aliyuncs.com';
31
+ const PROFILE = process.env.OPTIMA_ALIYUN_PROFILE || 'aliyun-optima';
32
+ const CODEUP_BASE = `https://codeup.aliyun.com/${ORG}`;
33
+ const SERVICES = {
34
+ 'agent-portal': { pipelineId: 5118519, repo: 'optima-portals', saeAppId: 'fe757f78-d18d-4480-9402-fe59d4721055' },
35
+ 'agentic-chat': { pipelineId: 5118520, repo: 'agentic-chat', saeAppId: '6aea1ce1-f813-4e1c-8e97-d1ecb5398e37' },
36
+ 'billing': { pipelineId: 5118521, repo: 'optima-billing', saeAppId: '09d8e292-dc64-4af8-bce5-0a56cb666921' },
37
+ 'browser-backend': { pipelineId: 5118522, repo: 'optima-browser-use', saeAppId: '1fced3f6-a80a-41a4-8f23-e4d5a467f8eb' },
38
+ 'commerce-backend': { pipelineId: 5107005, repo: 'commerce-backend', saeAppId: '49d09808-508c-471a-9560-553c49a67f72' },
39
+ 'commerce-rq-scheduler': { pipelineId: 5118523, repo: 'commerce-backend', saeAppId: '1d2810ef-889e-4675-b6e6-a299e4722e68' },
40
+ 'commerce-rq-worker': { pipelineId: 5118524, repo: 'commerce-backend', saeAppId: 'd661da87-6d24-40b8-93ed-e1c967d8abe2' },
41
+ 'gateway-core': { pipelineId: 5118525, repo: 'optima-gateway', saeAppId: '9326b7ff-da52-48d6-86db-9c4a884be108' },
42
+ 'gw-admin': { pipelineId: 5118526, repo: 'optima-gateway', saeAppId: '90e0daf7-7910-46b8-b5bf-a0b8bcc60859' },
43
+ 'kb-backend': { pipelineId: 5118527, repo: 'kb-skills', saeAppId: 'c7f65160-9d9e-416e-9e36-5439010d2b2d' },
44
+ 'ops-portal': { pipelineId: 5118529, repo: 'optima-portals', saeAppId: '41d0ee66-8402-4e72-bfda-8eba75d1270c' },
45
+ 'optima-generation': { pipelineId: 5118530, repo: 'optima-gen', saeAppId: '327856d5-8b18-4e15-bfd8-b8bd3b807ffa' },
46
+ 'optima-generation-worker': { pipelineId: 5118531, repo: 'optima-gen', saeAppId: 'ad75b3b0-9ff4-443d-b0ef-81886bc7aa60' },
47
+ 'optima-scout': { pipelineId: 5117336, repo: 'optima-scout', saeAppId: 'bac2c3a4-90ce-49a6-81d5-caa38cb5c807' },
48
+ 'optima-sentinel': { pipelineId: 5118532, repo: 'optima-sentinel', saeAppId: '3759db7b-4640-4a04-8436-4f241f0ec9d9' },
49
+ 'optima-sentinel-worker': { pipelineId: 5118533, repo: 'optima-sentinel', saeAppId: '7c975caf-ebfb-4bb3-91c4-2a8c83d1d2e5' },
50
+ 'optima-skills': { pipelineId: 5118534, repo: 'optima-skills', saeAppId: '90457be3-5eb3-4efc-a362-1788dcc51921' },
51
+ 'user-auth': { pipelineId: 5118510, repo: 'user-auth', saeAppId: '36efc3a6-5b42-49fa-8db2-0865aa0c25d2' },
52
+ 'user-auth-admin': { pipelineId: 5118535, repo: 'user-auth', saeAppId: '7feb9cc3-4731-4ea8-964c-4870a9e63afb' },
53
+ 'yzsgo-api': { pipelineId: 5118536, repo: 'yzsgo', saeAppId: 'd0af7f66-ef66-4587-9e93-250d01ee3cf6' },
54
+ };
55
+ function sh(cmd, args) {
56
+ try {
57
+ return (0, node_child_process_1.execFileSync)(cmd, args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim();
58
+ }
59
+ catch (e) {
60
+ return '';
61
+ }
62
+ }
63
+ function devops(action, kv, retries = 3) {
64
+ const args = ['devops', action, '--organizationId', ORG, '--endpoint', ENDPOINT, '--profile', PROFILE];
65
+ for (const [k, v] of Object.entries(kv))
66
+ args.push(`--${k}`, v);
67
+ for (let i = 0; i < retries; i++) {
68
+ const out = sh('aliyun', args);
69
+ try {
70
+ return JSON.parse(out);
71
+ }
72
+ catch { /* aliyun 偶发空返回/超时,重试 */ }
73
+ }
74
+ return {};
75
+ }
76
+ const sleep = (ms) => new Promise(r => setTimeout(r, ms));
77
+ async function main() {
78
+ const argv = process.argv.slice(2);
79
+ if (argv.includes('--list') || argv.length === 0) {
80
+ console.log('可发服务(cn-stage):\n ' + Object.keys(SERVICES).sort().join('\n '));
81
+ console.log('\n用法: optima-cn-deploy <service> [--branch feat/xxx] [--no-wait]');
82
+ process.exit(argv.includes('--list') ? 0 : 1);
83
+ }
84
+ const svcName = argv[0];
85
+ const svc = SERVICES[svcName];
86
+ if (!svc) {
87
+ console.error(`✗ 未知服务 ${svcName}(--list 看全量)`);
88
+ process.exit(1);
89
+ }
90
+ const bIdx = argv.indexOf('--branch');
91
+ const branch = bIdx >= 0 ? argv[bIdx + 1] : 'main';
92
+ const noWait = argv.includes('--no-wait');
93
+ // 1. GitHub HEAD + mirror 同步
94
+ const ghSha = sh('gh', ['api', `repos/Optima-Chat/${svc.repo}/commits/${branch}`, '--jq', '.sha']);
95
+ if (!ghSha) {
96
+ console.error(`✗ GitHub 上无 ${svc.repo}@${branch}(或 gh 未登录)`);
97
+ process.exit(1);
98
+ }
99
+ const repos = devops('ListRepositories', { perPage: '100' });
100
+ const repoId = (repos.result || []).find((r) => r.name === svc.repo)?.Id;
101
+ if (!repoId) {
102
+ console.error(`✗ Codeup 无 mirror 仓 ${svc.repo}`);
103
+ process.exit(1);
104
+ }
105
+ const ghTok = sh('gh', ['auth', 'token']);
106
+ devops('TriggerRepositoryMirrorSync', { repositoryId: String(repoId), account: 'xbfool', token: ghTok });
107
+ let synced = false;
108
+ for (let i = 0; i < 30; i++) {
109
+ const b = devops('GetBranchInfo', { repositoryId: String(repoId), branchName: branch });
110
+ if (b?.result?.commit?.id === ghSha) {
111
+ synced = true;
112
+ break;
113
+ }
114
+ await sleep(10000);
115
+ }
116
+ if (!synced) {
117
+ console.error('✗ 300s 内 Codeup mirror 未追平 GitHub,中止(检查 mirror 凭证)');
118
+ process.exit(1);
119
+ }
120
+ console.log(`✓ mirror 已追平 ${svc.repo}@${branch} = ${ghSha.slice(0, 10)}`);
121
+ // 2. 触发(凭证由云效变量组 41970 供给,无需注入)
122
+ const kv = { pipelineId: String(svc.pipelineId) };
123
+ if (branch !== 'main') {
124
+ kv.params = JSON.stringify({ runningBranchs: { [`${CODEUP_BASE}/${svc.repo}.git`]: branch } });
125
+ }
126
+ const start = devops('StartPipelineRun', kv);
127
+ const runId = start.pipelineRunId;
128
+ if (!runId) {
129
+ console.error(`✗ 启动失败: ${JSON.stringify(start).slice(0, 150)}`);
130
+ process.exit(1);
131
+ }
132
+ console.log(`▶ ${svcName} run#${runId} 已启动 (branch=${branch})`);
133
+ console.log(` https://flow.aliyun.com/pipelines/${svc.pipelineId}/current`);
134
+ if (noWait)
135
+ return;
136
+ // 3. 轮询
137
+ let status = '';
138
+ for (;;) {
139
+ const d = devops('GetPipelineRun', { pipelineId: String(svc.pipelineId), pipelineRunId: String(runId) });
140
+ status = d?.pipelineRun?.status || status;
141
+ if (['SUCCESS', 'FAIL', 'CANCELED', 'ERROR'].includes(status))
142
+ break;
143
+ await sleep(45000);
144
+ }
145
+ console.log(`${status === 'SUCCESS' ? '✅' : '❌'} ${svcName} run#${runId}: ${status}`);
146
+ if (status !== 'SUCCESS')
147
+ process.exit(1);
148
+ // 4. SAE ImageUrl 校验(空返回重试守卫)
149
+ if (svc.saeAppId) {
150
+ let img = '';
151
+ for (let i = 0; i < 3; i++) {
152
+ const raw = sh('aliyun', ['sae', '--profile', PROFILE, '--region', 'cn-beijing',
153
+ 'DescribeApplicationConfig', '--AppId', svc.saeAppId]);
154
+ try {
155
+ img = JSON.parse(raw)?.Data?.ImageUrl || '';
156
+ break;
157
+ }
158
+ catch {
159
+ await sleep(3000);
160
+ }
161
+ }
162
+ if (!img) {
163
+ console.log('⚠️ SAE 配置查询连续空返回,跳过 ImageUrl 校验(流水线本身已 SUCCESS)');
164
+ return;
165
+ }
166
+ const tag = img.split(':').pop() || '';
167
+ const ok = ghSha.startsWith(tag);
168
+ console.log(`${ok ? '✓' : '✗'} SAE ImageUrl tag=${tag} vs 目标 ${ghSha.slice(0, 10)}`);
169
+ if (!ok)
170
+ process.exit(1);
171
+ }
172
+ }
173
+ main().catch(e => { console.error('✗', e?.message || e); process.exit(1); });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@optima-chat/dev-skills",
3
- "version": "0.10.0",
3
+ "version": "0.12.0",
4
4
  "description": "Claude Code Skills for Optima development team - cross-environment collaboration tools",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -17,7 +17,8 @@
17
17
  "optima-product": "dist/bin/helpers/product.js",
18
18
  "optima-query-db": "dist/bin/helpers/query-db.js",
19
19
  "optima-show-env": "dist/bin/helpers/show-env.js",
20
- "optima-verify-health": "dist/bin/helpers/verify-health.js"
20
+ "optima-verify-health": "dist/bin/helpers/verify-health.js",
21
+ "optima-cn-deploy": "dist/bin/helpers/cn-deploy.js"
21
22
  },
22
23
  "scripts": {
23
24
  "postinstall": "node scripts/install.js",
@@ -62,4 +63,4 @@
62
63
  "ts-node": "^10.9.2",
63
64
  "typescript": "^5.9.3"
64
65
  }
65
- }
66
+ }