-
Notifications
You must be signed in to change notification settings - Fork 33
Expand file tree
/
Copy pathsetup.sh
More file actions
executable file
·1401 lines (1284 loc) · 43 KB
/
setup.sh
File metadata and controls
executable file
·1401 lines (1284 loc) · 43 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
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ENV_FILE="${ROOT_DIR}/.env"
ENV_EXAMPLE_FILE="${ROOT_DIR}/.env.example"
INSTALL_STATE_FILE="${ROOT_DIR}/install/.state.env"
MODULE_HELPER="${ROOT_DIR}/install/module_registry.py"
MODULES_FILE="${ROOT_DIR}/install/modules.yaml"
COMPOSE_FILE="${ROOT_DIR}/compose.yaml"
COMPOSE_PROD_FILE="${ROOT_DIR}/compose.prod.yaml"
SETUP_SKIP_COMPOSE="${ARKLOOP_SETUP_SKIP_COMPOSE:-0}"
SETUP_LANG="${ARKLOOP_SETUP_LANG:-}"
USE_PROD_IMAGES="${ARKLOOP_PROD:-0}"
HOST_OS=""
HAS_KVM="0"
DETECTED_DOCKER_SOCKET=""
DOCKER_OK="0"
COMPOSE_OK="0"
COMPOSE_BASE_CMD=()
HAD_ENV_FILE_BEFORE_INSTALL="0"
normalize_setup_lang() {
local lang="$1"
case "$lang" in
"")
local detected="${LC_ALL:-${LC_MESSAGES:-${LANG:-}}}"
case "$detected" in
zh*|ZH*) printf 'zh-CN' ;;
*) printf 'en' ;;
esac
;;
zh|zh-CN|zh_CN|cn|CN) printf 'zh-CN' ;;
en|en-US|en_US|en-GB|en_GB) printf 'en' ;;
*) fail "Unsupported setup language: $lang" ;;
esac
}
setup_lang() {
if [ -z "$SETUP_LANG" ]; then
SETUP_LANG="$(normalize_setup_lang "")"
fi
printf '%s' "$SETUP_LANG"
}
t() {
local key="$1"
local lang
lang="$(setup_lang)"
case "$lang:$key" in
zh-CN:usage)
cat <<'EOF'
用法:
./setup.sh install [flags]
./setup.sh doctor [--gateway-port <port>] [--lang zh-CN|en]
./setup.sh status [--lang zh-CN|en]
./setup.sh upgrade [--prod] [--version <tag>] [--yes] [--lang zh-CN|en]
./setup.sh uninstall [--purge] [--yes] [--lang zh-CN|en]
install flags:
--profile standard|full
--mode self-hosted|saas
--memory none|openviking
--sandbox none|docker|firecracker
--console lite|full
--browser off|on
--web-tools builtin|self-hosted
--gateway on|off
--gateway-port <port>
--lang zh-CN|en
--non-interactive
--prod 使用预构建镜像(compose.prod.yaml)
说明:
- browser=on 仅支持 sandbox=docker
EOF
;;
en:usage)
cat <<'EOF'
Usage:
./setup.sh install [flags]
./setup.sh doctor [--gateway-port <port>] [--lang zh-CN|en]
./setup.sh status [--lang zh-CN|en]
./setup.sh upgrade [--prod] [--version <tag>] [--yes] [--lang zh-CN|en]
./setup.sh uninstall [--purge] [--yes] [--lang zh-CN|en]
install flags:
--profile standard|full
--mode self-hosted|saas
--memory none|openviking
--sandbox none|docker|firecracker
--console lite|full
--browser off|on
--web-tools builtin|self-hosted
--gateway on|off
--gateway-port <port>
--lang zh-CN|en
--non-interactive
--prod Use pre-built images (compose.prod.yaml)
Notes:
- browser=on only works with sandbox=docker
EOF
;;
zh-CN:missing_dependency) printf '缺少依赖:%s' "$2" ;;
en:missing_dependency) printf 'Missing dependency: %s' "$2" ;;
zh-CN:missing_env_example) printf '缺少 %s' "$2" ;;
en:missing_env_example) printf 'Missing %s' "$2" ;;
zh-CN:unknown_secret_kind) printf '未知 secret 生成类型:%s' "$2" ;;
en:unknown_secret_kind) printf 'Unknown secret generator kind: %s' "$2" ;;
zh-CN:install_validation_failed) printf '安装参数校验失败' ;;
en:install_validation_failed) printf 'Install argument validation failed' ;;
zh-CN:prompt_profile) printf '部署档位(standard/full)' ;;
en:prompt_profile) printf 'Deployment profile (standard/full)' ;;
zh-CN:prompt_mode) printf '部署模式(self-hosted/saas)' ;;
en:prompt_mode) printf 'Deployment mode (self-hosted/saas)' ;;
zh-CN:prompt_memory) printf '记忆系统(none/openviking)' ;;
en:prompt_memory) printf 'Memory system (none/openviking)' ;;
zh-CN:prompt_sandbox) printf '代码执行(none/docker/firecracker)' ;;
en:prompt_sandbox) printf 'Code execution (none/docker/firecracker)' ;;
zh-CN:prompt_web_tools) printf '搜索/抓取(builtin/self-hosted)' ;;
en:prompt_web_tools) printf 'Search/scraping (builtin/self-hosted)' ;;
zh-CN:prompt_console) printf 'Console(lite/full)' ;;
en:prompt_console) printf 'Console (lite/full)' ;;
zh-CN:prompt_browser) printf '浏览器模块(off/on)' ;;
en:prompt_browser) printf 'Browser module (off/on)' ;;
zh-CN:prompt_gateway) printf 'Gateway(on/off)' ;;
en:prompt_gateway) printf 'Gateway (on/off)' ;;
zh-CN:prompt_gateway_port) printf 'Gateway 端口' ;;
en:prompt_gateway_port) printf 'Gateway port' ;;
zh-CN:missing_docker_socket) printf '未找到可用的用户态 Docker socket' ;;
en:missing_docker_socket) printf 'No usable user-space Docker socket found' ;;
zh-CN:docker_unavailable) printf 'Docker 不可用' ;;
en:docker_unavailable) printf 'Docker is unavailable' ;;
zh-CN:compose_unavailable) printf 'docker compose 不可用' ;;
en:compose_unavailable) printf 'docker compose is unavailable' ;;
zh-CN:firecracker_linux_only) printf 'firecracker 仅支持 Linux' ;;
en:firecracker_linux_only) printf 'firecracker is only supported on Linux' ;;
zh-CN:kvm_missing) printf '当前宿主未检测到 KVM' ;;
en:kvm_missing) printf 'KVM was not detected on this host' ;;
zh-CN:gateway_port_in_use) printf '端口 %s 已被占用' "$2" ;;
en:gateway_port_in_use) printf 'Port %s is already in use' "$2" ;;
zh-CN:preflight_failed) printf 'pre-flight 检测未通过' ;;
en:preflight_failed) printf 'Pre-flight checks failed' ;;
zh-CN:stale_postgres_volume) printf '检测到旧的 PostgreSQL 数据卷,但当前 .env 是新生成的。请执行 ./setup.sh uninstall --purge --yes 清理旧卷,或恢复原来的 .env。' ;;
en:stale_postgres_volume) printf 'An existing PostgreSQL data volume was found, but the current .env was freshly generated. Run ./setup.sh uninstall --purge --yes to remove the old volume, or restore the previous .env.' ;;
zh-CN:unknown_arg) printf '未知参数:%s' "$2" ;;
en:unknown_arg) printf 'Unknown argument: %s' "$2" ;;
zh-CN:invalid_port) printf '无效端口:%s' "$2" ;;
en:invalid_port) printf 'Invalid port: %s' "$2" ;;
zh-CN:install_plan) printf '安装方案:profile=%s mode=%s memory=%s sandbox=%s console=%s browser=%s web-tools=%s gateway-port=%s' "$2" "$3" "$4" "$5" "$6" "$7" "$8" "$9" ;;
en:install_plan) printf 'Install plan: profile=%s mode=%s memory=%s sandbox=%s console=%s browser=%s web-tools=%s gateway-port=%s' "$2" "$3" "$4" "$5" "$6" "$7" "$8" "$9" ;;
zh-CN:skip_compose) printf '已跳过 Compose 执行(ARKLOOP_SETUP_SKIP_COMPOSE=1)' ;;
en:skip_compose) printf 'Skipped Compose execution (ARKLOOP_SETUP_SKIP_COMPOSE=1)' ;;
zh-CN:starting_modules) printf '启动模块:%s' "$2" ;;
en:starting_modules) printf 'Starting modules: %s' "$2" ;;
zh-CN:starting_gateway) printf '启动 Gateway' ;;
en:starting_gateway) printf 'Starting Gateway' ;;
zh-CN:service_health_timeout) printf '服务健康检查超时,请执行 ./setup.sh status 查看详情' ;;
en:service_health_timeout) printf 'Service health checks timed out, run ./setup.sh status for details' ;;
zh-CN:gateway_health_failed) printf 'Gateway 健康检查失败' ;;
en:gateway_health_failed) printf 'Gateway health check failed' ;;
zh-CN:console_not_ready) printf 'Console 入口未就绪' ;;
en:console_not_ready) printf 'Console entry is not ready' ;;
zh-CN:install_done) printf '安装完成' ;;
en:install_done) printf 'Install completed' ;;
zh-CN:entry_url) printf '入口地址:http://localhost:%s' "$2" ;;
en:entry_url) printf 'Entry URL: http://localhost:%s' "$2" ;;
zh-CN:next_step_console) printf '下一步:如上方已打印管理员初始化地址,请优先打开它;否则直接登录 Console。' ;;
en:next_step_console) printf 'Next: open the admin bootstrap URL above if one was printed; otherwise log in to Console directly.' ;;
zh-CN:install_done_no_gateway) printf '安装完成(未启用 Gateway)' ;;
en:install_done_no_gateway) printf 'Install completed (Gateway disabled)' ;;
zh-CN:status_metadata_missing) printf '未发现 setup.sh 安装元数据,仅输出当前 compose 状态' ;;
en:status_metadata_missing) printf 'No setup.sh install metadata found, printing current compose state only' ;;
zh-CN:upgrade_prereq_failed) printf 'upgrade 前置检查失败:Docker / Compose 不可用' ;;
en:upgrade_prereq_failed) printf 'Upgrade pre-check failed: Docker / Compose is unavailable' ;;
zh-CN:upgrade_no_install) printf '未找到安装记录,请先执行 ./setup.sh install' ;;
en:upgrade_no_install) printf 'No installation found. Please run ./setup.sh install first.' ;;
zh-CN:upgrade_current_state) printf '当前安装状态:profile=%s mode=%s' "$2" "$3" ;;
en:upgrade_current_state) printf 'Current install state: profile=%s mode=%s' "$2" "$3" ;;
zh-CN:upgrade_confirm) printf '确认升级?[y/N]: ' ;;
en:upgrade_confirm) printf 'Proceed with upgrade? [y/N]: ' ;;
zh-CN:upgrade_pulling) printf '正在拉取最新镜像...' ;;
en:upgrade_pulling) printf 'Pulling latest images...' ;;
zh-CN:upgrade_building) printf '正在重新构建服务...' ;;
en:upgrade_building) printf 'Rebuilding services...' ;;
zh-CN:upgrade_migrating) printf '正在执行数据库迁移...' ;;
en:upgrade_migrating) printf 'Running database migrations...' ;;
zh-CN:upgrade_restarting) printf '正在重启服务...' ;;
en:upgrade_restarting) printf 'Restarting services...' ;;
zh-CN:upgrade_health_wait) printf '等待服务健康检查...' ;;
en:upgrade_health_wait) printf 'Waiting for service health checks...' ;;
zh-CN:upgrade_done) printf '升级完成' ;;
en:upgrade_done) printf 'Upgrade completed' ;;
zh-CN:upgrade_failed) printf '升级失败' ;;
en:upgrade_failed) printf 'Upgrade failed' ;;
zh-CN:upgrade_version_set) printf '目标版本已设置为 %s' "$2" ;;
en:upgrade_version_set) printf 'Target version set to %s' "$2" ;;
zh-CN:upgrade_prod_note) printf '使用预构建镜像模式' ;;
en:upgrade_prod_note) printf 'Using pre-built images mode' ;;
zh-CN:confirm_uninstall) printf '确认卸载 Arkloop?默认保留卷与 .env [y/N]: ' ;;
en:confirm_uninstall) printf 'Uninstall Arkloop? Volumes and .env are kept by default [y/N]: ' ;;
zh-CN:cancelled) printf '已取消' ;;
en:cancelled) printf 'Cancelled' ;;
zh-CN:uninstall_done) printf '卸载完成' ;;
en:uninstall_done) printf 'Uninstall completed' ;;
zh-CN:unknown_command) printf '未知命令:%s' "$2" ;;
en:unknown_command) printf 'Unknown command: %s' "$2" ;;
*) printf '%s' "$key" ;;
esac
}
print_usage() {
t usage
}
log() {
printf '[setup] %s\n' "$*"
}
warn() {
printf '[setup] warning: %s\n' "$*" >&2
}
fail() {
printf '[setup] error: %s\n' "$*" >&2
exit 1
}
require_command() {
command -v "$1" >/dev/null 2>&1 || fail "$(t missing_dependency "$1")"
}
trim() {
local value="$1"
value="${value#${value%%[![:space:]]*}}"
value="${value%${value##*[![:space:]]}}"
printf '%s' "$value"
}
detect_host() {
local uname_out
uname_out="$(uname -s)"
case "$uname_out" in
Linux)
if [ -f /proc/version ] && grep -qi microsoft /proc/version; then
HOST_OS="wsl2"
else
HOST_OS="linux"
fi
;;
Darwin)
HOST_OS="macos"
;;
*)
HOST_OS="macos"
;;
esac
if [ "$HOST_OS" = "linux" ] && [ -c /dev/kvm ]; then
HAS_KVM="1"
else
HAS_KVM="0"
fi
}
check_docker_tools() {
if command -v docker >/dev/null 2>&1; then
if docker info >/dev/null 2>&1; then
DOCKER_OK="1"
fi
fi
if [ "$DOCKER_OK" = "1" ] && docker compose version >/dev/null 2>&1; then
COMPOSE_OK="1"
fi
}
python_env_get() {
python3 - "$ENV_FILE" "$1" <<'PY'
import sys
from pathlib import Path
path = Path(sys.argv[1])
key = sys.argv[2]
if not path.exists():
raise SystemExit(0)
for raw in path.read_text(encoding='utf-8').splitlines():
line = raw.strip()
if not line or line.startswith('#') or '=' not in raw:
continue
k, v = raw.split('=', 1)
if k.strip() == key:
print(v)
PY
}
python_env_set() {
python3 - "$ENV_FILE" "$1" "$2" <<'PY'
import sys
from pathlib import Path
path = Path(sys.argv[1])
key = sys.argv[2]
value = sys.argv[3]
if path.exists():
lines = path.read_text(encoding='utf-8').splitlines()
else:
lines = []
out = []
updated = False
for raw in lines:
if raw.strip().startswith('#') or '=' not in raw:
out.append(raw)
continue
current_key, _ = raw.split('=', 1)
if current_key.strip() == key:
out.append(f"{key}={value}")
updated = True
else:
out.append(raw)
if not updated:
if out and out[-1] != '':
out.append('')
out.append(f"{key}={value}")
path.write_text("\n".join(out) + "\n", encoding='utf-8')
PY
}
python_env_delete() {
python3 - "$ENV_FILE" "$1" <<'PY'
import sys
from pathlib import Path
path = Path(sys.argv[1])
key = sys.argv[2]
if not path.exists():
raise SystemExit(0)
lines = path.read_text(encoding='utf-8').splitlines()
out = []
for raw in lines:
if raw.strip().startswith('#') or '=' not in raw:
out.append(raw)
continue
current_key, _ = raw.split('=', 1)
if current_key.strip() != key:
out.append(raw)
path.write_text("\n".join(out) + "\n", encoding='utf-8')
PY
}
python_state_get() {
python3 - "$INSTALL_STATE_FILE" "$1" <<'PY'
import sys
from pathlib import Path
path = Path(sys.argv[1])
key = sys.argv[2]
if not path.exists():
raise SystemExit(0)
for raw in path.read_text(encoding='utf-8').splitlines():
line = raw.strip()
if not line or line.startswith('#') or '=' not in raw:
continue
k, v = raw.split('=', 1)
if k.strip() == key:
print(v)
PY
}
python_state_set() {
python3 - "$INSTALL_STATE_FILE" "$1" "$2" <<'PY'
import sys
from pathlib import Path
path = Path(sys.argv[1])
path.parent.mkdir(parents=True, exist_ok=True)
key = sys.argv[2]
value = sys.argv[3]
if path.exists():
lines = path.read_text(encoding='utf-8').splitlines()
else:
lines = []
out = []
updated = False
for raw in lines:
if raw.strip().startswith('#') or '=' not in raw:
out.append(raw)
continue
current_key, _ = raw.split('=', 1)
if current_key.strip() == key:
out.append(f"{key}={value}")
updated = True
else:
out.append(raw)
if not updated:
if out and out[-1] != '':
out.append('')
out.append(f"{key}={value}")
path.write_text("\n".join(out) + "\n", encoding='utf-8')
PY
}
ensure_env_file() {
[ -f "$ENV_EXAMPLE_FILE" ] || fail "$(t missing_env_example "$ENV_EXAMPLE_FILE")"
if [ ! -f "$ENV_FILE" ]; then
cp "$ENV_EXAMPLE_FILE" "$ENV_FILE"
fi
}
generate_hex() {
if command -v openssl >/dev/null 2>&1; then
openssl rand -hex "$1"
else
python3 - "$1" <<'PY'
import secrets, sys
print(secrets.token_hex(int(sys.argv[1])))
PY
fi
}
generate_base64() {
if command -v openssl >/dev/null 2>&1; then
openssl rand -base64 "$1" | tr -d '\n'
else
python3 - "$1" <<'PY'
import base64, secrets, sys
print(base64.b64encode(secrets.token_bytes(int(sys.argv[1]))).decode())
PY
fi
}
ensure_secret() {
local key="$1"
local kind="$2"
local current
current="$(python_env_get "$key")"
case "$key" in
ARKLOOP_POSTGRES_PASSWORD)
if [ -n "$current" ] && [ "$current" != "please_change_me" ]; then return; fi
;;
ARKLOOP_REDIS_PASSWORD)
if [ -n "$current" ] && [ "$current" != "arkloop_redis" ]; then return; fi
;;
ARKLOOP_AUTH_JWT_SECRET)
case "$current" in
""|please_change_me*) ;;
*) return ;;
esac
;;
ARKLOOP_ENCRYPTION_KEY)
case "$current" in
""|please_generate_with_*) ;;
*) return ;;
esac
;;
ARKLOOP_SANDBOX_AUTH_TOKEN|ARKLOOP_S3_SECRET_KEY)
if [ -n "$current" ] && [ "$current" != "please_change_me" ]; then return; fi
;;
*)
if [ -n "$current" ]; then return; fi
;;
esac
local generated=""
case "$kind" in
hex16) generated="$(generate_hex 16)" ;;
hex32) generated="$(generate_hex 32)" ;;
base64_48) generated="$(generate_base64 48)" ;;
*) fail "$(t unknown_secret_kind "$kind")" ;;
esac
python_env_set "$key" "$generated"
}
set_if_empty() {
local key="$1"
local value="$2"
local current
current="$(python_env_get "$key")"
if [ -z "$current" ]; then
python_env_set "$key" "$value"
fi
}
set_value() {
python_env_set "$1" "$2"
}
set_install_state() {
python_state_set "$1" "$2"
}
port_in_use() {
python3 - "$1" <<'PY'
import socket, sys
port = int(sys.argv[1])
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
sock.settimeout(0.2)
sys.exit(0 if sock.connect_ex(("127.0.0.1", port)) == 0 else 1)
PY
}
detect_docker_socket() {
local explicit="${ARKLOOP_SANDBOX_DOCKER_SOCKET_PATH:-}"
if [ -z "$explicit" ]; then
explicit="$(python_env_get ARKLOOP_SANDBOX_DOCKER_SOCKET_PATH)"
fi
if [ -n "$explicit" ] && [ "$explicit" != "/var/run/docker.sock" ] && [ -S "$explicit" ]; then
DETECTED_DOCKER_SOCKET="$explicit"
return
fi
local candidates=()
case "$HOST_OS" in
linux)
if [ -n "${XDG_RUNTIME_DIR:-}" ]; then
candidates+=("${XDG_RUNTIME_DIR}/docker.sock")
fi
candidates+=("/run/user/$(id -u)/docker.sock" "$HOME/.docker/run/docker.sock")
;;
macos)
candidates+=("$HOME/.docker/run/docker.sock")
;;
wsl2)
candidates+=("/mnt/wsl/docker-desktop/shared-sockets/guest-services/docker.sock" "$HOME/.docker/run/docker.sock")
;;
esac
local candidate
for candidate in "${candidates[@]}"; do
if [ -S "$candidate" ]; then
DETECTED_DOCKER_SOCKET="$candidate"
return
fi
done
DETECTED_DOCKER_SOCKET=""
}
compose_base_cmd() {
local profiles_text="$1"
COMPOSE_BASE_CMD=(docker compose -f "$COMPOSE_FILE")
if [ "$USE_PROD_IMAGES" = "1" ]; then
COMPOSE_BASE_CMD+=(-f "$COMPOSE_PROD_FILE")
fi
local line
while IFS= read -r line; do
[ -n "$line" ] || continue
COMPOSE_BASE_CMD+=(--profile "$line")
done <<EOF
$profiles_text
EOF
}
read_lines_to_array() {
local text="$1"
local target_name="$2"
eval "$target_name=()"
local line
while IFS= read -r line; do
[ -n "$line" ] || continue
eval "$target_name+=(\"\$line\")"
done <<EOF
$text
EOF
}
resolve_plan() {
local profile="$1"
local mode="$2"
local memory="$3"
local sandbox="$4"
local console="$5"
local browser="$6"
local web_tools="$7"
local gateway="$8"
local cmd=(python3 "$MODULE_HELPER" resolve --modules "$MODULES_FILE" --host-os "$HOST_OS")
if [ "$HAS_KVM" = "1" ]; then
cmd+=(--has-kvm)
fi
[ -n "$profile" ] && cmd+=(--profile "$profile")
[ -n "$mode" ] && cmd+=(--mode "$mode")
[ -n "$memory" ] && cmd+=(--memory "$memory")
[ -n "$sandbox" ] && cmd+=(--sandbox "$sandbox")
[ -n "$console" ] && cmd+=(--console "$console")
[ -n "$browser" ] && cmd+=(--browser "$browser")
[ -n "$web_tools" ] && cmd+=(--web-tools "$web_tools")
[ -n "$gateway" ] && cmd+=(--gateway "$gateway")
local output
if ! output="$("${cmd[@]}")"; then
fail "$(t install_validation_failed)"
fi
eval "$output"
}
prompt_choice() {
local label="$1"
local default_value="$2"
local result=""
printf '%s [%s]: ' "$label" "$default_value" >&2
IFS= read -r result || true
result="$(trim "$result")"
if [ -z "$result" ]; then
result="$default_value"
fi
printf '%s' "$result"
}
validate_port() {
local port="$1"
case "$port" in
''|*[!0-9]*) return 1 ;;
esac
[ "$port" -ge 1 ] && [ "$port" -le 65535 ]
}
collect_install_inputs() {
local profile="$1"
local mode="$2"
local memory="$3"
local sandbox="$4"
local console="$5"
local browser="$6"
local web_tools="$7"
local gateway="$8"
local gateway_port="$9"
if [ "${NON_INTERACTIVE:-0}" = "1" ]; then
INSTALL_PROFILE="$profile"
INSTALL_MODE="$mode"
INSTALL_MEMORY="$memory"
INSTALL_SANDBOX="$sandbox"
INSTALL_CONSOLE="$console"
INSTALL_BROWSER="$browser"
INSTALL_WEB_TOOLS="$web_tools"
INSTALL_GATEWAY="$gateway"
INSTALL_GATEWAY_PORT="$gateway_port"
return
fi
INSTALL_PROFILE="$(prompt_choice "$(t prompt_profile)" "${profile:-standard}")"
INSTALL_MODE="$(prompt_choice "$(t prompt_mode)" "${mode:-self-hosted}")"
INSTALL_MEMORY="$(prompt_choice "$(t prompt_memory)" "${memory:-}")"
INSTALL_SANDBOX="$(prompt_choice "$(t prompt_sandbox)" "${sandbox:-}")"
INSTALL_WEB_TOOLS="$(prompt_choice "$(t prompt_web_tools)" "${web_tools:-}")"
INSTALL_CONSOLE="$(prompt_choice "$(t prompt_console)" "${console:-}")"
INSTALL_BROWSER="$(prompt_choice "$(t prompt_browser)" "${browser:-off}")"
INSTALL_GATEWAY="$(prompt_choice "$(t prompt_gateway)" "${gateway:-on}")"
INSTALL_GATEWAY_PORT="$(prompt_choice "$(t prompt_gateway_port)" "${gateway_port:-19000}")"
}
compose_ps_lines() {
if [ "$COMPOSE_OK" != "1" ]; then
return 0
fi
local raw
raw="$(${COMPOSE_BASE_CMD[@]} ps -a --format json 2>/dev/null || true)"
python3 - <<'PY' "$raw"
import json, sys
raw = sys.argv[1].strip()
if not raw:
raise SystemExit(0)
items = []
try:
parsed = json.loads(raw)
if isinstance(parsed, dict):
items = [parsed]
elif isinstance(parsed, list):
items = parsed
except Exception:
for line in raw.splitlines():
line = line.strip()
if not line:
continue
items.append(json.loads(line))
for item in items:
print("\t".join([
str(item.get("Service", "")),
str(item.get("State", "")),
str(item.get("Health", "")),
str(item.get("ExitCode", "")),
]))
PY
}
service_status_line() {
local target_service="$1"
local line
while IFS= read -r line; do
[ -n "$line" ] || continue
local service state health exit_code
service="${line%%$'\t'*}"
if [ "$service" = "$target_service" ]; then
printf '%s' "$line"
return 0
fi
done <<EOF
$(compose_ps_lines)
EOF
return 1
}
compose_project_name() {
basename "$ROOT_DIR" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9_-]//g'
}
named_volume_exists() {
local name="$1"
docker volume inspect "$name" >/dev/null 2>&1
}
service_ready() {
local service="$1"
local record state health exit_code rest
if ! record="$(service_status_line "$service")"; then
return 1
fi
rest="${record#*$'\t'}"
state="${rest%%$'\t'*}"
rest="${rest#*$'\t'}"
health="${rest%%$'\t'*}"
exit_code="${record##*$'\t'}"
case "$service" in
migrate)
[ "$state" = "exited" ] && [ "$exit_code" = "0" ]
return
;;
esac
if [ "$health" = "healthy" ]; then
return 0
fi
[ "$state" = "running" ] && [ -z "$health" ]
}
wait_for_http() {
local url="$1"
local timeout_seconds="$2"
local started_at now
started_at="$(date +%s)"
while true; do
if curl -fsS "$url" >/dev/null 2>&1; then
return 0
fi
now="$(date +%s)"
if [ $((now - started_at)) -ge "$timeout_seconds" ]; then
return 1
fi
sleep 2
done
}
bootstrap_init_url() {
local gateway_port="$1"
local endpoint="http://127.0.0.1:${gateway_port}/v1/bootstrap/init"
local tmp_file http_code payload token expires_at
tmp_file="$(mktemp)"
http_code="$(curl -sS -o "$tmp_file" -w '%{http_code}' -X POST "$endpoint" || true)"
payload="$(cat "$tmp_file" 2>/dev/null || true)"
rm -f "$tmp_file"
case "$http_code" in
201)
token="$(python3 -c 'import json,sys; data=json.load(sys.stdin); print(data.get("token", ""))' <<<"$payload" 2>/dev/null || true)"
expires_at="$(python3 -c 'import json,sys; data=json.load(sys.stdin); print(data.get("expires_at", ""))' <<<"$payload" 2>/dev/null || true)"
if [ -z "$token" ]; then
warn "bootstrap token 创建失败:响应缺少 token"
return 0
fi
printf '管理员初始化地址:http://localhost:%s/bootstrap/%s
' "$gateway_port" "$token"
if [ -n "$expires_at" ]; then
printf '过期时间:%s
' "$expires_at"
fi
;;
409)
log "已存在平台管理员,跳过 bootstrap URL"
;;
*)
warn "bootstrap token 创建失败(status=${http_code:-unknown})"
;;
esac
}
wait_for_services() {
local -a services=("$@")
local started_at now
started_at="$(date +%s)"
while true; do
local all_ready="1"
local service
for service in "${services[@]}"; do
if ! service_ready "$service"; then
all_ready="0"
break
fi
done
if [ "$all_ready" = "1" ]; then
return 0
fi
now="$(date +%s)"
if [ $((now - started_at)) -ge 180 ]; then
return 1
fi
sleep 3
done
}
apply_runtime_env() {
local gateway_port pg_user pg_db pg_pass redis_pass console_upstream
gateway_port="$INSTALL_GATEWAY_PORT"
[ -n "$gateway_port" ] || gateway_port="$(python_env_get ARKLOOP_GATEWAY_PORT)"
[ -n "$gateway_port" ] || gateway_port="19000"
validate_port "$gateway_port" || fail "$(t invalid_port "$gateway_port")"
set_value ARKLOOP_GATEWAY_PORT "$gateway_port"
pg_user="$(python_env_get ARKLOOP_POSTGRES_USER)"
[ -n "$pg_user" ] || pg_user="arkloop"
pg_db="$(python_env_get ARKLOOP_POSTGRES_DB)"
[ -n "$pg_db" ] || pg_db="arkloop"
pg_pass="$(python_env_get ARKLOOP_POSTGRES_PASSWORD)"
redis_pass="$(python_env_get ARKLOOP_REDIS_PASSWORD)"
set_value DATABASE_URL "postgresql://${pg_user}:${pg_pass}@127.0.0.1:5432/${pg_db}"
set_value ARKLOOP_DATABASE_URL "postgresql://${pg_user}:${pg_pass}@127.0.0.1:5432/${pg_db}"
set_value ARKLOOP_PGBOUNCER_URL "postgresql://${pg_user}:${pg_pass}@127.0.0.1:5433/${pg_db}"
set_value ARKLOOP_REDIS_URL "redis://:${redis_pass}@127.0.0.1:6379/0"
set_value ARKLOOP_GATEWAY_REDIS_URL "redis://:${redis_pass}@127.0.0.1:6379/1"
set_if_empty ARKLOOP_GATEWAY_CORS_ALLOWED_ORIGINS "http://localhost:19080,http://localhost:19081,http://localhost:19082"
case "$RESOLVED_CONSOLE" in
lite) console_upstream="http://console-lite:80" ;;
full) console_upstream="http://console:80" ;;
*) console_upstream="" ;;
esac
set_value ARKLOOP_GATEWAY_FRONTEND_UPSTREAM "$console_upstream"
case "$RESOLVED_MEMORY" in
openviking|none) python_env_delete ARKLOOP_OPENVIKING_BASE_URL ;;
esac
case "$RESOLVED_SANDBOX" in
docker)
set_value ARKLOOP_SANDBOX_PROVIDER "docker"
[ -n "$DETECTED_DOCKER_SOCKET" ] || fail "$(t missing_docker_socket)"
set_value ARKLOOP_SANDBOX_DOCKER_SOCKET_PATH "$DETECTED_DOCKER_SOCKET"
;;
firecracker)
set_value ARKLOOP_SANDBOX_PROVIDER "firecracker"
python_env_delete ARKLOOP_SANDBOX_DOCKER_SOCKET_PATH
;;
none)
python_env_delete ARKLOOP_SANDBOX_PROVIDER
python_env_delete ARKLOOP_SANDBOX_DOCKER_SOCKET_PATH
;;
esac
python_env_delete ARKLOOP_SANDBOX_BASE_URL
# SaaS mode: PGBouncer, S3 storage, security hardening
if [ "$RESOLVED_MODE" = "saas" ]; then
# When PGBouncer is selected, route Docker traffic through it
if printf '%s' "$SELECTED_MODULES" | grep -q pgbouncer; then
set_value ARKLOOP_DOCKER_DATABASE_URL "postgresql://${pg_user}:${pg_pass}@pgbouncer:5432/${pg_db}"
set_value ARKLOOP_DOCKER_DATABASE_DIRECT_URL "postgresql://${pg_user}:${pg_pass}@postgres:5432/${pg_db}"
fi
# When SeaweedFS is selected, switch storage backend to S3
if printf '%s' "$SELECTED_MODULES" | grep -q seaweedfs; then
set_value ARKLOOP_STORAGE_BACKEND "s3"
set_if_empty ARKLOOP_S3_ENDPOINT "http://127.0.0.1:9000"
set_if_empty ARKLOOP_S3_ENDPOINT_DOCKER "http://seaweedfs:8333"
set_if_empty ARKLOOP_S3_REGION "us-east-1"
fi
# SaaS: disable fake-IP trust, set Turnstile placeholders
set_value ARKLOOP_OUTBOUND_TRUST_FAKE_IP "false"
set_if_empty ARKLOOP_TURNSTILE_SECRET_KEY ""
set_if_empty ARKLOOP_TURNSTILE_SITE_KEY ""
set_if_empty ARKLOOP_TURNSTILE_ALLOWED_HOST ""
fi
if [ "$RESOLVED_BROWSER" = "on" ]; then
set_value ARKLOOP_SANDBOX_WARM_BROWSER "1"
else
set_value ARKLOOP_SANDBOX_WARM_BROWSER "0"
fi
python_env_delete ARKLOOP_APP_BASE_URL
python_env_delete ARKLOOP_WEB_SEARCH_PROVIDER
python_env_delete ARKLOOP_WEB_SEARCH_SEARXNG_BASE_URL
python_env_delete ARKLOOP_WEB_FETCH_PROVIDER
python_env_delete ARKLOOP_WEB_FETCH_FIRECRAWL_BASE_URL
python_env_delete ARKLOOP_INSTALL_PROFILE
python_env_delete ARKLOOP_INSTALL_MODE
python_env_delete ARKLOOP_INSTALL_MEMORY
python_env_delete ARKLOOP_INSTALL_SANDBOX
python_env_delete ARKLOOP_INSTALL_CONSOLE
python_env_delete ARKLOOP_INSTALL_BROWSER
python_env_delete ARKLOOP_INSTALL_WEB_TOOLS
python_env_delete ARKLOOP_INSTALL_GATEWAY
python_env_delete ARKLOOP_INSTALL_MODULES
python_env_delete ARKLOOP_SETUP_LANG
set_install_state ARKLOOP_INSTALL_PROFILE "$RESOLVED_PROFILE"
set_install_state ARKLOOP_INSTALL_MODE "$RESOLVED_MODE"
set_install_state ARKLOOP_INSTALL_MEMORY "$RESOLVED_MEMORY"
set_install_state ARKLOOP_INSTALL_SANDBOX "$RESOLVED_SANDBOX"
set_install_state ARKLOOP_INSTALL_CONSOLE "$RESOLVED_CONSOLE"
set_install_state ARKLOOP_INSTALL_BROWSER "$RESOLVED_BROWSER"
set_install_state ARKLOOP_INSTALL_WEB_TOOLS "$RESOLVED_WEB_TOOLS"
set_install_state ARKLOOP_INSTALL_GATEWAY "$RESOLVED_GATEWAY"
set_install_state ARKLOOP_INSTALL_MODULES "$(printf '%s' "$SELECTED_MODULES" | paste -sd, -)"
set_install_state ARKLOOP_SETUP_LANG "$(setup_lang)"
}
preflight_install() {
local failures=0
require_command python3
require_command curl
detect_host
check_docker_tools
detect_docker_socket
local project_name postgres_volume
project_name="$(compose_project_name)"
postgres_volume="${project_name}_postgres_data"
if [ "$HAD_ENV_FILE_BEFORE_INSTALL" = "0" ] && named_volume_exists "$postgres_volume"; then
warn "$(t stale_postgres_volume)"
failures=1
fi
if [ "$DOCKER_OK" != "1" ]; then
warn "$(t docker_unavailable)"
failures=1
fi
if [ "$COMPOSE_OK" != "1" ]; then
warn "$(t compose_unavailable)"
failures=1
fi
if [ "$RESOLVED_SANDBOX" = "firecracker" ]; then
if [ "$HOST_OS" != "linux" ]; then
warn "$(t firecracker_linux_only)"
failures=1
fi
if [ "$HAS_KVM" != "1" ]; then
warn "$(t kvm_missing)"
failures=1
fi
fi
if [ "$RESOLVED_SANDBOX" = "docker" ] && [ -z "$DETECTED_DOCKER_SOCKET" ]; then
warn "$(t missing_docker_socket)"
failures=1
fi
compose_base_cmd "$COMPOSE_PROFILES"
local gateway_port
gateway_port="$(python_env_get ARKLOOP_GATEWAY_PORT || true)"
[ -n "$gateway_port" ] || gateway_port="19000"
if [ "$RESOLVED_GATEWAY" = "on" ] && port_in_use "$gateway_port"; then
if ! service_ready gateway >/dev/null 2>&1; then
warn "$(t gateway_port_in_use "$gateway_port")"
failures=1
fi
fi
[ "$failures" -eq 0 ] || fail "$(t preflight_failed)"
}
run_install() {
local profile="" mode="" memory="" sandbox="" console="" browser="" web_tools="" gateway="" gateway_port=""
NON_INTERACTIVE="0"
while [ "$#" -gt 0 ]; do
case "$1" in
--profile) profile="$2"; shift 2 ;;
--mode) mode="$2"; shift 2 ;;
--memory) memory="$2"; shift 2 ;;
--sandbox) sandbox="$2"; shift 2 ;;
--console) console="$2"; shift 2 ;;
--browser) browser="$2"; shift 2 ;;
--web-tools) web_tools="$2"; shift 2 ;;
--gateway) gateway="$2"; shift 2 ;;
--gateway-port) gateway_port="$2"; shift 2 ;;
--lang) SETUP_LANG="$(normalize_setup_lang "$2")"; shift 2 ;;
--non-interactive) NON_INTERACTIVE="1"; shift ;;
--prod) USE_PROD_IMAGES="1"; shift ;;
-h|--help) print_usage; exit 0 ;;
*) fail "$(t unknown_arg "$1")" ;;
esac
done
detect_host
if [ -f "$ENV_FILE" ]; then
HAD_ENV_FILE_BEFORE_INSTALL="1"
else
HAD_ENV_FILE_BEFORE_INSTALL="0"
fi
collect_install_inputs "$profile" "$mode" "$memory" "$sandbox" "$console" "$browser" "$web_tools" "$gateway" "$gateway_port"
resolve_plan "$INSTALL_PROFILE" "$INSTALL_MODE" "$INSTALL_MEMORY" "$INSTALL_SANDBOX" "$INSTALL_CONSOLE" "$INSTALL_BROWSER" "$INSTALL_WEB_TOOLS" "$INSTALL_GATEWAY"
ensure_env_file
ensure_secret ARKLOOP_POSTGRES_PASSWORD hex16
ensure_secret ARKLOOP_REDIS_PASSWORD hex16