@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.
- package/codex/skills/doctor/SKILL.md +45 -0
- package/codex/skills/doctor/agents/openai.yaml +4 -0
- package/codex/skills/doctor/scripts/doctor.sh +386 -0
- package/codex/skills/git-release/SKILL.md +11 -6
- package/codex/skills/online-debug-guard/SKILL.md +82 -0
- package/codex/skills/online-debug-guard/agents/openai.yaml +4 -0
- package/lib/cli/commands/deploy.js +1 -1
- package/lib/cli/dx-cli.js +8 -5
- package/lib/cli/help.js +2 -2
- package/lib/telegram-webhook.js +119 -21
- package/lib/vercel-deploy.js +17 -3
- package/package.json +1 -1
|
@@ -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,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.
|
|
27
|
-
|
|
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
|
-
|
|
31
|
-
|
|
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
|
+
```
|
|
@@ -40,7 +40,7 @@ export function parseTelegramWebhookFlags(argv = []) {
|
|
|
40
40
|
|
|
41
41
|
const dryRun = args.includes('--webhook-dry-run') ? true : undefined
|
|
42
42
|
|
|
43
|
-
//
|
|
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
|
|
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 --
|
|
206
|
+
dx deploy telegram-bot --dev --no-strict-webhook # 开发环境显式降级为仅告警
|
|
207
207
|
dx deploy all --staging # 串行部署 front + admin
|
|
208
208
|
`)
|
|
209
209
|
return
|
package/lib/telegram-webhook.js
CHANGED
|
@@ -17,11 +17,35 @@ function normalizeDeployUrl(raw) {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
export function parseDeployUrlFromDeployOutput(output) {
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
241
|
+
return {
|
|
242
|
+
status: 'failed',
|
|
243
|
+
reason: 'set_webhook_failed',
|
|
244
|
+
strict,
|
|
245
|
+
message,
|
|
246
|
+
webhookUrl,
|
|
247
|
+
}
|
|
172
248
|
}
|
|
173
249
|
|
|
174
|
-
logger.error(
|
|
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)
|
|
191
|
-
|
|
192
|
-
|
|
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)
|
|
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)
|
|
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
|
-
|
|
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
|
}
|
package/lib/vercel-deploy.js
CHANGED
|
@@ -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
|
|
486
|
-
|
|
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
|
-
|
|
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
|
}
|