Dew-OF-Aurora 1 месяц назад
Родитель
Сommit
695ee4831d

+ 25 - 3
README.md

@@ -89,6 +89,20 @@ sudo journalctl -u vmess-domain-rotator.service -n 120 --no-pager
 sudo bash scripts/uninstall_debian.sh
 ```
 
+### 3.4 日志格式(服务器模式)
+
+`scripts/run_update_and_commit.sh`、`scripts/install_debian.sh`、`scripts/uninstall_debian.sh` 的日志统一为:
+
+```text
+[YYYY-MM-DD HH:MM:SS] [vmess-domain-rotator] <message>
+```
+
+Git 提交信息中也会带 UTC 时间戳,例如:
+
+```text
+chore: rotate preferred value to example.com (2026-05-11T13:03:16Z)
+```
+
 ---
 
 ## 4. 本地 Python `cfst` 模式
@@ -161,8 +175,8 @@ python3 scripts/domain_updater.py --config config.local.json --print-output-sett
 
 ```bash
 cd /jffs/vmess
-sh scripts/router_local_update.sh ./config_router.conf
-sh scripts/router_local_http.sh ./config_router.conf
+sh scripts/router_local_update.sh config_router.conf
+sh scripts/router_local_http.sh config_router.conf
 ```
 
 说明:
@@ -202,7 +216,7 @@ while [ "$i" -le 5 ]; do
 done
 
 cru d vmess_rotate
-cru a vmess_rotate "0 */3 * * * cd $VMESS_DIR && sh scripts/router_local_update.sh $CONFIG >> $UPDATE_LOG 2>&1"
+cru a vmess_rotate "0 * * * * cd $VMESS_DIR && sh scripts/router_local_update.sh $CONFIG >> $UPDATE_LOG 2>&1"
 
 sleep 10
 start_vmess_http
@@ -236,6 +250,14 @@ curl http://127.0.0.1:8080/current_ip.txt
 # 2) 或停止旧进程后重启
 ```
 
+### 5.7 日志格式(路由器模式)
+
+`scripts/router_local_update.sh` 与 `scripts/router_local_http.sh` 的日志统一为:
+
+```text
+[YYYY-MM-DD HH:MM:SS] [router-local|router-http] <message>
+```
+
 ---
 
 ## 6. Sub-Store 脚本模式说明

+ 52 - 40
scripts/install_debian.sh

@@ -1,6 +1,18 @@
 #!/usr/bin/env bash
 set -euo pipefail
 
+timestamp() {
+	date '+%Y-%m-%d %H:%M:%S'
+}
+
+log() {
+	printf '[%s] %s\n' "$(timestamp)" "$*"
+}
+
+log_err() {
+	printf '[%s] %s\n' "$(timestamp)" "$*" >&2
+}
+
 SERVICE_NAME="vmess-domain-rotator"
 RUN_USER=""
 RUN_GROUP=""
@@ -113,7 +125,7 @@ while [[ $# -gt 0 ]]; do
 			exit 0
 			;;
 		*)
-			echo "Unknown option: $1" >&2
+			log_err "Unknown option: $1"
 			usage
 			exit 1
 			;;
@@ -121,19 +133,19 @@ while [[ $# -gt 0 ]]; do
 done
 
 if [[ "$(id -u)" -ne 0 ]]; then
-	echo "Please run as root (use sudo)." >&2
+	log_err "Please run as root (use sudo)."
 	exit 1
 fi
 
 if ! command -v runuser >/dev/null 2>&1; then
-	echo "Error: runuser is required on Debian for configuring service-user git credentials" >&2
+	log_err "Error: runuser is required on Debian for configuring service-user git credentials"
 	exit 1
 fi
 
 SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
 if ! git -C "$SOURCE_DIR" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
-	echo "Error: Current directory is not a git repository." >&2
-	echo "This script must be run from within a git repository." >&2
+	log_err "Error: Current directory is not a git repository."
+	log_err "This script must be run from within a git repository."
 	exit 1
 fi
 APP_DIR="$SOURCE_DIR"
@@ -145,7 +157,7 @@ elif [[ "$CONFIG_PATH" != /* ]]; then
 fi
 
 if [[ ! -r "$CONFIG_PATH" ]]; then
-	echo "Error: config file not found or unreadable: $CONFIG_PATH" >&2
+	log_err "Error: config file not found or unreadable: $CONFIG_PATH"
 	exit 1
 fi
 
@@ -158,37 +170,37 @@ if [[ -n "${SUDO_USER:-}" ]] && [[ "$RUN_GROUP_SET" != "1" ]]; then
 fi
 
 if [[ -z "$RUN_USER" ]]; then
-	echo "Error: Could not determine service user. Please run with sudo or specify --user" >&2
+	log_err "Error: Could not determine service user. Please run with sudo or specify --user"
 	exit 1
 fi
 
 if [[ -z "$RUN_GROUP" ]]; then
-	echo "Error: Could not determine service group. Please run with sudo or specify --group" >&2
+	log_err "Error: Could not determine service group. Please run with sudo or specify --group"
 	exit 1
 fi
 
 if [[ ! "$GIT_PUSH_ENABLED" =~ ^[01]$ ]]; then
-	echo "Error: --git-push must be 0 or 1" >&2
+	log_err "Error: --git-push must be 0 or 1"
 	exit 1
 fi
 
 if [[ ! "$GIT_USE_CREDENTIAL_STORE" =~ ^[01]$ ]]; then
-	echo "Error: --git-use-credential-store must be 0 or 1" >&2
+	log_err "Error: --git-use-credential-store must be 0 or 1"
 	exit 1
 fi
 
 if [[ -z "$GIT_PUSH_REMOTE" ]]; then
-	echo "Error: --git-push-remote cannot be empty" >&2
+	log_err "Error: --git-push-remote cannot be empty"
 	exit 1
 fi
 
 if [[ -n "$GIT_HTTP_TOKEN" ]] && [[ -n "$GIT_HTTP_TOKEN_FILE" ]]; then
-	echo "Error: provide either --git-http-token or --git-http-token-file, not both" >&2
+	log_err "Error: provide either --git-http-token or --git-http-token-file, not both"
 	exit 1
 fi
 
 if [[ -n "$GIT_HTTP_TOKEN_FILE" ]] && [[ ! -r "$GIT_HTTP_TOKEN_FILE" ]]; then
-	echo "Error: cannot read token file: $GIT_HTTP_TOKEN_FILE" >&2
+	log_err "Error: cannot read token file: $GIT_HTTP_TOKEN_FILE"
 	exit 1
 fi
 
@@ -197,19 +209,19 @@ if [[ -n "$GIT_HTTP_TOKEN_FILE" ]]; then
 fi
 
 if [[ -n "$GIT_HTTP_TOKEN" ]] && [[ -z "$GIT_HTTP_USERNAME" ]]; then
-	echo "Error: --git-http-username cannot be empty when token is set" >&2
+	log_err "Error: --git-http-username cannot be empty when token is set"
 	exit 1
 fi
 
 if [[ -n "$GIT_HTTP_TOKEN" ]] && [[ "$RUN_USER" == "root" ]]; then
-	echo "Error: refusing to store git token for root service user" >&2
-	echo "Use --user <non-root> or disable push with --git-push 0" >&2
+	log_err "Error: refusing to store git token for root service user"
+	log_err "Use --user <non-root> or disable push with --git-push 0"
 	exit 1
 fi
 
 RUN_HOME="$(getent passwd "$RUN_USER" | cut -d: -f6)"
 if [[ -z "$RUN_HOME" ]]; then
-	echo "Error: could not determine home directory for user: $RUN_USER" >&2
+	log_err "Error: could not determine home directory for user: $RUN_USER"
 	exit 1
 fi
 
@@ -242,7 +254,7 @@ chmod 750 "$SERVICE_STATE_DIR"
 if [[ "$GIT_PUSH_ENABLED" == "1" ]]; then
 	REMOTE_URL="$(git -C "$APP_DIR" remote get-url "$GIT_PUSH_REMOTE" 2>/dev/null || true)"
 	if [[ -z "$REMOTE_URL" ]]; then
-		echo "Warning: remote '$GIT_PUSH_REMOTE' not found now. Push may fail until remote is configured." >&2
+		log_err "Warning: remote '$GIT_PUSH_REMOTE' not found now. Push may fail until remote is configured."
 	fi
 fi
 
@@ -261,8 +273,8 @@ if [[ -n "$GIT_HTTP_TOKEN" ]]; then
 			run_as_service_user git config --global credential.helper "$helper_value"
 			printf 'url=%s\nusername=%s\npassword=%s\n\n' "$REMOTE_URL" "$GIT_HTTP_USERNAME" "$GIT_HTTP_TOKEN" | run_as_service_user git credential approve
 		else
-			echo "Warning: token provided but remote is not HTTPS; credential.helper store setup skipped." >&2
-			echo "Warning: fallback to header-token-file auth mode for this install." >&2
+			log_err "Warning: token provided but remote is not HTTPS; credential.helper store setup skipped."
+			log_err "Warning: fallback to header-token-file auth mode for this install."
 			GIT_USE_CREDENTIAL_STORE="0"
 		fi
 	fi
@@ -337,23 +349,23 @@ systemctl daemon-reload
 systemctl enable --now "${SERVICE_NAME}.timer"
 systemctl start "${SERVICE_NAME}.service"
 
-echo ""
-echo "✓ Installation complete!"
-echo ""
-echo "Configuration:"
-echo "  Working directory: ${APP_DIR}"
-echo "  Config path: ${CONFIG_PATH}"
-echo "  Service user: ${RUN_USER}"
-echo "  Service group: ${RUN_GROUP}"
-echo "  Timer interval: ${INTERVAL}"
-echo "  Push enabled: ${GIT_PUSH_ENABLED}"
-echo "  Push remote: ${GIT_PUSH_REMOTE}"
-echo "  Auth mode: ${AUTH_MODE}"
-echo "  Env file: ${ENV_FILE}"
-echo ""
-echo "Commands:"
-echo "  Check status: systemctl status ${SERVICE_NAME}.timer"
-echo "  View logs:    journalctl -u ${SERVICE_NAME}.service -n 50 --no-pager"
-echo "  Manual run:   sudo systemctl start ${SERVICE_NAME}.service"
-echo "  Force commit: sudo -u ${RUN_USER} /bin/bash ${APP_DIR}/scripts/run_update_and_commit.sh --force-commit ${CONFIG_PATH}"
-echo ""
+log ""
+log "✓ Installation complete!"
+log ""
+log "Configuration:"
+log "  Working directory: ${APP_DIR}"
+log "  Config path: ${CONFIG_PATH}"
+log "  Service user: ${RUN_USER}"
+log "  Service group: ${RUN_GROUP}"
+log "  Timer interval: ${INTERVAL}"
+log "  Push enabled: ${GIT_PUSH_ENABLED}"
+log "  Push remote: ${GIT_PUSH_REMOTE}"
+log "  Auth mode: ${AUTH_MODE}"
+log "  Env file: ${ENV_FILE}"
+log ""
+log "Commands:"
+log "  Check status: systemctl status ${SERVICE_NAME}.timer"
+log "  View logs:    journalctl -u ${SERVICE_NAME}.service -n 50 --no-pager"
+log "  Manual run:   sudo systemctl start ${SERVICE_NAME}.service"
+log "  Force commit: sudo -u ${RUN_USER} /bin/bash ${APP_DIR}/scripts/run_update_and_commit.sh --force-commit ${CONFIG_PATH}"
+log ""

+ 32 - 20
scripts/router_local_http.sh

@@ -1,13 +1,25 @@
 #!/bin/sh
 set -eu
 
+timestamp() {
+  date '+%Y-%m-%d %H:%M:%S'
+}
+
+log() {
+  printf '[%s] %s\n' "$(timestamp)" "$*"
+}
+
+log_err() {
+  printf '[%s] %s\n' "$(timestamp)" "$*" >&2
+}
+
 SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
 APP_DIR=$(CDPATH= cd -- "$SCRIPT_DIR/.." && pwd)
 CONFIG_PATH=${1:-"$APP_DIR/config_router.conf"}
 CONFIG_DIR=$(CDPATH= cd -- "$(dirname -- "$CONFIG_PATH")" && pwd)
 
 if [ ! -r "$CONFIG_PATH" ]; then
-  echo "[router-http] config not found: $CONFIG_PATH" >&2
+  log_err "[router-http] config not found: $CONFIG_PATH"
   exit 1
 fi
 
@@ -65,32 +77,32 @@ resolve_busybox() {
 }
 
 print_busybox_debug() {
-  echo "[router-http] debug CONFIG_PATH=$CONFIG_PATH" >&2
-  echo "[router-http] debug CONFIG_DIR=$CONFIG_DIR" >&2
-  echo "[router-http] debug APP_DIR=$APP_DIR" >&2
-  echo "[router-http] debug BUSYBOX_BIN(raw)=$BUSYBOX_BIN" >&2
+  log_err "[router-http] debug CONFIG_PATH=$CONFIG_PATH"
+  log_err "[router-http] debug CONFIG_DIR=$CONFIG_DIR"
+  log_err "[router-http] debug APP_DIR=$APP_DIR"
+  log_err "[router-http] debug BUSYBOX_BIN(raw)=$BUSYBOX_BIN"
 
   resolved_from_cfg=$(resolve_path_from_config "$BUSYBOX_BIN")
-  echo "[router-http] debug BUSYBOX_BIN(resolved)=$resolved_from_cfg" >&2
+  log_err "[router-http] debug BUSYBOX_BIN(resolved)=$resolved_from_cfg"
 
   if [ -e "$resolved_from_cfg" ]; then
     if [ -x "$resolved_from_cfg" ]; then
-      echo "[router-http] debug resolved BusyBox exists and is executable" >&2
+      log_err "[router-http] debug resolved BusyBox exists and is executable"
     else
-      echo "[router-http] debug resolved BusyBox exists but is NOT executable; run: chmod +x $resolved_from_cfg" >&2
+      log_err "[router-http] debug resolved BusyBox exists but is NOT executable; run: chmod +x $resolved_from_cfg"
     fi
   else
-    echo "[router-http] debug resolved BusyBox does not exist" >&2
+    log_err "[router-http] debug resolved BusyBox does not exist"
   fi
 
   if [ -e "$APP_DIR/busybox_armv7l" ]; then
     if [ -x "$APP_DIR/busybox_armv7l" ]; then
-      echo "[router-http] debug fallback $APP_DIR/busybox_armv7l exists and is executable" >&2
+      log_err "[router-http] debug fallback $APP_DIR/busybox_armv7l exists and is executable"
     else
-      echo "[router-http] debug fallback $APP_DIR/busybox_armv7l exists but is NOT executable; run: chmod +x $APP_DIR/busybox_armv7l" >&2
+      log_err "[router-http] debug fallback $APP_DIR/busybox_armv7l exists but is NOT executable; run: chmod +x $APP_DIR/busybox_armv7l"
     fi
   else
-    echo "[router-http] debug fallback $APP_DIR/busybox_armv7l does not exist" >&2
+    log_err "[router-http] debug fallback $APP_DIR/busybox_armv7l does not exist"
   fi
 }
 
@@ -115,7 +127,7 @@ ensure_index() {
 start_httpd() {
   busybox_bin=$(resolve_busybox || true)
   if [ -n "$busybox_bin" ] && busybox_has_applet "$busybox_bin" httpd; then
-    echo "[router-http] serving $RUNTIME_DIR on 0.0.0.0:$HTTP_PORT via $busybox_bin httpd"
+    log "[router-http] serving $RUNTIME_DIR on 0.0.0.0:$HTTP_PORT via $busybox_bin httpd"
     "$busybox_bin" httpd -f -p "$HTTP_PORT" -h "$RUNTIME_DIR" &
     httpd_pid=$!
     trap 'kill "$httpd_pid" 2>/dev/null || true' INT TERM EXIT
@@ -124,22 +136,22 @@ start_httpd() {
   fi
 
   if [ -n "$busybox_bin" ]; then
-    echo "[router-http] selected BusyBox has no httpd applet: $busybox_bin" >&2
+    log_err "[router-http] selected BusyBox has no httpd applet: $busybox_bin"
   fi
   if command -v httpd >/dev/null 2>&1; then
-    echo "[router-http] found system httpd at $(command -v httpd), but it is not used" >&2
-    echo "[router-http] ASUS firmware httpd is usually the router admin web server and does not support serving an arbitrary -h directory" >&2
+    log_err "[router-http] found system httpd at $(command -v httpd), but it is not used"
+    log_err "[router-http] ASUS firmware httpd is usually the router admin web server and does not support serving an arbitrary -h directory"
   fi
 
   print_busybox_debug
-  echo "[router-http] usable BusyBox httpd applet not found" >&2
-  echo "[router-http] set BUSYBOX_BIN in config_router.conf to your full BusyBox binary, for example: BUSYBOX_BIN=\"./busybox_armv7l\"" >&2
+  log_err "[router-http] usable BusyBox httpd applet not found"
+  log_err "[router-http] set BUSYBOX_BIN in config_router.conf to your full BusyBox binary, for example: BUSYBOX_BIN=\"./busybox_armv7l\""
   exit 1
 }
 
 if [ ! -d "$RUNTIME_DIR" ]; then
-  echo "[router-http] runtime dir not found: $RUNTIME_DIR" >&2
-  echo "[router-http] run: sh scripts/router_local_update.sh $CONFIG_PATH" >&2
+  log_err "[router-http] runtime dir not found: $RUNTIME_DIR"
+  log_err "[router-http] run: sh scripts/router_local_update.sh $CONFIG_PATH"
   exit 1
 fi
 

+ 20 - 8
scripts/router_local_update.sh

@@ -1,12 +1,24 @@
 #!/bin/sh
 set -eu
 
+timestamp() {
+  date '+%Y-%m-%d %H:%M:%S'
+}
+
+log() {
+  printf '[%s] %s\n' "$(timestamp)" "$*"
+}
+
+log_err() {
+  printf '[%s] %s\n' "$(timestamp)" "$*" >&2
+}
+
 SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
 APP_DIR=$(CDPATH= cd -- "$SCRIPT_DIR/.." && pwd)
 CONFIG_PATH=${1:-"$APP_DIR/config_router.conf"}
 
 if [ ! -r "$CONFIG_PATH" ]; then
-  echo "[router-local] config not found: $CONFIG_PATH" >&2
+  log_err "[router-local] config not found: $CONFIG_PATH"
   exit 1
 fi
 
@@ -257,7 +269,7 @@ run_cfst() (
 log_update_summary() {
   selected_ip="$1"
   source_count="$2"
-  echo "[router-local] update_at=$UPDATED_AT selected_ip=$selected_ip candidates=$source_count"
+  log "[router-local] update_at=$UPDATED_AT selected_ip=$selected_ip candidates=$source_count"
   awk -F',' -v top_n="$TOP_N" '
     function trim(v) {
       gsub(/\r/, "", v)
@@ -288,10 +300,10 @@ if [ "$CFST_SKIP_RUN" != "1" ]; then
   if ! run_cfst; then
     if [ -n "$LAST_GOOD_IP" ]; then
       write_error_with_fallback "$LAST_GOOD_IP" "cfst run failed"
-      echo "[router-local] cfst run failed, fallback to last good ip: $LAST_GOOD_IP"
+      log "[router-local] cfst run failed, fallback to last good ip: $LAST_GOOD_IP"
       exit 0
     fi
-    echo "[router-local] cfst run failed and no last good ip available" >&2
+    log_err "[router-local] cfst run failed and no last good ip available"
     exit 1
   fi
 fi
@@ -299,10 +311,10 @@ fi
 if [ ! -s "$RESULT_PATH" ]; then
   if [ -n "$LAST_GOOD_IP" ]; then
     write_error_with_fallback "$LAST_GOOD_IP" "cfst result file missing or empty"
-    echo "[router-local] empty result, fallback to last good ip: $LAST_GOOD_IP"
+    log "[router-local] empty result, fallback to last good ip: $LAST_GOOD_IP"
     exit 0
   fi
-  echo "[router-local] result file missing or empty: $RESULT_PATH" >&2
+  log_err "[router-local] result file missing or empty: $RESULT_PATH"
   exit 1
 fi
 
@@ -314,10 +326,10 @@ TOP_CANDIDATES_JSON=$(build_top_candidates_json)
 if [ -z "$BEST_IP" ]; then
   if [ -n "$LAST_GOOD_IP" ]; then
     write_error_with_fallback "$LAST_GOOD_IP" "no valid ip found in cfst result"
-    echo "[router-local] no valid ip found, fallback to last good ip: $LAST_GOOD_IP"
+    log "[router-local] no valid ip found, fallback to last good ip: $LAST_GOOD_IP"
     exit 0
   fi
-  echo "[router-local] no valid ip found in result file" >&2
+  log_err "[router-local] no valid ip found in result file"
   exit 1
 fi
 

+ 35 - 23
scripts/run_update_and_commit.sh

@@ -1,6 +1,18 @@
 #!/usr/bin/env bash
 set -euo pipefail
 
+timestamp() {
+  date '+%Y-%m-%d %H:%M:%S'
+}
+
+log() {
+  printf '[%s] %s\n' "$(timestamp)" "$*"
+}
+
+log_err() {
+  printf '[%s] %s\n' "$(timestamp)" "$*" >&2
+}
+
 SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
 DEFAULT_APP_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
 APP_DIR="$(git -C "$DEFAULT_APP_DIR" rev-parse --show-toplevel 2>/dev/null || printf '%s' "$DEFAULT_APP_DIR")"
@@ -10,7 +22,7 @@ if [[ "${1:-}" == "--force-commit" ]]; then
   shift
 fi
 if [[ ! "$force_commit" =~ ^[01]$ ]]; then
-  echo "[vmess-domain-rotator] invalid GIT_FORCE_COMMIT=${force_commit}, expected 0 or 1"
+  log "[vmess-domain-rotator] invalid GIT_FORCE_COMMIT=${force_commit}, expected 0 or 1"
   exit 1
 fi
 CONFIG_PATH="${1:-${APP_DIR}/config.server.json}"
@@ -85,36 +97,36 @@ PY
 /usr/bin/python3 "${APP_DIR}/scripts/domain_updater.py" --config "$CONFIG_PATH"
 
 if [[ ! -f "$SELECTED_TEXT_FILE" ]]; then
-  echo "[vmess-domain-rotator] selected value file missing after updater run (${SELECTED_TEXT_FILE}), skip git commit"
+  log "[vmess-domain-rotator] selected value file missing after updater run (${SELECTED_TEXT_FILE}), skip git commit"
   exit 0
 fi
 
 after="$(tr -d '\r\n' < "$SELECTED_TEXT_FILE")"
 
 if [[ -z "$after" ]]; then
-  echo "[vmess-domain-rotator] empty selected value, skip git commit"
+  log "[vmess-domain-rotator] empty selected value, skip git commit"
   exit 0
 fi
 
 selected_rel="$(repo_relpath "$SELECTED_TEXT_FILE")"
 if [[ -z "$selected_rel" ]]; then
-  echo "[vmess-domain-rotator] selected value file is outside repo (${SELECTED_TEXT_FILE}), skip git commit"
+  log "[vmess-domain-rotator] selected value file is outside repo (${SELECTED_TEXT_FILE}), skip git commit"
   exit 0
 fi
 
 if ! command -v git >/dev/null 2>&1; then
-  echo "[vmess-domain-rotator] git not found, skip git commit"
+  log "[vmess-domain-rotator] git not found, skip git commit"
   exit 0
 fi
 
 if ! git -C "$APP_DIR" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
-  echo "[vmess-domain-rotator] not a git repo at ${APP_DIR}, skip git commit"
-  echo "[vmess-domain-rotator] hint: reinstall service from a git clone path"
+  log "[vmess-domain-rotator] not a git repo at ${APP_DIR}, skip git commit"
+  log "[vmess-domain-rotator] hint: reinstall service from a git clone path"
   exit 0
 fi
 
 if ! git -C "$APP_DIR" rev-parse --verify HEAD >/dev/null 2>&1; then
-  echo "[vmess-domain-rotator] repo has no commits yet, skip git commit"
+  log "[vmess-domain-rotator] repo has no commits yet, skip git commit"
   exit 0
 fi
 
@@ -146,7 +158,7 @@ else
       git -C "$APP_DIR" worktree add --force --detach "$work_dir"
       git -C "$work_dir" checkout --orphan "$runtime_branch"
       git -C "$work_dir" rm -rf . >/dev/null 2>&1 || true
-      echo "[vmess-domain-rotator] initialized branch ${runtime_branch}"
+      log "[vmess-domain-rotator] initialized branch ${runtime_branch}"
     fi
   fi
 fi
@@ -160,7 +172,7 @@ fi
 
 work_branch="$(git -C "$work_dir" symbolic-ref --quiet --short HEAD 2>/dev/null || true)"
 if [[ "$work_branch" != "$runtime_branch" ]]; then
-  echo "[vmess-domain-rotator] safety check failed: worktree branch is '${work_branch:-detached}', expected '${runtime_branch}'"
+  log "[vmess-domain-rotator] safety check failed: worktree branch is '${work_branch:-detached}', expected '${runtime_branch}'"
   exit 1
 fi
 
@@ -170,7 +182,7 @@ if before_raw="$(git -C "$work_dir" show "HEAD:${selected_rel}" 2>/dev/null)"; t
 fi
 
 if [[ "$force_commit" != "1" ]] && [[ -n "$before" ]] && [[ "$after" == "$before" ]]; then
-  echo "[vmess-domain-rotator] selected value unchanged (${after}), skip git commit and push"
+  log "[vmess-domain-rotator] selected value unchanged (${after}), skip git commit and push"
   exit 0
 fi
 
@@ -179,7 +191,7 @@ tracked_rel_files=()
 for src in "${tracked_src_files[@]}"; do
   rel="$(repo_relpath "$src")"
   if [[ -z "$rel" ]]; then
-    echo "[vmess-domain-rotator] skip non-repo output file: ${src}"
+    log "[vmess-domain-rotator] skip non-repo output file: ${src}"
     continue
   fi
   tracked_rel_files+=("$rel")
@@ -193,7 +205,7 @@ for src in "${tracked_src_files[@]}"; do
 done
 
 if [[ "${#tracked_rel_files[@]}" -eq 0 ]]; then
-  echo "[vmess-domain-rotator] no repo-local output files from config (${CONFIG_PATH}), skip git commit"
+  log "[vmess-domain-rotator] no repo-local output files from config (${CONFIG_PATH}), skip git commit"
   exit 0
 fi
 
@@ -205,14 +217,14 @@ if git -C "$work_dir" diff --cached --quiet; then
 fi
 
 if [[ "$staged_changed" == "0" ]] && [[ "$force_commit" != "1" ]]; then
-  echo "[vmess-domain-rotator] no staged changes for ${runtime_branch}, skip git commit"
+  log "[vmess-domain-rotator] no staged changes for ${runtime_branch}, skip git commit"
   exit 0
 fi
 
 commit_extra_args=()
 if [[ "$staged_changed" == "0" ]] && [[ "$force_commit" == "1" ]]; then
   commit_extra_args+=(--allow-empty)
-  echo "[vmess-domain-rotator] force commit enabled with unchanged content, creating empty commit"
+  log "[vmess-domain-rotator] force commit enabled with unchanged content, creating empty commit"
 fi
 
 commit_message="chore: rotate preferred value to ${after} (${ts})"
@@ -226,34 +238,34 @@ git -C "$work_dir" \
   commit "${commit_extra_args[@]}" -m "$commit_message"
 
 if [[ "$push_enabled" != "1" ]]; then
-  echo "[vmess-domain-rotator] git push disabled by GIT_PUSH_ENABLED=${push_enabled}"
+  log "[vmess-domain-rotator] git push disabled by GIT_PUSH_ENABLED=${push_enabled}"
 elif [[ -z "$push_remote" ]]; then
   if [[ "$push_required" == "1" ]]; then
-    echo "[vmess-domain-rotator] no remote found but push is required"
+    log "[vmess-domain-rotator] no remote found but push is required"
     exit 1
   fi
-  echo "[vmess-domain-rotator] no remote found, skip git push"
+  log "[vmess-domain-rotator] no remote found, skip git push"
 else
   push_ok="0"
   if git -C "$work_dir" rev-parse --abbrev-ref --symbolic-full-name "@{u}" >/dev/null 2>&1; then
     if git_auth "$work_dir" push "$push_remote" "$runtime_branch:$runtime_branch"; then
       push_ok="1"
-      echo "[vmess-domain-rotator] pushed to ${push_remote}/${runtime_branch}"
+      log "[vmess-domain-rotator] pushed to ${push_remote}/${runtime_branch}"
     fi
   else
     if git_auth "$work_dir" push -u "$push_remote" "$runtime_branch:$runtime_branch"; then
       push_ok="1"
-      echo "[vmess-domain-rotator] pushed to ${push_remote}/${runtime_branch} (set upstream)"
+      log "[vmess-domain-rotator] pushed to ${push_remote}/${runtime_branch} (set upstream)"
     fi
   fi
 
   if [[ "$push_ok" != "1" ]]; then
-    echo "[vmess-domain-rotator] git push failed"
-    echo "[vmess-domain-rotator] hint: configure non-interactive auth (credential.helper store, SSH deploy key, or GIT_HTTP_USERNAME/GIT_HTTP_TOKEN)"
+    log "[vmess-domain-rotator] git push failed"
+    log "[vmess-domain-rotator] hint: configure non-interactive auth (credential.helper store, SSH deploy key, or GIT_HTTP_USERNAME/GIT_HTTP_TOKEN)"
     if [[ "$push_required" == "1" ]]; then
       exit 1
     fi
   fi
 fi
 
-echo "[vmess-domain-rotator] committed output changes on ${runtime_branch}: selected value ${after} from ${RUNTIME_DIR}"
+log "[vmess-domain-rotator] committed output changes on ${runtime_branch}: selected value ${after} from ${RUNTIME_DIR}"

+ 25 - 13
scripts/uninstall_debian.sh

@@ -1,6 +1,18 @@
 #!/usr/bin/env bash
 set -euo pipefail
 
+timestamp() {
+	date '+%Y-%m-%d %H:%M:%S'
+}
+
+log() {
+	printf '[%s] %s\n' "$(timestamp)" "$*"
+}
+
+log_err() {
+	printf '[%s] %s\n' "$(timestamp)" "$*" >&2
+}
+
 SERVICE_NAME="vmess-domain-rotator"
 REMOVE_AUTH_FILES="1"
 
@@ -40,7 +52,7 @@ while [[ $# -gt 0 ]]; do
 			exit 0
 			;;
 		*)
-			echo "Unknown option: $1" >&2
+			log_err "Unknown option: $1"
 			usage
 			exit 1
 			;;
@@ -48,7 +60,7 @@ while [[ $# -gt 0 ]]; do
 done
 
 if [[ "$(id -u)" -ne 0 ]]; then
-	echo "Please run as root (use sudo)." >&2
+	log_err "Please run as root (use sudo)."
 	exit 1
 fi
 
@@ -57,23 +69,23 @@ STATE_DIR="/var/lib/${SERVICE_NAME}"
 
 # Stop and disable timer
 if systemctl list-unit-files | grep -q "^${SERVICE_NAME}.timer"; then
-	echo "Stopping and disabling ${SERVICE_NAME}.timer..."
+	log "Stopping and disabling ${SERVICE_NAME}.timer..."
 	systemctl disable --now "${SERVICE_NAME}.timer" || true
 fi
 
 # Stop service if running
 if systemctl list-unit-files | grep -q "^${SERVICE_NAME}.service"; then
-	echo "Stopping ${SERVICE_NAME}.service..."
+	log "Stopping ${SERVICE_NAME}.service..."
 	systemctl stop "${SERVICE_NAME}.service" || true
 fi
 
 # Remove systemd unit files
-echo "Removing systemd unit files..."
+log "Removing systemd unit files..."
 rm -f "/etc/systemd/system/${SERVICE_NAME}.service"
 rm -f "/etc/systemd/system/${SERVICE_NAME}.timer"
 
 if [[ "$REMOVE_AUTH_FILES" == "1" ]]; then
-	echo "Removing auth/env files..."
+	log "Removing auth/env files..."
 	rm -f "$ENV_FILE"
 	rm -rf "$STATE_DIR"
 fi
@@ -81,13 +93,13 @@ fi
 systemctl daemon-reload
 systemctl reset-failed
 
-echo ""
-echo "✓ Uninstall complete!"
-echo ""
+log ""
+log "✓ Uninstall complete!"
+log ""
 if [[ "$REMOVE_AUTH_FILES" == "1" ]]; then
-	echo "Note: Service env/auth files have been removed."
+	log "Note: Service env/auth files have been removed."
 else
-	echo "Note: Service env/auth files were kept."
+	log "Note: Service env/auth files were kept."
 fi
-echo "      Your git repository and project files have been preserved."
-echo ""
+log "      Your git repository and project files have been preserved."
+log ""

+ 8 - 3
substore/operator_template.js

@@ -14,6 +14,11 @@ const CACHE_KEY = "vmess-domain-rotator:current";
 const CACHE_TTL_MS = 5 * 60 * 1000;
 const JSON_VALUE_KEYS = ["domain", "ip"];
 
+function logWithTime(message) {
+  const ts = new Date().toISOString();
+  console.log(`[${ts}] ${message}`);
+}
+
 async function fetchRuntimeJson(url, sourceName) {
   const $ = $substore;
   const { body, statusCode } = await $.http.get({
@@ -216,7 +221,7 @@ async function operator(proxies = [], targetPlatform, context) {
       value = decision.value;
       cache.set(CACHE_KEY, value, CACHE_TTL_MS);
     } catch (e) {
-      console.log(`[vmess-domain-rotator] fetch failed: ${e.message}`);
+      logWithTime(`[vmess-domain-rotator] fetch failed: ${e.message}`);
       return proxies;
     }
   }
@@ -234,11 +239,11 @@ async function operator(proxies = [], targetPlatform, context) {
 
   if (decision) {
     const t = decision.telemetry;
-    console.log(
+    logWithTime(
       `[vmess-domain-rotator] mode=${VALUE_SOURCE_MODE}, chosen=${decision.chosenSource}, reason=${decision.reason}, router_status=${t.router.status || "n/a"}, server_status=${t.server.status || "n/a"}, router_speed_raw=${t.router.speedRaw || "n/a"}, router_speed_mb_per_s=${t.router.speedMBps == null ? "n/a" : t.router.speedMBps.toFixed(3)}, speed_parse=${t.router.speedParse}, threshold_mb_per_s=${ROUTER_MIN_SPEED_MB_PER_S}, value=${value}, updated=${updated}, total=${proxies.length}, target=${targetPlatform}`
     );
   } else {
-    console.log(`[vmess-domain-rotator] mode=${VALUE_SOURCE_MODE}, chosen=cache, value=${value}, updated=${updated}, total=${proxies.length}, target=${targetPlatform}`);
+    logWithTime(`[vmess-domain-rotator] mode=${VALUE_SOURCE_MODE}, chosen=cache, value=${value}, updated=${updated}, total=${proxies.length}, target=${targetPlatform}`);
   }
 
   return proxies;