forked from smilefufu/crabot
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdev.sh
More file actions
executable file
·450 lines (387 loc) · 15.6 KB
/
Copy pathdev.sh
File metadata and controls
executable file
·450 lines (387 loc) · 15.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
#!/bin/bash
# Crabot Dev - 开发模式启动脚本
# 用法: ./dev.sh 构建 + 启动 Module Manager(前台,含 Vite)
# ./dev.sh stop 停止所有开发服务
# ./dev.sh build 仅构建(含 Admin Web 前端 dist/web/),不启动
set -e
set -o pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
# PATH 兜底:脚本是非交互 shell,不会 source ~/.bashrc,若用户安装后在当前
# shell 直接跑 dev.sh,uv / 全局 crabot 会找不到。
if [ -d "$HOME/.local/bin" ]; then
case ":$PATH:" in
*":$HOME/.local/bin:"*) : ;;
*) export PATH="$HOME/.local/bin:$PATH" ;;
esac
fi
# dev 模式强制单实例:写死端口和 DATA_DIR
MM_PORT=19000
ADMIN_RPC_PORT=19001
ADMIN_WEB_PORT=3000
TMP_PAGE_PORT=19099 # 子模块池尾保留 slot(MM range_end=19098 让位),见 scripts/start.mjs
export CRABOT_TMP_PAGE_PORT=$TMP_PAGE_PORT
DATA_DIR="${DATA_DIR:-$SCRIPT_DIR/data}"
MEMORY_DIR="${MEMORY_DIR:-$SCRIPT_DIR/crabot-memory}"
# 颜色
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
DIM='\033[2m'
NC='\033[0m'
log_info() { echo -e "${GREEN}[dev]${NC} $1"; }
log_warn() { echo -e "${YELLOW}[dev]${NC} $1"; }
log_error() { echo -e "${RED}[dev]${NC} $1"; }
log_dim() { echo -e "${DIM}$1${NC}"; }
cleanup_placeholder_pnpm_workspaces() {
local file rel
for file in "$SCRIPT_DIR"/crabot-*/pnpm-workspace.yaml; do
[ -f "$file" ] || continue
if grep -q '^allowBuilds:' "$file" && grep -q 'set this to true or false' "$file"; then
rel="${file#$SCRIPT_DIR/}"
log_warn "移除 pnpm approve-builds 占位文件: $rel"
rm -f "$file"
fi
done
}
# ── 环境变量 ──────────────────────────────────────────────
load_env() {
for f in "$SCRIPT_DIR/.env"; do
if [ -f "$f" ]; then
while IFS= read -r line; do
line="${line%%#*}"
line="$(echo "$line" | xargs)"
if [ -n "$line" ]; then
key="${line%%=*}"
val="${line#*=}"
if [ -z "${!key}" ]; then
export "$key=$val"
fi
fi
done < "$f"
fi
done
export CRABOT_ADMIN_PASSWORD="${CRABOT_ADMIN_PASSWORD:-admin123}"
export CRABOT_JWT_SECRET="${CRABOT_JWT_SECRET:-$(openssl rand -hex 32 2>/dev/null || echo dev-secret)}"
export DATA_DIR="$DATA_DIR"
# NO_PROXY 兜底:用户开系统代理时,Python httpx (trust_env=True) 会把 localhost RPC
# 也走代理 → 502 Bad Gateway,memory 模块 register 失败卡在 starting。
local loopback="localhost,127.0.0.1,::1"
if [ -n "${NO_PROXY:-}" ]; then
case ",$NO_PROXY," in
*,localhost,*) ;;
*) export NO_PROXY="$NO_PROXY,$loopback" ;;
esac
else
export NO_PROXY="$loopback"
fi
export no_proxy="$NO_PROXY"
}
# ── Node 依赖同步 ─────────────────────────────────────────
#
# 策略:每个模块比较 pnpm-lock.yaml 与 node_modules/.modules.yaml 的 mtime,
# 仅当 lock 更新(git pull 拉到新版本)或 node_modules 缺失时才跑 pnpm install。
# 常态下整体 < 100ms(仅做 mtime 比较);有变更时仅对受影响模块并行同步。
NODE_MODULES_DIRS=(
crabot-shared
crabot-core
crabot-admin
crabot-admin/web
crabot-agent
crabot-channel-wechat
crabot-channel-telegram
crabot-channel-feishu
crabot-channel-dingtalk
crabot-mcp-tools
)
needs_sync() {
local mod="$1"
local lock="$SCRIPT_DIR/$mod/pnpm-lock.yaml"
local mark="$SCRIPT_DIR/$mod/node_modules/.modules.yaml"
[ ! -f "$lock" ] && return 1 # 无 lock 不在管控范围
[ ! -d "$SCRIPT_DIR/$mod/node_modules" ] && return 0 # 首次或被清空
[ ! -f "$mark" ] && return 0 # 之前可能是 npm 装的
[ "$lock" -nt "$mark" ] && return 0 # lock 新于上次安装
return 1
}
sync_node_deps() {
cleanup_placeholder_pnpm_workspaces
if ! command -v corepack &>/dev/null; then
log_warn "corepack 未找到(需要 Node 16.13+),跳过依赖同步"
log_dim " 首次安装请运行: ./install.sh --from-source"
return 0
fi
local pending=()
for mod in "${NODE_MODULES_DIRS[@]}"; do
[ -d "$SCRIPT_DIR/$mod" ] || continue
if needs_sync "$mod"; then
pending+=("$mod")
fi
done
if [ "${#pending[@]}" -eq 0 ]; then
return 0
fi
log_info "同步 Node 依赖(${#pending[@]} 个模块需要更新)..."
local pids=()
for mod in "${pending[@]}"; do
log_dim " $mod"
(cd "$SCRIPT_DIR/$mod" && corepack pnpm install --prefer-offline >/tmp/.crabot-dev-sync-"${mod//\//-}".log 2>&1) &
pids+=($!)
done
local fail=0
for i in "${!pids[@]}"; do
if ! wait "${pids[$i]}"; then
log_error "${pending[$i]} 同步失败:"
tail -10 "/tmp/.crabot-dev-sync-${pending[$i]//\//-}.log" | sed 's/^/ /'
fail=1
fi
done
rm -f /tmp/.crabot-dev-sync-*.log
[ "$fail" -eq 1 ] && exit 1
}
# ── 构建 ──────────────────────────────────────────────────
# crabot-shared 编译后,比对依赖方 .pnpm 缓存里的 dist 文件清单。
# 文件清单不一致(增删文件)的依赖方必须 install --force 重建 hard-link 集合。
sync_shared_links() {
local shared_dist="$SCRIPT_DIR/crabot-shared/dist"
[ -d "$shared_dist" ] || return 0
local shared_sig
shared_sig="$(cd "$shared_dist" && find . -type f | LC_ALL=C sort | shasum | awk '{print $1}')"
local need_resync=()
for mod in crabot-core crabot-admin crabot-agent crabot-channel-wechat crabot-channel-telegram crabot-channel-feishu crabot-channel-dingtalk; do
[ -d "$SCRIPT_DIR/$mod" ] || continue
local linked_dist
linked_dist=$(ls -d "$SCRIPT_DIR/$mod"/node_modules/.pnpm/crabot-shared@*/node_modules/crabot-shared/dist 2>/dev/null | head -1)
if [ -z "$linked_dist" ] || [ ! -d "$linked_dist" ]; then
need_resync+=("$mod")
continue
fi
local linked_sig
linked_sig="$(cd "$linked_dist" && find . -type f | LC_ALL=C sort | shasum | awk '{print $1}')"
if [ "$shared_sig" != "$linked_sig" ]; then
need_resync+=("$mod")
fi
done
if [ "${#need_resync[@]}" -eq 0 ]; then
return 0
fi
log_info "crabot-shared 文件清单变更,重链 ${#need_resync[@]} 个依赖方..."
local pids=()
for mod in "${need_resync[@]}"; do
log_dim " $mod"
(cd "$SCRIPT_DIR/$mod" && corepack pnpm install --force --prefer-offline \
>/tmp/.crabot-dev-relink-"${mod//\//-}".log 2>&1) &
pids+=($!)
done
local fail=0
for i in "${!pids[@]}"; do
if ! wait "${pids[$i]}"; then
log_error "${need_resync[$i]} 重链失败:"
tail -10 "/tmp/.crabot-dev-relink-${need_resync[$i]//\//-}.log" | sed 's/^/ /'
fail=1
fi
done
rm -f /tmp/.crabot-dev-relink-*.log
[ "$fail" -eq 1 ] && exit 1
}
build_all() {
cleanup_placeholder_pnpm_workspaces
log_info "构建 TypeScript 模块..."
# crabot-shared 必须先编译
if [ -d "$SCRIPT_DIR/crabot-shared" ]; then
log_dim " crabot-shared"
(cd "$SCRIPT_DIR/crabot-shared" && corepack pnpm run build 2>&1 | sed 's/^/ /') || {
log_error "crabot-shared 构建失败"
exit 1
}
# pnpm 把 crabot-shared/dist/* 用 hard-link 链到每个依赖方的 .pnpm 缓存。
# 「修改已有文件」共享 inode 自动同步,但「新增/删除文件」必须 install --force
# 才能重建 link 集合,否则依赖方启动时会 ERR_MODULE_NOT_FOUND。
sync_shared_links
fi
local fail=0
for mod in crabot-core crabot-admin crabot-agent crabot-channel-wechat crabot-channel-telegram crabot-channel-feishu crabot-channel-dingtalk; do
if [ ! -d "$SCRIPT_DIR/$mod" ]; then
continue
fi
# dev 模式下 Admin 用 tsx --watch 直接跑源码,不需要编译
if [ "$mod" = "crabot-admin" ] && [ "${CRABOT_DEV:-}" = "true" ]; then
log_dim " $mod (跳过,dev 模式用 tsx)"
continue
fi
log_dim " $mod"
(cd "$SCRIPT_DIR/$mod" && corepack pnpm run build 2>&1 | sed 's/^/ /') || {
log_error "$mod 构建失败"
fail=1
}
done
if [ "$fail" -eq 1 ]; then
exit 1
fi
# node-pty 的 spawn-helper 在 macOS 上需要可执行权限
local spawn_helper="$SCRIPT_DIR/crabot-admin/node_modules/node-pty/prebuilds/darwin-arm64/spawn-helper"
if [ -f "$spawn_helper" ] && [ ! -x "$spawn_helper" ]; then
chmod +x "$spawn_helper"
log_info "已修复 node-pty spawn-helper 权限"
fi
# Admin Web 前端(输出到 crabot-admin/dist/web/,由 port 3000 静态服务)
# 注意:dev 模式下访问 5173 走 Vite HMR,无需依赖此构建产物;
# 但访问 port 3000 或非 dev 启动时必须有此产物,否则前端永远是旧的。
if [ -d "$SCRIPT_DIR/crabot-admin/web" ]; then
log_dim " crabot-admin/web"
(cd "$SCRIPT_DIR/crabot-admin/web" && corepack pnpm run build 2>&1 | sed 's/^/ /') || {
log_error "crabot-admin/web 构建失败"
exit 1
}
fi
log_info "构建完成"
}
# ── Scrapling ─────────────────────────────────────────────
check_scrapling() {
local scrapling_min="0.4.4"
if ! command -v scrapling &>/dev/null; then
log_info "安装 Scrapling(Browser Use 功能需要)..."
pip install -i https://pypi.org/simple/ "scrapling[ai]" -q || {
log_warn "Scrapling 安装失败,Browser Use 功能不可用"
return 1
}
log_info "Scrapling 已安装"
fi
# 检查版本
local cur
cur=$(pip show scrapling 2>/dev/null | grep Version | awk '{print $2}')
if [ -n "$cur" ] && [ "$(printf '%s\n' "$scrapling_min" "$cur" | sort -V | head -1)" != "$scrapling_min" ]; then
log_warn "Scrapling 版本过低 ($cur < $scrapling_min),升级中..."
pip install -i https://pypi.org/simple/ -U "scrapling[ai]" -q || log_warn "Scrapling 升级失败,继续使用当前版本"
fi
return 0
}
# ── Memory 依赖同步 ──────────────────────────────────────
sync_memory_deps() {
if [ ! -d "$MEMORY_DIR" ]; then
return 0
fi
if ! command -v uv &>/dev/null; then
log_warn "uv 未安装,跳过 Memory 依赖同步"
log_dim " 安装: curl -LsSf https://astral.sh/uv/install.sh | sh"
return 0
fi
log_info "同步 Memory 依赖..."
(cd "$MEMORY_DIR" && uv sync --frozen 2>/dev/null) || {
log_warn "Memory 依赖同步失败"
return 0
}
mkdir -p "$DATA_DIR/memory"
}
# ── 停止 ──────────────────────────────────────────────────
stop_all() {
log_info "停止开发服务..."
# 优雅关闭 Module Manager(会级联关闭所有子模块,含 Vite)
curl --noproxy '*' -s -X POST "http://localhost:$MM_PORT/shutdown" \
-H "Content-Type: application/json" -d '{}' 2>/dev/null || true
# 等待 MM 进程真正退出(最多 15 秒),确保 Admin saveData() 完成
local waited=0
while pgrep -f "crabot-core/dist/main.js" >/dev/null 2>&1; do
sleep 1
waited=$((waited + 1))
if [ "$waited" -ge 15 ]; then
log_warn "Module Manager 未在 15 秒内退出,强制终止"
break
fi
done
# 杀掉残留进程(MM 级联关闭可能未完全生效)
pkill -f "crabot-core/dist/main.js" 2>/dev/null || true
pkill -f "crabot-admin/dist/main.js" 2>/dev/null || true
pkill -f "crabot-agent/dist/main.js" 2>/dev/null || true
pkill -f "vite.*crabot-admin/web" 2>/dev/null || true
# agent onStop() 可能卡住(MCP/LSP disconnect 无响应),SIGTERM 无效;强杀
pkill -9 -f "crabot-agent/dist/main.js" 2>/dev/null || true
# 清理 Crabot 管理的 Chrome 实例
if [ -f "$DATA_DIR/browser/chrome.pid" ]; then
kill "$(cat "$DATA_DIR/browser/chrome.pid")" 2>/dev/null || true
rm -f "$DATA_DIR/browser/chrome.pid"
fi
# 兜底释放所有已知端口
for port in "$MM_PORT" "$ADMIN_RPC_PORT" "$ADMIN_WEB_PORT"; do
local pid
pid=$(lsof -ti :"$port" 2>/dev/null) || true
if [ -n "$pid" ]; then
kill -9 "$pid" 2>/dev/null || true
fi
done
log_info "已停止"
}
# ── 启动 ──────────────────────────────────────────────────
start() {
export CRABOT_DEV=true
load_env
mkdir -p "$DATA_DIR/admin" "$DATA_DIR/agent" "$DATA_DIR/memory"
check_scrapling || true # 非阻塞,仅提示
# 1. 同步 Node 依赖(lock 没变时秒过)
sync_node_deps
# 2. 准备 Memory 依赖
sync_memory_deps
# 3. 构建(Admin 跳过,用 tsx --watch 直接跑源码)
build_all
# 3. macOS 权限预检(computer-use MCP 需要屏幕录制 + 辅助功能权限)
if [ "$(uname -s)" = "Darwin" ]; then
local mcp_config="$DATA_DIR/admin/mcp-servers.json"
if [ -f "$mcp_config" ] && node -e "
const cfg = require('$mcp_config');
process.exit(cfg.find(s => s.name === 'computer-use' && s.enabled) ? 0 : 1);
" 2>/dev/null; then
log_info "检查 computer-use 权限..."
local tmp_ss="/tmp/.crabot-permission-check.png"
if node -e "try{require('child_process').execFileSync('screencapture',['-x','-t','png','$tmp_ss'],{timeout:5000});process.exit(0)}catch{process.exit(1)}" 2>/dev/null; then
log_info " 屏幕录制 ✓"
else
log_warn " 屏幕录制权限未授予 node — 请在系统弹窗中允许"
fi
rm -f "$tmp_ss"
if node -e "try{require('child_process').execFileSync('osascript',['-e','tell application \"System Events\" to return name of first process'],{timeout:5000});process.exit(0)}catch{process.exit(1)}" 2>/dev/null; then
log_info " 辅助功能 ✓"
else
log_warn " 辅助功能权限未授予 node — 请在系统弹窗中允许"
fi
fi
fi
# 4. 单实例预检
if [ -f "$DATA_DIR/mm.pid" ]; then
local existing_pid
existing_pid=$(cat "$DATA_DIR/mm.pid")
if kill -0 "$existing_pid" 2>/dev/null; then
log_error "already running (pid=$existing_pid). Run './dev.sh stop' first."
exit 1
fi
rm -f "$DATA_DIR/mm.pid"
fi
# 5. Module Manager(后台 spawn + 捕获 PID + trap 清理;前台 wait 保持 Ctrl-C 体验)
#
# stdout/stderr 同时落盘到 module-manager.log —— 包含 MM 自己的 health check
# 失败警告 / auto-restart 决策 / 子进程 spawn 等关键事件。早期版本只 exec 到
# 终端导致 agent 被 SIGKILL 时 MM 端的诊断信息全部丢失。
log_info "启动 Module Manager..."
cd "$SCRIPT_DIR/crabot-core"
local mm_log="$DATA_DIR/logs/module-manager.log"
mkdir -p "$(dirname "$mm_log")"
# 用 process substitution 让 tee 不切走主进程;node 自己当 wait 的目标
node dist/main.js > >(tee -a "$mm_log") 2>&1 &
local mm_pid=$!
echo "$mm_pid" > "$DATA_DIR/mm.pid"
trap 'kill -TERM '"$mm_pid"' 2>/dev/null; rm -f "'"$DATA_DIR"'/mm.pid"' INT TERM EXIT
wait "$mm_pid"
}
# ── 主入口 ────────────────────────────────────────────────
case "${1:-start}" in
start) start ;;
stop) stop_all ;;
build) load_env && build_all ;;
*)
echo "用法: $0 [start|stop|build]"
echo ""
echo " start 启动开发环境(默认;Vite HMR 在 5173)"
echo " stop 停止所有开发服务"
echo " build 仅构建(TypeScript 模块 + Admin Web 前端 dist/web/)"
exit 1
;;
esac