返回 CodeWhale
doctor.sh
1 #!/usr/bin/env bash
2 set -euo pipefail
3
4 CODEWHALE_USER="${CODEWHALE_USER:-${DEEPSEEK_USER:-codewhale}}"
5 CODEWHALE_ROOT="${CODEWHALE_ROOT:-${DEEPSEEK_ROOT:-/opt/codewhale}}"
6 WHALEBRO_ROOT="${WHALEBRO_ROOT:-/opt/whalebro}"
7 if [[ -z "${RUNTIME_ENV:-}" ]]; then
8 if [[ -f /etc/codewhale/runtime.env || ! -f /etc/deepseek/runtime.env ]]; then
9 RUNTIME_ENV="/etc/codewhale/runtime.env"
10 else
11 RUNTIME_ENV="/etc/deepseek/runtime.env"
12 fi
13 fi
14 REPO_ROOT="${REPO_ROOT:-${WHALEBRO_ROOT}/codewhale}"
15 BRIDGE_KIND="${CODEWHALE_BRIDGE:-${DEEPSEEK_BRIDGE:-feishu}}"
16
17 case "${BRIDGE_KIND}" in
18 feishu|lark)
19 if [[ -z "${BRIDGE_ENV:-}" ]]; then
20 if [[ -f /etc/codewhale/feishu-bridge.env || ! -f /etc/deepseek/feishu-bridge.env ]]; then
21 BRIDGE_ENV="/etc/codewhale/feishu-bridge.env"
22 else
23 BRIDGE_ENV="/etc/deepseek/feishu-bridge.env"
24 fi
25 fi
26 BRIDGE_DIR="${BRIDGE_DIR:-${CODEWHALE_ROOT}/bridge}"
27 BRIDGE_UNIT="${BRIDGE_UNIT:-codewhale-feishu-bridge}"
28 BRIDGE_PACKAGE="${BRIDGE_PACKAGE:-integrations/feishu-bridge}"
29 ;;
30 telegram)
31 if [[ -z "${BRIDGE_ENV:-}" ]]; then
32 if [[ -f /etc/codewhale/telegram-bridge.env || ! -f /etc/deepseek/telegram-bridge.env ]]; then
33 BRIDGE_ENV="/etc/codewhale/telegram-bridge.env"
34 else
35 BRIDGE_ENV="/etc/deepseek/telegram-bridge.env"
36 fi
37 fi
38 BRIDGE_DIR="${BRIDGE_DIR:-${CODEWHALE_ROOT}/telegram-bridge}"
39 BRIDGE_UNIT="${BRIDGE_UNIT:-codewhale-telegram-bridge}"
40 BRIDGE_PACKAGE="${BRIDGE_PACKAGE:-integrations/telegram-bridge}"
41 ;;
42 *)
43 echo "Unknown bridge '${BRIDGE_KIND}'. Use CODEWHALE_BRIDGE=feishu or CODEWHALE_BRIDGE=telegram." >&2
44 exit 1
45 ;;
46 esac
47
48 failures=0
49 warnings=0
50
51 section() {
52 printf '\n== %s ==\n' "$1"
53 }
54
55 pass() {
56 printf '[ok] %s\n' "$1"
57 }
58
59 warn() {
60 warnings=$((warnings + 1))
61 printf '[warn] %s\n' "$1"
62 }
63
64 fail() {
65 failures=$((failures + 1))
66 printf '[fail] %s\n' "$1"
67 }
68
69 have_command() {
70 command -v "$1" >/dev/null 2>&1
71 }
72
73 env_value() {
74 local file="$1"
75 local key="$2"
76 [[ -f "${file}" ]] || return 0
77 grep -E "^[[:space:]]*(export[[:space:]]+)?${key}=" "${file}" \
78 | tail -n 1 \
79 | sed -E "s/^[[:space:]]*(export[[:space:]]+)?${key}=//; s/^[[:space:]]+//; s/[[:space:]]+$//; s/^['\"]//; s/['\"]$//" \
80 || true
81 }
82
83 env_value_any() {
84 local file="$1"
85 shift
86 local value
87 for key in "$@"; do
88 value="$(env_value "${file}" "${key}")"
89 if [[ -n "${value}" ]]; then
90 printf '%s\n' "${value}"
91 return 0
92 fi
93 done
94 return 0
95 }
96
97 is_placeholder() {
98 local value
99 value="$(printf '%s' "${1:-}" | tr '[:upper:]' '[:lower:]')"
100 [[ -z "${value}" || "${value}" == *replace-with* || "${value}" == *xxxxxxxx* || "${value}" == "changeme" ]]
101 }
102
103 file_mode() {
104 if stat -c '%a' "$1" >/dev/null 2>&1; then
105 stat -c '%a' "$1"
106 else
107 stat -f '%Lp' "$1"
108 fi
109 }
110
111 check_commands() {
112 section "Runtime tools"
113 for cmd in git curl node npm systemctl ss; do
114 if have_command "${cmd}"; then
115 pass "${cmd} is installed"
116 else
117 warn "${cmd} is not on PATH"
118 fi
119 done
120 }
121
122 check_node() {
123 section "Node"
124 if ! have_command node; then
125 fail "node is required for the phone bridge"
126 return
127 fi
128 local major
129 major="$(node -p "Number(process.versions.node.split('.')[0])" 2>/dev/null || echo 0)"
130 if [[ "${major}" =~ ^[0-9]+$ ]] && (( major >= 18 )); then
131 pass "Node.js major version is ${major}"
132 else
133 fail "Node.js 18+ is required; found ${major}"
134 fi
135 }
136
137 check_workspace() {
138 section "Workspace"
139 [[ -d "${WHALEBRO_ROOT}" ]] && pass "${WHALEBRO_ROOT} exists" || fail "${WHALEBRO_ROOT} is missing"
140 [[ -d "${REPO_ROOT}/.git" ]] && pass "${REPO_ROOT} is a git checkout" || fail "${REPO_ROOT} is not a git checkout"
141 [[ -d "${WHALEBRO_ROOT}/worktrees" ]] && pass "${WHALEBRO_ROOT}/worktrees exists" || warn "${WHALEBRO_ROOT}/worktrees is missing"
142 if [[ -f "${WHALEBRO_ROOT}/AGENTS.md" ]]; then
143 pass "${WHALEBRO_ROOT}/AGENTS.md exists"
144 else
145 warn "${WHALEBRO_ROOT}/AGENTS.md is missing"
146 fi
147 }
148
149 check_binaries() {
150 section "CodeWhale binaries"
151 local cargo_bin="/home/${CODEWHALE_USER}/.cargo/bin"
152 local codewhale="${cargo_bin}/codewhale"
153 local tui="${cargo_bin}/codewhale-tui"
154 if [[ -x "${codewhale}" ]]; then
155 pass "${codewhale} is executable"
156 "${codewhale}" --version 2>/dev/null | sed 's/^/[info] codewhale version: /' || warn "codewhale --version failed"
157 else
158 fail "${codewhale} is missing or not executable"
159 fi
160 if [[ -x "${tui}" ]]; then
161 pass "${tui} is executable"
162 "${tui}" --version 2>/dev/null | sed 's/^/[info] codewhale-tui version: /' || warn "codewhale-tui --version failed"
163 else
164 fail "${tui} is missing or not executable"
165 fi
166 }
167
168 check_env_file() {
169 local file="$1"
170 local label="$2"
171 if [[ ! -f "${file}" ]]; then
172 fail "${label} env file is missing: ${file}"
173 return
174 fi
175 pass "${label} env file exists"
176 local mode
177 mode="$(file_mode "${file}")"
178 local world="${mode: -1}"
179 if [[ "${world}" =~ ^[0-9]+$ ]] && (( world > 0 )); then
180 fail "${label} env file is world-readable (${mode})"
181 else
182 pass "${label} env file is not world-readable (${mode})"
183 fi
184 }
185
186 check_env() {
187 section "Environment"
188 check_env_file "${RUNTIME_ENV}" "runtime"
189 check_env_file "${BRIDGE_ENV}" "bridge"
190
191 local runtime_token bridge_token workspace domain allow_groups allow_unlisted provider
192 runtime_token="$(env_value_any "${RUNTIME_ENV}" CODEWHALE_RUNTIME_TOKEN DEEPSEEK_RUNTIME_TOKEN)"
193 bridge_token="$(env_value_any "${BRIDGE_ENV}" CODEWHALE_RUNTIME_TOKEN DEEPSEEK_RUNTIME_TOKEN)"
194 workspace="$(env_value_any "${BRIDGE_ENV}" CODEWHALE_WORKSPACE DEEPSEEK_WORKSPACE)"
195 provider="$(env_value_any "${RUNTIME_ENV}" CODEWHALE_PROVIDER DEEPSEEK_PROVIDER)"
196
197 if [[ "${BRIDGE_KIND}" == "telegram" ]]; then
198 allow_groups="$(env_value "${BRIDGE_ENV}" TELEGRAM_ALLOW_GROUPS)"
199 allow_unlisted="$(env_value_any "${BRIDGE_ENV}" TELEGRAM_ALLOW_UNLISTED CODEWHALE_ALLOW_UNLISTED DEEPSEEK_ALLOW_UNLISTED)"
200 else
201 domain="$(env_value "${BRIDGE_ENV}" FEISHU_DOMAIN)"
202 allow_groups="$(env_value "${BRIDGE_ENV}" FEISHU_ALLOW_GROUPS)"
203 allow_unlisted="$(env_value_any "${BRIDGE_ENV}" CODEWHALE_ALLOW_UNLISTED DEEPSEEK_ALLOW_UNLISTED)"
204 fi
205
206 if is_placeholder "${runtime_token}"; then
207 fail "runtime token is missing or still a placeholder"
208 else
209 pass "runtime token is set"
210 fi
211 if is_placeholder "${bridge_token}"; then
212 fail "bridge token is missing or still a placeholder"
213 else
214 pass "bridge token is set"
215 fi
216 if [[ -n "${runtime_token}" && -n "${bridge_token}" && "${runtime_token}" != "${bridge_token}" ]]; then
217 fail "runtime and bridge tokens do not match"
218 elif [[ -n "${runtime_token}" && -n "${bridge_token}" ]]; then
219 pass "runtime and bridge tokens match"
220 fi
221 if is_placeholder "${provider}"; then
222 warn "runtime provider is missing or still a placeholder"
223 else
224 pass "runtime provider is ${provider}"
225 fi
226 [[ "${workspace}" == "${WHALEBRO_ROOT}" || "${workspace}" == "${WHALEBRO_ROOT}/"* ]] \
227 && pass "bridge workspace is under ${WHALEBRO_ROOT}" \
228 || warn "bridge workspace is outside ${WHALEBRO_ROOT}: ${workspace:-unset}"
229 if [[ "${BRIDGE_KIND}" != "telegram" ]]; then
230 [[ "${domain:-feishu}" == "feishu" || "${domain:-feishu}" == "lark" || "${domain:-feishu}" == https://open.* ]] \
231 && pass "FEISHU_DOMAIN is ${domain:-feishu}" \
232 || fail "FEISHU_DOMAIN must be feishu, lark, or an https://open.* URL"
233 fi
234 [[ "${allow_groups:-false}" == "true" && "${allow_unlisted:-false}" == "true" ]] \
235 && fail "group control cannot run with allow-unlisted=true" \
236 || pass "group/unlisted mode is not openly combined"
237 }
238
239 check_validator() {
240 section "Bridge config validator"
241 local validator="${BRIDGE_DIR}/scripts/validate-config.mjs"
242 if [[ ! -f "${validator}" ]]; then
243 validator="${REPO_ROOT}/${BRIDGE_PACKAGE}/scripts/validate-config.mjs"
244 fi
245 if [[ ! -f "${validator}" ]]; then
246 warn "bridge config validator is not installed"
247 return
248 fi
249 local runner=(node)
250 if [[ "${EUID}" -eq 0 ]] && id -u "${CODEWHALE_USER}" >/dev/null 2>&1 && have_command sudo; then
251 runner=(sudo -u "${CODEWHALE_USER}" node)
252 fi
253 if "${runner[@]}" "${validator}" --env "${BRIDGE_ENV}" --runtime-env "${RUNTIME_ENV}" --workspace-root "${WHALEBRO_ROOT}" --check-filesystem; then
254 pass "bridge config validator passed"
255 else
256 fail "bridge config validator reported blocking issues"
257 fi
258 }
259
260 check_systemd() {
261 section "systemd"
262 if ! have_command systemctl || [[ ! -d /run/systemd/system ]]; then
263 warn "systemd is not available in this environment"
264 return
265 fi
266 for unit in codewhale-runtime "${BRIDGE_UNIT}"; do
267 [[ -f "/etc/systemd/system/${unit}.service" ]] \
268 && pass "${unit}.service is installed" \
269 || fail "${unit}.service is missing"
270 systemctl is-enabled --quiet "${unit}" \
271 && pass "${unit} is enabled" \
272 || warn "${unit} is not enabled"
273 systemctl is-active --quiet "${unit}" \
274 && pass "${unit} is active" \
275 || fail "${unit} is not active"
276 done
277 }
278
279 check_bridge_install() {
280 section "Bridge install"
281 [[ -f "${BRIDGE_DIR}/package.json" ]] && pass "${BRIDGE_DIR}/package.json exists" || fail "bridge package.json is missing"
282 [[ -f "${BRIDGE_DIR}/src/index.mjs" ]] && pass "${BRIDGE_DIR}/src/index.mjs exists" || fail "bridge entrypoint is missing"
283 if [[ "${BRIDGE_KIND}" == "telegram" ]]; then
284 pass "Telegram bridge has no required production npm dependencies"
285 elif [[ -d "${BRIDGE_DIR}/node_modules/@larksuiteoapi/node-sdk" ]]; then
286 pass "Lark SDK dependency is installed"
287 else
288 warn "Lark SDK dependency is not installed under ${BRIDGE_DIR}/node_modules"
289 fi
290 }
291
292 check_localhost_health() {
293 section "Localhost health"
294 local port token
295 port="$(env_value_any "${RUNTIME_ENV}" CODEWHALE_RUNTIME_PORT DEEPSEEK_RUNTIME_PORT)"
296 port="${port:-7878}"
297 token="$(env_value_any "${BRIDGE_ENV}" CODEWHALE_RUNTIME_TOKEN DEEPSEEK_RUNTIME_TOKEN)"
298
299 if have_command ss; then
300 local listeners
301 listeners="$(ss -ltn 2>/dev/null | awk -v port=":${port}" '$4 ~ port {print $4}' || true)"
302 if grep -qE "^127\\.0\\.0\\.1:${port}$|^\\[::1\\]:${port}$" <<<"${listeners}"; then
303 pass "runtime port ${port} is bound to localhost"
304 elif [[ -n "${listeners}" ]]; then
305 fail "runtime port ${port} is listening on a non-local address: ${listeners//$'\n'/, }"
306 else
307 fail "runtime port ${port} is not listening"
308 fi
309 else
310 warn "ss is unavailable; skipping bind-address check"
311 fi
312
313 if ! have_command curl; then
314 warn "curl is unavailable; skipping HTTP checks"
315 return
316 fi
317
318 if curl -fsS --max-time 3 "http://127.0.0.1:${port}/health" >/dev/null; then
319 pass "/health responds on localhost"
320 else
321 fail "/health did not respond on localhost:${port}"
322 fi
323
324 if is_placeholder "${token}"; then
325 warn "runtime token is not usable; skipping /v1/runtime/info auth check"
326 return
327 fi
328
329 local tmp
330 tmp="$(mktemp)"
331 if curl -fsS --max-time 3 -H "Authorization: Bearer ${token}" \
332 "http://127.0.0.1:${port}/v1/runtime/info" >"${tmp}"; then
333 if node -e '
334 const fs = require("fs");
335 const data = JSON.parse(fs.readFileSync(process.argv[1], "utf8"));
336 if (data.bind_host !== "127.0.0.1") process.exit(2);
337 if (data.auth_required !== true) process.exit(3);
338 ' "${tmp}"; then
339 pass "/v1/runtime/info reports localhost bind and auth_required=true"
340 else
341 fail "/v1/runtime/info did not report localhost bind with auth enabled"
342 fi
343 else
344 fail "/v1/runtime/info did not respond with bearer auth"
345 fi
346 rm -f "${tmp}"
347 }
348
349 main() {
350 printf 'Tencent Lighthouse CodeWhale doctor (%s bridge)\n' "${BRIDGE_KIND}"
351 check_commands
352 check_node
353 check_workspace
354 check_binaries
355 check_env
356 check_bridge_install
357 check_validator
358 check_systemd
359 check_localhost_health
360
361 section "Summary"
362 printf '%s failure(s), %s warning(s)\n' "${failures}" "${warnings}"
363 (( failures == 0 ))
364 }
365
366 main "$@"
367
367 lines BASH