NSDF 助手

DeepFlood & NodeSeek 论坛增强脚本(基于 NSaide 改造)

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         NSDF 助手
// @namespace    https://www.deepflood.com/
// @version      0.2.2
// @description  DeepFlood & NodeSeek 论坛增强脚本(基于 NSaide 改造)
// @author       zen1zi
// @license      GPL-3.0
// @match        https://www.deepflood.com/*
// @match        https://deepflood.com/*
// @match        https://www.nodeseek.com/*
// @match        https://nodeseek.com/*
// @icon         https://www.deepflood.com/favicon.ico
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_deleteValue
// @grant        GM_xmlhttpRequest
// @grant        GM_registerMenuCommand
// @grant        GM_unregisterMenuCommand
// @grant        GM_listValues
// @grant        GM_info
// @grant        unsafeWindow
// @run-at       document-start
// ==/UserScript==
(function () {
  "use strict";

  console.log("[DF助手] 脚本开始加载");

  const HOST_SITE_MAP = {
    "www.deepflood.com": { id: "deepflood", name: "DeepFlood" },
    "deepflood.com": { id: "deepflood", name: "DeepFlood" },
    "www.nodeseek.com": { id: "nodeseek", name: "NodeSeek" },
    "nodeseek.com": { id: "nodeseek", name: "NodeSeek" },
  };

  const currentHost = window.location.host;
  const siteMeta = HOST_SITE_MAP[currentHost] || {
    id: "unknown",
    name: currentHost,
  };
  const siteInfo = Object.freeze({
    ...siteMeta,
    host: currentHost,
    origin: window.location.origin,
    isDeepFlood: siteMeta.id === "deepflood",
    isNodeSeek: siteMeta.id === "nodeseek",
  });

  if (siteInfo.id === "unknown") {
    console.warn("[DF助手] 检测到未配置站点,尝试按当前域名加载");
  } else {
    console.log(`[DF助手] 当前站点: ${siteInfo.name} (${siteInfo.host})`);
  }

  const CONFIG_URL =
    "https://raw.githubusercontent.com/zen1zi/NSDF_Plus/main/modules/config.json";
  const CACHE_EXPIRY = 30 * 60 * 1000;
  const CACHE_KEY_PREFIX = "df_module_cache_";
  const CONFIG_CACHE_KEY = "df_config_cache";
  const MODULE_ENABLED_KEY_PREFIX = "df_MODULE_ENABLED_";
  const LEGACY_MODULE_ENABLED_KEY_PREFIX = "module_";
  const LEGACY_MODULE_ENABLED_KEY_SUFFIX = "_enabled";

  const normalizeModuleIdForKey = (id) => {
    if (typeof id !== "string") {
      return "";
    }

    return id
      .replace(/([a-z0-9])([A-Z])/g, "$1_$2")
      .replace(/[^a-z0-9]/gi, "_")
      .toUpperCase();
  };

  const getCachedData = (key) => {
    const cached = GM_getValue(key);
    if (typeof cached !== "string" || cached.length === 0) {
      console.log(`[DF助手] 缓存未命中: ${key}`);
      return null;
    }

    try {
      const { data, timestamp } = JSON.parse(cached);
      const age = Date.now() - timestamp;
      if (age > CACHE_EXPIRY) {
        console.log(
          `[DF助手] 缓存已过期: ${key} (${Math.round(age / 1000)}s 前)`,
        );
        deleteCachedData(key, { silent: true });
        return null;
      }
      console.log(`[DF助手] 缓存命中: ${key} (${Math.round(age / 1000)}s 前)`);
      return data;
    } catch (error) {
      console.warn(`[DF助手] 缓存数据解析失败: ${key}`, error);
      deleteCachedData(key, { silent: true });
      return null;
    }
  };

  const setCachedData = (key, data) => {
    try {
      GM_setValue(
        key,
        JSON.stringify({
          data,
          timestamp: Date.now(),
        }),
      );
      console.log(`[DF助手] 缓存已保存: ${key}`);
    } catch (error) {
      console.error(`[DF助手] 缓存保存失败: ${key}`, error);
    }
  };

  const deleteCachedData = (key, { silent = false } = {}) => {
    try {
      GM_deleteValue(key);
      if (!silent) {
        console.log(`[DF助手] 缓存已清理: ${key}`);
      }
    } catch (error) {
      console.error(`[DF助手] 缓存清理失败: ${key}`, error);
    }
  };

  const fetchWithCache = (url, cacheKey, retryCount = 3) => {
    return new Promise((resolve, reject) => {
      const cached = getCachedData(cacheKey);
      if (cached) {
        resolve(cached);
        return;
      }

      console.log(`[DF助手] 开始远程获取: ${url}`);

      const attemptFetch = (attempt) => {
        GM_xmlhttpRequest({
          method: "GET",
          url: `${url}?t=${Date.now()}`,
          timeout: 10000,
          nocache: true,
          headers: {
            "Cache-Control": "no-cache",
            Pragma: "no-cache",
          },
          onload: (response) => {
            if (response.status === 200) {
              try {
                const data = response.responseText;
                setCachedData(cacheKey, data);
                console.log(`[DF助手] 远程获取成功: ${url}`);
                resolve(data);
              } catch (error) {
                console.error(`[DF助手] 数据处理失败: ${url}`, error);
                reject(error);
              }
            } else {
              const error = new Error(
                `HTTP ${response.status}: ${response.statusText}`,
              );
              if (attempt < retryCount) {
                console.warn(
                  `[DF助手] 请求失败,重试 ${attempt}/${retryCount}: ${url}`,
                  error.message,
                );
                setTimeout(() => attemptFetch(attempt + 1), 1000 * attempt);
              } else {
                console.error(`[DF助手] 请求最终失败: ${url}`, error);
                reject(error);
              }
            }
          },
          onerror: (error) => {
            if (attempt < retryCount) {
              console.warn(
                `[DF助手] 网络错误,重试 ${attempt}/${retryCount}: ${url}`,
                error,
              );
              setTimeout(() => attemptFetch(attempt + 1), 1000 * attempt);
            } else {
              console.error(`[DF助手] 网络错误最终失败: ${url}`, error);
              reject(error);
            }
          },
          ontimeout: () => {
            const error = new Error("请求超时");
            if (attempt < retryCount) {
              console.warn(
                `[DF助手] 请求超时,重试 ${attempt}/${retryCount}: ${url}`,
              );
              setTimeout(() => attemptFetch(attempt + 1), 1000 * attempt);
            } else {
              console.error(`[DF助手] 请求超时最终失败: ${url}`);
              reject(error);
            }
          },
        });
      };

      attemptFetch(1);
    });
  };

  const loadConfig = async () => {
    try {
      const configText = await fetchWithCache(CONFIG_URL, CONFIG_CACHE_KEY);
      return JSON.parse(configText);
    } catch (error) {
      console.error("[DF助手] 配置加载失败:", error);
      throw error;
    }
  };

  const loadModule = async (moduleInfo) => {
    const cacheKey = `${CACHE_KEY_PREFIX}${moduleInfo.id}`;
    const loadStartTime = Date.now();

    try {
      console.log(`[DF助手] 开始加载模块: ${moduleInfo.name}`);
      const moduleCode = await fetchWithCache(moduleInfo.url, cacheKey);

      // 检查模块代码安全性 - 简单的恶意代码检测
      if (
        moduleCode.includes("eval(") &&
        !moduleCode.includes("// Safe eval")
      ) {
        console.warn(`[DF助手] 模块 ${moduleInfo.name} 包含可疑代码,跳过加载`);
        return;
      }

      eval(moduleCode);
      const loadTime = Date.now() - loadStartTime;
      console.log(`[DF助手] 模块加载成功: ${moduleInfo.name} (${loadTime}ms)`);

      // 记录模块加载性能
      window.DF.dev.moduleLoadTimes = window.DF.dev.moduleLoadTimes || {};
      window.DF.dev.moduleLoadTimes[moduleInfo.id] = loadTime;
    } catch (error) {
      console.error(`[DF助手] 模块 ${moduleInfo.name} 加载失败:`, error);

      // 尝试从缓存中清除损坏的模块
      deleteCachedData(cacheKey, { silent: true });
      console.log(`[DF助手] 已清理损坏的模块缓存: ${moduleInfo.id}`);

      throw error;
    }
  };

  const createDF = () => {
    window.DF = {
      version: GM_info.script.version,
      modules: new Map(),
      isReady: false,
      site: siteInfo,

      registerModule(moduleDefinition) {
        if (!moduleDefinition || !moduleDefinition.id || !moduleDefinition.init)
          return;

        const normalizedId = normalizeModuleIdForKey(moduleDefinition.id);
        const enabledKey = `${MODULE_ENABLED_KEY_PREFIX}${normalizedId}`;
        let enabled = GM_getValue(enabledKey);

        if (typeof enabled === "undefined") {
          const legacyKey = `${LEGACY_MODULE_ENABLED_KEY_PREFIX}${moduleDefinition.id}${LEGACY_MODULE_ENABLED_KEY_SUFFIX}`;
          const legacyValue = GM_getValue(legacyKey);

          if (typeof legacyValue !== "undefined") {
            enabled = legacyValue;
            try {
              GM_setValue(enabledKey, legacyValue);
              GM_deleteValue(legacyKey);
              console.log(
                `[DF助手] 已迁移模块启用状态: ${moduleDefinition.id}`,
              );
            } catch (migrationError) {
              console.warn(
                `[DF助手] 模块启用状态迁移失败: ${moduleDefinition.id}`,
                migrationError,
              );
            }
          } else {
            enabled = true;
            try {
              GM_setValue(enabledKey, enabled);
            } catch (error) {
              console.warn(
                `[DF助手] 默认写入模块启用状态失败: ${moduleDefinition.id}`,
                error,
              );
            }
          }
        }

        if (typeof enabled === "string") {
          enabled = enabled.toLowerCase() === "true";
        } else {
          enabled = Boolean(enabled);
        }

        const module = {
          ...moduleDefinition,
          enabled: enabled,
          enabledStorageKey: enabledKey,
        };

        this.modules.set(moduleDefinition.id, module);
        console.log(`[DF助手] 模块已注册: ${module.name}`);
      },

      init() {
        if (this.isReady) return;

        const enabledModules = Array.from(this.modules.values()).filter(
          (m) => m.enabled,
        );
        console.log(
          `[DF助手] 开始初始化 ${enabledModules.length} 个已启用模块`,
        );

        Promise.all(
          enabledModules.map(
            (module) =>
              new Promise((resolve) => {
                try {
                  module.init();
                  console.log(`[DF助手] 模块初始化成功: ${module.name}`);
                  resolve();
                } catch (error) {
                  console.error(
                    `[DF助手] 模块 ${module.name} 初始化失败:`,
                    error,
                  );
                  resolve();
                }
              }),
          ),
        ).then(() => {
          this.isReady = true;
          console.log("[DF助手] 所有模块初始化完成");
        });
      },
    };

    window.DF.getSiteUrl = (path = "") => {
      if (typeof path !== "string" || path.length === 0) {
        return window.DF.site.origin;
      }

      if (/^https?:\/\//i.test(path)) {
        return path;
      }

      const normalizedPath = path.startsWith("/") ? path : `/${path}`;
      return `${window.DF.site.origin}${normalizedPath}`;
    };

    window.DFRegisterModule = (moduleDefinition) => {
      window.DF.registerModule(moduleDefinition);
    };

    // 缓存工具 - 可供模块使用
    window.DF.cache = {
      get: getCachedData,
      set: setCachedData,
      delete: deleteCachedData,
      fetch: fetchWithCache,
      expiry: CACHE_EXPIRY,
      keyPrefix: CACHE_KEY_PREFIX,
    };

    // 开发者工具
    window.DF.dev = {
      clearCache() {
        const removedKeys = [];

        try {
          const storedKeys = GM_listValues();
          storedKeys.forEach((key) => {
            if (key === CONFIG_CACHE_KEY || key.startsWith(CACHE_KEY_PREFIX)) {
              deleteCachedData(key, { silent: true });
              removedKeys.push(key);
            }
          });
        } catch (error) {
          console.error("[DF助手] 遍历缓存键失败:", error);
        }

        console.log(`[DF助手] 已清理缓存: ${removedKeys.length} 项`);
        return removedKeys;
      },
      getCacheInfo() {
        const info = {};
        let storedKeys = [];

        try {
          storedKeys = GM_listValues();
        } catch (error) {
          console.error("[DF助手] 获取缓存信息失败:", error);
        }

        const storedSet = new Set(storedKeys);
        info.config = storedSet.has(CONFIG_CACHE_KEY) ? "已缓存" : "未缓存";
        window.DF.modules.forEach((module, id) => {
          const moduleCacheKey = `${CACHE_KEY_PREFIX}${id}`;
          info[id] = storedSet.has(moduleCacheKey) ? "已缓存" : "未缓存";
        });
        return info;
      },
      getModuleHealth() {
        const health = {};
        window.DF.modules.forEach((module, id) => {
          health[id] = {
            id: module.id,
            name: module.name,
            enabled: module.enabled,
            loaded: !!module.init,
            cached: typeof GM_getValue(`${CACHE_KEY_PREFIX}${id}`) === "string",
          };
        });
        return health;
      },
    };
  };

  const initializeModules = async () => {
    const initStartTime = Date.now();

    try {
      console.log("[DF助手] 开始初始化模块系统");
      createDF();

      const config = await loadConfig();
      console.log(
        `[DF助手] 配置加载成功,发现 ${config.modules.length} 个模块`,
      );

      // 并行加载模块,但容错处理
      const moduleLoadPromises = config.modules.map(async (moduleInfo) => {
        try {
          await loadModule(moduleInfo);
          return { success: true, module: moduleInfo.id };
        } catch (error) {
          console.error(`[DF助手] 模块 ${moduleInfo.id} 加载失败:`, error);
          return { success: false, module: moduleInfo.id, error };
        }
      });

      const results = await Promise.all(moduleLoadPromises);
      const successful = results.filter((r) => r.success).length;
      const failed = results.filter((r) => !r.success);

      console.log(
        `[DF助手] 模块加载完成: ${successful}/${config.modules.length} 成功`,
      );

      if (failed.length > 0) {
        console.warn(
          "[DF助手] 加载失败的模块:",
          failed.map((f) => f.module),
        );
      }

      if (window.DF.modules.size > 0) {
        window.DF.init();
        const initTime = Date.now() - initStartTime;
        console.log(
          `[DF助手] 初始化完成 (${initTime}ms),已加载 ${window.DF.modules.size} 个模块`,
        );

        // 注册菜单命令
        GM_registerMenuCommand("⚙️ 清理缓存", () => {
          window.DF.dev.clearCache();
          alert("缓存已清理,请刷新页面");
        });
      } else {
        console.warn("[DF助手] 没有模块成功加载");
      }
    } catch (error) {
      console.error("[DF助手] 初始化失败:", error);

      // 降级处理:尝试只加载设置模块
      try {
        console.log("[DF助手] 尝试降级启动...");
        const fallbackConfig = {
          modules: [
            {
              id: "settings",
              name: "设置面板",
              url: "https://raw.githubusercontent.com/zen1zi/NSDF_Plus/main/modules/settings/index.js",
            },
          ],
        };

        await loadModule(fallbackConfig.modules[0]);
        if (window.DF.modules.size > 0) {
          window.DF.init();
          console.log("[DF助手] 降级启动成功");
        }
      } catch (fallbackError) {
        console.error("[DF助手] 降级启动也失败:", fallbackError);
      }
    }
  };

  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", initializeModules);
  } else {
    initializeModules();
  }
})();