-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathsetup-opencode.sh
More file actions
executable file
·1802 lines (1677 loc) · 85.9 KB
/
Copy pathsetup-opencode.sh
File metadata and controls
executable file
·1802 lines (1677 loc) · 85.9 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
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/bin/bash
# ============================================================================
# OpenCode 一键配置脚本
# 使用 Bun 作为运行时,自动安装所有依赖
# 支持 Linux / macOS / WSL
# ============================================================================
SETUP_VERSION="v1.0" # 动态升级 P0:版本常量,--upgrade 比对用(git 克隆场景以 git describe --tags --always 覆盖)
if [ -z "${BASH_VERSION:-}" ]; then
echo "检测到非 bash 环境,自动以 bash 重新执行..."
exec bash "$0" "$@"
fi
set -e
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
# 管道/升级场景: ~/.local/bin 可能不在默认 PATH,探测组件前扩入(webmap/opstate 等安装位)
export PATH="$HOME/.local/bin:$PATH"
# 参数解析(动态升级 P0): --version/--upgrade/-h 即时退出,不进 12 步主流程;
# 未识别参数忽略(与历史行为一致,主流程本就不吃参数)
for _arg in "$@"; do
case "$_arg" in
--version)
if [ -d "$SCRIPT_DIR/.git" ] && command -v git >/dev/null 2>&1; then
_gc="$(git -C "$SCRIPT_DIR" rev-parse --short HEAD 2>/dev/null || true)"
echo "setup-opencode.sh $SETUP_VERSION${_gc:+ (git: $_gc)}"
else
echo "setup-opencode.sh $SETUP_VERSION"
fi
exit 0
;;
--upgrade)
# P1(动态升级,计划 .omo/plans/dynamic-upgrade.md): 只置旗不退出——
# 状态还原入口块依赖 CONFIG_DIR 与 detect_components(定义在其后),
# 故在 P0 基建段落落地(见 upgrade_restore_context),随后落入 12 步主流程
UPGRADE_MODE=1
;;
-h|--help)
echo "用法: ./setup-opencode.sh [选项]"
echo " (无参数) 默认: 全新安装或幂等重跑(选装: 交互菜单 / INSTALL_* 环境变量)"
echo " --upgrade 动态升级已装机器到最新(自动自更新脚本+读状态清单还原选装上下文,不剥已装接线)"
echo " --version 打印脚本版本"
exit 0
;;
esac
done
APT_UPDATED=0 # 确保 apt install 前 lists 就绪(全新容器/镜像跳过测速时仍可装包)
apt_ensure_update() {
[ "$APT_UPDATED" = 1 ] && return 0
command -v apt-get >/dev/null 2>&1 || return 0
timeout -s KILL 180 $SUDO apt-get update -qq >/dev/null 2>&1 || true
APT_UPDATED=1
}
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
# ------------------------------------------------------------------
# 状态清单(动态升级 P0 基建,计划: .omo/plans/dynamic-upgrade.md): 在场探测
# 组件 + 原子写 $CONFIG_DIR/.setup-state.json,P1 --upgrade 读它还原安装上下文。
# 在场守则(AGENTS.md): 每项写前实测(command -v/文件在场/配置实读),禁止静态
# 猜测;P0 写入与 P1 读取共用同一套探测(单一事实源,清单漂移时不信旧清单)。
# ------------------------------------------------------------------
# 脚本版本: git 克隆场景以 git describe --tags --always 覆盖常量,curl 安装用户无 .git 用常量
resolve_setup_version() {
local gd=""
if [ -d "$SCRIPT_DIR/.git" ] && command -v git >/dev/null 2>&1; then
gd="$(git -C "$SCRIPT_DIR" describe --tags --always 2>/dev/null || true)"
fi
printf '%s' "${gd:-$SETUP_VERSION}"
}
# 组件在场探测 → 单行 json 片段("name": true/false, ...),供 write_state_file 组装
detect_components() {
local out="" k present
for k in opencode bun node rtk codegraph webmap opstate mineru; do
if command -v "$k" >/dev/null 2>&1; then present=true; else present=false; fi
out="${out:+$out, }\"$k\": $present"
done
# local_memory: 本地记忆层(docs/memory/facts.md 在场; mem0 三件套已退役)
if [ -f "$CONFIG_DIR/docs/memory/facts.md" ]; then present=true; else present=false; fi
out="$out, \"local_memory\": $present"
# dcp: 以 opencode.json 实注册为准(与安装段装后验证/收尾汇总同一判据)
if grep -q 'opencode-dcp' "$CONFIG_DIR/opencode.json" 2>/dev/null; then present=true; else present=false; fi
out="$out, \"dcp\": $present"
# gsd: 官方安装器落 command/gsd-*(与步骤 9 同一判据;disabled_mcps 无关)
if ls "$CONFIG_DIR/command/gsd-"* >/dev/null 2>&1; then present=true; else present=false; fi
out="$out, \"gsd\": $present"
# superpowers_router: 路由插件三件套之锚(sp-router.ts)
if [ -f "$CONFIG_DIR/plugins/sp-router.ts" ]; then present=true; else present=false; fi
out="$out, \"superpowers_router\": $present"
# cmolecules_cron: 夜间自进化定时任务在 crontab 实测在册
if crontab -l 2>/dev/null | grep -Fq 'skillopt-sleep'; then present=true; else present=false; fi
out="$out, \"cmolecules_cron\": $present"
printf '%s' "$out"
}
# 组装状态清单并原子写(tmp+rename);python3 缺失降级: 跳过+黄警(下次运行重生成)
write_state_file() {
if ! command -v python3 >/dev/null 2>&1; then
echo -e "${YELLOW}⚠ 无 python3,跳过状态清单写入(状态文件是升级基建,缺了下次重生成)${NC}"
return 0
fi
if python3 - "$CONFIG_DIR" "$(resolve_setup_version)" "$(detect_components)" << 'PYEOF'
import datetime, json, os, sys
config_dir, ver, comps = sys.argv[1], sys.argv[2], sys.argv[3]
state = {}
state["script_version"] = ver
state["installed_at"] = datetime.datetime.now().astimezone().isoformat(timespec="seconds")
state["components"] = json.loads("{" + comps + "}")
# flags 均从 opencode.json 实读;permission_mode 按权限红线模板特征推断
# (gen-permissions.sh 三档: 标准 59 条/无头 8 条均含 rm -rf*+curl*|*sh deny;
# 沙箱删本机破坏类 deny、留网络不可逆 deny)
state["flags"] = {}
try:
cfg = json.load(open(os.path.join(config_dir, "opencode.json")))
if not isinstance(cfg, dict):
cfg = None
except Exception:
cfg = None
if cfg is None:
state["flags"]["model"] = ""
state["flags"]["permission_mode"] = "unknown"
else:
state["flags"]["model"] = cfg.get("model", "")
bash = cfg.get("permission", {}).get("bash", {})
if not isinstance(bash, dict) or not bash:
mode = "unknown"
elif bash.get("curl*|*sh") == "deny" and bash.get("rm -rf *") != "deny":
mode = "sandbox"
elif len(bash) == 59:
mode = "standard"
elif len(bash) == 8:
mode = "headless"
else:
mode = "custom"
state["flags"]["permission_mode"] = mode
path = os.path.join(config_dir, ".setup-state.json")
tmp = path + ".tmp"
with open(tmp, "w") as f:
json.dump(state, f, indent=2, ensure_ascii=False)
f.write("\n")
os.replace(tmp, path)
PYEOF
then
echo -e "${GREEN}✓ 状态清单 → $CONFIG_DIR/.setup-state.json(vP1 升级账本)${NC}"
return 0
else
echo -e "${YELLOW}⚠ 状态清单写入失败(JSON 损坏/目录不可写?),下次运行重生成${NC}"
return 0
fi
}
# P1 --upgrade 入口(计划 .omo/plans/dynamic-upgrade.md): 状态清单→UC_*→
# INSTALL_*/CONFIRM_AGPL 环境变量还原,随后正常走 12 步(幂等段自会跳过已装)。
# 依赖 CONFIG_DIR 与 detect_components,故在参数解析(--upgrade 置旗处)之后的
# 主流程里调用;核心是修"裸重跑误剥"——不带开关重跑会走"未选"路径(如剥 mcp.gsd)。
upgrade_restore_context() {
UPGRADE_MODE=1
STATE_FILE="$CONFIG_DIR/.setup-state.json"
if [ -f "$STATE_FILE" ] && command -v python3 >/dev/null 2>&1; then
cp "$STATE_FILE" "$STATE_FILE.prev" 2>/dev/null || true
# eval stderr 抑制: 组件名带连字符的键(skillopt-sleep)不是合法 shell 赋值名,
# 会以 command not found 噪声出场;其余键照常赋值(还原只依赖合法键)
eval "$(python3 -c "
import json
c=json.load(open('$STATE_FILE')).get('components',{})
f=json.load(open('$STATE_FILE')).get('flags',{})
for k,v in c.items(): print(f'UC_{k}={str(v).lower()}')
print('UC_MODEL='+repr(f.get('model','')))" 2>/dev/null)" 2>/dev/null || true
echo "✓ 状态清单已读: $(python3 -c "import json;print(json.load(open('$STATE_FILE')).get('script_version','?'))") → $SETUP_VERSION"
else
echo "无状态清单(v1.0 前安装?),考古模式现场重建"
# 考古: 调 P0 探测逻辑重新组装(规格原 sed 单行解析在实测中不可用——
# detect_components 输出为单行,逐段切分才是可靠重组方式)
_arch="$(detect_components)"
IFS=',' read -ra _arch_pairs <<< "$_arch"
for _p in "${_arch_pairs[@]}"; do
_k="${_p%%:*}"; _v="${_p##*:}"
_k="${_k//\"/}"; _k="${_k// /}"; _v="${_v// /}"
case "$_k" in [a-zA-Z_][a-zA-Z0-9_]*) eval "UC_${_k}=$_v" 2>/dev/null || true ;; esac
done
fi
# 状态→环境变量还原(核心: 修裸重跑误剥)
[ "${UC_dcp:-false}" = "true" ] && export INSTALL_DCP=1 CONFIRM_AGPL=1
[ "${UC_mineru:-false}" = "true" ] && export INSTALL_MINERU=1
[ "${UC_gsd:-false}" = "true" ] && export INSTALL_GSD=1
[ "${UC_superpowers_router:-false}" = "true" ] && export SUPERPOWERS_ROUTER=1
# mem0→local_memory 改造: 旧键(mem0/skillopt_sleep)保留兼容既有状态清单,新键以 docs/memory 在场为准
[ "${UC_mem0:-false}" = "true" ] || [ "${UC_skillopt_sleep:-false}" = "true" ] || [ "${UC_local_memory:-false}" = "true" ] && export INSTALL_CMODULES=1
export UPGRADE_MODE
}
# 计时: step_begin 记起点, step_end 输出耗时, EXIT 时打印汇总表便于排查瓶颈
_step_t0=0
STEP_TIMES=()
STEP_NAMES=()
step_begin() { _step_t0=$(date +%s); }
step_end() {
local n="$1" name="$2" d
d=$(( $(date +%s) - _step_t0 ))
STEP_TIMES[$n]="$d"
STEP_NAMES[$n]="$name"
echo -e "${BLUE} - 步骤${n} 耗时 ${d}s${NC}"
}
step_summary() {
local i total=0
echo ""
echo -e "${YELLOW}=== 各环节耗时 ===${NC}"
for i in $(seq 1 12); do
[ -z "${STEP_TIMES[$i]:-}" ] && continue
printf " [%s/12] %s: %ss\n" "$i" "${STEP_NAMES[$i]}" "${STEP_TIMES[$i]}"
total=$(( total + STEP_TIMES[$i] ))
done
echo -e "${YELLOW}总耗时: ${total}s${NC}"
}
trap step_summary EXIT
echo -e "${GREEN}========================================${NC}"
echo -e "${GREEN} OpenCode 一键配置脚本 (Bun 版)${NC}"
echo -e "${GREEN}========================================${NC}"
echo ""
# ------------------------------------------------------------------
# 目录配置(支持环境变量覆盖)
# ------------------------------------------------------------------
CONFIG_DIR="${OPENCODE_CONFIG_DIR:-$HOME/.config/opencode}"
CLAUDE_DIR="${CLAUDE_CONFIG_DIR:-$HOME/.claude}"
NPM_REGISTRY="${NPM_REGISTRY:-https://registry.npmmirror.com}"
# root 环境无需 sudo;非 root 且有 sudo 时自动补前缀
SUDO=""
if [ "$(id -u)" != "0" ] && command -v sudo &> /dev/null; then
SUDO="sudo"
fi
echo -e "${BLUE}目标目录:${NC}"
echo " OpenCode: $CONFIG_DIR"
echo " Claude: $CLAUDE_DIR"
echo ""
# P1 --upgrade: 参数解析处只置旗,这里(CONFIG_DIR 与探测函数就绪后)还原选装上下文
if [ "${UPGRADE_MODE:-0}" = "1" ]; then
# P2 自更新: 先把脚本自身更到最新,再以新版跑升级
# 防循环: 新版重跑此段时无更新即 NEW_C=0 / curl 下载与自身 diff 相同 → 不 exec,天然终止
SCRIPT_GIT_DIR="$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel 2>/dev/null || true)"
if [ -n "$SCRIPT_GIT_DIR" ]; then
git -C "$SCRIPT_DIR" fetch origin main >/dev/null 2>&1 || true
NEW_C="$(git -C "$SCRIPT_DIR" rev-list HEAD..origin/main --count 2>/dev/null || echo 0)"
if [ "$NEW_C" -gt 0 ]; then
if git -C "$SCRIPT_DIR" pull --ff-only origin main >/dev/null 2>&1; then
echo "✓ 脚本自更新: 拉取 $NEW_C 个新提交,重启新版执行升级"
exec bash "$SCRIPT_DIR/setup-opencode.sh" --upgrade
else
echo "⚠ 本地有改动无法 ff-pull,以当前版本继续升级(自更新跳过)"
fi
fi
else
# curl 安装用户: 下载 main 最新脚本,校验后替换自身重启
# 双校验防坏文件: bash -n 语法 + 大小>50KB;diff 相同=已是最新 → 不替换不重启
NEW_SH="$(mktemp)"
if curl -fsSL --max-time 30 "https://raw.githubusercontent.com/Liber1917/opencode-setup/main/setup-opencode.sh" -o "$NEW_SH" 2>/dev/null \
|| curl -fsSL --max-time 30 "https://gh-proxy.com/https://raw.githubusercontent.com/Liber1917/opencode-setup/main/setup-opencode.sh" -o "$NEW_SH" 2>/dev/null; then
# 管道安装(curl|bash)陷阱: $0="bash"/无实体脚本,自更新替换(cp 到 $0)不可行;
# 且管道拉到的本就是 main 最新——跳过自更新,直接以当前内容执行升级
_pipe_mode=0
case "$0" in bash|sh|''|/dev/*|*pipe*|*fd/*) _pipe_mode=1 ;; esac
if [ "$_pipe_mode" = "1" ]; then
echo " - 管道模式: 本次已是 main 最新内容,跳过自更新直接升级"
else
# 版本前进校验(oct 实测教训: CDN 缓存"合法旧版"放行):
# 下载侧 SETUP_VERSION 必须 != 本地才替换;同版本=缓存毒,拒替换
DL_VER="$(grep -m1 '^SETUP_VERSION=' "$NEW_SH" 2>/dev/null | cut -d'"' -f2)"
if bash -n "$NEW_SH" && [ "$(wc -c < "$NEW_SH")" -gt 50000 ] \
&& ! diff -q "$NEW_SH" "$0" >/dev/null 2>&1 \
&& [ -n "$DL_VER" ] && [ "$DL_VER" != "$SETUP_VERSION" ]; then
cp "$NEW_SH" "$0" && chmod +x "$0" && rm -f "$NEW_SH"
echo "✓ 脚本自更新完成(curl): $SETUP_VERSION → $DL_VER,重启新版执行升级"
exec bash "$0" --upgrade
elif [ "${DL_VER:-}" = "$SETUP_VERSION" ]; then
echo " - 远端与本地同版($SETUP_VERSION,CDN 可能缓存),以当前版本继续升级"
fi
fi
rm -f "$NEW_SH" 2>/dev/null || true
fi
fi
upgrade_restore_context
fi
# 确保 curl(下载依赖);缺失时尝试 apt 安装,失败仅提示
if ! command -v curl &> /dev/null; then
echo -e "${YELLOW}⚠ curl 未安装,尝试安装...${NC}"
if command -v apt-get &> /dev/null && timeout -s KILL 120 apt-get update >/dev/null 2>&1 && timeout -s KILL 120 apt-get install -y curl >/dev/null 2>&1; then
echo -e "${GREEN}✓ curl 已安装${NC}"
else
echo -e "${YELLOW}⚠ curl 安装失败,后续下载步骤可能不可用${NC}"
fi
fi
# ------------------------------------------------------------------
# 步骤 1: 检测已有配置
# ------------------------------------------------------------------
echo -e "${YELLOW}[1/12] 检测已有配置...${NC}"
step_begin
if [ -f "$CONFIG_DIR/opencode.json" ] || [ -f "$CONFIG_DIR/oh-my-openagent.json" ]; then
echo -e "${YELLOW}⚠ 发现现有配置文件${NC}"
# P1 升级模式: 不问覆盖,直接走保留分支(升级只加不破,已装配置以在场为准)
[ "${UPGRADE_MODE:-0}" = "1" ] && overwrite=n
if [ -t 0 ] && [ "${UPGRADE_MODE:-0}" != "1" ]; then
echo -n "是否备份后重新生成? (y/n) [n]: "
read -r overwrite
overwrite=${overwrite:-n}
else
overwrite=n # U-10: 管道安装(非交互)默认不覆盖,防 read 吞脚本后续行
echo -e "${YELLOW} 非交互/升级模式: 保留现有配置${NC}"
fi
if [[ $overwrite =~ ^[Yy]$ ]]; then
backup_dir="$HOME/opencode-backup-$(date +%Y%m%d-%H%M%S)"
mkdir -p "$backup_dir"
[ -f "$CONFIG_DIR/opencode.json" ] && cp "$CONFIG_DIR/opencode.json" "$backup_dir/"
[ -f "$CONFIG_DIR/oh-my-openagent.json" ] && cp "$CONFIG_DIR/oh-my-openagent.json" "$backup_dir/"
[ -f "$CONFIG_DIR/package.json" ] && cp "$CONFIG_DIR/package.json" "$backup_dir/"
echo -e "${GREEN}✓ 已备份到: $backup_dir${NC}"
else
echo "跳过配置生成,使用现有配置。"
SKIP_CONFIG=1
fi
fi
# ------------------------------------------------------------------
# 步骤 2: 创建目录结构
# ------------------------------------------------------------------
step_end 1 "检测已有配置"
# ------------------------------------------------------------------
# 交互式选装菜单: 步骤 1 后、任何安装动作前;选中即设 INSTALL_*/SUPERPOWERS_ROUTER,
# 与环境变量路径共用同一组开关(菜单只是交互前端,不引入第二套状态)。
# 门(任一命中即跳过,直接走环境变量语义):
# ⓪ 升级模式(UPGRADE_MODE=1)——状态清单即答案,不重问(P1)
# ① SETUP_INTERACTIVE=0 强制关菜单(最高优先级)
# ② 任一选装变量(INSTALL_GSD/INSTALL_DCP/INSTALL_MINERU/SUPERPOWERS_ROUTER/
# INSTALL_CMODULES/CONFIRM_AGPL)已在环境中显式设置——用户已给路径,不打扰
# ③ 非交互终端([ -t 0 ] 为假: curl|bash 管道/CI)——行为与历史版本完全一致;
# SETUP_FORCE_MENU=1 为无 TTY 调试入口(供回归测试从 stdin 喂输入)
# ------------------------------------------------------------------
interactive_component_menu() {
# 升级模式不跳过菜单: 已装组件标注[✓],用户可增量勾选新组件(2026-09-12 用户裁定:
# "升级不应该静默,应该也有选装内容的交互")。非交互管道升级仍走状态清单还原(零打扰)。
[ "${SETUP_INTERACTIVE:-1}" = "0" ] && return 0
[ -n "${INSTALL_GSD:-}${INSTALL_DCP:-}${INSTALL_MINERU:-}${SUPERPOWERS_ROUTER:-}${INSTALL_CMODULES:-}${CONFIRM_AGPL:-}" ] && return 0
if [ "${SETUP_FORCE_MENU:-0}" != "1" ] && [ ! -t 0 ]; then
# 管道升级: 无 TTY 不能交互,但必须通知用户新增了哪些可选组件(不静默)
if [ "${UPGRADE_MODE:-0}" = "1" ]; then
_new_env=""
[ "${UC_gsd:-false}" != "true" ] && _new_env="${_new_env} INSTALL_GSD=1"
[ "${UC_dcp:-false}" != "true" ] && _new_env="${_new_env} INSTALL_DCP=1 CONFIRM_AGPL=1"
[ "${UC_mineru:-false}" != "true" ] && _new_env="${_new_env} INSTALL_MINERU=1"
[ "${UC_superpowers_router:-false}" != "true" ] && _new_env="${_new_env} SUPERPOWERS_ROUTER=1"
[ "${UC_local_memory:-false}" != "true" ] && [ "${UC_cmolecules_cron:-false}" != "true" ] && _new_env="${_new_env} INSTALL_CMODULES=1"
if [ -n "$_new_env" ]; then
echo -e "${BLUE} ✦ 管道升级无法交互选装,以下组件未装。一键补装(复制执行):${NC}" >&2
echo -e "${BLUE} curl -fsSL \"https://gh-proxy.com/https://raw.githubusercontent.com/Liber1917/opencode-setup/main/setup-opencode.sh\" | env $_new_env bash${NC}" >&2
fi
fi
return 0
fi
echo ""
echo -e "${YELLOW}═══════════════════════════════════${NC}"
echo -e "${YELLOW} 可选组件(全免费,默认都不装)${NC}"
echo -e "${YELLOW}═══════════════════════════════════${NC}"
# 升级模式: 已装组件预选标注[✓],输入编号可增减;全新安装: 全[ ]可选
_mk(){ if [ "${UPGRADE_MODE:-0}" = "1" ] && [ "${UC_$1:-false}" = "true" ]; then echo "[✓]"; else echo "[ ]"; fi; }
echo " 1. GSD 工作流 $(_mk gsd) 多阶段项目管理(/gsd-* 命令,用户显式驱动)"
echo " 2. DCP 上下文压缩 $(_mk dcp) 长会话自动压缩(AGPL-3.0,装前需确认)"
echo " 3. MinerU 文档解析 $(_mk mineru) PDF→Markdown 本地版(免费无限量,磁盘 20GB+)"
echo " 4. superpowers 路由 $(_mk superpowers_router) 技能清单渐进披露(默认官方急加载)"
echo " 5. 记忆/自进化 $(_mk local_memory) 本地记忆(docs/memory 零外发)+SkillOpt 夜间提炼(草稿区审批制)"
if [ "${UPGRADE_MODE:-0}" = "1" ]; then
echo " (升级: [✓]=已装保留; 输入编号=新增安装; 直接回车=保持现状)"
fi
echo -e "${YELLOW}───────────────────────────────────${NC}"
echo -n ' 输入要启用的编号(空格分隔,如 "1 3";直接回车=全不装): '
local attempt answer sel ok picks=""
for attempt in 1 2 3; do
answer=""
read -r answer || answer="" # stdin 耗尽(EOF)按全不装,不挂死
if [ -z "${answer//[[:space:]]/}" ]; then
picks=""
break
fi
ok=1
picks=""
for sel in $answer; do
case "$sel" in
1|2|3|4|5) picks="$picks$sel " ;;
*) ok=0 ;;
esac
done
[ "$ok" = "1" ] && break
picks="" # 非法轮次丢弃残留选择("1 x" 不留 1)
if [ "$attempt" -lt 3 ]; then
echo -e "${YELLOW} ⚠ 无效输入 \"${answer}\"(合法: 1-5,空格分隔),请重输(${attempt}/3)${NC}"
echo -n ' 输入要启用的编号(空格分隔,如 "1 3";直接回车=全不装): '
else
echo -e "${YELLOW} ⚠ 连续 3 次无效输入,按全不装继续${NC}"
fi
done
# 校验通过才落变量,保证非法轮次零残留
local names=""
for sel in $picks; do
case "$sel" in
1) INSTALL_GSD=1; names="${names:+$names, }GSD" ;;
2) INSTALL_DCP=1; names="${names:+$names, }DCP" ;;
3) INSTALL_MINERU=1; names="${names:+$names, }MinerU" ;;
4) SUPERPOWERS_ROUTER=1; names="${names:+$names, }superpowers路由" ;;
5) INSTALL_CMODULES=1; names="${names:+$names, }记忆/自进化" ;;
esac
done
echo ""
if [ -n "$names" ]; then
echo -e "${GREEN}→ 将安装: ${names}${NC}"
[ "${INSTALL_DCP:-0}" = "1" ] && echo -e "${BLUE} - DCP 的 AGPL 确认将在安装时进行(本菜单不绕过 CONFIRM_AGPL 确认门)${NC}"
else
echo -e "${BLUE} - 未选择任何选装组件(全不装,与默认行为一致)${NC}"
fi
echo ""
return 0
}
# 区分 INSTALL_CMODULES 来源: 环境显式给定(按非交互铁律,装完不问定时,只打印
# 开启命令) vs 菜单选中(装完且 [ -t 0 ] 时交互问)——须在菜单调用前捕获
INSTALL_CMODULES_PRESET="${INSTALL_CMODULES:-0}"
interactive_component_menu
step_begin
echo -e "${YELLOW}[2/12] 创建配置目录...${NC}"
mkdir -p "$CONFIG_DIR"
mkdir -p "$CONFIG_DIR/skills"
mkdir -p "$CLAUDE_DIR"
echo -e "${GREEN}✓ 目录已创建${NC}"
# ------------------------------------------------------------------
# 步骤 3: 生成配置文件
# ------------------------------------------------------------------
step_end 2 "创建配置目录"
if [ "${SKIP_CONFIG:-0}" != "1" ]; then
step_begin
echo -e "${YELLOW}[3/12] 生成配置文件...${NC}"
# opencode.json
# superpowers 接入方式: 官方急加载(默认) 或 路由模式(渐进披露,-90% token)
# 注意: 本地插件(sp-router/opencode-env)不进 plugin 数组——数组只认 npm 包,
# 本地文件靠 ~/.config/opencode/plugins/*.ts 自动发现(rtk.ts 同款姿势)
if [ "${SUPERPOWERS_ROUTER:-0}" = "1" ]; then
SP_PLUGIN_LINE=''
else
SP_PLUGIN_LINE=', "superpowers@git+https://github.com/jnMetaCode/superpowers-zh.git"'
fi
cat > "$CONFIG_DIR/opencode.json" << EOF
{
"\$schema": "https://opencode.ai/config.json",
"model": "zhipuai-coding-plan/glm-5.3",
"plugin": ["oh-my-openagent@latest"$SP_PLUGIN_LINE],
"permission": {
"read": {
"~/.config/opencode/*": "allow",
"~/.claude/*": "allow"
},
"external_directory": {
"~/.config/opencode/*": "allow",
"~/.claude/*": "allow"
}
}
}
EOF
if command -v python3 >/dev/null 2>&1; then
python3 -c "import json; c=json.load(open('$CONFIG_DIR/opencode.json')); assert c.get('model') and c.get('plugin'), 'model/plugin 丢失'" \
&& echo -e "${GREEN} ✓ opencode.json(含 model+plugin,JSON 已校验)${NC}" \
|| { echo -e "${RED} ✗ opencode.json 生成损坏(JSON 非法或 model/plugin 丢失),中止——静默损坏曾致 403${NC}"; exit 1; }
else
echo -e "${YELLOW} ⚠ 无 python3,跳过 JSON 校验(建议安装后重跑)${NC}"
fi
# oh-my-openagent.json
# 显式 model 必须存在:fallbackChain patch 盖不住 category 解析路径,空配置会
# 落到源码内置链(anthropic)→ 子代理 403 静默死(2026-08-30 实测坐实)。
# 换模型: OMO_MODEL=<provider/model> 重跑,或直接编辑本文件。
# disabled_mcps: 禁 omo 内置白名单里的 grep_app(E-security spec 已记录移除,
# 职能被本地 AST+web 搜索替代)。opencode.json 侧从未注册过它,单侧禁用即净。
OMO_MODEL="${OMO_MODEL:-zhipuai-coding-plan/glm-5.3}"
cat > "$CONFIG_DIR/oh-my-openagent.json" << EOF
{
"\$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-openagent/dev/assets/oh-my-opencode.schema.json",
"disabled_mcps": ["grep_app"],
"agents": {
"hephaestus": {"model": "$OMO_MODEL"},
"oracle": {"model": "$OMO_MODEL"},
"librarian": {"model": "$OMO_MODEL"},
"explore": {"model": "$OMO_MODEL"},
"multimodal-looker": {"model": "$OMO_MODEL"},
"prometheus": {"model": "$OMO_MODEL"},
"metis": {"model": "$OMO_MODEL"},
"momus": {"model": "$OMO_MODEL"},
"atlas": {"model": "$OMO_MODEL"},
"sisyphus-junior": {"model": "$OMO_MODEL"}
},
"categories": {
"visual-engineering": {"model": "$OMO_MODEL"},
"ultrabrain": {"model": "$OMO_MODEL"},
"deep": {"model": "$OMO_MODEL"},
"artistry": {"model": "$OMO_MODEL"},
"quick": {"model": "$OMO_MODEL"},
"unspecified-low": {"model": "$OMO_MODEL"},
"unspecified-high": {"model": "$OMO_MODEL"},
"writing": {"model": "$OMO_MODEL"}
}
}
EOF
echo -e "${GREEN} ✓ oh-my-openagent.json(agents+categories 显式 model=$OMO_MODEL,堵死回退链路由)${NC}"
# Claude settings
if [ ! -f "$CLAUDE_DIR/settings.json" ]; then
cat > "$CLAUDE_DIR/settings.json" << 'EOF'
{
"hooks": {
"SessionStart": [],
"PostToolUse": [],
"PreToolUse": []
}
}
EOF
echo -e "${GREEN} ✓ settings.json (Claude)${NC}"
else
echo -e "${BLUE} - settings.json 已存在,跳过${NC}"
fi
step_end 3 "生成配置文件"
else
# 保留现有配置路径(非交互默认/交互选 n): 存量 oh-my-openagent.json 可能
# 无 disabled_mcps(E-security spec 记录的 grep_app 移除只落在开发者本机,
# setup 从未写盘)——python3 幂等合并: 现有列表 ∪ ["grep_app"],原子写
# tmp+rename,用户自设条目不覆盖;无 python3 则跳过并黄警。
if [ -f "$CONFIG_DIR/oh-my-openagent.json" ] && command -v python3 >/dev/null 2>&1; then
python3 - "$CONFIG_DIR/oh-my-openagent.json" << 'PYEOF' \
&& echo -e "${GREEN} ✓ disabled_mcps 已含 grep_app(幂等合并,用户自设条目保留)${NC}" \
|| echo -e "${YELLOW} ⚠ disabled_mcps 合并失败(JSON 损坏?),文件保持原样${NC}"
import json, os, sys
p = sys.argv[1]
with open(p) as f:
c = json.load(f)
c["disabled_mcps"] = sorted(set(c.get("disabled_mcps", [])) | {"grep_app"})
tmp = p + ".tmp"
with open(tmp, "w") as f:
json.dump(c, f, indent=2, ensure_ascii=False)
f.write("\n")
os.replace(tmp, p)
PYEOF
elif [ -f "$CONFIG_DIR/oh-my-openagent.json" ]; then
echo -e "${YELLOW} ⚠ 无 python3,跳过 disabled_mcps 幂等合并(建议安装后重跑)${NC}"
fi
fi
# ------------------------------------------------------------------
# 步骤 4: apt 源测速优化(仅 apt 系系统;官方最快则不动,已自定义则跳过)
# ------------------------------------------------------------------
step_begin
echo -e "${YELLOW}[4/12] apt 源测速优化...${NC}"
if ! command -v apt-get &> /dev/null || [ "$SKIP_APT_MIRROR" = "1" ]; then
echo -e "${BLUE} - 非 apt 系统或已跳过(SKIP_APT_MIRROR=1),跳过源优化${NC}"
elif ! command -v curl &> /dev/null; then
echo -e "${YELLOW}⚠ curl 未安装,跳过 apt 源测速(不影响其他步骤)${NC}"
else
# 支持两种源文件格式:传统 sources.list / 24.04+ deb822 ubuntu.sources
APT_SOURCES=""
for f in /etc/apt/sources.list /etc/apt/sources.list.d/ubuntu.sources; do
[ -f "$f" ] && APT_SOURCES="$f" && break
done
if [ -z "$APT_SOURCES" ]; then
echo -e "${YELLOW}⚠ 未找到 apt 源文件,跳过源优化${NC}"
else
# 官方域名未被替换过才执行;FORCE_APT_MIRROR=1 强制重测
if [ "$FORCE_APT_MIRROR" = "1" ] || grep -Eq 'archive\.ubuntu\.com|security\.ubuntu\.com' "$APT_SOURCES"; then
APT_CODENAME="$(grep -E '^VERSION_CODENAME=' /etc/os-release | cut -d= -f2)"
[ -z "$APT_CODENAME" ] && APT_CODENAME="jammy"
echo -e "${BLUE} 测速中 (发行版 $APT_CODENAME)...${NC}"
BEST_MIRROR=""
BEST_SPEED=0
for M in mirrors.ustc.edu.cn mirrors.aliyun.com mirrors.tuna.tsinghua.edu.cn mirrors.huaweicloud.com mirrors.cloud.tencent.com mirrors.163.com archive.ubuntu.com; do
# 单次下载测速(速度取两次采样最大值,避免抖动)
SPEED=0
for _ in 1 2; do
S="$(curl -fsSL --connect-timeout 5 --max-time 10 -o /dev/null -w '%{speed_download}' "http://$M/ubuntu/dists/$APT_CODENAME/Release" 2>/dev/null || true)"
S="${S%.*}"
[ "${S:-0}" -gt "$SPEED" ] 2>/dev/null && SPEED="$S"
done
if [ "${SPEED:-0}" -gt 0 ] 2>/dev/null; then
echo -e " ${M}: $((SPEED / 1024)) KB/s"
if [ "$SPEED" -gt "$BEST_SPEED" ] 2>/dev/null; then
BEST_SPEED="$SPEED"
BEST_MIRROR="$M"
fi
else
echo -e " ${M}: 不可达"
fi
done
if [ -n "$BEST_MIRROR" ]; then
if [ "$BEST_MIRROR" = "archive.ubuntu.com" ]; then
echo -e "${GREEN}✓ 官方源最快 ($((BEST_SPEED / 1024)) KB/s),保持不动${NC}"
else
cp "$APT_SOURCES" "${APT_SOURCES}.bak"
# 仅替换主机名,保留协议与路径(同时覆盖传统与 deb822 格式)
$SUDO sed -i "s|//archive\.ubuntu\.com|//$BEST_MIRROR|g; s|//security\.ubuntu\.com|//$BEST_MIRROR|g" "$APT_SOURCES"
echo -e "${GREEN}✓ 已切换至 $BEST_MIRROR ($((BEST_SPEED / 1024)) KB/s)${NC}"
echo -e "${BLUE} 原文件已备份: ${APT_SOURCES}.bak${NC}"
# 真实机器常带第三方源(PPA/docker 等),apt 无超时会无限挂起(2026-09-08 jammy 实测)
if timeout -s KILL 180 $SUDO apt-get update >/dev/null 2>&1; then
echo -e "${GREEN}✓ apt update 验证通过${NC}"
else
echo -e "${YELLOW}⚠ apt update 验证失败/超时,已自动还原原源${NC}"
$SUDO cp "${APT_SOURCES}.bak" "$APT_SOURCES" 2>/dev/null || true
echo -e "${YELLOW} (原源仍可用;如需重试: 跳过本步 SKIP_APT_MIRROR=1,稍后手动 apt-get update 排查第三方源)${NC}"
fi
fi
else
echo -e "${YELLOW}⚠ 所有源均不可达(网络受限?),保持原配置${NC}"
fi
else
echo -e "${BLUE} - apt 源已自定义,跳过(FORCE_APT_MIRROR=1 可强制重测)${NC}"
fi
fi
fi
# ------------------------------------------------------------------
# 步骤 5: 检查前置依赖
# ------------------------------------------------------------------
step_end 4 "apt 源测速优化"
step_begin
echo -e "${YELLOW}[5/12] 检查前置依赖...${NC}"
# Bun 安装脚本需要 unzip
if ! command -v unzip &> /dev/null; then
echo -e "${YELLOW}⚠ 缺少 unzip,正在安装...${NC}"
if command -v apt-get &> /dev/null; then
apt_ensure_update
# 判定以 unzip 实际在场为准,不以 apt 退出码为准(oct 实测: 包装进程被信号杀,
# 但 apt 已完成安装,按退出码判死会误杀整个 setup);失败重试一次再复核
timeout -s KILL 300 $SUDO apt-get install -y unzip || \
timeout -s KILL 300 $SUDO apt-get install -y unzip || true
elif command -v yum &> /dev/null; then
$SUDO yum install -y unzip
elif command -v brew &> /dev/null; then
brew install unzip
elif command -v apk &> /dev/null; then
apk add unzip
else
echo -e "${RED}✗ 无法自动安装 unzip${NC}"
echo " 请手动安装后重新运行"
exit 1
fi
echo -e "${GREEN}✓ unzip 安装成功${NC}"
else
echo -e "${GREEN}✓ unzip 已就绪${NC}"
fi
# OpenCode 引导脚本需要 node(找到原生二进制后会切换到原生运行)
if ! command -v node &> /dev/null; then
echo -e "${YELLOW}⚠ 缺少 node,正在安装...${NC}"
# 优先 npmmirror node 二进制(国内快),失败回退发行版包管理器
NODE_INSTALLED=0
if command -v curl &> /dev/null && command -v tar &> /dev/null; then
NODE_ARCH="x64"
case "$(uname -m)" in
x86_64) NODE_ARCH="x64" ;;
aarch64) NODE_ARCH="arm64" ;;
*) NODE_ARCH="" ;;
esac
if [ -n "$NODE_ARCH" ]; then
NODE_TMP="$(mktemp -d)"
NODE_FILE=""
NODE_V=""
# 优先 LTS 系列(v24 → v22),取目录 JSON 中最新 linux 包名
for V in latest-v24.x latest-v22.x; do
NODE_FILE="$(curl -fsSL --connect-timeout 8 --max-time 20 "https://registry.npmmirror.com/-/binary/node/$V/" 2>/dev/null | grep -o "\"name\":\"node-v[0-9.]*-linux-$NODE_ARCH.tar.xz\"" | head -1 | cut -d'"' -f4 || true)"
if [ -n "$NODE_FILE" ]; then
NODE_V="$V"
break
fi
done
if [ -n "$NODE_FILE" ] && curl -fsSL --connect-timeout 8 --max-time 180 -o "$NODE_TMP/node.tar.xz" "https://registry.npmmirror.com/-/binary/node/$NODE_V/$NODE_FILE" 2>/dev/null; then
:
fi
NODE_DIR="$(tar -tJf "$NODE_TMP/node.tar.xz" 2>/dev/null | head -1 | cut -d/ -f1)"
if [ -n "$NODE_DIR" ] && tar -xJf "$NODE_TMP/node.tar.xz" -C "$NODE_TMP" 2>/dev/null; then
mkdir -p /usr/local/lib/nodejs
cp -r "$NODE_TMP/$NODE_DIR" /usr/local/lib/nodejs/
ln -sf "/usr/local/lib/nodejs/$NODE_DIR/bin/node" /usr/local/bin/node
ln -sf "/usr/local/lib/nodejs/$NODE_DIR/bin/npm" /usr/local/bin/npm
ln -sf "/usr/local/lib/nodejs/$NODE_DIR/bin/npx" /usr/local/bin/npx
NODE_INSTALLED=1
echo -e "${GREEN}✓ node 安装成功(npmmirror 二进制,$(node --version))${NC}"
fi
rm -rf "$NODE_TMP"
fi
fi
if [ "$NODE_INSTALLED" != "1" ]; then
echo -e "${YELLOW}npmmirror 下载失败,回退系统包管理器...${NC}"
if command -v apt-get &> /dev/null; then
curl -fsSL https://deb.nodesource.com/setup_lts.x | ${SUDO:+$SUDO -E }bash - 2>/dev/null
apt_ensure_update
# F1 修复(收官回归 r2/r2c/r3 实测): 裸 timeout 在 set -e 下超时即静默 137 退出,
# 走不到下方失败引导。比照 pip 回退: 重试一次+||true+由在场判定成败
timeout -s KILL 300 $SUDO apt-get install -y nodejs || \
timeout -s KILL 300 $SUDO apt-get install -y nodejs || true
elif command -v yum &> /dev/null; then
curl -fsSL https://rpm.nodesource.com/setup_lts.x | ${SUDO:+$SUDO -E }bash - 2>/dev/null
$SUDO yum install -y nodejs
elif command -v brew &> /dev/null; then
brew install node
else
echo -e "${RED}✗ 无法自动安装 node${NC}"
echo " 请手动安装后重新运行"
exit 1
fi
fi
if command -v node &> /dev/null; then
echo -e "${GREEN}✓ node 安装成功 ($(node --version))${NC}"
else
echo -e "${RED}✗ node 安装失败,请手动安装${NC}"
exit 1
fi
else
echo -e "${GREEN}✓ node 已就绪 ($(node --version))${NC}"
fi
echo -e "${YELLOW}配置 npm 镜像源 ($NPM_REGISTRY)...${NC}"
if ! grep -q "^registry=" "$HOME/.npmrc" 2>/dev/null; then
echo "registry=$NPM_REGISTRY" >> "$HOME/.npmrc"
echo -e "${GREEN}✓ npm 镜像源已配置 (npm/npx/bun 共用)${NC}"
else
echo -e "${BLUE} - ~/.npmrc 已有 registry 配置,跳过${NC}"
fi
# Python 生态: 无 pip 则用 ensurepip 引导, pip 可用时才配置中科大 PyPI 镜像
if command -v python3 &> /dev/null; then
PIP_READY=0
if python3 -m pip --version &> /dev/null; then
PIP_READY=1
else
echo -e "${YELLOW}⚠ 缺少 pip,正在引导安装...${NC}"
if python3 -m ensurepip --upgrade >/dev/null 2>&1 && python3 -m pip --version &> /dev/null; then
PIP_READY=1
echo -e "${GREEN}✓ pip 引导完成 ($(python3 -m pip --version 2>/dev/null | cut -d' ' -f2))${NC}"
elif command -v apt-get >/dev/null 2>&1; then
# 精简镜像常裁剪 python3-venv 致 ensurepip 不可用(oct/jammy 实测), 回退 apt 装 python3-pip
echo -e "${BLUE} - ensurepip 失败, 回退 apt 安装 python3-pip...${NC}"
apt_ensure_update
if timeout -s KILL 300 $SUDO apt-get install -y python3-pip >/dev/null 2>&1 && python3 -m pip --version &> /dev/null; then
PIP_READY=1
echo -e "${GREEN}✓ pip 经 apt 回退安装完成 ($(python3 -m pip --version 2>/dev/null | cut -d' ' -f2))${NC}"
else
echo -e "${YELLOW}⚠ ensurepip 与 apt 回退均失败(可手动执行: sudo apt install python3-pip)${NC}"
fi
else
echo -e "${YELLOW}⚠ ensurepip 失败(可手动执行: sudo apt install python3-pip)${NC}"
fi
fi
if [ "$PIP_READY" = "1" ]; then
PIP_CONF="$HOME/.config/pip/pip.conf"
if [ -f "$HOME/.pip/pip.conf" ]; then
PIP_CONF="$HOME/.pip/pip.conf"
fi
if ! grep -q "index-url" "$PIP_CONF" 2>/dev/null; then
mkdir -p "$(dirname "$PIP_CONF")"
printf '[global]\nindex-url = https://mirrors.ustc.edu.cn/pypi/simple\ntrusted-host = mirrors.ustc.edu.cn\n' > "$PIP_CONF"
echo -e "${GREEN}✓ PyPI 镜像源已配置 (中科大, $PIP_CONF)${NC}"
else
echo -e "${BLUE} - pip 已有 index-url 配置,跳过${NC}"
fi
fi
fi
# ------------------------------------------------------------------
# 步骤 6: 安装 Bun 运行时
# ------------------------------------------------------------------
step_end 5 "检查前置依赖"
step_begin
echo -e "${YELLOW}[6/12] 安装 Bun 运行时...${NC}"
ensure_bun() {
local bun_cmd=""
if command -v bun >/dev/null 2>&1; then
bun_cmd="bun"
elif [ -f "$HOME/.bun/bin/bun" ]; then
export PATH="$HOME/.bun/bin:$PATH"
bun_cmd="$HOME/.bun/bin/bun"
elif [ -f "/usr/local/bin/bun" ]; then
bun_cmd="/usr/local/bin/bun"
fi
if [ -n "$bun_cmd" ] && "$bun_cmd" --version >/dev/null 2>&1; then
echo -e "${GREEN}✓ Bun 已安装 ($("$bun_cmd" --version))${NC}"
return 0
fi
if [ -n "$bun_cmd" ]; then
echo -e "${YELLOW}⚠ 检测到损坏的 Bun,清理后重新安装...${NC}"
rm -f "$HOME/.bun/bin/bun" "$HOME/.bun/bin/bunx"
fi
echo "正在安装 Bun(优先 npm 镜像,失败回退 npmmirror 二进制,再回退官方脚本)..."
if npm i -g bun >/dev/null 2>&1 && command -v bun >/dev/null 2>&1 && bun --version >/dev/null 2>&1; then
echo -e "${GREEN}✓ Bun 安装成功 ($(bun --version))${NC}"
return 0
fi
echo -e "${YELLOW}⚠ npm 安装失败,尝试 npmmirror 二进制...${NC}"
if command -v unzip >/dev/null 2>&1 && command -v curl >/dev/null 2>&1; then
BUN_ARCH="x64"
case "$(uname -m)" in
x86_64) BUN_ARCH="x64" ;;
aarch64) BUN_ARCH="aarch64" ;;
*) BUN_ARCH="" ;;
esac
if [ -n "$BUN_ARCH" ]; then
BUN_VERSION="$(curl -fsSL --connect-timeout 8 --max-time 20 "https://registry.npmmirror.com/-/binary/bun/" 2>/dev/null | grep -o '"name":"bun-v[0-9.]*/"' | sed 's/"name":"//; s/\/"//' | sort -V | tail -1 || true)"
BUN_TMP="$(mktemp -d)"
if [ -n "$BUN_VERSION" ] && curl -fsSL --connect-timeout 8 --max-time 180 -o "$BUN_TMP/bun.zip" "https://registry.npmmirror.com/-/binary/bun/$BUN_VERSION/bun-linux-$BUN_ARCH.zip" 2>/dev/null && unzip -qo "$BUN_TMP/bun.zip" -d "$BUN_TMP" && [ -f "$BUN_TMP/bun-linux-$BUN_ARCH/bun" ]; then
mkdir -p "$HOME/.bun/bin"
install -m 0755 "$BUN_TMP/bun-linux-$BUN_ARCH/bun" "$HOME/.bun/bin/bun"
ln -sf "$HOME/.bun/bin/bun" "$HOME/.bun/bin/bunx"
export PATH="$HOME/.bun/bin:$PATH"
rm -rf "$BUN_TMP"
echo -e "${GREEN}✓ Bun 安装成功 (npmmirror 二进制, $(bun --version))${NC}"
return 0
fi
rm -rf "$BUN_TMP"
fi
fi
echo -e "${YELLOW}⚠ npmmirror 下载失败,回退官方安装脚本...${NC}"
curl -fsSL https://bun.sh/install | bash
if [ -f "$HOME/.bun/bin/bun" ]; then
export PATH="$HOME/.bun/bin:$PATH"
echo -e "${GREEN}✓ Bun 安装成功 ($(bun --version))${NC}"
else
echo -e "${RED}✗ Bun 安装失败,请手动安装: curl -fsSL https://bun.sh/install | bash${NC}"
exit 1
fi
}
ensure_bun
if [ ! -f "$HOME/.bunfig.toml" ]; then
printf '[install]\nregistry = "%s"\n' "$NPM_REGISTRY" > "$HOME/.bunfig.toml"
echo -e "${GREEN}✓ Bun registry 已配置 ($NPM_REGISTRY)${NC}"
else
echo -e "${BLUE} - ~/.bunfig.toml 已存在,跳过${NC}"
fi
# 确保 bun 在 PATH 中
if ! command -v bun &> /dev/null; then
export PATH="$HOME/.bun/bin:$PATH"
fi
# 如果 shell 配置中还没有 bun 路径,自动写入
BUN_PATH_LINE='export PATH="$HOME/.bun/bin:$PATH"'
if ! grep -q '\.bun/bin' "$HOME/.bashrc" 2>/dev/null; then
echo "" >> "$HOME/.bashrc"
echo "# Bun" >> "$HOME/.bashrc"
echo "$BUN_PATH_LINE" >> "$HOME/.bashrc"
echo -e "${GREEN}✓ Bun 路径已写入 ~/.bashrc${NC}"
fi
# 确保当前会话也能用
export PATH="$HOME/.bun/bin:$PATH"
# ------------------------------------------------------------------
# 步骤 7: 安装 OpenCode
# ------------------------------------------------------------------
step_end 6 "安装 Bun 运行时"
step_begin
echo -e "${YELLOW}[7/12] 安装 OpenCode...${NC}"
# 确保 Bun 路径优先(避免 WSL 下 Windows npm 版本抢在前)
export PATH="$HOME/.bun/bin:$PATH"
if [ -f "$HOME/.bun/bin/opencode" ]; then
echo -e "${GREEN}✓ OpenCode 已安装 ($(opencode --version))${NC}"
else
echo "正在通过 Bun 安装 OpenCode..."
bun install -g opencode-ai
if [ -f "$HOME/.bun/bin/opencode" ]; then
echo -e "${GREEN}✓ OpenCode 安装成功 ($(opencode --version))${NC}"
else
echo -e "${RED}✗ OpenCode 安装失败${NC}"
echo " 请手动安装: bun install -g opencode-ai"
exit 1
fi
fi
# 检查是否有 Windows npm 安装的 opencode 冲突
WINDOWS_OPENCODE=$(command -v opencode 2>/dev/null || true)
if [ -n "$WINDOWS_OPENCODE" ] && echo "$WINDOWS_OPENCODE" | grep -q "/mnt/"; then
echo -e "${YELLOW}⚠ 检测到 WSL 下存在 Windows npm 安装的 opencode${NC}"
echo " 当前优先级: $HOME/.bun/bin > $WINDOWS_OPENCODE"
echo " 如果输入 opencode 仍报错,请检查 PATH 顺序"
fi
# ------------------------------------------------------------------
# 步骤 8: 安装 oh-my-openagent 插件
# ------------------------------------------------------------------
step_end 7 "安装 OpenCode"
step_begin
echo -e "${YELLOW}[8/12] 安装 oh-my-openagent 插件...${NC}"
cd "$CONFIG_DIR"
if [ ! -d "node_modules" ] || [ ! -d "node_modules/oh-my-openagent" ]; then
bun add oh-my-openagent@latest 2>&1 | tail -3
echo -e "${GREEN}✓ oh-my-openagent 插件安装完成${NC}"
else
echo -e "${GREEN}✓ oh-my-openagent 插件已存在${NC}"
fi
echo -e "${YELLOW}应用 omo 模型跟随补丁...${NC}"
OMO_PATCH_FILE="$(mktemp /tmp/omo-patch.XXXXXX.mjs)"
cat > "$OMO_PATCH_FILE" << 'OMO_PATCH_EOF'
import { readFileSync, writeFileSync } from "node:fs";
const file = process.argv[2];
if (!file) {
console.error("usage: node omo-follow-system-default.mjs <dist/index.js>");
process.exit(1);
}
let src = readFileSync(file, "utf8");
let changed = 0;
const p1Marker = "Model resolved via system default (before hardcoded fallback chain)";
if (!src.includes(p1Marker)) {
const anchor =
' log3("No available model found in user fallback_models, falling through to hardcoded chain");\n' +
" }\n" +
" }\n" +
" if (fallbackChain && fallbackChain.length > 0) {";
const replacement =
' log3("No available model found in user fallback_models, falling through to hardcoded chain");\n' +
" }\n" +
" }\n" +
" if (systemDefaultModel !== undefined) {\n" +
' log3("Model resolved via system default (before hardcoded fallback chain)", { model: systemDefaultModel });\n' +
' return { model: systemDefaultModel, provenance: "system-default", attempted };\n' +
" }\n" +
" if (fallbackChain && fallbackChain.length > 0) {";
if (!src.includes(anchor)) throw new Error("patch1 anchor not found (omo dist changed)");
src = src.replace(anchor, replacement);
const tail =
" if (systemDefaultModel === undefined) {\n" +
' log3("No model resolved - systemDefaultModel not configured");\n' +
" return;\n" +
" }\n" +
' log3("Model resolved via system default", { model: systemDefaultModel });\n' +
' return { model: systemDefaultModel, provenance: "system-default", attempted };\n';
if (!src.includes(tail)) throw new Error("patch1 tail not found");
src = src.replace(tail, "");
changed++;
}
const p2Marker = "[resolveModelForDelegateTask] system default before hardcoded chain";
if (!src.includes(p2Marker)) {
const anchor = " const fallbackChain = input.fallbackChain;\n" + " if (fallbackChain && fallbackChain.length > 0) {";
const replacement =
" const systemDefaultModel = normalizeModel(input.systemDefaultModel);\n" +
" if (systemDefaultModel) {\n" +
' deps.log?.("[resolveModelForDelegateTask] system default before hardcoded chain", { model: systemDefaultModel });\n' +
" return { model: systemDefaultModel };\n" +
" }\n" +
" const fallbackChain = input.fallbackChain;\n" +
" if (fallbackChain && fallbackChain.length > 0) {";
if (!src.includes(anchor)) throw new Error("patch2 anchor not found");
src = src.replace(anchor, replacement);
const tail =
" const systemDefaultModel = normalizeModel(input.systemDefaultModel);\n" +