Dew-OF-Aurora 3 settimane fa
parent
commit
cedbe6cb1e

+ 7 - 0
AGENTS.md

@@ -0,0 +1,7 @@
+# AGENTS.md
+
+本仓库的 agent 指引以 [`CLAUDE.md`](./CLAUDE.md) 为唯一权威来源。
+
+请直接阅读并遵循 `CLAUDE.md`,其中包含:仓库职责、文件地图、常用命令(语法校验/运行/systemd 安装卸载)、测试约定、关键约束与「Where to Look」导航。
+
+> 为避免文档漂移,本文件不再重复内容。所有更新只维护 `CLAUDE.md`。

+ 15 - 0
scripts/install_debian.sh

@@ -116,6 +116,19 @@ validate_interval() {
 	fi
 }
 
+validate_hour_range() {
+	local label="$1" value="$2" min="$3" max="$4"
+	if [[ ! "$value" =~ ^[0-9]+$ ]]; then
+		log_err "Error: $label must be an integer from ${min} to ${max}, got: '$value'"
+		exit 1
+	fi
+	local n=$((10#$value))
+	if [[ "$n" -lt "$min" ]] || [[ "$n" -gt "$max" ]]; then
+		log_err "Error: $label must be from ${min} to ${max}, got: '$value'"
+		exit 1
+	fi
+}
+
 validate_tz() {
 	local value="$1"
 	if [[ ! "$value" =~ ^[A-Za-z0-9_+/.-]+$ ]] || [[ ${#value} -gt 64 ]]; then
@@ -260,6 +273,8 @@ fi
 validate_ident "service user" "$RUN_USER"
 validate_ident "service group" "$RUN_GROUP"
 validate_tz "$PEAK_TZ"
+validate_hour_range "--peak-start" "$PEAK_START_HOUR" 0 23
+validate_hour_range "--peak-end" "$PEAK_END_HOUR" 1 24
 validate_path "APP_DIR" "$APP_DIR"
 validate_path "CONFIG_PATH" "$CONFIG_PATH"
 

+ 2 - 1
scripts/router_local_update.sh

@@ -157,7 +157,7 @@ write_success_outputs() {
 }
 EOF
 
-  if [ "$selected_ip" != "${LAST_GOOD_IP:-}" ] || [ ! -f "$STATE_PATH" ]; then
+  if [ "$selected_ip" != "${LAST_GOOD_IP:-}" ] || [ ! -f "$STATE_PATH" ] || [ "${LAST_STATE_STATUS:-}" != "ok" ]; then
     cat > "$STATE_PATH" <<EOF
 {
   "updated_at": "$UPDATED_AT",
@@ -310,6 +310,7 @@ log_update_summary() {
 }
 
 LAST_GOOD_IP=$(json_get_string "$STATE_LAST_GOOD_KEY" "$STATE_PATH")
+LAST_STATE_STATUS=$(json_get_string "status" "$STATE_PATH")
 
 if [ "$CFST_SKIP_RUN" != "1" ]; then
   if ! run_cfst; then

+ 48 - 2
scripts/run_update_and_commit.sh

@@ -141,9 +141,26 @@ else:
 PY
 }
 
+json_field() {
+  local key="$1"
+  /usr/bin/python3 -c '
+import json
+import sys
+
+key = sys.argv[1]
+try:
+    obj = json.load(sys.stdin)
+except Exception:
+    print("")
+else:
+    value = obj.get(key, "")
+    print("" if value is None else str(value))
+' "$key"
+}
+
 updater_output="$(/usr/bin/python3 "${APP_DIR}/scripts/domain_updater.py" --config "$CONFIG_PATH")"
 printf '%s\n' "$updater_output"
-updater_status="$(printf '%s' "$updater_output" | /usr/bin/python3 -c 'import json,sys; print(json.load(sys.stdin).get("status",""))' 2>/dev/null || echo "")"
+updater_status="$(printf '%s' "$updater_output" | json_field status 2>/dev/null || echo "")"
 if [[ "$updater_status" == "error_use_last_good" ]]; then
   log "[vmess-domain-rotator] warning: updater using fallback value (source error)"
 fi
@@ -161,6 +178,8 @@ if [[ -z "$after" ]]; then
 fi
 
 selected_rel="$(repo_relpath "$SELECTED_TEXT_FILE")"
+selected_json_rel="$(repo_relpath "$SELECTED_JSON_FILE")"
+vars_rel="$(repo_relpath "$VARS_FILE")"
 if [[ -z "$selected_rel" ]]; then
   log "[vmess-domain-rotator] selected value file is outside repo (${SELECTED_TEXT_FILE}), skip git commit"
   exit 0
@@ -238,7 +257,34 @@ if before_raw="$(git -C "$work_dir" show "HEAD:${selected_rel}" 2>/dev/null)"; t
   before="$(printf '%s' "$before_raw" | tr -d '\r\n')"
 fi
 
-if [[ "$force_commit" != "1" ]] && [[ -n "$before" ]] && [[ "$after" == "$before" ]]; then
+before_status=""
+if [[ -n "$selected_json_rel" ]] && before_json_raw="$(git -C "$work_dir" show "HEAD:${selected_json_rel}" 2>/dev/null)"; then
+  before_status="$(printf '%s' "$before_json_raw" | json_field status 2>/dev/null || echo "")"
+fi
+
+before_vars_status=""
+if [[ -n "$vars_rel" ]] && before_vars_raw="$(git -C "$work_dir" show "HEAD:${vars_rel}" 2>/dev/null)"; then
+  before_vars_status="$(printf '%s' "$before_vars_raw" | json_field STATUS 2>/dev/null || echo "")"
+fi
+
+status_compared="0"
+status_unchanged="0"
+if [[ -n "$updater_status" ]]; then
+  status_unchanged="1"
+  if [[ -n "$before_status" ]]; then
+    status_compared="1"
+    [[ "$updater_status" != "$before_status" ]] && status_unchanged="0"
+  fi
+  if [[ -n "$before_vars_status" ]]; then
+    status_compared="1"
+    [[ "$updater_status" != "$before_vars_status" ]] && status_unchanged="0"
+  fi
+  if [[ "$status_compared" != "1" ]]; then
+    status_unchanged="0"
+  fi
+fi
+
+if [[ "$force_commit" != "1" ]] && [[ -n "$before" ]] && [[ "$after" == "$before" ]] && [[ "$status_unchanged" == "1" ]]; then
   log "[vmess-domain-rotator] selected value unchanged (${after}), skip git commit and push"
   exit 0
 fi

+ 46 - 9
substore/operator_template.js

@@ -210,6 +210,43 @@ async function selectValueByMode() {
   throw new Error(`invalid VALUE_SOURCE_MODE=${VALUE_SOURCE_MODE}, expected: default | server_only | router_only`);
 }
 
+function normalizeIPv4(raw) {
+  const parts = raw.split(".");
+  if (parts.length !== 4) return null;
+  for (const part of parts) {
+    if (!/^\d{1,3}$/.test(part)) return null;
+    const n = Number(part);
+    if (!Number.isInteger(n) || n < 0 || n > 255) return null;
+  }
+  return parts.map((part) => String(Number(part))).join(".");
+}
+
+function normalizeIPv6(raw) {
+  let value = raw;
+  if (value.startsWith("[") || value.endsWith("]")) {
+    if (!(value.startsWith("[") && value.endsWith("]"))) return null;
+    value = value.slice(1, -1);
+  }
+
+  if (!value || !/^[0-9a-f:]+$/.test(value)) return null;
+  if ((value.match(/::/g) || []).length > 1) return null;
+
+  const hasCompression = value.includes("::");
+  const sides = value.split("::");
+  const left = sides[0] ? sides[0].split(":") : [];
+  const right = sides.length > 1 && sides[1] ? sides[1].split(":") : [];
+  const groups = left.concat(right);
+
+  if (groups.some((part) => !/^[0-9a-f]{1,4}$/.test(part))) return null;
+  if (hasCompression) {
+    if (groups.length >= 8) return null;
+  } else if (groups.length !== 8) {
+    return null;
+  }
+
+  return value;
+}
+
 function extractOverrideDomain() {
   const args = typeof $arguments !== "undefined" ? $arguments : {};
 
@@ -225,16 +262,17 @@ function extractOverrideDomain() {
   if (!raw) return null;
 
   // 基本格式校验:域名或 IPv4/IPv6
-  const domainRe = /^([a-z0-9]([a-z0-9-]*[a-z0-9])?\.)+[a-z]{2,}$/;
-  const ipv4Re = /^(\d{1,3}\.){3}\d{1,3}$/;
-  const ipv6Re = /^\[?[0-9a-f:]+\]?$/;
+  const domainRe = /^([a-z0-9]([a-z0-9-]*[a-z0-9])?\.)+(?=[a-z0-9-]{2,}$)(?=[a-z0-9-]*[a-z])[a-z0-9]([a-z0-9-]*[a-z0-9])?$/;
+  const ipv4 = normalizeIPv4(raw);
+  if (ipv4) return ipv4;
 
-  if (!domainRe.test(raw) && !ipv4Re.test(raw) && !ipv6Re.test(raw)) {
-    logWithTime(`[vmess-domain-rotator] invalid override domain format: "${raw}", ignoring`);
-    return null;
-  }
+  const ipv6 = normalizeIPv6(raw);
+  if (ipv6) return ipv6;
 
-  return raw;
+  if (domainRe.test(raw)) return raw;
+
+  logWithTime(`[vmess-domain-rotator] invalid override domain format: "${raw}", ignoring`);
+  return null;
 }
 
 async function operator(proxies = [], targetPlatform, context) {
@@ -283,4 +321,3 @@ async function operator(proxies = [], targetPlatform, context) {
 
   return proxies;
 }
-