heiyuheiyu commited on
Commit
645fb68
·
verified ·
1 Parent(s): 7dcf428

Upload Dockerfile

Browse files
Files changed (1) hide show
  1. Dockerfile +426 -155
Dockerfile CHANGED
@@ -497,93 +497,35 @@ provide5_models_name5="${provide5_models_name5:-$provide5_models_name1}"
497
  provide5_models_api5="${provide5_models_api5:-$provide5_models_api1}"
498
  provide5_models_input5="${provide5_models_input5:-$provide5_models_input1}"
499
 
500
- # google model2~5 fallback
501
- google_model2="${google_model2:-$google_model1}"
502
- google_model3="${google_model3:-$google_model1}"
503
- google_model4="${google_model4:-$google_model1}"
504
- google_model5="${google_model5:-$google_model1}"
505
-
506
- # primary_model / primary_imageModel fallback
507
- primary_model="${primary_model:-${provide1}/${provide1_models_id1}}"
508
- primary_imageModel="${primary_imageModel:-${provide1}/${provide1_models_id1}}"
509
-
510
  PROVIDERS_JSON=$(python3 /usr/local/bin/build_providers.py)
511
 
512
- # ── 動態 Channel 配置:DINGTALK 變量未設置時只寫入 disabled 配置,避免健康檢查拋出未捕獲異常崩潰進程 ──
513
- if [ -n "${DINGTALK_clientId}" ] && [ -n "${DINGTALK_clientSecret}" ]; then
514
- DINGTALK_CHANNEL_JSON='"dingtalk-connector": {
 
515
  "enabled": true,
516
- "clientId": "${DINGTALK_clientId}",
517
- "clientSecret": "${DINGTALK_clientSecret}"
518
- },'
 
 
 
 
 
 
 
 
519
  else
520
- DINGTALK_CHANNEL_JSON='"dingtalk-connector": {
521
- "enabled": false
522
- },'
523
  fi
524
 
525
  cat > /root/.openclaw/openclaw.json <<EOT
526
  {
527
- "models": {
528
- "mode": "merge",
529
- "providers": {
530
  ${PROVIDERS_JSON}
531
- }
532
- },
533
- "agents": {
534
- "defaults": {
535
- "model": {
536
- "primary": "${primary_model}"
537
- },
538
- "imageModel": {
539
- "primary": "${primary_imageModel}"
540
- },
541
- "models": {
542
- "${provide1}/${provide1_models_id1}": {},
543
- "${provide1}/${provide1_models_id2}": {},
544
- "${provide1}/${provide1_models_id3}": {},
545
- "${provide1}/${provide1_models_id4}": {},
546
- "${provide1}/${provide1_models_id5}": {},
547
- "${provide2}/${provide2_models_id1}": {},
548
- "${provide2}/${provide2_models_id2}": {},
549
- "${provide2}/${provide2_models_id3}": {},
550
- "${provide2}/${provide2_models_id4}": {},
551
- "${provide2}/${provide2_models_id5}": {},
552
- "${provide3}/${provide3_models_id1}": {},
553
- "${provide3}/${provide3_models_id2}": {},
554
- "${provide3}/${provide3_models_id3}": {},
555
- "${provide3}/${provide3_models_id4}": {},
556
- "${provide3}/${provide3_models_id5}": {},
557
- "${provide4}/${provide4_models_id1}": {},
558
- "${provide4}/${provide4_models_id2}": {},
559
- "${provide4}/${provide4_models_id3}": {},
560
- "${provide4}/${provide4_models_id4}": {},
561
- "${provide4}/${provide4_models_id5}": {},
562
- "${provide5}/${provide5_models_id1}": {},
563
- "${provide5}/${provide5_models_id2}": {},
564
- "${provide5}/${provide5_models_id3}": {},
565
- "${provide5}/${provide5_models_id4}": {},
566
- "${provide5}/${provide5_models_id5}": {},
567
- "google/${google_model1}": {},
568
- "google/${google_model2}": {},
569
- "google/${google_model3}": {},
570
- "google/${google_model4}": {},
571
- "google/${google_model5}": {}
572
- }
573
- }
574
  },
575
  "gateway": {
576
- "mode": "local",
577
- "bind": "lan",
578
- "port": ${PORT},
579
- "trustedProxies": [
580
- "0.0.0.0/0",
581
- "10.0.0.0/8",
582
- "172.16.0.0/12",
583
- "192.168.0.0/16"
584
- ],
585
  "auth": {
586
- "mode": "token",
587
  "token": "${OPENCLAW_PASSWORD}"
588
  },
589
  "controlUi": {
@@ -848,29 +790,316 @@ EOF
848
 
849
  RUN chmod +x /usr/local/bin/start-openclaw
850
 
851
- # 12. code-server + nginx 啟動包裝腳本
852
  # ─────────────────────────────────────────────────────────────────────────────
853
- # 架構說明:
 
 
 
854
  # HF Space 對外只暴露單一端口 $PORT(默認 7860),因此:
855
  # - nginx 監聽 $PORT(對外,由本腳本在運行時寫入配置)
856
  # - openclaw gateway 監聽 7862(內部,通過覆蓋 PORT=7862 傳給 start-openclaw)
857
- # - code-server 監聽 13337(內部)
858
  # nginx 路由:/ide/ -> code-server:13337(nginx 剝離 /ide/ 前綴),/ -> gateway:7862
859
  #
860
- # 關���設計:
861
- # start-openclaw 原腳本完全不修改,末尾是 `exec openclaw gateway run --port $PORT`。
862
- # 此處以普通後台進程方式 exec啟動它,並覆蓋 PORT=7862 使 gateway 監聽內部端口。
863
- # start-openclaw 腳本內所有後台任務(備份循環、微信登錄、設備配對審批等)均在
864
- # `exec openclaw gateway run` 前通過 & 後台啟動,因此全部照常運行,不受影響。
 
 
865
  #
866
- # code-server 說明
867
- # code-server 已移除 --base-path 參數多年前即廢棄),
868
- # subpath 反向代理通過 nginx location /ide/ + proxy_pass 末尾帶 / 實現路徑剝離。
869
- # 登錄後 302 重定通過 proxy_redirect 重寫回 /ide/ 前綴否則會跳到 / 而非 /ide/。
 
 
870
  #
871
- # nginx 配置注意
872
- # nginx 配置文件不支持 bash 變量語法(${})
873
- # 所有動態端口值通過 sed 替換佔位符在運行寫入。
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
874
  RUN cat <<'EOF' > /usr/local/bin/start-openclaw-code-server
875
  #!/bin/bash
876
  set -e
@@ -880,21 +1109,40 @@ LISTEN_PORT="${PORT:-7860}"
880
  # openclaw gateway 內部端口(避免與 nginx 對外端口衝突)
881
  GATEWAY_PORT=7862
882
  # code-server 內部端口
883
- CODE_SERVER_PORT=13337
 
 
884
 
885
- echo "=== start-openclaw-code-server ==="
886
- echo "External listen port : ${LISTEN_PORT}"
887
- echo "Gateway internal port : ${GATEWAY_PORT}"
 
 
 
888
  echo "code-server internal port: ${CODE_SERVER_PORT}"
 
 
889
 
890
  # ── 1. 後台啟動原版 start-openclaw(覆蓋 PORT 使 gateway 監聽內部端口)──────
891
- # start-openclaw 內所有邏輯(恢復備份、寫配置、後台備份循環、微信登錄、設備配對審批等)
892
- # 全部照常執行,僅 gateway 監聽端口從 $PORT 改為內部 $GATEWAY_PORT
893
  mkdir -p /root/.openclaw/logs
894
  PORT=${GATEWAY_PORT} bash /usr/local/bin/start-openclaw 2>&1 | tee /root/.openclaw/logs/openclaw-main.log &
895
  echo "start-openclaw launched in background (gateway port=${GATEWAY_PORT})"
896
 
897
- # ── 2. 等待 gateway 內部就緒最多 120 秒)──────────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
 
 
 
898
  echo "Waiting for openclaw gateway on internal port ${GATEWAY_PORT}..."
899
  for i in $(seq 1 60); do
900
  if curl -fsS http://127.0.0.1:${GATEWAY_PORT}/ >/dev/null 2>&1; then
@@ -904,48 +1152,44 @@ for i in $(seq 1 60); do
904
  sleep 2
905
  done
906
 
907
- # ── 3. 啟動 code-server(後台,僅本機監聽)──────────────────────────────────
908
- # 注意:code-server 已移除 --base-path 參數,subpath 由 nginx 的 proxy_pass 末尾 / 處理
909
- mkdir -p /root/.openclaw/logs /root/.code-server
910
-
911
- # 強制寫入 config.yaml,確保 bind-addr 正確。
912
- # code-server 會讀取 $PORT 環境變量作為端口(HF 注入 PORT=7860
913
- # 無論 --bind-addr 還是 config.yaml,$PORT 都可能覆蓋端口設置。
914
- # 解決方案:先寫入 config.yaml 指定正確端口,再 unset PORT 後啟動。
915
- mkdir -p /root/.config/code-server
916
- cat > /root/.config/code-server/config.yaml <<CODESERVERCFG
917
- bind-addr: 127.0.0.1:${CODE_SERVER_PORT}
918
- auth: password
919
- password: ${CODE_SERVER_PASSWORD:-changeme123!}
920
- cert: false
921
- CODESERVERCFG
922
- echo "code-server config.yaml written (bind-addr: 127.0.0.1:${CODE_SERVER_PORT})"
923
-
924
- # unset PORT:防止 code-server 讀取 $PORT=7860 覆蓋我們設定的端口
925
- ( unset PORT; code-server \
926
- --disable-telemetry \
927
- --disable-update-check \
928
- --user-data-dir /root/.code-server \
929
- --extensions-dir /root/.code-server/extensions \
930
- /root/.openclaw \
931
- 2>&1 | tee /root/.openclaw/logs/code-server.log ) &
932
- echo "code-server launched (port=${CODE_SERVER_PORT})"
933
-
934
- # 等待 code-server 端口就緒(最多 60 秒)
935
- echo "Waiting for code-server on port ${CODE_SERVER_PORT}..."
936
- for i in $(seq 1 30); do
937
- if curl -fsS http://127.0.0.1:${CODE_SERVER_PORT}/ >/dev/null 2>&1; then
938
- echo "code-server is up after $((i * 2))s."
939
- break
940
- fi
941
- sleep 2
942
- done
943
 
944
- # ── 4. 生成 nginx 配置並啟動 ────────────────────────────────────────────────
945
- # nginx 不支持 bash 變量語法,所有動態值通過 sed 替換佔位符寫入
946
  rm -f /etc/nginx/sites-enabled/default /etc/nginx/conf.d/default.conf
947
 
948
- cat > /etc/nginx/conf.d/openclaw-ide.conf <<'NGINX'
 
 
 
 
 
949
  server {
950
  listen PLACEHOLDER_LISTEN_PORT;
951
  server_name _;
@@ -957,36 +1201,58 @@ server {
957
  # 關鍵:Cloudflare 做 SSL 終結,nginx 只收到 http 請求。
958
  # 若 nginx 生成絕對 URL(如 301/302 Location),會帶上 http://host:PORT,
959
  # 導致瀏覽器被重定向到帶明確端口的 http URL,被 Cloudflare 拒絕(400 Bad Request)。
960
- # absolute_redirect off → 所有 nginx 內部重定向(含 proxy_redirect)輸出相對路徑
961
- # port_in_redirect off → 禁止 nginx 在重定向 URL 中附加監聽端口
962
  absolute_redirect off;
963
  port_in_redirect off;
964
 
965
- # IDE(code-server):nginx 剝離 /ide/ 前綴後轉發到 code-server 根路徑
966
- # proxy_pass 末尾帶 / 是關鍵nginx 自動將 /ide/xxx 重寫為 /xxx 再轉
967
- # proxy_redirectcode-server 登錄 302轉到 /需重寫回 /ide/
968
- # 由於 absolute_redirect off,這裡輸出的是相對路徑,不帶 scheme/host/port
969
  location /ide/ {
970
  proxy_pass http://127.0.0.1:PLACEHOLDER_CODE_SERVER_PORT/;
971
  proxy_http_version 1.1;
972
- proxy_set_header Host $host;
973
- proxy_set_header Upgrade $http_upgrade;
974
  proxy_set_header Connection "upgrade";
975
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
976
- proxy_set_header X-Forwarded-Proto $scheme;
977
  proxy_redirect / /ide/;
978
  proxy_read_timeout 86400;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
979
  }
980
 
981
- # 其餘請求:代理到 openclaw gateway
 
 
 
 
 
 
 
 
 
 
982
  location / {
983
  proxy_pass http://127.0.0.1:PLACEHOLDER_GATEWAY_PORT/;
984
  proxy_http_version 1.1;
985
- proxy_set_header Host $host;
986
- proxy_set_header Upgrade $http_upgrade;
987
  proxy_set_header Connection "upgrade";
988
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
989
- proxy_set_header X-Forwarded-Proto $scheme;
990
  proxy_read_timeout 86400;
991
  }
992
  }
@@ -1010,9 +1276,14 @@ RUN chmod +x /usr/local/bin/start-openclaw-code-server
1010
 
1011
  EXPOSE 7860
1012
 
1013
- # IDE(code-server)訪問方式(Space 啟動後):
1014
  # 主界面(OpenClaw): https://<your-space>.hf.space/
1015
  # IDE(VS Code) : https://<your-space>.hf.space/ide/
1016
- # 密碼由環境變量 CODE_SERVER_PASSWORD 控制
1017
- # 默認:changeme123! ← 建議在 HF Space Secrets 中覆蓋
1018
- CMD ["/usr/local/bin/start-openclaw-code-server"]
 
 
 
 
 
 
497
  provide5_models_api5="${provide5_models_api5:-$provide5_models_api1}"
498
  provide5_models_input5="${provide5_models_input5:-$provide5_models_input1}"
499
 
 
 
 
 
 
 
 
 
 
 
500
  PROVIDERS_JSON=$(python3 /usr/local/bin/build_providers.py)
501
 
502
+ # openclaw.json DINGTALK_CHANNEL_JSON 部分
503
+ if [ -n "$DINGTALK_APP_KEY" ] && [ -n "$DINGTALK_APP_SECRET" ] && [ -n "$DINGTALK_ROBOT_CODE" ]; then
504
+ DINGTALK_CHANNEL_JSON=$(cat <<DINGTALKEOF
505
+ "dingtalk": {
506
  "enabled": true,
507
+ "appKey": "${DINGTALK_APP_KEY}",
508
+ "appSecret": "${DINGTALK_APP_SECRET}",
509
+ "robotCode": "${DINGTALK_ROBOT_CODE}",
510
+ "dmPolicy": "open",
511
+ "groupPolicy": "open",
512
+ "messageType": "markdown",
513
+ "debug": false,
514
+ "allowFrom": ["*"]
515
+ },
516
+ DINGTALKEOF
517
+ )
518
  else
519
+ DINGTALK_CHANNEL_JSON=""
 
 
520
  fi
521
 
522
  cat > /root/.openclaw/openclaw.json <<EOT
523
  {
524
+ "providers": {
 
 
525
  ${PROVIDERS_JSON}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
526
  },
527
  "gateway": {
 
 
 
 
 
 
 
 
 
528
  "auth": {
 
529
  "token": "${OPENCLAW_PASSWORD}"
530
  },
531
  "controlUi": {
 
790
 
791
  RUN chmod +x /usr/local/bin/start-openclaw
792
 
 
793
  # ─────────────────────────────────────────────────────────────────────────────
794
+ # 12. code-server 按需啟停管理器 + nginx 啟動包裝腳本
795
+ # ─────────────────────────────────────────────────────────────────────────────
796
+ #
797
+ # 【架構說明】
798
  # HF Space 對外只暴露單一端口 $PORT(默認 7860),因此:
799
  # - nginx 監聽 $PORT(對外,由本腳本在運行時寫入配置)
800
  # - openclaw gateway 監聽 7862(內部,通過覆蓋 PORT=7862 傳給 start-openclaw)
801
+ # - code-server 監聽 13337(內部,按需啟停
802
  # nginx 路由:/ide/ -> code-server:13337(nginx 剝離 /ide/ 前綴),/ -> gateway:7862
803
  #
804
+ # 【按需啟停機制】(無需 nginx Lua 模塊,純 bash + Python 實現)
805
+ #
806
+ # 核心組件 1:/usr/local/bin/cs-managerPython 守護進程
807
+ # - 監聽 Unix socket:/tmp/cs-manager.sock
808
+ # - 接收來自 nginx 的觸發請求(通過 nginx error_page + proxy_pass 機制)
809
+ # - 負責啟動、停止 code-server,維護"最後活躍時間戳"文件
810
+ # - 每分鐘檢查一次,若超過 IDE_IDLE_MINUTES 分鐘無請求,自動 kill code-server
811
  #
812
+ # 核心組件 2nginx 配置(/ide/ location)
813
+ # - 設置 proxy_connect_timeout 極短1s),code-server 未啟動時快速失敗
814
+ # - 通過 error_page 502/504 -> @ide_wakeup 捕獲連接失敗
815
+ # - @ide_wakeup location:反代理到 cs-managercs-manager 啟動 code-server
816
+ # 並返回 HTML 自動刷新頁(前端 5s 後自動重試 /ide/ 直到 code-server 就緒)
817
+ # - 正常請求到達 /ide/ 時,同時通過 access_log + post_action 更新活躍時間戳
818
  #
819
+ # 活躍時間戳更新機制(輕量方案)
820
+ # - nginx 每次成功代理 /ide/ 請求時同步 touch /tmp/cs-last-access 文件
821
+ # - cs-manager 守護進程讀取此文件的 mtime 判斷是否超
822
+ # - nginx 通過 post_action 子請求觸發 /ide-heartbeat/ 內部 location,
823
+ # 該 location 執行 shell 命令 touch /tmp/cs-last-access(通過 fastcgi 或直接 proxy)
824
+ # - 實際採用更簡單方案:cs-manager 同時監聽 /tmp/cs-manager.sock,
825
+ # nginx 的 @ide_wakeup 和一個輕量心跳 location 都向它發請求
826
+ #
827
+ # 【環境變量】
828
+ # IDE_IDLE_MINUTES 閒置自動關閉時間(分鐘),默認 30
829
+ # CODE_SERVER_PASSWORD code-server 登錄密碼,默認 changeme123!
830
+
831
+ # ── 12a. 寫入 cs-manager 守護進程 ───────────────────────────────────────────
832
+ RUN cat <<'PYEOF' > /usr/local/bin/cs-manager
833
+ #!/usr/bin/env python3
834
+ """
835
+ cs-manager: code-server 按需啟停守護進程
836
+
837
+ 監聽 Unix socket,提供兩個 HTTP 端點:
838
+ GET /wakeup - 觸發啟動 code-server(若未運行),返回"啟動中"等待頁
839
+ GET /heartbeat - 更新最後活躍時間(nginx 每次成功代理 /ide/ 後調用)
840
+
841
+ 後台定時任務:
842
+ 每 60 秒檢查一次,若超過 IDE_IDLE_MINUTES 分鐘無 heartbeat,kill code-server
843
+ """
844
+
845
+ import os, sys, time, signal, subprocess, threading, socket, re
846
+ from http.server import HTTPServer, BaseHTTPRequestHandler
847
+
848
+ # ── 配置 ─────────────────────────────────────────────────────────────────────
849
+ SOCK_PATH = "/tmp/cs-manager.sock"
850
+ CS_PORT = int(os.environ.get("CODE_SERVER_PORT", "13337"))
851
+ CS_PASSWORD = os.environ.get("CODE_SERVER_PASSWORD", "changeme123!")
852
+ CS_USER_DATA_DIR = "/root/.code-server"
853
+ CS_EXTENSIONS_DIR= "/root/.code-server/extensions"
854
+ CS_WORKSPACE = "/root/.openclaw"
855
+ CS_LOG = "/root/.openclaw/logs/code-server.log"
856
+ IDLE_MINUTES = int(os.environ.get("IDE_IDLE_MINUTES", "30"))
857
+ LAST_ACCESS_FILE = "/tmp/cs-last-access"
858
+ CS_PID_FILE = "/tmp/cs-server.pid"
859
+ CHECK_INTERVAL = 60 # 秒
860
+
861
+ # ── 全局狀態 ──────────────────────────────────────────────────────────────────
862
+ _lock = threading.Lock()
863
+ _starting = False # 正在啟動中,防止並發重複啟動
864
+
865
+ # ── 工具函數 ──────────────────────────────────────────────────────────────────
866
+
867
+ def log(msg):
868
+ ts = time.strftime("%Y-%m-%d %H:%M:%S")
869
+ print(f"[cs-manager {ts}] {msg}", flush=True)
870
+
871
+ def touch_last_access():
872
+ try:
873
+ with open(LAST_ACCESS_FILE, "w") as f:
874
+ f.write(str(time.time()))
875
+ os.utime(LAST_ACCESS_FILE, None)
876
+ except Exception as e:
877
+ log(f"touch_last_access error: {e}")
878
+
879
+ def get_cs_pid():
880
+ """讀取 PID 文件,返回 code-server PID(若進程存在),否則返回 None"""
881
+ try:
882
+ with open(CS_PID_FILE) as f:
883
+ pid = int(f.read().strip())
884
+ os.kill(pid, 0) # 探測進程是否存在
885
+ return pid
886
+ except Exception:
887
+ return None
888
+
889
+ def is_cs_running():
890
+ return get_cs_pid() is not None
891
+
892
+ def is_cs_port_ready():
893
+ """嘗試 TCP 連接 code-server 端口,確認服務真正可用"""
894
+ try:
895
+ s = socket.create_connection(("127.0.0.1", CS_PORT), timeout=1)
896
+ s.close()
897
+ return True
898
+ except Exception:
899
+ return False
900
+
901
+ def start_cs():
902
+ """啟動 code-server,非阻塞,返回後進程在後台運行"""
903
+ global _starting
904
+ with _lock:
905
+ if _starting or is_cs_running():
906
+ return
907
+ _starting = True
908
+
909
+ try:
910
+ log(f"Starting code-server on port {CS_PORT}...")
911
+ os.makedirs(os.path.dirname(CS_LOG), exist_ok=True)
912
+ os.makedirs(CS_USER_DATA_DIR, exist_ok=True)
913
+
914
+ # 寫入 code-server config.yaml
915
+ cfg_dir = "/root/.config/code-server"
916
+ os.makedirs(cfg_dir, exist_ok=True)
917
+ with open(os.path.join(cfg_dir, "config.yaml"), "w") as f:
918
+ f.write(f"bind-addr: 127.0.0.1:{CS_PORT}\n")
919
+ f.write(f"auth: password\n")
920
+ f.write(f"password: {CS_PASSWORD}\n")
921
+ f.write(f"cert: false\n")
922
+
923
+ env = os.environ.copy()
924
+ env.pop("PORT", None) # 防止 code-server 讀取 PORT=7860 覆蓋端口
925
+
926
+ log_file = open(CS_LOG, "a")
927
+ proc = subprocess.Popen(
928
+ [
929
+ "code-server",
930
+ "--disable-telemetry",
931
+ "--disable-update-check",
932
+ f"--user-data-dir={CS_USER_DATA_DIR}",
933
+ f"--extensions-dir={CS_EXTENSIONS_DIR}",
934
+ CS_WORKSPACE,
935
+ ],
936
+ stdout=log_file,
937
+ stderr=log_file,
938
+ env=env,
939
+ start_new_session=True, # 脫離當前進程組,避免隨父進程終止
940
+ )
941
+
942
+ # 寫入 PID 文件
943
+ with open(CS_PID_FILE, "w") as f:
944
+ f.write(str(proc.pid))
945
+ log(f"code-server started, PID={proc.pid}")
946
+ touch_last_access()
947
+ except Exception as e:
948
+ log(f"start_cs error: {e}")
949
+ finally:
950
+ with _lock:
951
+ _starting = False
952
+
953
+ def stop_cs():
954
+ """終止 code-server 進程"""
955
+ pid = get_cs_pid()
956
+ if pid is None:
957
+ log("stop_cs: code-server not running, skip")
958
+ return
959
+ try:
960
+ os.kill(pid, signal.SIGTERM)
961
+ log(f"code-server (PID={pid}) terminated (SIGTERM)")
962
+ except Exception as e:
963
+ log(f"stop_cs error: {e}")
964
+ try:
965
+ os.remove(CS_PID_FILE)
966
+ except Exception:
967
+ pass
968
+
969
+ # ── 閒置檢測定時器 ────────────────────────────────────────────────────────────
970
+
971
+ def idle_checker():
972
+ while True:
973
+ time.sleep(CHECK_INTERVAL)
974
+ try:
975
+ if not is_cs_running():
976
+ continue
977
+ try:
978
+ mtime = os.path.getmtime(LAST_ACCESS_FILE)
979
+ except FileNotFoundError:
980
+ mtime = 0
981
+ idle_secs = time.time() - mtime
982
+ idle_mins = idle_secs / 60
983
+ if idle_mins >= IDLE_MINUTES:
984
+ log(f"Idle for {idle_mins:.1f} min (threshold={IDLE_MINUTES} min), stopping code-server...")
985
+ stop_cs()
986
+ else:
987
+ remaining = IDLE_MINUTES - idle_mins
988
+ log(f"code-server running, idle {idle_mins:.1f}/{IDLE_MINUTES} min (auto-stop in {remaining:.1f} min)")
989
+ except Exception as e:
990
+ log(f"idle_checker error: {e}")
991
+
992
+ # ── HTTP 請求處理 ─────────────────────────────────────────────────────────────
993
+
994
+ WAKEUP_HTML = """\
995
+ <!DOCTYPE html>
996
+ <html lang="zh">
997
+ <head>
998
+ <meta charset="utf-8">
999
+ <meta http-equiv="refresh" content="5;url=/ide/">
1000
+ <title>IDE 啟動中...</title>
1001
+ <style>
1002
+ body {{ font-family: sans-serif; display:flex; align-items:center;
1003
+ justify-content:center; height:100vh; margin:0; background:#1e1e1e; color:#ccc; }}
1004
+ .box {{ text-align:center; }}
1005
+ .spinner {{ width:48px; height:48px; border:5px solid #555;
1006
+ border-top-color:#0078d4; border-radius:50%;
1007
+ animation:spin 1s linear infinite; margin:0 auto 20px; }}
1008
+ @keyframes spin {{ to {{ transform:rotate(360deg) }} }}
1009
+ p {{ margin:6px 0; }}
1010
+ small {{ color:#888; }}
1011
+ </style>
1012
+ </head>
1013
+ <body>
1014
+ <div class="box">
1015
+ <div class="spinner"></div>
1016
+ <p>VS Code IDE 正在啟動,請稍候...</p>
1017
+ <p><small>頁面將在 5 秒後自動重試,或手動 <a href="/ide/" style="color:#0078d4">刷新</a></small></p>
1018
+ </div>
1019
+ </body>
1020
+ </html>
1021
+ """
1022
+
1023
+ class CSManagerHandler(BaseHTTPRequestHandler):
1024
+ def log_message(self, fmt, *args):
1025
+ pass # 靜默 access log,避免刷日誌
1026
+
1027
+ def do_GET(self):
1028
+ if self.path.startswith("/wakeup"):
1029
+ self._handle_wakeup()
1030
+ elif self.path.startswith("/heartbeat"):
1031
+ self._handle_heartbeat()
1032
+ else:
1033
+ self.send_response(404)
1034
+ self.end_headers()
1035
+
1036
+ def _handle_wakeup(self):
1037
+ """
1038
+ nginx @ide_wakeup 調用此端點。
1039
+ 若 code-server 已在運行(端口可達),返回 204 讓 nginx 繼續 retry proxy。
1040
+ 若未運行,觸發後台啟動,返回 200 "啟動中"等待頁。
1041
+ """
1042
+ if is_cs_port_ready():
1043
+ # code-server 已就緒,返回 204;nginx 應重新 proxy_pass 到 code-server
1044
+ # 注意:nginx @ide_wakeup 收到 204 後需重定向回 /ide/
1045
+ touch_last_access()
1046
+ self.send_response(302)
1047
+ self.send_header("Location", "/ide/")
1048
+ self.end_headers()
1049
+ else:
1050
+ # 觸發啟動(後台),返回等待頁
1051
+ if not is_cs_running():
1052
+ t = threading.Thread(target=start_cs, daemon=True)
1053
+ t.start()
1054
+ html = WAKEUP_HTML.encode()
1055
+ self.send_response(200)
1056
+ self.send_header("Content-Type", "text/html; charset=utf-8")
1057
+ self.send_header("Content-Length", str(len(html)))
1058
+ self.end_headers()
1059
+ self.wfile.write(html)
1060
+
1061
+ def _handle_heartbeat(self):
1062
+ """nginx 成功代理 /ide/ 請求後調用,更新活躍時間"""
1063
+ touch_last_access()
1064
+ self.send_response(204)
1065
+ self.end_headers()
1066
+
1067
+
1068
+ class UnixSocketHTTPServer(HTTPServer):
1069
+ """在 Unix domain socket 上監聽的 HTTPServer"""
1070
+ address_family = socket.AF_UNIX
1071
+
1072
+ def server_bind(self):
1073
+ try:
1074
+ os.unlink(self.server_address)
1075
+ except FileNotFoundError:
1076
+ pass
1077
+ super().server_bind()
1078
+ os.chmod(self.server_address, 0o666)
1079
+
1080
+
1081
+ # ── 主入口 ────────────────────────────────────────────────────────────────────
1082
+
1083
+ if __name__ == "__main__":
1084
+ log(f"cs-manager starting (idle_timeout={IDLE_MINUTES} min, cs_port={CS_PORT})")
1085
+ log(f"Listening on Unix socket: {SOCK_PATH}")
1086
+
1087
+ # 啟動閒置檢測後台線程
1088
+ t = threading.Thread(target=idle_checker, daemon=True)
1089
+ t.start()
1090
+
1091
+ # 啟動 HTTP 服務(Unix socket)
1092
+ server = UnixSocketHTTPServer(SOCK_PATH, CSManagerHandler)
1093
+ try:
1094
+ server.serve_forever()
1095
+ except KeyboardInterrupt:
1096
+ log("cs-manager shutting down")
1097
+ server.server_close()
1098
+ PYEOF
1099
+
1100
+ RUN chmod +x /usr/local/bin/cs-manager
1101
+
1102
+ # ── 12b. 寫入主啟動腳本 ──────────────────────────────────────────────────────
1103
  RUN cat <<'EOF' > /usr/local/bin/start-openclaw-code-server
1104
  #!/bin/bash
1105
  set -e
 
1109
  # openclaw gateway 內部端口(避免與 nginx 對外端口衝突)
1110
  GATEWAY_PORT=7862
1111
  # code-server 內部端口
1112
+ CODE_SERVER_PORT="${CODE_SERVER_PORT:-13337}"
1113
+ # code-server 閒置自動關閉時間(分鐘),默認 30 分鐘
1114
+ IDE_IDLE_MINUTES="${IDE_IDLE_MINUTES:-30}"
1115
 
1116
+ export CODE_SERVER_PORT IDE_IDLE_MINUTES
1117
+ export CODE_SERVER_PASSWORD="${CODE_SERVER_PASSWORD:-changeme123!}"
1118
+
1119
+ echo "=== start-openclaw-code-server (按需啟停模式) ==="
1120
+ echo "External listen port : ${LISTEN_PORT}"
1121
+ echo "Gateway internal port : ${GATEWAY_PORT}"
1122
  echo "code-server internal port: ${CODE_SERVER_PORT}"
1123
+ echo "IDE idle auto-stop : ${IDE_IDLE_MINUTES} min"
1124
+ echo "code-server starts on first /ide/ visit (NOT at boot)"
1125
 
1126
  # ── 1. 後台啟動原版 start-openclaw(覆蓋 PORT 使 gateway 監聽內部端口)──────
 
 
1127
  mkdir -p /root/.openclaw/logs
1128
  PORT=${GATEWAY_PORT} bash /usr/local/bin/start-openclaw 2>&1 | tee /root/.openclaw/logs/openclaw-main.log &
1129
  echo "start-openclaw launched in background (gateway port=${GATEWAY_PORT})"
1130
 
1131
+ # ── 2. 啟動 cs-manager 守護進程後台)───────────────────────────────────────
1132
+ # cs-manager 監聽 Unix socket,負責 code-server 的按需啟動和閒置關閉
1133
+ python3 /usr/local/bin/cs-manager 2>&1 | tee /root/.openclaw/logs/cs-manager.log &
1134
+ echo "cs-manager launched in background"
1135
+
1136
+ # 等待 cs-manager socket 就緒(最多 10 秒)
1137
+ for i in $(seq 1 20); do
1138
+ if [ -S /tmp/cs-manager.sock ]; then
1139
+ echo "cs-manager socket ready after $((i * 0))s"
1140
+ break
1141
+ fi
1142
+ sleep 0.5
1143
+ done
1144
+
1145
+ # ── 3. 等待 gateway 內部就緒(最多 120 秒)───────────────────────────────────
1146
  echo "Waiting for openclaw gateway on internal port ${GATEWAY_PORT}..."
1147
  for i in $(seq 1 60); do
1148
  if curl -fsS http://127.0.0.1:${GATEWAY_PORT}/ >/dev/null 2>&1; then
 
1152
  sleep 2
1153
  done
1154
 
1155
+ # ── 4. 生成 nginx 配置並啟動 ─────────────────────────────────────────────────
1156
+ #
1157
+ # 按需啟停工作原理:
1158
+ #
1159
+ # [正常訪問 /ide/(code-server 已運行)]
1160
+ # 瀏覽器 nginx /ide/ proxy_pass code-server:13337(直接成功
1161
+ # nginx sub_request /ide-heartbeat/ cs-manager /heartbeat(更新時間戳)
1162
+ #
1163
+ # [首次訪問 /ide/code-server 未運行)]
1164
+ # 瀏覽器 nginx /ide/ → proxy_pass code-server:13337(連接失敗,502)
1165
+ # → error_page 502 504 @ide_wakeup
1166
+ # → nginx @ide_wakeup → proxy_pass cs-manager /wakeup(通過 http_proxy 到 unix socket)
1167
+ # → cs-manager 後台啟動 code-server,返回"啟動中"HTML(5s 自動刷新)
1168
+ # 5秒後瀏覽器自動重試 /ide/ → 若 code-server 已就緒則正常進入
1169
+ #
1170
+ # [心跳更新(code-server 運行期間)]
1171
+ # nginx 每次成功代理 /ide/ 後,通過 post_action 向 /ide-heartbeat/ 發子請求
1172
+ # cs-manager 更新 /tmp/cs-last-access 時間戳
1173
+ #
1174
+ # [閒置超時自動關閉]
1175
+ # cs-manager 後台每 60s 檢查 /tmp/cs-last-access 的 mtime
1176
+ # 若超過 IDE_IDLE_MINUTES 分鐘未更新,SIGTERM 殺掉 code-server 進程
1177
+ #
1178
+ # nginx 使用 http_proxy 訪問 cs-manager Unix socket 的方法:
1179
+ # upstream cs_manager { server unix:/tmp/cs-manager.sock; }
1180
+ # 然後 proxy_pass http://cs_manager/...;
1181
+ #
1182
+ # 注意:nginx 不支持直接 proxy_pass unix socket 在 error_page 內聯 location,
1183
+ # 因此使用命名 upstream 塊繞過此限制。
 
 
 
 
 
 
 
1184
 
 
 
1185
  rm -f /etc/nginx/sites-enabled/default /etc/nginx/conf.d/default.conf
1186
 
1187
+ cat > /etc/nginx/conf.d/openclaw-ide.conf <<NGINX
1188
+ # cs-manager Unix socket upstream(按需啟停控制器)
1189
+ upstream cs_manager {
1190
+ server unix:/tmp/cs-manager.sock;
1191
+ }
1192
+
1193
  server {
1194
  listen PLACEHOLDER_LISTEN_PORT;
1195
  server_name _;
 
1201
  # 關鍵:Cloudflare 做 SSL 終結,nginx 只收到 http 請求。
1202
  # 若 nginx 生成絕對 URL(如 301/302 Location),會帶上 http://host:PORT,
1203
  # 導致瀏覽器被重定向到帶明確端口的 http URL,被 Cloudflare 拒絕(400 Bad Request)。
 
 
1204
  absolute_redirect off;
1205
  port_in_redirect off;
1206
 
1207
+ # ── /ide/ location(按需啟停核心)────────────────────────────────────
1208
+ # proxy_connect_timeout 設為 2scode-server 未運行時快速失敗觸 error_page
1209
+ # post_action每次成功代理 cs-manager 發心跳,更新活躍時間戳
 
1210
  location /ide/ {
1211
  proxy_pass http://127.0.0.1:PLACEHOLDER_CODE_SERVER_PORT/;
1212
  proxy_http_version 1.1;
1213
+ proxy_set_header Host \$host;
1214
+ proxy_set_header Upgrade \$http_upgrade;
1215
  proxy_set_header Connection "upgrade";
1216
+ proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
1217
+ proxy_set_header X-Forwarded-Proto \$scheme;
1218
  proxy_redirect / /ide/;
1219
  proxy_read_timeout 86400;
1220
+ # 快速檢測 code-server 是否在線(2s 即失敗,觸發 error_page)
1221
+ proxy_connect_timeout 2s;
1222
+ # code-server 未運行時,502/504 觸發喚醒流程
1223
+ error_page 502 504 @ide_wakeup;
1224
+ # 成功代理後,後台子請求更新心跳時間戳(post_action 是 nginx 內置指令)
1225
+ post_action /ide-heartbeat/;
1226
+ }
1227
+
1228
+ # ── 心跳端點(供 post_action 調用,更新活躍時間)────────────────────────
1229
+ # internal 指令確保此 location 只能由 nginx 內部請求訪問,外部瀏覽器無法直接訪問
1230
+ location /ide-heartbeat/ {
1231
+ internal;
1232
+ proxy_pass http://cs_manager/heartbeat;
1233
+ proxy_connect_timeout 1s;
1234
+ proxy_read_timeout 2s;
1235
  }
1236
 
1237
+ # ── 喚醒 location(code-server 未運行時的 fallback)────────────────────
1238
+ # 命名 location(@前綴)只能由 error_page 跳轉,不能直接 URL 訪問
1239
+ location @ide_wakeup {
1240
+ proxy_pass http://cs_manager/wakeup;
1241
+ proxy_http_version 1.1;
1242
+ proxy_set_header Host \$host;
1243
+ proxy_connect_timeout 5s;
1244
+ proxy_read_timeout 30s;
1245
+ }
1246
+
1247
+ # ── 其餘請求:代理到 openclaw gateway ────────────────────────────────────
1248
  location / {
1249
  proxy_pass http://127.0.0.1:PLACEHOLDER_GATEWAY_PORT/;
1250
  proxy_http_version 1.1;
1251
+ proxy_set_header Host \$host;
1252
+ proxy_set_header Upgrade \$http_upgrade;
1253
  proxy_set_header Connection "upgrade";
1254
+ proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
1255
+ proxy_set_header X-Forwarded-Proto \$scheme;
1256
  proxy_read_timeout 86400;
1257
  }
1258
  }
 
1276
 
1277
  EXPOSE 7860
1278
 
1279
+ # 訪問方式(Space 啟動後):
1280
  # 主界面(OpenClaw): https://<your-space>.hf.space/
1281
  # IDE(VS Code) : https://<your-space>.hf.space/ide/
1282
+ # 首次訪問時自動啟動 code-server(約 10~20 秒)
1283
+ # 密碼由環境變量 CODE_SERVER_PASSWORD 控制,默認:changeme123!
1284
+ # 閒置 IDE_IDLE_MINUTES 分鐘(默認 30)無訪問後自動關閉
1285
+ #
1286
+ # 環境變量:
1287
+ # CODE_SERVER_PASSWORD IDE 登錄密碼(建議在 HF Space Secrets 中設置)
1288
+ # IDE_IDLE_MINUTES IDE 閒置自動關閉時間(分鐘),默認 30
1289
+ CMD ["/usr/local/bin/start-openclaw-code-server"]