@ranger1/dx 0.1.66 → 0.1.68

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.
@@ -0,0 +1,45 @@
1
+ ---
2
+ name: doctor
3
+ description: Use when 需要在本机一次性体检并修复 Codex 开发环境,包括 python3/python 别名、dx 初始化、agent-browser+Chromium、ripgrep 与 multi_agent 特性状态。
4
+ ---
5
+
6
+ # Doctor
7
+
8
+ ## 概览
9
+
10
+ 执行本技能时,优先运行 `scripts/doctor.sh`,由脚本完成并行检测、自动修复、最多三轮重试与最终报告。
11
+
12
+ ## 适用场景
13
+
14
+ - 新机器初始化 Codex 开发环境。
15
+ - 发现命令缺失或版本漂移,希望一次性修复。
16
+ - 需要确认 `codex features list` 中 `multi_agent` 为 `experimental true`。
17
+
18
+ ## 执行步骤
19
+
20
+ 1. 直接运行:
21
+
22
+ ```bash
23
+ CODEX_HOME="${CODEX_HOME:-$HOME/.codex}"
24
+ bash "$CODEX_HOME/skills/doctor/scripts/doctor.sh"
25
+ ```
26
+
27
+ 2. 若需限制轮次(默认 3):
28
+
29
+ ```bash
30
+ CODEX_HOME="${CODEX_HOME:-$HOME/.codex}"
31
+ bash "$CODEX_HOME/skills/doctor/scripts/doctor.sh" --max-rounds 3
32
+ ```
33
+
34
+ ## 脚本职责
35
+
36
+ - 并行检测:`python3`、`python` 别名、`pnpm`、`dx`、`agent-browser`、`rg`、`multi_agent`。
37
+ - 自动修复:按平台选择安装器修复缺失项。
38
+ - 强制执行:每轮都运行 `pnpm add -g @ranger1/dx@latest && dx initial`。
39
+ - agent-browser:安装/升级并执行 Chromium 安装。
40
+ - 结果输出:展示每项状态、版本、关键信息;全部通过则退出 0,否则最多三轮后退出 1。
41
+
42
+ ## 注意
43
+
44
+ - 某些安装步骤可能需要管理员权限(例如 `sudo` 或 Homebrew 写权限)。
45
+ - 若系统缺少包管理器,脚本会给出明确失败原因。
@@ -0,0 +1,4 @@
1
+ interface:
2
+ display_name: "Doctor"
3
+ short_description: "并行检测并自动修复 Codex 开发环境关键依赖与特性开关"
4
+ default_prompt: "使用 $doctor 对当前系统执行并行体检、自动修复与三轮内收敛验证,并输出最终版本报告。"
@@ -0,0 +1,386 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ MAX_ROUNDS=3
5
+ while [[ $# -gt 0 ]]; do
6
+ case "$1" in
7
+ --max-rounds)
8
+ MAX_ROUNDS="${2:-3}"
9
+ shift 2
10
+ ;;
11
+ *)
12
+ shift
13
+ ;;
14
+ esac
15
+ done
16
+
17
+ if ! [[ "$MAX_ROUNDS" =~ ^[0-9]+$ ]] || [[ "$MAX_ROUNDS" -lt 1 ]]; then
18
+ echo "[doctor] --max-rounds 必须是正整数" >&2
19
+ exit 2
20
+ fi
21
+
22
+ CACHE_ROOT="${PWD}/.cache/doctor"
23
+ mkdir -p "$CACHE_ROOT"
24
+
25
+ LAST_CHECK_DIR=""
26
+ DX_FORCE_OK=0
27
+ DX_FORCE_MSG=""
28
+
29
+ run_silent() {
30
+ "$@" >/dev/null 2>&1
31
+ }
32
+
33
+ detect_pkg_manager() {
34
+ if command -v brew >/dev/null 2>&1; then echo "brew"; return; fi
35
+ if command -v apt-get >/dev/null 2>&1; then echo "apt"; return; fi
36
+ if command -v dnf >/dev/null 2>&1; then echo "dnf"; return; fi
37
+ if command -v yum >/dev/null 2>&1; then echo "yum"; return; fi
38
+ if command -v pacman >/dev/null 2>&1; then echo "pacman"; return; fi
39
+ echo "none"
40
+ }
41
+
42
+ install_python3() {
43
+ local pm
44
+ pm="$(detect_pkg_manager)"
45
+ case "$pm" in
46
+ brew)
47
+ brew install python
48
+ ;;
49
+ apt)
50
+ sudo apt-get update && sudo apt-get install -y python3 python3-venv python3-pip
51
+ ;;
52
+ dnf)
53
+ sudo dnf install -y python3 python3-pip
54
+ ;;
55
+ yum)
56
+ sudo yum install -y python3 python3-pip
57
+ ;;
58
+ pacman)
59
+ sudo pacman -Sy --noconfirm python
60
+ ;;
61
+ *)
62
+ echo "未检测到可用包管理器,无法安装 python3" >&2
63
+ return 1
64
+ ;;
65
+ esac
66
+ }
67
+
68
+ ensure_python_alias() {
69
+ local py3 py3_dir target_dir target
70
+ py3="$(command -v python3 || true)"
71
+ if [[ -z "$py3" ]]; then
72
+ echo "python3 不存在,无法创建 python 别名" >&2
73
+ return 1
74
+ fi
75
+
76
+ if command -v python >/dev/null 2>&1; then
77
+ return 0
78
+ fi
79
+
80
+ py3_dir="$(dirname "$py3")"
81
+ if [[ -w "$py3_dir" ]]; then
82
+ target_dir="$py3_dir"
83
+ elif [[ -w "/usr/local/bin" ]]; then
84
+ target_dir="/usr/local/bin"
85
+ elif [[ -w "/opt/homebrew/bin" ]]; then
86
+ target_dir="/opt/homebrew/bin"
87
+ else
88
+ target_dir="$HOME/.local/bin"
89
+ mkdir -p "$target_dir"
90
+ fi
91
+
92
+ target="$target_dir/python"
93
+ ln -sf "$py3" "$target"
94
+
95
+ if [[ "$target_dir" == "$HOME/.local/bin" ]]; then
96
+ export PATH="$HOME/.local/bin:$PATH"
97
+ fi
98
+
99
+ command -v python >/dev/null 2>&1
100
+ }
101
+
102
+ install_rg() {
103
+ local pm
104
+ pm="$(detect_pkg_manager)"
105
+ case "$pm" in
106
+ brew)
107
+ brew install ripgrep
108
+ ;;
109
+ apt)
110
+ sudo apt-get update && sudo apt-get install -y ripgrep
111
+ ;;
112
+ dnf)
113
+ sudo dnf install -y ripgrep
114
+ ;;
115
+ yum)
116
+ sudo yum install -y ripgrep
117
+ ;;
118
+ pacman)
119
+ sudo pacman -Sy --noconfirm ripgrep
120
+ ;;
121
+ *)
122
+ echo "未检测到可用包管理器,无法安装 rg" >&2
123
+ return 1
124
+ ;;
125
+ esac
126
+ }
127
+
128
+ install_pnpm() {
129
+ if command -v pnpm >/dev/null 2>&1; then
130
+ return 0
131
+ fi
132
+
133
+ if command -v npm >/dev/null 2>&1; then
134
+ npm install -g pnpm@latest
135
+ elif command -v brew >/dev/null 2>&1; then
136
+ brew install pnpm
137
+ else
138
+ echo "缺少 npm/brew,无法安装 pnpm" >&2
139
+ return 1
140
+ fi
141
+ }
142
+
143
+ ensure_agent_browser() {
144
+ if command -v npm >/dev/null 2>&1; then
145
+ npm install -g agent-browser@latest
146
+ elif command -v pnpm >/dev/null 2>&1; then
147
+ pnpm add -g agent-browser@latest
148
+ elif command -v brew >/dev/null 2>&1; then
149
+ brew install agent-browser
150
+ else
151
+ echo "缺少 npm/pnpm/brew,无法安装 agent-browser" >&2
152
+ return 1
153
+ fi
154
+
155
+ if ! command -v agent-browser >/dev/null 2>&1; then
156
+ echo "agent-browser 安装后仍不可用" >&2
157
+ return 1
158
+ fi
159
+
160
+ if ! run_silent agent-browser install; then
161
+ run_silent agent-browser install --with-deps || {
162
+ echo "agent-browser Chromium 安装失败" >&2
163
+ return 1
164
+ }
165
+ fi
166
+ }
167
+
168
+ ensure_multi_agent() {
169
+ if ! command -v codex >/dev/null 2>&1; then
170
+ echo "codex 命令不存在" >&2
171
+ return 1
172
+ fi
173
+ codex features enable multi_agent >/dev/null 2>&1 || true
174
+ local line
175
+ line="$(codex features list 2>/dev/null | awk '$1=="multi_agent" {print $0}')"
176
+ [[ -n "$line" ]] || return 1
177
+ echo "$line" | grep -E "experimental[[:space:]]+true" >/dev/null 2>&1
178
+ }
179
+
180
+ force_dx() {
181
+ if ! install_pnpm; then
182
+ DX_FORCE_OK=0
183
+ DX_FORCE_MSG="pnpm 不可用,无法执行 dx 强制初始化"
184
+ return 1
185
+ fi
186
+
187
+ if pnpm add -g @ranger1/dx@latest >/dev/null 2>&1 && dx initial >/dev/null 2>&1; then
188
+ DX_FORCE_OK=1
189
+ DX_FORCE_MSG="已执行 pnpm add -g @ranger1/dx@latest && dx initial"
190
+ return 0
191
+ fi
192
+
193
+ DX_FORCE_OK=0
194
+ DX_FORCE_MSG="强制命令执行失败:pnpm add -g @ranger1/dx@latest && dx initial"
195
+ return 1
196
+ }
197
+
198
+ write_check_file() {
199
+ local key="$1" ok="$2" ver="$3" msg="$4" file="$5"
200
+ printf '%s|%s|%s|%s\n' "$key" "$ok" "$ver" "$msg" >"$file"
201
+ }
202
+
203
+ read_field() {
204
+ local key="$1" field="$2"
205
+ local f="$LAST_CHECK_DIR/${key}.res"
206
+ if [[ ! -f "$f" ]]; then
207
+ echo ""
208
+ return 0
209
+ fi
210
+ awk -F'|' -v idx="$field" '{print $idx}' "$f"
211
+ }
212
+
213
+ check_ok() {
214
+ local key="$1"
215
+ [[ "$(read_field "$key" 2)" == "1" ]]
216
+ }
217
+
218
+ run_parallel_checks() {
219
+ local round="$1"
220
+ local dir="$CACHE_ROOT/round-${round}"
221
+ mkdir -p "$dir"
222
+
223
+ (
224
+ if command -v python3 >/dev/null 2>&1; then
225
+ write_check_file "python3" "1" "$(python3 --version 2>&1 | head -n1)" "ok" "$dir/python3.res"
226
+ else
227
+ write_check_file "python3" "0" "" "python3 未安装" "$dir/python3.res"
228
+ fi
229
+ ) &
230
+
231
+ (
232
+ if command -v python >/dev/null 2>&1; then
233
+ write_check_file "python_alias" "1" "$(python --version 2>&1 | head -n1)" "ok" "$dir/python_alias.res"
234
+ else
235
+ write_check_file "python_alias" "0" "" "python 别名不可用" "$dir/python_alias.res"
236
+ fi
237
+ ) &
238
+
239
+ (
240
+ if command -v pnpm >/dev/null 2>&1; then
241
+ write_check_file "pnpm" "1" "pnpm $(pnpm --version 2>/dev/null | head -n1)" "ok" "$dir/pnpm.res"
242
+ else
243
+ write_check_file "pnpm" "0" "" "pnpm 未安装" "$dir/pnpm.res"
244
+ fi
245
+ ) &
246
+
247
+ (
248
+ if command -v dx >/dev/null 2>&1; then
249
+ local v
250
+ v="$(dx --version 2>/dev/null | head -n1 || true)"
251
+ if [[ -z "$v" ]]; then
252
+ v="$(dx -v 2>/dev/null | head -n1 || true)"
253
+ fi
254
+ write_check_file "dx" "1" "${v:-dx (版本未知)}" "ok" "$dir/dx.res"
255
+ else
256
+ write_check_file "dx" "0" "" "dx 未安装" "$dir/dx.res"
257
+ fi
258
+ ) &
259
+
260
+ (
261
+ if command -v agent-browser >/dev/null 2>&1; then
262
+ write_check_file "agent_browser" "1" "$(agent-browser --version 2>/dev/null | head -n1)" "ok" "$dir/agent_browser.res"
263
+ else
264
+ write_check_file "agent_browser" "0" "" "agent-browser 未安装" "$dir/agent_browser.res"
265
+ fi
266
+ ) &
267
+
268
+ (
269
+ if command -v rg >/dev/null 2>&1; then
270
+ write_check_file "rg" "1" "$(rg --version 2>/dev/null | head -n1)" "ok" "$dir/rg.res"
271
+ else
272
+ write_check_file "rg" "0" "" "rg 未安装" "$dir/rg.res"
273
+ fi
274
+ ) &
275
+
276
+ (
277
+ if command -v codex >/dev/null 2>&1; then
278
+ local line
279
+ line="$(codex features list 2>/dev/null | awk '$1=="multi_agent" {print $0}')"
280
+ if [[ -n "$line" ]] && echo "$line" | grep -E "experimental[[:space:]]+true" >/dev/null 2>&1; then
281
+ write_check_file "multi_agent" "1" "$line" "ok" "$dir/multi_agent.res"
282
+ else
283
+ write_check_file "multi_agent" "0" "${line:-missing}" "multi_agent 不是 experimental true" "$dir/multi_agent.res"
284
+ fi
285
+ else
286
+ write_check_file "multi_agent" "0" "" "codex 命令不存在" "$dir/multi_agent.res"
287
+ fi
288
+ ) &
289
+
290
+ wait
291
+ LAST_CHECK_DIR="$dir"
292
+ }
293
+
294
+ all_good() {
295
+ local keys="python3 python_alias pnpm dx agent_browser rg multi_agent"
296
+ local k
297
+ for k in $keys; do
298
+ if ! check_ok "$k"; then
299
+ return 1
300
+ fi
301
+ done
302
+ [[ "$DX_FORCE_OK" == "1" ]]
303
+ }
304
+
305
+ print_report() {
306
+ local round="$1"
307
+ echo
308
+ echo "===== Doctor 报告(第 ${round} 轮)====="
309
+ printf '%-14s | %-4s | %-40s | %s\n' "检查项" "状态" "版本" "说明"
310
+ printf '%-14s-+-%-4s-+-%-40s-+-%s\n' "--------------" "----" "----------------------------------------" "------------------------------"
311
+
312
+ local keys="python3 python_alias pnpm dx agent_browser rg multi_agent"
313
+ local k ok txt ver msg
314
+ for k in $keys; do
315
+ ok="$(read_field "$k" 2)"
316
+ txt="FAIL"
317
+ [[ "$ok" == "1" ]] && txt="PASS"
318
+ ver="$(read_field "$k" 3)"
319
+ msg="$(read_field "$k" 4)"
320
+ printf '%-14s | %-4s | %-40s | %s\n' "$k" "$txt" "$ver" "$msg"
321
+ done
322
+
323
+ if [[ "$DX_FORCE_OK" == "1" ]]; then
324
+ printf '%-14s | %-4s | %-40s | %s\n' "dx_force" "PASS" "@ranger1/dx@latest" "$DX_FORCE_MSG"
325
+ else
326
+ printf '%-14s | %-4s | %-40s | %s\n' "dx_force" "FAIL" "@ranger1/dx@latest" "$DX_FORCE_MSG"
327
+ fi
328
+
329
+ if command -v node >/dev/null 2>&1; then
330
+ echo "node: $(node -v 2>/dev/null | head -n1)"
331
+ fi
332
+ if command -v npm >/dev/null 2>&1; then
333
+ echo "npm: $(npm -v 2>/dev/null | head -n1)"
334
+ fi
335
+ }
336
+
337
+ for round in $(seq 1 "$MAX_ROUNDS"); do
338
+ echo "[doctor] 第 ${round}/${MAX_ROUNDS} 轮:并行检测"
339
+ run_parallel_checks "$round"
340
+
341
+ echo "[doctor] 第 ${round}/${MAX_ROUNDS} 轮:修复阶段"
342
+
343
+ if ! check_ok "python3"; then
344
+ echo "[doctor] 安装 python3"
345
+ install_python3 || true
346
+ fi
347
+
348
+ if ! check_ok "python_alias"; then
349
+ echo "[doctor] 建立 python -> python3 调用能力"
350
+ ensure_python_alias || true
351
+ fi
352
+
353
+ if ! check_ok "rg"; then
354
+ echo "[doctor] 安装 rg"
355
+ install_rg || true
356
+ fi
357
+
358
+ if ! check_ok "agent_browser"; then
359
+ echo "[doctor] 安装/升级 agent-browser 并安装 Chromium"
360
+ ensure_agent_browser || true
361
+ else
362
+ echo "[doctor] agent-browser 已存在,执行升级与 Chromium 安装"
363
+ ensure_agent_browser || true
364
+ fi
365
+
366
+ if ! check_ok "multi_agent"; then
367
+ echo "[doctor] 修正 multi_agent 特性开关"
368
+ ensure_multi_agent || true
369
+ fi
370
+
371
+ echo "[doctor] 强制执行 dx 安装与初始化"
372
+ force_dx || true
373
+
374
+ echo "[doctor] 第 ${round}/${MAX_ROUNDS} 轮:复检"
375
+ run_parallel_checks "${round}-post"
376
+ print_report "$round"
377
+
378
+ if all_good; then
379
+ echo "[doctor] 全部检查通过"
380
+ exit 0
381
+ fi
382
+
383
+ done
384
+
385
+ echo "[doctor] 达到最大轮次 ${MAX_ROUNDS},仍有未通过项"
386
+ exit 1
@@ -7,7 +7,7 @@ description: 在 Git 仓库中执行标准化版本发布流程并自动生成
7
7
 
8
8
  ## 目标
9
9
 
10
- 在 `release/vX.Y.Z` 或 `release/vX.Y.Z-<prerelease>.N` 分支上,完成从发布前检查到 GitHub Release 创建的全流程。
10
+ 在 `release/vX.Y.Z` 或 `release/vX.Y.Z-<prerelease>.N` 分支上,完成从发布前检查到 GitHub Release 创建的全流程;若当前不在 release 分支,则先从最新 `main` 自动创建目标 release 分支。
11
11
 
12
12
  ## 执行原则
13
13
 
@@ -23,12 +23,17 @@ description: 在 Git 仓库中执行标准化版本发布流程并自动生成
23
23
  1. 检查工作区是否干净:`git status --porcelain`。
24
24
  2. 若存在未提交变更,列出文件并终止流程。
25
25
  3. 检查当前分支:`git branch --show-current`。
26
- 4. 仅接受以下正则:`^release/v\d+\.\d+\.\d+(-(alpha|beta|rc)\.\d+)?$`。
27
- 5. 从分支名提取版本号,例如:
26
+ 4. 若当前分支不匹配 `^release/v\d+\.\d+\.\d+(-(alpha|beta|rc)\.\d+)?$`,执行以下自动建分支流程(仅此场景执行):
27
+ - 同步远程与本地 `main`:`git fetch origin main --tags && git checkout main && git pull --ff-only origin main`。
28
+ - 获取上一个已发布版本(优先):`gh release list --limit 1 --json tagName,publishedAt --jq '.[0].tagName'`;若为空则回退 `git describe --tags --abbrev=0`。
29
+ - 解析版本号并将最后一位加一(例如 `v1.2.3 -> v1.2.4`),得到新分支版本 `<NEXT_VERSION>`。
30
+ - 创建并切换分支:`git checkout -b release/v<NEXT_VERSION>`。
31
+ 5. 再次检查当前分支,必须匹配:`^release/v\d+\.\d+\.\d+(-(alpha|beta|rc)\.\d+)?$`。
32
+ 6. 从分支名提取版本号,例如:
28
33
  - `release/v1.2.3` -> `v1.2.3` -> `1.2.3`
29
34
  - `release/v1.2.3-beta.2` -> `v1.2.3-beta.2` -> `1.2.3-beta.2`
30
- 6. 检查目标 tag 是否已存在:`git tag -l "v<VERSION>"`。
31
- 7. 向用户确认版本号;若用户修改版本号,重新校验格式与 tag 冲突。
35
+ 7. 检查目标 tag 是否已存在:`git tag -l "v<VERSION>"`。
36
+ 8. 向用户确认版本号;若用户修改版本号,重新校验格式与 tag 冲突。
32
37
 
33
38
  ### 二、更新版本号
34
39
 
@@ -156,7 +161,7 @@ EOF
156
161
  以下任一情况出现时终止流程并给出明确原因:
157
162
 
158
163
  - 工作区存在未提交修改。
159
- - 当前分支不符合 release 分支命名规则。
164
+ - 当前分支不符合 release 分支命名规则,且无法从 `main` 自动创建 release 分支。
160
165
  - 版本号格式非法或与现有 tag 冲突。
161
166
  - 自上次发布以来无新提交。
162
167
 
@@ -0,0 +1,82 @@
1
+ ---
2
+ name: online-debug-guard
3
+ description: 在线调试安全护栏技能。用于排查数据库、Redis、服务配置等线上或准线上问题时,强制执行环境识别、模式校验和本地环境文件存在性检查,防止误连误操作。触发场景:用户提到在线调试、连库排查、查 Redis、生产环境排障、按指定环境排查问题。
4
+ ---
5
+
6
+ # 在线调试安全护栏
7
+
8
+ ## 概览
9
+
10
+ 执行在线调试前先做硬性门禁,未通过即停止。仅在门禁通过后再进行排查动作,默认使用只读方式获取信息。
11
+
12
+ ## 执行流程(必须按顺序)
13
+
14
+ 1. 识别目标环境
15
+ 2. 校验协作模式与环境文件
16
+ 3. 门禁通过后执行调试
17
+
18
+ 任一步失败都必须立即终止,不得继续后续步骤。
19
+
20
+ ## 步骤 1:识别目标环境
21
+
22
+ 只接受 `development`、`staging`、`production` 三种值。
23
+
24
+ 如果用户调用技能时未显式给出环境:
25
+ - 先询问用户当前环境(只给这三个选项)。
26
+ - 若用户未回复或信息仍不明确,则默认 `development`,并明确告知本次按 `development` 执行。
27
+
28
+ 如果用户给了其他环境名(例如 `prod1`、`test`):
29
+ - 立即终止。
30
+ - 明确提示“环境无效,仅支持 development/staging/production”。
31
+
32
+ ## 步骤 2:门禁校验
33
+
34
+ ### 2.1 production 环境的模式限制
35
+
36
+ 当环境是 `production` 时,必须先确认当前处于 `Plan` 模式。
37
+
38
+ 如果不是 `Plan` 模式:
39
+ - 立即终止。
40
+ - 使用如下提示:
41
+
42
+ ```text
43
+ 已终止:当前为 production 环境,但会话不在 Plan 模式。
44
+ 请切换到 Plan 模式后再继续在线调试。
45
+ ```
46
+
47
+ ### 2.2 环境文件存在性校验
48
+
49
+ 将项目根目录作为基准,检查以下文件:
50
+ - `.env.${ENV_NAME}`
51
+ - `.env.${ENV_NAME}.local`
52
+
53
+ 校验规则:
54
+ - 两个文件都必须存在。
55
+ - 若 `.env.${ENV_NAME}.local` 不存在,视为环境指定有误,立即终止。
56
+
57
+ 终止提示模板:
58
+
59
+ ```text
60
+ 已终止:未找到 .env.${ENV_NAME}.local,判定环境指定有误。
61
+ 请确认环境仅为 development/staging/production,且对应本地覆盖文件存在。
62
+ ```
63
+
64
+ ## 步骤 3:在线调试执行规范
65
+
66
+ 门禁通过后,才允许执行调试。
67
+
68
+ 调试时遵循:
69
+ - 默认只读:优先使用查询、检查、对比,不做写入。
70
+ - 允许使用环境文件中的连接信息访问数据库、Redis 等依赖来定位问题。
71
+ - 在未获用户明确授权前,禁止执行会改变状态的操作(例如写库、删键、迁移、重启、发布)。
72
+ - 输出结论时必须包含:目标环境、已通过的门禁项、关键证据、下一步建议。
73
+
74
+ ## 标准开场模板
75
+
76
+ ```text
77
+ 开始在线调试前先执行安全门禁:
78
+ 1) 确认环境(development/staging/production)
79
+ 2) production 场景校验是否为 Plan 模式
80
+ 3) 校验 .env.${ENV_NAME} 与 .env.${ENV_NAME}.local 是否存在
81
+ 任一失败将立即终止,避免误操作。
82
+ ```
@@ -0,0 +1,4 @@
1
+ interface:
2
+ display_name: "Online Debug Guard"
3
+ short_description: "Guarded workflow for environment-aware online debugging"
4
+ default_prompt: "执行安全的在线调试流程,先判定环境与模式,再执行只读排查。"
@@ -40,7 +40,7 @@ export function parseTelegramWebhookFlags(argv = []) {
40
40
 
41
41
  const dryRun = args.includes('--webhook-dry-run') ? true : undefined
42
42
 
43
- // 默认值由下游根据 environment 决定,这里只负责覆盖
43
+ // 默认值由下游决定(当前为默认严格),这里仅处理显式覆盖
44
44
  const strict = args.includes('--strict-webhook')
45
45
  ? true
46
46
  : args.includes('--no-strict-webhook')
package/lib/cli/dx-cli.js CHANGED
@@ -295,6 +295,13 @@ class DxCli {
295
295
 
296
296
  this.validateInputs()
297
297
 
298
+ // Fail fast for unknown commands before dependency/env startup checks.
299
+ if (this.command && !this.commandHandlers[this.command]) {
300
+ logger.error(`未知命令: ${this.command}`)
301
+ showHelp()
302
+ process.exit(1)
303
+ }
304
+
298
305
  // Commands that should work without env/dependency checks.
299
306
  // - help: printing help should never require env vars.
300
307
  // - status: should be available even when env is incomplete.
@@ -344,11 +351,7 @@ class DxCli {
344
351
  }
345
352
 
346
353
  const handler = this.commandHandlers[command]
347
- if (!handler) {
348
- logger.error(`未知命令: ${command}`)
349
- showHelp()
350
- process.exit(1)
351
- }
354
+ if (!handler) return
352
355
 
353
356
  await handler(subArgs)
354
357
  }
package/lib/cli/help.js CHANGED
@@ -194,7 +194,7 @@ script 子命令:
194
194
  Telegram Webhook(仅 target=telegram-bot 生效):
195
195
  --webhook-path <path> 对外 webhook 路径(默认 /api/webhook)
196
196
  --webhook-dry-run 只打印将设置的 URL,不调用 Telegram API
197
- --strict-webhook 强制严格校验(Webhook 不生效则 deploy 失败)
197
+ --strict-webhook 显式开启严格校验(默认已严格)
198
198
  --no-strict-webhook 关闭严格校验(仅告警)
199
199
 
200
200
  常见示例:
@@ -203,7 +203,7 @@ script 子命令:
203
203
  dx deploy telegram-bot --staging # 部署 Telegram Bot + 自动配置 Webhook
204
204
  dx deploy telegram-bot --staging --webhook-path /webhook # 使用短路径(rewrite 到 /api/webhook)
205
205
  dx deploy telegram-bot --prod --webhook-dry-run # 仅打印,不实际调用 Telegram
206
- dx deploy telegram-bot --prod --no-strict-webhook # 生产环境也仅告警(不推荐)
206
+ dx deploy telegram-bot --dev --no-strict-webhook # 开发环境显式降级为仅告警
207
207
  dx deploy all --staging # 串行部署 front + admin
208
208
  `)
209
209
  return
@@ -17,11 +17,35 @@ function normalizeDeployUrl(raw) {
17
17
  }
18
18
 
19
19
  export function parseDeployUrlFromDeployOutput(output) {
20
- const s = String(output || '')
21
- const matches = [...s.matchAll(/(https?:\/\/)?([a-z0-9-]+\.vercel\.app)\b/gi)]
22
- if (matches.length === 0) return null
23
- const last = matches[matches.length - 1]
24
- return `https://${last[2]}`
20
+ const lines = String(output || '')
21
+ .split(/\r?\n/)
22
+ .map(line => String(line || '').trim())
23
+ .filter(Boolean)
24
+
25
+ const pickUrl = line => {
26
+ const m = line.match(/(https?:\/\/)?([a-z0-9-]+\.vercel\.app)\b/i)
27
+ return m ? `https://${m[2]}` : null
28
+ }
29
+
30
+ for (const line of lines) {
31
+ if (!/^production\s*:/i.test(line)) continue
32
+ const url = pickUrl(line)
33
+ if (url) return url
34
+ }
35
+
36
+ for (const line of lines) {
37
+ if (!/^preview\s*:/i.test(line)) continue
38
+ const url = pickUrl(line)
39
+ if (url) return url
40
+ }
41
+
42
+ for (const line of lines) {
43
+ if (/to deploy to production/i.test(line)) continue
44
+ const url = pickUrl(line)
45
+ if (url) return url
46
+ }
47
+
48
+ return null
25
49
  }
26
50
 
27
51
  export function parseDeployUrlFromVercelListOutput(output, projectNameHint) {
@@ -72,7 +96,7 @@ export async function handleTelegramBotDeploy(environment, projectId, orgId, tok
72
96
  strict: strictOverride,
73
97
  } = options || {}
74
98
 
75
- const strictDefault = environment !== 'development'
99
+ const strictDefault = true
76
100
 
77
101
  const strictEnv = process.env.DX_TELEGRAM_WEBHOOK_STRICT != null
78
102
  ? !['0', 'false', 'no'].includes(String(process.env.DX_TELEGRAM_WEBHOOK_STRICT).toLowerCase())
@@ -103,12 +127,25 @@ export async function handleTelegramBotDeploy(environment, projectId, orgId, tok
103
127
  logger.error(` - ${v}`)
104
128
  })
105
129
 
130
+ const message = 'Telegram Webhook 配置失败:缺少必需环境变量'
106
131
  if (strict) {
107
- throw new Error('Telegram Webhook 配置失败:缺少必需环境变量')
132
+ return {
133
+ status: 'failed',
134
+ reason: 'missing_env_vars',
135
+ strict,
136
+ message,
137
+ missingVars,
138
+ }
108
139
  }
109
140
 
110
141
  logger.warn('跳过 Webhook 配置,请手动设置')
111
- return
142
+ return {
143
+ status: 'warning',
144
+ reason: 'missing_env_vars',
145
+ strict,
146
+ message,
147
+ missingVars,
148
+ }
112
149
  }
113
150
 
114
151
  try {
@@ -122,12 +159,22 @@ export async function handleTelegramBotDeploy(environment, projectId, orgId, tok
122
159
  projectNameHint,
123
160
  })
124
161
  if (!deploymentUrl) {
162
+ const message = '无法获取 Vercel 部署 URL'
125
163
  if (strict) {
126
- throw new Error('无法获取 Vercel 部署 URL')
164
+ return {
165
+ status: 'failed',
166
+ reason: 'missing_deploy_url',
167
+ strict,
168
+ message,
169
+ }
127
170
  }
128
-
129
171
  logger.error('无法获取 Vercel 部署 URL,跳过 Webhook 配置')
130
- return
172
+ return {
173
+ status: 'warning',
174
+ reason: 'missing_deploy_url',
175
+ strict,
176
+ message,
177
+ }
131
178
  }
132
179
 
133
180
  const webhookUrl = `${deploymentUrl}${webhookPath}`
@@ -135,7 +182,13 @@ export async function handleTelegramBotDeploy(environment, projectId, orgId, tok
135
182
 
136
183
  if (dryRun) {
137
184
  logger.warn('DX_TELEGRAM_WEBHOOK_DRY_RUN=1,已跳过 setWebhook/getWebhookInfo 调用')
138
- return
185
+ return {
186
+ status: 'warning',
187
+ reason: 'dry_run',
188
+ strict,
189
+ message: 'DX_TELEGRAM_WEBHOOK_DRY_RUN=1',
190
+ webhookUrl,
191
+ }
139
192
  }
140
193
 
141
194
  // 3. 调用 Telegram API 设置 Webhook
@@ -163,15 +216,38 @@ export async function handleTelegramBotDeploy(environment, projectId, orgId, tok
163
216
  logger.info(`Webhook URL: ${webhookUrl}`)
164
217
 
165
218
  // 4. 验证 Webhook 状态
166
- await verifyWebhook(botToken, webhookUrl, { strict })
219
+ const verifyResult = await verifyWebhook(botToken, webhookUrl, { strict })
220
+ if (verifyResult.status === 'success') {
221
+ return {
222
+ status: 'success',
223
+ reason: 'verified',
224
+ strict,
225
+ webhookUrl,
226
+ }
227
+ }
228
+
229
+ return {
230
+ status: strict ? 'failed' : 'warning',
231
+ reason: 'verify_failed',
232
+ strict,
233
+ message: verifyResult.message,
234
+ webhookUrl,
235
+ }
167
236
  }
168
237
  else {
169
238
  const desc = result.description || '未知错误'
239
+ const message = `Telegram Webhook 设置失败: ${desc}`
170
240
  if (strict) {
171
- throw new Error(`Telegram Webhook 设置失败: ${desc}`)
241
+ return {
242
+ status: 'failed',
243
+ reason: 'set_webhook_failed',
244
+ strict,
245
+ message,
246
+ webhookUrl,
247
+ }
172
248
  }
173
249
 
174
- logger.error(`Telegram Webhook 设置失败: ${desc}`)
250
+ logger.error(message)
175
251
  logger.info('请手动执行以下命令(不要把明文 token/secret 写进日志):')
176
252
  const manualPayload = JSON.stringify({
177
253
  url: webhookUrl,
@@ -181,15 +257,28 @@ export async function handleTelegramBotDeploy(environment, projectId, orgId, tok
181
257
  logger.info(
182
258
  `curl -X POST "https://api.telegram.org/bot<YOUR_BOT_TOKEN>/setWebhook" -H "Content-Type: application/json" -d '${manualPayload}' --silent`,
183
259
  )
260
+ return {
261
+ status: 'warning',
262
+ reason: 'set_webhook_failed',
263
+ strict,
264
+ message,
265
+ webhookUrl,
266
+ }
184
267
  }
185
268
  }
186
269
  catch (error) {
187
270
  const message = error?.message || String(error)
188
271
  logger.error(`Webhook 配置失败: ${message}`)
189
272
 
190
- if (strict) throw error
191
-
192
- logger.warn('请手动设置 Webhook(参考 apps/telegram-bot/README.md)')
273
+ if (!strict) {
274
+ logger.warn('请手动设置 Webhook(参考 apps/telegram-bot/README.md)')
275
+ }
276
+ return {
277
+ status: strict ? 'failed' : 'warning',
278
+ reason: 'runtime_error',
279
+ strict,
280
+ message,
281
+ }
193
282
  }
194
283
  }
195
284
 
@@ -315,19 +404,28 @@ async function verifyWebhook(botToken, expectedWebhookUrl, options = {}) {
315
404
 
316
405
  if (expectedWebhookUrl && info.url !== expectedWebhookUrl) {
317
406
  const message = `Webhook 未生效:期望 ${expectedWebhookUrl},实际 ${info.url || '(empty)'}`
318
- if (strict) throw new Error(message)
407
+ if (strict) {
408
+ return { status: 'failed', message }
409
+ }
319
410
  logger.warn(message)
411
+ return { status: 'warning', message }
320
412
  }
413
+ return { status: 'success' }
321
414
  }
322
415
  else {
323
416
  const desc = result?.description || '未知错误'
324
417
  const message = `getWebhookInfo 失败: ${desc}`
325
- if (strict) throw new Error(message)
418
+ if (strict) {
419
+ return { status: 'failed', message }
420
+ }
326
421
  logger.warn(message)
422
+ return { status: 'warning', message }
327
423
  }
328
424
  }
329
425
  catch (error) {
330
- if (strict) throw error
426
+ const message = error?.message || String(error)
427
+ if (strict) return { status: 'failed', message }
331
428
  logger.warn('无法验证 Webhook 状态')
429
+ return { status: 'warning', message: '无法验证 Webhook 状态' }
332
430
  }
333
431
  }
@@ -260,6 +260,7 @@ export async function deployToVercel(target, options = {}) {
260
260
  const {
261
261
  environment = 'staging',
262
262
  telegramWebhook = null,
263
+ telegramWebhookHandler = null,
263
264
  strictContext = true,
264
265
  run = runVercel
265
266
  } = options
@@ -482,13 +483,26 @@ export async function deployToVercel(target, options = {}) {
482
483
 
483
484
  // Telegram Bot 部署成功后自动设置 Webhook(并做严格校验)
484
485
  if (t === 'telegram-bot') {
485
- const { handleTelegramBotDeploy } = await import('./telegram-webhook.js')
486
- await handleTelegramBotDeploy(environment, projectId, orgId, token, {
486
+ const resolvedWebhookHandler = telegramWebhookHandler
487
+ || (await import('./telegram-webhook.js')).handleTelegramBotDeploy
488
+ const webhookResult = await resolvedWebhookHandler(environment, projectId, orgId, token, {
487
489
  deployOutput,
488
490
  projectNameHint: 'telegram-bot',
489
491
  ...(telegramWebhook || {})
490
492
  })
491
- logger.success(`${t} 部署成功(Webhook 已校验)`)
493
+
494
+ if (webhookResult?.status === 'success') {
495
+ logger.success(`${t} 部署成功(Webhook 已校验)`)
496
+ } else if (webhookResult?.status === 'failed') {
497
+ const reason = webhookResult?.message || webhookResult?.reason || '未知原因'
498
+ logger.error(`${t} 部署成功,但 Webhook 配置失败: ${reason}`)
499
+ process.exitCode = 1
500
+ return
501
+ } else {
502
+ const reason = webhookResult?.message || webhookResult?.reason || '未严格校验'
503
+ logger.warn(`${t} 部署成功,但 Webhook 未完成校验: ${reason}`)
504
+ logger.success(`${t} 部署成功`)
505
+ }
492
506
  } else {
493
507
  logger.success(`${t} 部署成功`)
494
508
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ranger1/dx",
3
- "version": "0.1.66",
3
+ "version": "0.1.68",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "repository": {