Greasy Fork

Greasy Fork is available in English.

光鸭云盘批量助手 V4

为光鸭云盘网页端提供批量重命名、重复项清理、移动整理、磁力云添加、秒传 JSON 转换/诊断、空目录扫描与删除等功能。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         光鸭云盘批量助手 V4
// @namespace    serenalee.guangyapan.batch-helper
// @version      0.5.85
// @description  为光鸭云盘网页端提供批量重命名、重复项清理、移动整理、磁力云添加、秒传 JSON 转换/诊断、空目录扫描与删除等功能。
// @author       Serena Lee
// @license      Copyright (c) 2026 Serena Lee. All rights reserved.
// @match        https://pan.quark.cn/*
// @match        https://drive.quark.cn/*
// @match        https://cloud.189.cn/*
// @match        *://*.123pan.com/*
// @match        *://*.123pan.cn/*
// @match        *://*.123684.cn/*
// @match        https://www.123pan.com/*
// @match        https://www.123pan.com/
// @match        http://www.123pan.com/*
// @match        https://123pan.com/*
// @match        https://123pan.com/
// @match        http://123pan.com/*
// @match        https://pan.baidu.com/*
// @match        https://yun.baidu.com/*
// @match        https://pan.xunlei.com/*
// @match        https://guangyapan.com/*
// @match        https://*.guangyapan.com/*
// @icon         https://image.868717.xyz/file/1776301692011_3.svg
// @run-at       document-idle
// @grant        GM_setClipboard
// @grant        GM_xmlhttpRequest
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// @grant        unsafeWindow
// @connect      drive.quark.cn
// @connect      drive-pc.quark.cn
// @connect      pc-api.uc.cn
// @connect      www.123pan.com
// @connect      123pan.com
// @connect      www.123pan.cn
// @connect      123pan.cn
// @connect      *.123pan.com
// @connect      *.123pan.cn
// @connect      cloud.189.cn
// @connect      pan.baidu.com
// @connect      yun.baidu.com
// @connect      api-pan.xunlei.com
// @connect      api.guangyapan.com
// @connect      api.themoviedb.org
// @connect      image.tmdb.org
// ==/UserScript==

(function () {
  'use strict';

  const SCRIPT_VERSION = '0.5.85';

  // =========================
  // 用户配置区:主要改这里
  // =========================
  const CONFIG = {
    debug: false,
    request: {
      apiHost: 'https://api.guangyapan.com',
      listPath: '/nd.bizuserres.s/v1/file/get_file_list',
      renamePath: '/nd.bizuserres.s/v1/file/rename',
      deletePath: '/nd.bizuserres.s/v1/file/delete_file',
      movePath: '/nd.bizuserres.s/v1/file/move_file',
      downloadPath: '/nd.bizuserres.s/v1/get_res_download_url',
      createDirPath: '/nd.bizuserres.s/v1/file/create_dir',
      taskStatusPath: '/nd.bizuserres.s/v1/get_task_status',
      resolveResPath: '/nd.bizcloudcollection.s/v1/resolve_res',
      cloudCreateTaskPath: '/nd.bizcloudcollection.s/v1/create_task',
      cloudListTaskPath: '/nd.bizcloudcollection.s/v1/list_task',
      // 自动抓不到时,再手工填写这些值。
      manualHeaders: {
        authorization: '',
        did: '',
        dt: '',
        appid: '',
        timestamp: '',
        signature: '',
        nonce: '',
      },
      // 自动抓不到当前目录时,再手工填写。
      manualListBody: {
        parentId: '',
        pageSize: 100,
        orderBy: 0,
        sortType: 0,
      },
    },
    batch: {
      delayMs: 300,
      confirmBeforeRun: true,
      stopOnError: false,
      taskPollMs: 1500,
      taskPollMaxTries: 180,
    },
    rename: {
      ruleMode: 'remove-leading-bracket',
      // 默认效果:删除开头第一个 [] 或 【】 以及里面的内容。
      // 可按顺序继续加规则。
      rules: [
        {
          enabled: true,
          type: 'regex',
          pattern: '^\\s*[\\[【][^\\]】]*[\\]】]\\s*',
          flags: 'u',
          replace: '',
        },
      ],
      output: {
        mode: 'keep-clean',
        addText: '',
        addPosition: 'suffix',
        addIgnoreExtension: true,
        findText: '',
        replaceText: '',
        formatStyle: 'text-and-index',
        formatText: '文件',
        formatPosition: 'suffix',
        startIndex: 0,
        template: '{clean}',
      },
      // 可用占位符:
      // {original} 原文件名
      // {clean}    应用 rules 后的名字
      // {base}     clean 去掉最后一个扩展名后的部分
      // {ext}      clean 的最后一个扩展名,例:.mkv
      // {fileId}   文件 ID
      // {index}    序号(格式命名或自定义模板时可用)
      template: '{clean}',
      trimResult: true,
      buildName(item, utils, context = {}) {
        const original = String(item.name || '');
        const clean = utils.applyRules(original, item);
        const ext = utils.getExt(clean);
        const base = utils.getBaseName(clean);
        const output = CONFIG.rename.output || {};
        const mode = output.mode || 'keep-clean';
        const renameIndex = Number(context.renameIndex || 0);
        const serial = Number(output.startIndex || 0) + renameIndex;

        if (mode === 'add-text') {
          const addText = String(output.addText || '');
          if (!addText) {
            return clean;
          }
          const ignoreExtension = output.addIgnoreExtension !== false;
          if (ignoreExtension && output.addPosition === 'suffix' && ext) {
            return `${base}${addText}${ext}`;
          }
          return output.addPosition === 'prefix' ? `${addText}${clean}` : `${clean}${addText}`;
        }

        if (mode === 'replace-text') {
          const findText = String(output.findText || '');
          if (!findText) {
            return clean;
          }
          return clean.split(findText).join(String(output.replaceText || ''));
        }

        if (mode === 'format') {
          const formatText = String(output.formatText || '').trim() || '文件';
          if (output.formatStyle === 'text-only') {
            return formatText;
          }
          return output.formatPosition === 'prefix' ? `${serial}${formatText}` : `${formatText}${serial}`;
        }

        if (mode === 'custom-template') {
          const template = String(output.template || CONFIG.rename.template || '{clean}').trim() || '{clean}';
          return utils.renderTemplate(template, {
            original,
            clean,
            base,
            ext,
            fileId: item.fileId,
            index: serial,
          });
        }

        return clean;
      },
    },
    filter: {
      // 想跳过某些名字时在这里写条件。
      // 例:item => !item.name.includes('不要改')
      predicate: () => true,
    },
    duplicate: {
      mode: 'numbers',
      numbers: '1,2,3',
      // 默认识别文件夹名末尾带 (1) / (2) / (3) 或中文括号版本。
      pattern: '[((]\\s*(?:1|2|3|1|2|3)\\s*[))]\\s*(?:\\.[a-zA-Z0-9]{1,12})?$',
      flags: 'u',
    },
    cloud: {
      maxFilesPerTask: 500,
      sourceDirPrefix: '磁力导入',
      createMagnetSubdir: false,
      listTaskPageSize: 50,
    },
    move: {
      targetParentId: '',
      batchSize: 20,
    },
    mediaOrganize: {
      tmdbApiKey: '',
      tmdbLanguage: 'zh-CN',
      rootParentId: '',
      useFolderNameFirst: true,
      includeTitleFolder: true,
      includeRegionFolder: true,
      includeSeasonFolder: true,
      moveBySourceFolder: false,
      cleanupEmptySourceFolders: true,
      skipDuplicateTargets: true,
      batchSize: 10,
    },
    download: {
      directBatchSize: 3,
      exportFormat: 'aria2',
    },
  };

  const LOG_PREFIX = '[光鸭云盘批量助手]';
  const CAPTURE_EVENT = '__GYP_BATCH_RENAME_CAPTURE__';
  const PAGE_REQUEST_EVENT = '__GYP_BATCH_RENAME_PAGE_REQUEST__';
  const PAGE_RESPONSE_EVENT = '__GYP_BATCH_RENAME_PAGE_RESPONSE__';
  const CONFIG_STORAGE_KEY = '__GYP_BATCH_RENAME_CONFIG_V1__';
  const QUARK_COOKIE_STORAGE_KEY = '__GYP_BATCH_RENAME_QUARK_COOKIE__';
  const GUANGYA_AUTH_STORAGE_KEY = '__GYP_BATCH_RENAME_GUANGYA_AUTH__';
  const GUANGYA_CODE_RES_TOKEN_INSTANT = 156;
  const GUANGYA_CODE_DIR_EXISTS = 159;
  const XUNLEI_API_BASE = 'https://api-pan.xunlei.com';
  const XUNLEI_CLIENT_ID = 'Xqp0kJBXWhwaTpB6';
  const DIRECT_DOWNLOAD_EXPORT_FORMATS = Object.freeze(['aria2', 'url']);
  const STATE = {
    headers: {},
    lastApiHeaders: null,
    lastListHeaders: null,
    lastListUrl: '',
    lastListBody: null,
    lastListItems: [],
    capturedLists: {},
    lastCapturedParentId: '',
    lastItemsSource: 'none',
    lastListResponse: null,
    lastListCapturedAt: 0,
    lastRenameRequest: null,
    duplicatePreviewItems: [],
    duplicateSelection: {},
    moveSelectionPreviewItems: [],
    moveSelectionExpectedCount: 0,
    moveSelectionSource: 'visible',
    moveSelectionWarning: '',
    mediaOrganizePreviewItems: [],
    mediaOrganizePlan: null,
    mediaOrganizeWarning: '',
    tmdbCache: {},
    directDownloadPreviewItems: [],
    directDownloadExpectedCount: 0,
    directDownloadWarning: '',
    lastDirectDownloadSummary: null,
    emptyDirSelection: {},
    magnetImportFiles: [],
    lastCloudImportSummary: null,
    lastCloudTaskList: null,
    lastMiaochuanJsonResult: null,
    shareLinkRows: [],
    shareLinkSelection: {},
    shareLinkMeta: null,
    miaochuanCapturedRows: [],
    miaochuanCapturedMap: {},
    lastMiaochuanCaptureAt: 0,
    lastMiaochuanCaptureUrl: '',
    lastXunleiHeaders: null,
    lastEmptyDirScan: null,
    activeTaskControl: null,
    lastProgressState: {
      visible: false,
      percent: 0,
      indeterminate: false,
      text: '',
    },
    installedAt: new Date().toISOString(),
  };
  const UI = {
    root: null,
    panel: null,
    mini: null,
    status: null,
    progressWrap: null,
    progressBar: null,
    progressText: null,
    pauseTaskButton: null,
    stopTaskButton: null,
    fields: {},
    summary: null,
    duplicateDetails: null,
    duplicateList: null,
    duplicateCount: null,
    moveDetails: null,
    moveSelectionList: null,
    moveSelectionCount: null,
    mediaOrganizeDetails: null,
    mediaOrganizeList: null,
    mediaOrganizeCount: null,
    directDownloadDetails: null,
    directDownloadList: null,
    directDownloadCount: null,
    directDownloadSummary: null,
    emptyDirList: null,
    emptyDirCount: null,
    emptyDirDetails: null,
    magnetDetails: null,
    magnetFileInput: null,
    magnetFileList: null,
    magnetFileCount: null,
    shareLinkDetails: null,
    shareLinkList: null,
    shareLinkCount: null,
    shareLinkSummary: null,
    miaochuanDetails: null,
    miaochuanFileInput: null,
    miaochuanDiagnosis: null,
    miaochuanOutput: null,
    miaochuanReport: null,
    miaochuanSource: null,
    miaochuanCapturedCount: null,
  };

  const KEEP_HEADER_NAMES = [
    'authorization',
    'did',
    'dt',
  ];
  const FORBIDDEN_FORWARD_HEADERS = new Set([
    'accept-encoding',
    'content-length',
    'cookie',
    'host',
    'origin',
    'priority',
    'referer',
  ]);
  const SAFE_FORWARD_HEADERS = new Set([
    'accept',
    'authorization',
    'content-type',
    'did',
    'dt',
    'x-device-id',
    'x-requested-with',
  ]);
  const DEFAULT_LEADING_BRACKET_PATTERN = '^\\s*[\\[【][^\\]】]*[\\]】]\\s*';
  const DEFAULT_DUPLICATE_NUMBERS = '1,2,3';
  const DIRECT_DOWNLOAD_MAX_DIRS = 600;
  const DIRECT_DOWNLOAD_MAX_FILES = 10000;
  const DIRECT_DOWNLOAD_MAX_PAGES_PER_DIR = 200;
  const SHARE_LINK_MAX_DIRS = 3000;
  const SHARE_LINK_MAX_FILES = 50000;
  const EMPTY_STATE_TEXT_PATTERNS = [
    /暂无文件/u,
    /空文件夹/u,
    /暂无数据/u,
    /文件夹为空/u,
    /没有文件/u,
    /这里空空如也/u,
    /什么都没有/u,
  ];
  const ROOT_DIRECTORY_NAMES = new Set([
    '我的云盘',
    '首页',
    '全部文件',
    '光鸭云盘',
    '文件',
  ]);
  const TRANSIENT_LIST_BODY_KEYS = new Set([
    'cursor',
    'nextCursor',
    'nextKey',
    'nextToken',
    'pageToken',
    'continueToken',
    'marker',
    'offset',
    'start',
    'startId',
    'startKey',
    'lastId',
    'lastKey',
    'lastFileId',
    'lastSortValue',
    'pageNo',
    'pageNum',
    'pageIndex',
    'page',
    'scrollId',
  ]);
  const KNOWN_COMPOUND_FILE_EXTENSIONS = [
    '.tar.gz',
    '.tar.bz2',
    '.tar.xz',
    '.user.js',
    '.d.ts',
  ];
  const KNOWN_FILE_EXTENSIONS = new Set([
    '7z',
    'aac',
    'ape',
    'ass',
    'avi',
    'azw3',
    'bmp',
    'bz2',
    'csv',
    'cue',
    'doc',
    'docx',
    'epub',
    'flac',
    'flv',
    'gif',
    'gz',
    'heic',
    'idx',
    'iso',
    'jpeg',
    'jpg',
    'json',
    'm4a',
    'm4v',
    'mkv',
    'mobi',
    'mov',
    'mp3',
    'mp4',
    'mpeg',
    'mpg',
    'mtv',
    'nfo',
    'ogg',
    'opus',
    'pdf',
    'png',
    'ppt',
    'pptx',
    'rar',
    'rm',
    'rmvb',
    'srt',
    'ssa',
    'strm',
    'sub',
    'sup',
    'tar',
    'tif',
    'tiff',
    'torrent',
    'ts',
    'txt',
    'vtt',
    'wav',
    'webm',
    'webp',
    'wmv',
    'xls',
    'xlsx',
    'xml',
    'xz',
    'yaml',
    'yml',
    'zip',
  ]);
  const CLOUD_VIDEO_EXTENSIONS = new Set([
    '3gp',
    'asf',
    'avi',
    'flv',
    'iso',
    'm2ts',
    'm4v',
    'mkv',
    'mov',
    'mp4',
    'mpeg',
    'mpg',
    'mtv',
    'rm',
    'rmvb',
    'ts',
    'vob',
    'webm',
    'wmv',
  ]);
  const CLOUD_JUNK_EXTENSIONS = new Set([
    'bmp',
    'gif',
    'jpeg',
    'jpg',
    'nfo',
    'png',
    'txt',
    'url',
    'webp',
  ]);
  const CLOUD_SKIP_NAME_PATTERNS = [
    /(^|[^\w])(sample|trailer|teaser|preview|screencap|poster|cover)([^\w]|$)/i,
    /预告|花絮|海报|封面|说明|访问|网址/i,
  ];
  const EMPTY_DIR_SCAN_MAX_DIRS = 3000;
  const EMPTY_DIR_SCAN_MAX_PAGES_PER_DIR = 200;
  const EMPTY_SCAN_EXTRA_FILE_EXTENSIONS = new Set([
    'apk',
    'cia',
    'ipa',
    'nsp',
    'nsz',
    'pkg',
    'xci',
    'xcz',
  ]);

  function getForwardableHeadersFromCaptured(headersLike) {
    const captured = sanitizeHeaders(headersLike);
    const forwardable = {};

    for (const [key, value] of Object.entries(captured)) {
      if (
        !key ||
        key.startsWith(':') ||
        key.startsWith('sec-') ||
        FORBIDDEN_FORWARD_HEADERS.has(key) ||
        !SAFE_FORWARD_HEADERS.has(key)
      ) {
        continue;
      }
      forwardable[key] = value;
    }

    return forwardable;
  }

  function pickFirstNonEmptyHeaders(...sources) {
    for (const source of sources) {
      if (source && Object.keys(sanitizeHeaders(source)).length) {
        return source;
      }
    }
    return null;
  }

  function log(...args) {
    if (CONFIG.debug) {
      console.log(LOG_PREFIX, ...args);
    }
  }

  function warn(...args) {
    console.warn(LOG_PREFIX, ...args);
  }

  function fail(...args) {
    console.error(LOG_PREFIX, ...args);
  }

  function sleep(ms) {
    return new Promise((resolve) => setTimeout(resolve, ms));
  }

  function waitForUiPaint(frames = 1) {
    const totalFrames = Math.max(1, Number(frames || 1));
    return new Promise((resolve) => {
      let remaining = totalFrames;
      const schedule = () => {
        if (typeof requestAnimationFrame === 'function') {
          requestAnimationFrame(() => {
            setTimeout(step, 0);
          });
          return;
        }
        setTimeout(step, 0);
      };
      const step = () => {
        remaining -= 1;
        if (remaining <= 0) {
          resolve();
          return;
        }
        schedule();
      };
      schedule();
    });
  }

  function safeJsonParse(value) {
    if (typeof value !== 'string') {
      return value;
    }
    try {
      return JSON.parse(value);
    } catch {
      return null;
    }
  }

  function normalizeDirectDownloadExportFormat(value) {
    const format = String(value || '').trim().toLowerCase();
    return DIRECT_DOWNLOAD_EXPORT_FORMATS.includes(format) ? format : 'aria2';
  }

  function getDirectDownloadExportFormatLabel(format) {
    switch (normalizeDirectDownloadExportFormat(format)) {
      case 'url':
        return 'IDM / 普通 URL';
      case 'aria2':
      default:
        return 'aria2';
    }
  }

  function loadPersistedConfig() {
    try {
      const raw = window.localStorage.getItem(CONFIG_STORAGE_KEY);
      if (!raw) {
        return;
      }

      const saved = JSON.parse(raw);
      if (saved && typeof saved === 'object') {
        if (saved.manualHeaders && typeof saved.manualHeaders === 'object') {
          Object.assign(CONFIG.request.manualHeaders, saved.manualHeaders);
        }
        if (saved.manualListBody && typeof saved.manualListBody === 'object') {
          Object.assign(CONFIG.request.manualListBody, saved.manualListBody);
        }
        if (saved.batch && typeof saved.batch === 'object') {
          if (saved.batch.delayMs != null && !Number.isNaN(Number(saved.batch.delayMs))) {
            CONFIG.batch.delayMs = Number(saved.batch.delayMs);
          }
        }
        if (typeof saved.renameTemplate === 'string') {
          CONFIG.rename.template = saved.renameTemplate;
        }
        if (typeof saved.renameRuleMode === 'string') {
          CONFIG.rename.ruleMode = saved.renameRuleMode;
        }
        if (saved.renameOutput && typeof saved.renameOutput === 'object') {
          Object.assign(CONFIG.rename.output, saved.renameOutput);
        }
        if (saved.firstRule && typeof saved.firstRule === 'object' && CONFIG.rename.rules[0]) {
          Object.assign(CONFIG.rename.rules[0], saved.firstRule);
        }
        if (saved.duplicate && typeof saved.duplicate === 'object') {
          if (typeof saved.duplicate.mode === 'string') {
            CONFIG.duplicate.mode = saved.duplicate.mode;
          }
          if (typeof saved.duplicate.numbers === 'string') {
            CONFIG.duplicate.numbers = saved.duplicate.numbers;
          }
          if (typeof saved.duplicate.pattern === 'string') {
            CONFIG.duplicate.pattern = saved.duplicate.pattern;
          }
          if (typeof saved.duplicate.flags === 'string') {
            CONFIG.duplicate.flags = saved.duplicate.flags;
          }
        }
        if (saved.cloud && typeof saved.cloud === 'object') {
          if (saved.cloud.maxFilesPerTask != null && !Number.isNaN(Number(saved.cloud.maxFilesPerTask))) {
            CONFIG.cloud.maxFilesPerTask = Math.max(1, Number(saved.cloud.maxFilesPerTask));
          }
          if (typeof saved.cloud.sourceDirPrefix === 'string') {
            CONFIG.cloud.sourceDirPrefix = saved.cloud.sourceDirPrefix;
          }
          if (typeof saved.cloud.createMagnetSubdir === 'boolean') {
            CONFIG.cloud.createMagnetSubdir = saved.cloud.createMagnetSubdir;
          }
          if (saved.cloud.listTaskPageSize != null && !Number.isNaN(Number(saved.cloud.listTaskPageSize))) {
            CONFIG.cloud.listTaskPageSize = Math.max(1, Number(saved.cloud.listTaskPageSize));
          }
        }
        if (saved.move && typeof saved.move === 'object') {
          if (typeof saved.move.targetParentId === 'string') {
            CONFIG.move.targetParentId = saved.move.targetParentId;
          }
          if (saved.move.batchSize != null && !Number.isNaN(Number(saved.move.batchSize))) {
            CONFIG.move.batchSize = Math.max(1, Number(saved.move.batchSize));
          }
        }
        if (saved.mediaOrganize && typeof saved.mediaOrganize === 'object') {
          if (typeof saved.mediaOrganize.tmdbApiKey === 'string') {
            CONFIG.mediaOrganize.tmdbApiKey = saved.mediaOrganize.tmdbApiKey;
          }
          if (typeof saved.mediaOrganize.rootParentId === 'string') {
            CONFIG.mediaOrganize.rootParentId = saved.mediaOrganize.rootParentId;
          }
          if (typeof saved.mediaOrganize.tmdbLanguage === 'string') {
            CONFIG.mediaOrganize.tmdbLanguage = saved.mediaOrganize.tmdbLanguage || 'zh-CN';
          }
          if (typeof saved.mediaOrganize.useFolderNameFirst === 'boolean') {
            CONFIG.mediaOrganize.useFolderNameFirst = saved.mediaOrganize.useFolderNameFirst;
          }
          if (typeof saved.mediaOrganize.includeTitleFolder === 'boolean') {
            CONFIG.mediaOrganize.includeTitleFolder = saved.mediaOrganize.includeTitleFolder;
          }
          if (typeof saved.mediaOrganize.includeRegionFolder === 'boolean') {
            CONFIG.mediaOrganize.includeRegionFolder = saved.mediaOrganize.includeRegionFolder;
          }
          if (typeof saved.mediaOrganize.includeSeasonFolder === 'boolean') {
            CONFIG.mediaOrganize.includeSeasonFolder = saved.mediaOrganize.includeSeasonFolder;
          }
          if (typeof saved.mediaOrganize.moveBySourceFolder === 'boolean') {
            CONFIG.mediaOrganize.moveBySourceFolder = saved.mediaOrganize.moveBySourceFolder;
          }
          if (typeof saved.mediaOrganize.cleanupEmptySourceFolders === 'boolean') {
            CONFIG.mediaOrganize.cleanupEmptySourceFolders = saved.mediaOrganize.cleanupEmptySourceFolders;
          }
          if (typeof saved.mediaOrganize.skipDuplicateTargets === 'boolean') {
            CONFIG.mediaOrganize.skipDuplicateTargets = saved.mediaOrganize.skipDuplicateTargets;
          }
          if (saved.mediaOrganize.batchSize != null && !Number.isNaN(Number(saved.mediaOrganize.batchSize))) {
            CONFIG.mediaOrganize.batchSize = Math.max(1, Number(saved.mediaOrganize.batchSize));
          }
        }
        if (saved.download && typeof saved.download === 'object') {
          if (saved.download.directBatchSize != null && !Number.isNaN(Number(saved.download.directBatchSize))) {
            CONFIG.download.directBatchSize = Math.max(1, Number(saved.download.directBatchSize));
          }
          if (saved.download.exportFormat != null) {
            CONFIG.download.exportFormat = normalizeDirectDownloadExportFormat(saved.download.exportFormat);
          }
        }
      }
    } catch (err) {
      warn('读取已保存配置失败:', err);
    }
  }

  function savePersistedConfig() {
    try {
      const payload = {
        manualHeaders: { ...CONFIG.request.manualHeaders },
        manualListBody: { ...CONFIG.request.manualListBody },
        batch: {
          delayMs: CONFIG.batch.delayMs,
        },
        renameTemplate: CONFIG.rename.template,
        renameRuleMode: CONFIG.rename.ruleMode,
        renameOutput: { ...CONFIG.rename.output },
        firstRule: CONFIG.rename.rules[0]
          ? {
              enabled: CONFIG.rename.rules[0].enabled !== false,
              type: CONFIG.rename.rules[0].type || 'regex',
              pattern: CONFIG.rename.rules[0].pattern || '',
              flags: CONFIG.rename.rules[0].flags || '',
              search: CONFIG.rename.rules[0].search || '',
              replace: CONFIG.rename.rules[0].replace || '',
            }
          : null,
        duplicate: {
          mode: CONFIG.duplicate.mode,
          numbers: CONFIG.duplicate.numbers,
          pattern: CONFIG.duplicate.pattern,
          flags: CONFIG.duplicate.flags,
        },
        cloud: {
          maxFilesPerTask: CONFIG.cloud.maxFilesPerTask,
          sourceDirPrefix: CONFIG.cloud.sourceDirPrefix,
          createMagnetSubdir: CONFIG.cloud.createMagnetSubdir,
          listTaskPageSize: CONFIG.cloud.listTaskPageSize,
        },
        move: {
          targetParentId: CONFIG.move.targetParentId,
          batchSize: CONFIG.move.batchSize,
        },
        mediaOrganize: {
          tmdbApiKey: CONFIG.mediaOrganize.tmdbApiKey,
          tmdbLanguage: CONFIG.mediaOrganize.tmdbLanguage,
          rootParentId: CONFIG.mediaOrganize.rootParentId,
          useFolderNameFirst: CONFIG.mediaOrganize.useFolderNameFirst !== false,
          includeTitleFolder: CONFIG.mediaOrganize.includeTitleFolder !== false,
          includeRegionFolder: CONFIG.mediaOrganize.includeRegionFolder !== false,
          includeSeasonFolder: CONFIG.mediaOrganize.includeSeasonFolder !== false,
          moveBySourceFolder: CONFIG.mediaOrganize.moveBySourceFolder !== false,
          cleanupEmptySourceFolders: CONFIG.mediaOrganize.cleanupEmptySourceFolders !== false,
          skipDuplicateTargets: CONFIG.mediaOrganize.skipDuplicateTargets !== false,
          batchSize: Math.max(1, Number(CONFIG.mediaOrganize.batchSize || 10)),
        },
        download: {
          directBatchSize: CONFIG.download.directBatchSize,
          exportFormat: normalizeDirectDownloadExportFormat(CONFIG.download.exportFormat),
        },
      };
      window.localStorage.setItem(CONFIG_STORAGE_KEY, JSON.stringify(payload));
    } catch (err) {
      warn('保存配置失败:', err);
    }
  }

  function sanitizeHeaders(headersLike) {
    const out = {};
    if (!headersLike) {
      return out;
    }

    if (headersLike instanceof Headers) {
      for (const [key, value] of headersLike.entries()) {
        out[String(key).toLowerCase()] = value;
      }
      return out;
    }

    if (Array.isArray(headersLike)) {
      for (const [key, value] of headersLike) {
        out[String(key).toLowerCase()] = value;
      }
      return out;
    }

    if (typeof headersLike === 'object') {
      for (const [key, value] of Object.entries(headersLike)) {
        out[String(key).toLowerCase()] = value;
      }
    }
    return out;
  }

  function isGuangyaPageHost() {
    return /(^|\.)guangyapan\.com$/i.test(window.location.hostname || '');
  }

  function normalizeGuangyaAuthorization(value) {
    const text = String(value || '').trim();
    if (!text) {
      return '';
    }
    return /^Bearer\s+/i.test(text) ? text : `Bearer ${text}`;
  }

  function getStoredGuangyaAuthorization() {
    try {
      if (typeof GM_getValue === 'function') {
        const value = GM_getValue(GUANGYA_AUTH_STORAGE_KEY, '');
        if (typeof value === 'string' && value.trim()) {
          return normalizeGuangyaAuthorization(value);
        }
      }
    } catch {
      /* ignore */
    }
    try {
      return normalizeGuangyaAuthorization(window.localStorage.getItem(GUANGYA_AUTH_STORAGE_KEY) || '');
    } catch {
      return '';
    }
  }

  function setStoredGuangyaAuthorization(value) {
    const auth = normalizeGuangyaAuthorization(value);
    if (!auth) {
      return;
    }
    try {
      if (typeof GM_setValue === 'function') {
        GM_setValue(GUANGYA_AUTH_STORAGE_KEY, auth);
      }
    } catch {
      /* ignore */
    }
    try {
      window.localStorage.setItem(GUANGYA_AUTH_STORAGE_KEY, auth);
    } catch {
      /* ignore */
    }
  }

  function rememberGuangyaAuthorizationFromHeaders(headersLike) {
    if (!isGuangyaPageHost()) {
      return;
    }
    const headers = sanitizeHeaders(headersLike);
    const auth = normalizeGuangyaAuthorization(headers.authorization);
    if (auth) {
      setStoredGuangyaAuthorization(auth);
    }
  }

  function mergeHeaders(headersLike) {
    const normalized = sanitizeHeaders(headersLike);
    for (const key of KEEP_HEADER_NAMES) {
      if (normalized[key]) {
        STATE.headers[key] = normalized[key];
      }
    }
    rememberGuangyaAuthorizationFromHeaders(normalized);
  }

  function getMergedHeaders() {
    const out = {};
    const manual = CONFIG.request.manualHeaders || {};

    for (const key of KEEP_HEADER_NAMES) {
      // 优先使用最新的 STATE (捕获到的),如果没有,再用 manual (保存的)
      out[key] = STATE.headers[key] || manual[key] || '';
    }
    return out;
  }

  function normalizeParentId(value) {
    return String(value || '').trim();
  }

  function getParentIdFromListBody(body) {
    return normalizeParentId(body && typeof body === 'object' ? body.parentId : '');
  }

  function sanitizeListBody(body = {}) {
    const source = body && typeof body === 'object' ? body : {};
    const out = {};

    for (const [key, value] of Object.entries(source)) {
      if (value === '' || value == null) {
        continue;
      }
      if (TRANSIENT_LIST_BODY_KEYS.has(String(key))) {
        continue;
      }
      out[key] = value;
    }

    return out;
  }

  function getCapturedItemKey(item) {
    if (!item || typeof item !== 'object') {
      return '';
    }

    const fileId = String(item.fileId || '').trim();
    if (fileId) {
      return `id:${fileId}`;
    }

    const name = normalizeDomName(item.name);
    return name ? `name:${name}` : '';
  }

  function createCapturedListBucket(parentId) {
    return {
      parentId,
      items: [],
      indexByKey: {},
      batchCount: 0,
      lastBatchSize: 0,
      listUrl: '',
      lastBody: null,
      updatedAt: '',
    };
  }

  function getCapturedListBucket(parentId, options = {}) {
    const key = normalizeParentId(parentId);
    if (!key) {
      return null;
    }

    if (!STATE.capturedLists[key] && options.create !== false) {
      STATE.capturedLists[key] = createCapturedListBucket(key);
    }
    return STATE.capturedLists[key] || null;
  }

  function getCapturedItemsByParentId(parentId) {
    const bucket = getCapturedListBucket(parentId, { create: false });
    return Array.isArray(bucket?.items) ? bucket.items : [];
  }

  function rebuildCapturedListBucketIndex(bucket) {
    if (!bucket || !Array.isArray(bucket.items)) {
      return;
    }

    const next = {};
    const deduped = [];
    for (const item of bucket.items) {
      const itemKey = getCapturedItemKey(item);
      if (!itemKey || Object.prototype.hasOwnProperty.call(next, itemKey)) {
        continue;
      }
      next[itemKey] = deduped.length;
      deduped.push(item);
    }

    bucket.items = deduped;
    bucket.indexByKey = next;
  }

  function mergeCapturedItems(parentId, items, meta = {}) {
    const normalizedParentId = normalizeParentId(parentId);
    const normalizedItems = dedupeItems(
      (items || []).map((item) => ({
        fileId: String(item?.fileId || ''),
        dirId: String(item?.dirId || item?.fileId || ''),
        dirIdCandidates: normalizeIdCandidates(item?.dirIdCandidates || [item?.dirId, item?.fileId]),
        name: String(item?.name || ''),
        parentId: String(item?.parentId || ''),
        isDir: item?.isDir === true,
        raw: item?.raw,
      }))
    );

    if (!normalizedParentId) {
      STATE.lastListItems = normalizedItems;
      STATE.lastItemsSource = 'api';
      return {
        items: normalizedItems,
        total: normalizedItems.length,
        added: normalizedItems.length,
        updated: 0,
        batchCount: normalizedItems.length ? 1 : 0,
        lastBatchSize: normalizedItems.length,
        parentId: '',
      };
    }

    const bucket = getCapturedListBucket(normalizedParentId);
    let added = 0;
    let updated = 0;

    for (const item of normalizedItems) {
      const itemKey = getCapturedItemKey(item);
      if (!itemKey) {
        continue;
      }

      if (Object.prototype.hasOwnProperty.call(bucket.indexByKey, itemKey)) {
        bucket.items[bucket.indexByKey[itemKey]] = item;
        updated += 1;
      } else {
        bucket.indexByKey[itemKey] = bucket.items.length;
        bucket.items.push(item);
        added += 1;
      }
    }

    if (meta.countAsBatch !== false) {
      bucket.batchCount += 1;
    }
    bucket.lastBatchSize = normalizedItems.length;
    bucket.updatedAt = new Date().toISOString();
    if (meta.listUrl) {
      bucket.listUrl = String(meta.listUrl);
    }
    if (meta.requestBody && typeof meta.requestBody === 'object') {
      bucket.lastBody = { ...meta.requestBody };
    }

    STATE.lastCapturedParentId = normalizedParentId;
    STATE.lastListItems = bucket.items;
    STATE.lastItemsSource = bucket.batchCount > 1 ? 'api-merged' : 'api';

    return {
      items: bucket.items,
      total: bucket.items.length,
      added,
      updated,
      batchCount: bucket.batchCount,
      lastBatchSize: bucket.lastBatchSize,
      parentId: normalizedParentId,
    };
  }

  function removeCapturedItemsByIds(fileIds) {
    const ids = new Set((fileIds || []).map((id) => String(id)).filter(Boolean));
    if (!ids.size) {
      return;
    }

    for (const bucket of Object.values(STATE.capturedLists || {})) {
      if (!bucket || !Array.isArray(bucket.items) || !bucket.items.length) {
        continue;
      }
      bucket.items = bucket.items.filter((item) => !ids.has(String(item.fileId || '')));
      rebuildCapturedListBucketIndex(bucket);
    }

    const currentBucket = getCapturedListBucket(STATE.lastCapturedParentId, { create: false });
    if (currentBucket) {
      STATE.lastListItems = currentBucket.items;
    }
  }

  function renameCapturedItem(fileId, newName) {
    const id = String(fileId || '').trim();
    if (!id) {
      return;
    }

    for (const bucket of Object.values(STATE.capturedLists || {})) {
      if (!bucket || !Array.isArray(bucket.items) || !bucket.items.length) {
        continue;
      }
      const index = bucket.indexByKey[`id:${id}`];
      if (typeof index === 'number' && bucket.items[index]) {
        bucket.items[index] = {
          ...bucket.items[index],
          name: String(newName || ''),
        };
      }
    }
  }

  function getCapturedListStats(parentId = '') {
    const normalizedParentId =
      normalizeParentId(parentId) ||
      getParentIdFromListBody(STATE.lastListBody) ||
      normalizeParentId(CONFIG.request.manualListBody.parentId) ||
      normalizeParentId(STATE.lastCapturedParentId);
    const bucket = getCapturedListBucket(normalizedParentId, { create: false });
    const fallbackItems = Array.isArray(STATE.lastListItems) ? STATE.lastListItems : [];

    return {
      parentId: normalizedParentId,
      total: bucket?.items?.length || fallbackItems.length || 0,
      lastBatchSize: bucket?.lastBatchSize || fallbackItems.length || 0,
      batchCount: bucket?.batchCount || (fallbackItems.length ? 1 : 0),
      listUrl: bucket?.listUrl || STATE.lastListUrl || '',
      updatedAt: bucket?.updatedAt || '',
    };
  }

  function getItemsSourceLabel(source = STATE.lastItemsSource) {
    if (source === 'api-merged') {
      return 'api(累计)';
    }
    if (source === 'dom') {
      return '页面可见项';
    }
    return source || 'none';
  }

  function getCurrentListContext() {
    const stats = getCapturedListStats();
    const bucket = getCapturedListBucket(stats.parentId, { create: false });
    const body = resolveListBody(bucket?.lastBody || STATE.lastListBody || {});
    return {
      parentId: body.parentId || stats.parentId || '',
      pageSize: body.pageSize || '',
      listUrl: stats.listUrl || '',
      capturedCount: stats.total,
      lastBatchSize: stats.lastBatchSize,
      batchCount: stats.batchCount,
    };
  }

  function getRequestHeaders() {
    const extra = getMergedHeaders();
    const captured = getForwardableHeadersFromCaptured(
      pickFirstNonEmptyHeaders(STATE.lastApiHeaders, STATE.lastListHeaders, STATE.lastRenameRequest?.headers)
    );
    return {
      ...captured,
      accept: 'application/json, text/plain, */*',
      'content-type': 'application/json',
      ...extra,
    };
  }

  function pickExistingKey(obj, candidates, fallback) {
    if (!obj || typeof obj !== 'object') {
      return fallback;
    }
    for (const key of candidates) {
      if (Object.prototype.hasOwnProperty.call(obj, key)) {
        return key;
      }
    }
    return fallback;
  }

  function getRenameUrl() {
    return STATE.lastRenameRequest?.url || `${CONFIG.request.apiHost}${CONFIG.request.renamePath}`;
  }

  function getDeleteUrl() {
    return `${CONFIG.request.apiHost}${CONFIG.request.deletePath}`;
  }

  function getMoveUrl() {
    return `${CONFIG.request.apiHost}${CONFIG.request.movePath}`;
  }

  function getDownloadUrl() {
    return `${CONFIG.request.apiHost}${CONFIG.request.downloadPath}`;
  }

  function getCreateDirUrl() {
    return `${CONFIG.request.apiHost}${CONFIG.request.createDirPath}`;
  }

  function getTaskStatusUrl() {
    return `${CONFIG.request.apiHost}${CONFIG.request.taskStatusPath}`;
  }

  function getResolveResUrl() {
    return `${CONFIG.request.apiHost}${CONFIG.request.resolveResPath}`;
  }

  function getCloudCreateTaskUrl() {
    return `${CONFIG.request.apiHost}${CONFIG.request.cloudCreateTaskPath}`;
  }

  function getCloudListTaskUrl() {
    return `${CONFIG.request.apiHost}${CONFIG.request.cloudListTaskPath}`;
  }

  function getRenameHeaders() {
    const forwardable = getForwardableHeadersFromCaptured(
      pickFirstNonEmptyHeaders(STATE.lastRenameRequest?.headers, STATE.lastApiHeaders, STATE.lastListHeaders)
    );
    return {
      ...forwardable,
      ...getRequestHeaders(),
    };
  }

  function getCommonApiRequestOptions(body, headers = getRequestHeaders()) {
    return {
      method: 'POST',
      headers,
      mode: 'cors',
      credentials: 'include',
      body: JSON.stringify(body),
    };
  }

  async function postJson(url, body, headers = getRequestHeaders()) {
    const response = await pageRequest(url, getCommonApiRequestOptions(body, headers));
    const payload = safeJsonParse(response.text || '');
    return {
      ok: response.ok,
      status: response.status,
      text: response.text || '',
      payload,
    };
  }

  function findFirstValueByKeys(node, keys) {
    if (!node || typeof node !== 'object') {
      return null;
    }

    if (Array.isArray(node)) {
      for (const item of node) {
        const found = findFirstValueByKeys(item, keys);
        if (found != null) {
          return found;
        }
      }
      return null;
    }

    for (const key of keys) {
      if (Object.prototype.hasOwnProperty.call(node, key) && node[key] != null) {
        return node[key];
      }
    }

    for (const value of Object.values(node)) {
      const found = findFirstValueByKeys(value, keys);
      if (found != null) {
        return found;
      }
    }

    return null;
  }

  function decodeUrlParam(value) {
    try {
      return decodeURIComponent(String(value || '').replace(/\+/g, '%20'));
    } catch {
      return String(value || '');
    }
  }

  function getMagnetQueryParams(magnetUrl) {
    const text = String(magnetUrl || '').trim();
    const query = text.includes('?') ? text.slice(text.indexOf('?') + 1) : '';
    return new URLSearchParams(query);
  }

  function getMagnetBtih(magnetUrl) {
    const xt = getMagnetQueryParams(magnetUrl).get('xt') || '';
    const matched = xt.match(/btih:([^&]+)/i);
    return matched ? String(matched[1] || '').trim() : '';
  }

  function getMagnetDisplayName(magnetUrl) {
    const params = getMagnetQueryParams(magnetUrl);
    const dn = params.get('dn');
    if (dn) {
      return decodeUrlParam(dn);
    }
    const btih = getMagnetBtih(magnetUrl);
    return btih ? `磁力_${btih.slice(0, 12)}` : '磁力资源';
  }

  function getMagnetIdentityKey(magnetUrl) {
    const btih = String(getMagnetBtih(magnetUrl) || '').trim().toLowerCase();
    if (btih) {
      return `btih:${btih}`;
    }
    const normalizedUrl = String(magnetUrl || '').trim().toLowerCase();
    return normalizedUrl ? `url:${normalizedUrl}` : '';
  }

  function sanitizeCloudDirName(name, fallback = '磁力资源') {
    const text = String(name || '')
      .replace(/[\\/:*?"<>|\u0000-\u001f]+/g, ' ')
      .replace(/\s+/g, ' ')
      .trim();
    const normalized = text || fallback;
    return normalized.length > 96 ? normalized.slice(0, 96).trim() : normalized;
  }

  function buildTimestampToken(date = new Date()) {
    const year = date.getFullYear();
    const month = String(date.getMonth() + 1).padStart(2, '0');
    const day = String(date.getDate()).padStart(2, '0');
    const hour = String(date.getHours()).padStart(2, '0');
    const minute = String(date.getMinutes()).padStart(2, '0');
    const second = String(date.getSeconds()).padStart(2, '0');
    return `${year}${month}${day}-${hour}${minute}${second}`;
  }

  function stripGenericExtension(name) {
    return String(name || '').replace(/\.[^.]+$/, '') || String(name || '');
  }

  function chunkArray(items, chunkSize) {
    const size = Math.max(1, Number(chunkSize || 1));
    const source = Array.isArray(items) ? items : [];
    const chunks = [];
    for (let index = 0; index < source.length; index += size) {
      chunks.push(source.slice(index, index + size));
    }
    return chunks;
  }

  function extractCreatedDirId(payload) {
    const value = findFirstValueByKeys(payload, ['dirId', 'dir_id', 'fileId', 'folderId', 'folder_id', 'id']);
    return value == null ? '' : String(value);
  }

  function isLikelyDirectoryId(value) {
    const text = String(value == null ? '' : value).trim();
    if (!text || text === '0') {
      return false;
    }
    if (/^(?:true|false|ok|success|null|undefined)$/iu.test(text)) {
      return false;
    }
    return /^[A-Za-z0-9_-]{6,}$/u.test(text) || /^\d{6,}$/u.test(text);
  }

  function collectObjectArrays(node, out = [], seen = new WeakSet()) {
    if (!node || typeof node !== 'object') {
      return out;
    }
    if (seen.has(node)) {
      return out;
    }
    seen.add(node);

    if (Array.isArray(node)) {
      if (node.length && node.every((item) => item && typeof item === 'object' && !Array.isArray(item))) {
        out.push(node);
      }
      for (const item of node) {
        collectObjectArrays(item, out, seen);
      }
      return out;
    }

    for (const value of Object.values(node)) {
      collectObjectArrays(value, out, seen);
    }
    return out;
  }

  function normalizeResolvedFileEntry(obj) {
    if (!obj || typeof obj !== 'object' || Array.isArray(obj)) {
      return null;
    }

    const strongIndex = toFiniteNumberOrNull(
      obj.fileIndex ?? obj.file_index ?? obj.fileNo ?? obj.file_no
    );
    const weakIndex = toFiniteNumberOrNull(
      obj.index ?? obj.idx ?? obj.seq
    );
    const index = strongIndex ?? weakIndex;
    if (index == null || index < 0) {
      return null;
    }

    const name = chooseBestNameCandidate([
      obj.name,
      obj.fileName,
      obj.file_name,
      obj.filename,
      obj.path,
      obj.filePath,
      obj.file_path,
      obj.fullPath,
      obj.full_path,
      obj.resName,
      obj.resourceName,
      obj.title,
    ]);

    if (
      strongIndex == null
      && !(
        name
        && !Object.values(obj).some((value) => Array.isArray(value) && value.some((item) => item && typeof item === 'object'))
      )
    ) {
      return null;
    }

    return {
      index,
      name,
      raw: obj,
      fromExplicitIndex: strongIndex != null,
    };
  }

  function scanResolvedFileEntries(node, out = [], seen = new WeakSet()) {
    if (!node || typeof node !== 'object') {
      return out;
    }
    if (seen.has(node)) {
      return out;
    }
    seen.add(node);

    if (Array.isArray(node)) {
      for (const item of node) {
        scanResolvedFileEntries(item, out, seen);
      }
      return out;
    }

    const normalized = normalizeResolvedFileEntry(node);
    if (normalized) {
      out.push(normalized);
    }

    for (const value of Object.values(node)) {
      scanResolvedFileEntries(value, out, seen);
    }
    return out;
  }

  function parseSizeLikeBytes(value) {
    if (value == null || value === '') {
      return 0;
    }

    if (typeof value === 'number') {
      return Number.isFinite(value) && value > 0 ? value : 0;
    }

    const text = String(value || '').trim();
    if (!text) {
      return 0;
    }
    if (/^\d+$/u.test(text)) {
      const numeric = Number(text);
      return Number.isFinite(numeric) && numeric > 0 ? numeric : 0;
    }

    const matched = text.match(/^(\d+(?:\.\d+)?)\s*(B|KB|MB|GB|TB|PB)$/iu);
    if (!matched) {
      return 0;
    }

    const number = Number(matched[1] || 0);
    const unit = String(matched[2] || 'B').toUpperCase();
    const scales = {
      B: 1,
      KB: 1024,
      MB: 1024 ** 2,
      GB: 1024 ** 3,
      TB: 1024 ** 4,
      PB: 1024 ** 5,
    };
    return Number.isFinite(number) && number > 0 ? number * (scales[unit] || 1) : 0;
  }

  function getResolvedEntrySizeBytes(entry) {
    const source = entry?.raw && typeof entry.raw === 'object' ? entry.raw : (entry && typeof entry === 'object' ? entry : {});
    const keys = [
      'size',
      'fileSize',
      'file_size',
      'contentLength',
      'content_length',
      'byteSize',
      'byte_size',
      'bytes',
    ];

    for (const key of keys) {
      const bytes = parseSizeLikeBytes(source[key]);
      if (bytes > 0) {
        return bytes;
      }
    }

    return 0;
  }

  function isLikelyResolvedLabelOnlyName(name = '') {
    const text = normalizeDomName(name).toLowerCase();
    if (!text) {
      return false;
    }

    return /^(视频|图片|其他|全部|全选|文件|文件名称|格式|大小|movie|movies|video|videos|image|images|other|others|all|selected|\d+\s*项)$/iu.test(text);
  }

  function isLikelyResolvedFileName(name = '') {
    const text = normalizeDomName(name);
    if (!text || isLikelyResolvedLabelOnlyName(text)) {
      return false;
    }

    const ext = getResolvedEntryExt({ name: text });
    if (ext && (KNOWN_FILE_EXTENSIONS.has(ext) || CLOUD_VIDEO_EXTENSIONS.has(ext) || CLOUD_JUNK_EXTENSIONS.has(ext) || EMPTY_SCAN_EXTRA_FILE_EXTENSIONS.has(ext))) {
      return true;
    }

    return /[\\/]/.test(text) || /\.[a-z0-9]{1,12}$/i.test(text);
  }

  function scoreResolvedEntry(entry) {
    if (!entry || typeof entry !== 'object') {
      return -1000;
    }

    const name = String(entry.name || '').trim();
    const ext = getResolvedEntryExt(entry);
    const sizeBytes = getResolvedEntrySizeBytes(entry);
    let score = 0;

    if (name) {
      score += Math.min(40, name.length);
    } else {
      score -= 40;
    }

    if (isLikelyResolvedFileName(name)) {
      score += 140;
    }
    if (ext) {
      score += 45;
    }
    if (ext && KNOWN_FILE_EXTENSIONS.has(ext)) {
      score += 80;
    }
    if (ext && EMPTY_SCAN_EXTRA_FILE_EXTENSIONS.has(ext)) {
      score += 80;
    }
    if (ext && CLOUD_VIDEO_EXTENSIONS.has(ext)) {
      score += 140;
    }
    if (sizeBytes > 0) {
      score += 120 + Math.min(140, Math.round((Math.log(sizeBytes + 1) / Math.log(1024)) * 35));
    }
    if (entry.fromExplicitIndex) {
      score += 25;
    }
    if (hasPositiveSizeLikeField(entry.raw)) {
      score += 30;
    }
    if (isLikelyJunkResolvedEntry(entry)) {
      score -= 12;
    }
    if (isLikelyResolvedLabelOnlyName(name)) {
      score -= 320;
    }

    return score;
  }

  function dedupeResolvedEntriesByIndex(entries) {
    const byIndex = new Map();
    for (const entry of Array.isArray(entries) ? entries : []) {
      if (!entry || !Number.isFinite(Number(entry.index))) {
        continue;
      }
      const index = Number(entry.index);
      const previous = byIndex.get(index);
      if (!previous || scoreResolvedEntry(entry) > scoreResolvedEntry(previous)) {
        byIndex.set(index, {
          ...entry,
          index,
        });
      }
    }
    return Array.from(byIndex.values()).sort((a, b) => a.index - b.index);
  }

  function buildPositionalResolvedEntries(arr) {
    return dedupeResolvedEntriesByIndex(
      (Array.isArray(arr) ? arr : [])
        .map((item, index) => {
          const name = chooseBestNameCandidate([
            item?.name,
            item?.fileName,
            item?.file_name,
            item?.filename,
            item?.path,
            item?.filePath,
            item?.file_path,
            item?.fullPath,
            item?.full_path,
            item?.resName,
            item?.resourceName,
            item?.title,
          ]);
          if (!name) {
            return null;
          }
          return {
            index,
            name,
            raw: item,
            fromExplicitIndex: false,
          };
        })
        .filter(Boolean)
    );
  }

  function isLikelyResolvedFileArrayKey(key = '') {
    const text = String(key || '').trim().toLowerCase();
    if (!text) {
      return false;
    }

    return /^(data|list|items|rows|records|files|filelist|file_list|fileinfos|file_infos|fileinfo|children|result|results|reslist|resource_list|resourcelist)$/i.test(text)
      || /(file|files|item|items|row|rows|record|records|result|results|child|children|resource)s?$/i.test(text);
  }

  function scoreResolvedFileCandidate(entries, meta = {}) {
    const normalized = dedupeResolvedEntriesByIndex(entries);
    if (!normalized.length) {
      return Number.NEGATIVE_INFINITY;
    }

    const namedCount = normalized.filter((entry) => String(entry.name || '').trim()).length;
    const fileLikeCount = normalized.filter((entry) => isLikelyResolvedFileName(entry.name || '')).length;
    const sizeCount = normalized.filter((entry) => getResolvedEntrySizeBytes(entry) > 0).length;
    const videoCount = normalized.filter((entry) => isLikelyVideoResolvedEntry(entry)).length;
    const labelOnlyCount = normalized.filter((entry) => isLikelyResolvedLabelOnlyName(entry.name || '')).length;
    const explicitCount = normalized.filter((entry) => entry.fromExplicitIndex).length;
    const lastKey = String((meta.pathKeys || [])[Math.max(0, (meta.pathKeys || []).length - 1)] || '').trim();
    const likelyKey = isLikelyResolvedFileArrayKey(lastKey);
    const pathBonus = Array.isArray(meta.pathKeys) && meta.pathKeys.some((key) => isLikelyResolvedFileArrayKey(key)) ? 120 : 0;
    const dataBonus = Array.isArray(meta.pathKeys) && meta.pathKeys[0] === 'data' ? 30 : 0;
    const entryScore = normalized.reduce((sum, entry) => sum + Math.max(-200, scoreResolvedEntry(entry)), 0);

    let score = entryScore;
    score += normalized.length * 22;
    score += namedCount * 15;
    score += fileLikeCount * 80;
    score += sizeCount * 110;
    score += videoCount * 100;
    score += explicitCount * 18;
    score += pathBonus + dataBonus + (likelyKey ? 160 : 0);

    if (labelOnlyCount === normalized.length) {
      score -= 900;
    } else {
      score -= labelOnlyCount * 180;
    }

    return score;
  }

  function collectResolvedFileEntryCandidates(node, out = [], options = {}) {
    const seen = options.seen || new WeakSet();
    const pathKeys = Array.isArray(options.pathKeys) ? options.pathKeys : [];
    const depth = Number(options.depth || 0);

    if (!node || typeof node !== 'object' || depth > 6) {
      return out;
    }
    if (seen.has(node)) {
      return out;
    }
    seen.add(node);

    if (Array.isArray(node)) {
      if (node.length && node.every((item) => item && typeof item === 'object' && !Array.isArray(item))) {
        const explicitEntries = dedupeResolvedEntriesByIndex(node.map((item) => normalizeResolvedFileEntry(item)).filter(Boolean));
        if (explicitEntries.length) {
          out.push({
            entries: explicitEntries,
            source: 'explicit-array',
            path: pathKeys.join('.'),
            score: scoreResolvedFileCandidate(explicitEntries, { pathKeys }),
          });
        }

        const positionalEntries = buildPositionalResolvedEntries(node);
        if (positionalEntries.length) {
          out.push({
            entries: positionalEntries,
            source: 'positional-array',
            path: pathKeys.join('.'),
            score: scoreResolvedFileCandidate(positionalEntries, { pathKeys }),
          });
        }
      }

      for (const item of node) {
        if (item && typeof item === 'object') {
          collectResolvedFileEntryCandidates(item, out, {
            seen,
            pathKeys,
            depth: depth + 1,
          });
        }
      }
      return out;
    }

    for (const [key, value] of Object.entries(node)) {
      if (!value || typeof value !== 'object') {
        continue;
      }
      collectResolvedFileEntryCandidates(value, out, {
        seen,
        pathKeys: [...pathKeys, key],
        depth: depth + 1,
      });
    }
    return out;
  }

  function pickBestResolvedFileCandidate(payload) {
    const candidates = collectResolvedFileEntryCandidates(payload);
    const explicitEntries = dedupeResolvedEntriesByIndex(scanResolvedFileEntries(payload));
    if (explicitEntries.length) {
      candidates.push({
        entries: explicitEntries,
        source: 'explicit-scan',
        path: '(scan)',
        score: scoreResolvedFileCandidate(explicitEntries, { pathKeys: [] }),
      });
    }

    const explicitIndexes = findFirstValueByKeys(payload, ['fileIndexes', 'file_indexes', 'indexes']);
    if (Array.isArray(explicitIndexes) && explicitIndexes.length) {
      const entries = dedupeResolvedEntriesByIndex(
        explicitIndexes
          .map((value) => toFiniteNumberOrNull(value))
          .filter((value) => value != null)
          .map((index) => ({
            index,
            name: '',
            raw: null,
            fromExplicitIndex: true,
          }))
      );
      if (entries.length) {
        candidates.push({
          entries,
          source: 'explicit-indexes',
          path: 'fileIndexes',
          score: scoreResolvedFileCandidate(entries, { pathKeys: ['fileIndexes'] }),
        });
      }
    }

    const total = extractResolvedFileCount(payload, 0);
    if (total > 0) {
      const entries = Array.from({ length: total }, (_, index) => ({
        index,
        name: '',
        raw: null,
        fromExplicitIndex: false,
      }));
      candidates.push({
        entries,
        source: 'count-fallback',
        path: 'total',
        score: scoreResolvedFileCandidate(entries, { pathKeys: ['total'] }),
      });
    }

    if (!candidates.length) {
      return null;
    }

    candidates.sort((left, right) => {
      if (right.score !== left.score) {
        return right.score - left.score;
      }
      if (right.entries.length !== left.entries.length) {
        return right.entries.length - left.entries.length;
      }
      return String(left.path || '').length - String(right.path || '').length;
    });

    return candidates[0] || null;
  }

  function extractResolvedFileEntries(payload) {
    return pickBestResolvedFileCandidate(payload)?.entries || [];
  }

  function extractResolvedFileCount(payload, fallback = 0) {
    const explicit = toFiniteNumberOrNull(
      findFirstValueByKeys(payload, ['fileCount', 'file_count', 'totalCount', 'total_count', 'count', 'total'])
    );
    if (explicit != null && explicit > 0) {
      return explicit;
    }
    return Math.max(0, Number(fallback || 0));
  }

  function getResolvedEntryExt(entry) {
    return String(getExt(entry?.name || '') || '').replace(/^\./, '').toLowerCase();
  }

  function isLikelyJunkResolvedEntry(entry) {
    const ext = getResolvedEntryExt(entry);
    const name = String(entry?.name || '');
    if (ext && CLOUD_JUNK_EXTENSIONS.has(ext)) {
      return true;
    }
    return CLOUD_SKIP_NAME_PATTERNS.some((pattern) => pattern.test(name));
  }

  function isLikelyVideoResolvedEntry(entry) {
    const ext = getResolvedEntryExt(entry);
    return Boolean(ext && CLOUD_VIDEO_EXTENSIONS.has(ext));
  }

  function selectResolvedEntriesForImport(entries) {
    const source = Array.isArray(entries) ? entries.filter(Boolean) : [];
    if (!source.length) {
      return [];
    }

    const videoEntries = source.filter((entry) => isLikelyVideoResolvedEntry(entry));
    if (videoEntries.length) {
      const nonSampleVideoEntries = videoEntries.filter((entry) => !isLikelyJunkResolvedEntry(entry));
      return nonSampleVideoEntries.length ? nonSampleVideoEntries : videoEntries;
    }

    const nonJunkEntries = source.filter((entry) => !isLikelyJunkResolvedEntry(entry));
    if (nonJunkEntries.length) {
      return nonJunkEntries;
    }

    return source;
  }

  function extractResolvedResourceName(payload, magnetUrl, fallback = '') {
    return sanitizeCloudDirName(
      chooseBestNameCandidate([
        getMagnetDisplayName(magnetUrl),
        findFirstValueByKeys(payload, ['resourceName', 'resName', 'taskName', 'title', 'name', 'displayName']),
        fallback,
      ]) || '磁力资源',
      '磁力资源'
    );
  }

  function looksLikeNameExistError(detail) {
    const text = getErrorText(detail).toLowerCase();
    return ['exist', 'exists', 'already', '重复', '已存在', '同名'].some((keyword) => text.includes(keyword));
  }

  async function createDirectory(dirName, parentId = '', options = {}) {
    const response = await postJson(
      getCreateDirUrl(),
      {
        dirName,
        parentId: String(parentId || ''),
        failIfNameExist: options.failIfNameExist !== false,
      },
      getRequestHeaders()
    );

    if (!response.ok || !isProbablySuccess(response.payload, response)) {
      throw new Error(getErrorText(response.payload || response.text || `HTTP ${response.status}`));
    }

    const dirId = extractCreatedDirId(response.payload);
    if (!dirId) {
      throw new Error(`创建目录成功但未返回目录 ID:${dirName}`);
    }

    return {
      dirId,
      dirName,
      response,
    };
  }

  async function createDirectoryWithFallback(baseName, parentId = '', options = {}) {
    const normalized = sanitizeCloudDirName(baseName, options.fallbackName || '磁力资源');
    const token = buildTimestampToken();
    const candidates = [
      normalized,
      `${normalized}-${token}`,
      `${normalized}-${token}-${Math.random().toString(36).slice(2, 6)}`,
    ].filter((value, index, list) => list.indexOf(value) === index);

    let lastError = null;
    for (const name of candidates) {
      try {
        return await createDirectory(name, parentId, {
          failIfNameExist: true,
        });
      } catch (err) {
        lastError = err;
        if (!looksLikeNameExistError(err) && name === normalized) {
          throw err;
        }
      }
    }

    throw lastError || new Error(`创建目录失败:${normalized}`);
  }

  async function resolveCloudResource(url) {
    return postJson(getResolveResUrl(), { url: String(url || '').trim() }, getRequestHeaders());
  }

  async function createCloudTask(fileIndexes, url, parentId) {
    return postJson(
      getCloudCreateTaskUrl(),
      {
        fileIndexes,
        url: String(url || '').trim(),
        parentId: String(parentId || ''),
      },
      getRequestHeaders()
    );
  }

  function normalizeCloudTaskRow(obj) {
    if (!obj || typeof obj !== 'object' || Array.isArray(obj)) {
      return null;
    }
    const taskId = findFirstValueByKeys(obj, ['taskId', 'task_id', 'id']);
    const status = findFirstValueByKeys(obj, ['taskStatus', 'task_status', 'status', 'state']);
    const url = findFirstValueByKeys(obj, ['url', 'sourceUrl', 'source_url']);
    const name = chooseBestNameCandidate([
      obj.name,
      obj.taskName,
      obj.resourceName,
      obj.resName,
      obj.displayName,
      getMagnetDisplayName(url || ''),
    ]);

    if (taskId == null || (!status && !url && !name)) {
      return null;
    }

    return {
      taskId: String(taskId),
      status: status == null ? '' : String(status),
      url: url == null ? '' : String(url),
      name,
      raw: obj,
    };
  }

  function scanCloudTaskRows(node, out = []) {
    if (!node || typeof node !== 'object') {
      return out;
    }
    if (Array.isArray(node)) {
      for (const item of node) {
        scanCloudTaskRows(item, out);
      }
      return out;
    }

    const normalized = normalizeCloudTaskRow(node);
    if (normalized) {
      out.push(normalized);
    }

    for (const value of Object.values(node)) {
      scanCloudTaskRows(value, out);
    }
    return out;
  }

  function extractCloudTaskRows(payload) {
    const rows = scanCloudTaskRows(payload);
    const seen = new Set();
    return rows.filter((row) => {
      if (!row.taskId || seen.has(row.taskId)) {
        return false;
      }
      seen.add(row.taskId);
      return true;
    });
  }

  async function listCloudTasks(options = {}) {
    const statuses = Array.isArray(options.statuses) && options.statuses.length ? options.statuses : [0, 1, 2, 3, 4];
    const pageSize = Math.max(1, Number(options.pageSize || CONFIG.cloud.listTaskPageSize || 50));
    const response = await postJson(
      getCloudListTaskUrl(),
      {
        pageSize,
        status: statuses,
      },
      getRequestHeaders()
    );
    STATE.lastCloudTaskList = response.payload || response.text || null;
    return response;
  }

  function extractMagnetLinks(text) {
    const matches = String(text || '').match(/magnet:\?[^\s"'<>]+/ig) || [];
    const seen = new Set();
    const out = [];
    for (const match of matches) {
      const magnet = String(match || '').trim().replace(/[),.;]+$/g, '');
      if (!magnet || seen.has(magnet)) {
        continue;
      }
      seen.add(magnet);
      out.push(magnet);
    }
    return out;
  }

  function extractMagnetLinksFromJsonNode(node, out = [], seen = new Set()) {
    if (node == null) {
      return out;
    }

    if (typeof node === 'string') {
      for (const magnet of extractMagnetLinks(node)) {
        if (!seen.has(magnet)) {
          seen.add(magnet);
          out.push(magnet);
        }
      }
      return out;
    }

    if (Array.isArray(node)) {
      for (const item of node) {
        extractMagnetLinksFromJsonNode(item, out, seen);
      }
      return out;
    }

    if (typeof node === 'object') {
      for (const value of Object.values(node)) {
        extractMagnetLinksFromJsonNode(value, out, seen);
      }
    }

    return out;
  }

  function extractMagnetLinksFromAnyContent(text, fileName = '') {
    const raw = String(text || '');
    const ext = String(fileName || '').toLowerCase();
    const magnetsFromText = extractMagnetLinks(raw);

    if (!/\.json$/i.test(ext)) {
      return magnetsFromText;
    }

    const json = safeJsonParse(raw);
    if (json == null) {
      return magnetsFromText;
    }

    const magnetsFromJson = extractMagnetLinksFromJsonNode(json);
    return magnetsFromJson.length ? magnetsFromJson : magnetsFromText;
  }

  async function readMagnetImportFiles(fileList, options = {}) {
    const files = Array.from(fileList || []).filter(Boolean);
    const onProgress = typeof options.onProgress === 'function' ? options.onProgress : null;
    const parsed = [];

    for (let index = 0; index < files.length; index += 1) {
      const file = files[index];
      const text = await file.text();
      const magnets = extractMagnetLinksFromAnyContent(text, file.name);
      parsed.push({
        key: [file.name, file.size, file.lastModified].join(':'),
        name: file.name,
        size: file.size,
        lastModified: file.lastModified,
        magnets,
        magnetCount: magnets.length,
        sampleMagnet: magnets[0] || '',
      });

      if (onProgress) {
        onProgress({
          visible: true,
          percent: Math.round(((index + 1) / Math.max(1, files.length)) * 100),
          indeterminate: false,
          text: `正在识别本地磁力文件 ${index + 1}/${files.length}:${file.name} | 磁力 ${magnets.length} 条`,
        });
      }
    }

    return parsed;
  }

  function setMagnetImportFiles(entries, options = {}) {
    const append = Boolean(options.append);
    const next = append ? [...(STATE.magnetImportFiles || [])] : [];
    const indexByKey = new Map(next.map((item, index) => [item.key, index]));

    for (const entry of entries || []) {
      if (!entry || !entry.key) {
        continue;
      }
      if (indexByKey.has(entry.key)) {
        next[indexByKey.get(entry.key)] = entry;
      } else {
        indexByKey.set(entry.key, next.length);
        next.push(entry);
      }
    }

    STATE.magnetImportFiles = next;
    renderMagnetImportList();
    if (UI.magnetDetails && next.length) {
      UI.magnetDetails.open = true;
    }
  }

  function getSelectedMagnetImportStats() {
    const files = Array.isArray(STATE.magnetImportFiles) ? STATE.magnetImportFiles : [];
    const magnets = files.reduce((sum, item) => sum + Number(item.magnetCount || item.magnets?.length || 0), 0);
    return {
      fileCount: files.length,
      magnetCount: magnets,
    };
  }

  function renderMagnetImportList() {
    if (!UI.magnetFileList || !UI.magnetFileCount) {
      return;
    }

    const files = Array.isArray(STATE.magnetImportFiles) ? STATE.magnetImportFiles : [];
    const stats = getSelectedMagnetImportStats();
    UI.magnetFileCount.textContent = `磁力文本 ${stats.fileCount} 个 / 磁力 ${stats.magnetCount} 条`;

    if (!files.length) {
      UI.magnetFileList.innerHTML = '<div class="gyp-import-empty">选择包含 magnet 链接的 txt 或 json 文件后,脚本会自动识别并按每批 500 文件拆分云添加。</div>';
      return;
    }

    UI.magnetFileList.innerHTML = files.map((item) => `
      <div class="gyp-import-row">
        <div class="gyp-import-name" title="${escapeHtml(item.name)}">${escapeHtml(item.name)}</div>
        <div class="gyp-import-meta">磁力 ${Number(item.magnetCount || 0)} 条${item.sampleMagnet ? ` | 示例:${escapeHtml(shortDisplayName(getMagnetDisplayName(item.sampleMagnet), 32))}` : ''}</div>
      </div>
    `).join('');
  }

  function renderEmptyDirScanList() {
    if (!UI.emptyDirList || !UI.emptyDirCount) {
      return;
    }

    const scan = STATE.lastEmptyDirScan || null;
    const items = Array.isArray(scan?.emptyDirs) ? scan.emptyDirs : [];
    const selected = items.filter((item) => STATE.emptyDirSelection[String(item.fileId || '')] !== false);
    UI.emptyDirCount.textContent = scan
      ? `删除勾选 ${selected.length}/${items.length} | 已扫目录 ${Number(scan.scannedDirs || 0)} 个${scan.truncated ? ' / 可能未扫全' : ''}`
      : '空目录 0 个';

    if (!scan) {
      UI.emptyDirList.innerHTML = '<div class="gyp-import-empty">点“扫描空目录”后,这里会列出当前目录树里最里层且完全空的目录。</div>';
      return;
    }

    if (!items.length) {
      UI.emptyDirList.innerHTML = `
        <div class="gyp-import-empty">
          ${scan.truncated
            ? `本次已扫描 ${Number(scan.scannedDirs || 0)} 个目录,暂未发现空目录;因为分页或目录数量较多,结果可能还不完整。`
            : `本次已扫描 ${Number(scan.scannedDirs || 0)} 个目录,当前目录树下没有发现最里层空目录。`}
        </div>
      `;
      return;
    }

    UI.emptyDirList.innerHTML = items.map((item) => {
      const metaParts = [];
      if (Number(item.depth || 0) > 0) {
        metaParts.push(`层级 ${Number(item.depth)}`);
      }
      metaParts.push(item.confidence === 'likely' ? '低置信度,建议确认后再删' : '高置信度');
      metaParts.push(`目录 ID: ${escapeHtml(String(item.fileId || ''))}`);
      return `
      <label class="gyp-empty-dir-row" data-confidence="${item.confidence === 'likely' ? 'likely' : 'confirmed'}">
        <input
          type="checkbox"
          data-action="toggle-empty-dir"
          data-file-id="${escapeHtml(String(item.fileId || ''))}"
          ${STATE.emptyDirSelection[String(item.fileId || '')] !== false ? 'checked' : ''}
        />
        <span class="gyp-empty-dir-main">
          <span class="gyp-empty-dir-path" title="${escapeHtml(item.path || item.name || '(当前目录)')}">${escapeHtml(item.path || item.name || '(当前目录)')}</span>
          <span class="gyp-empty-dir-meta">${metaParts.join(' | ')}</span>
        </span>
      </label>
    `;
    }).join('');
  }

  function setEmptyDirScanResult(summary, options = {}) {
    if (!summary || !Array.isArray(summary.emptyDirs)) {
      STATE.lastEmptyDirScan = summary || null;
      STATE.emptyDirSelection = {};
      renderEmptyDirScanList();
      return;
    }

    const preserveSelection = Boolean(options.preserveSelection);
    const nextSelection = {};
    const emptyDirs = summary.emptyDirs
      .filter((item) => item && item.fileId)
      .map((item) => ({
        ...item,
        fileId: String(item.fileId),
        dirId: String(item.dirId || item.fileId),
        dirIdCandidates: normalizeIdCandidates(item.dirIdCandidates || [item.dirId, item.fileId]),
        confidence: item.confidence === 'likely' ? 'likely' : 'confirmed',
      }));

    for (const item of emptyDirs) {
      if (preserveSelection && Object.prototype.hasOwnProperty.call(STATE.emptyDirSelection, item.fileId)) {
        nextSelection[item.fileId] = STATE.emptyDirSelection[item.fileId] !== false;
      } else {
        nextSelection[item.fileId] = item.confidence !== 'likely';
      }
    }

    STATE.lastEmptyDirScan = {
      ...summary,
      emptyDirs,
    };
    STATE.emptyDirSelection = nextSelection;
    renderEmptyDirScanList();
  }

  function getSelectedEmptyDirItems() {
    const items = Array.isArray(STATE.lastEmptyDirScan?.emptyDirs) ? STATE.lastEmptyDirScan.emptyDirs : [];
    return items.filter((item) => STATE.emptyDirSelection[String(item.fileId || '')] !== false);
  }

  function removeEmptyDirScanItemsByIds(fileIds) {
    const deletedIds = new Set((fileIds || []).map((id) => String(id)).filter(Boolean));
    if (!deletedIds.size || !STATE.lastEmptyDirScan) {
      return;
    }

    removeCapturedItemsByIds(Array.from(deletedIds));
    setEmptyDirScanResult({
      ...STATE.lastEmptyDirScan,
      emptyDirs: (STATE.lastEmptyDirScan.emptyDirs || []).filter((item) => !deletedIds.has(String(item.fileId || ''))),
    }, {
      preserveSelection: true,
    });
  }

  async function fetchDirectoryItemsByParentId(parentId, options = {}) {
    const normalizedParentId = String(parentId || '').trim();
    if (!normalizedParentId) {
      return {
        items: [],
        pageCount: 0,
        truncated: false,
      };
    }

    const pageSize = Math.max(1, Number(options.pageSize || UI.fields.pageSize?.value || CONFIG.request.manualListBody.pageSize || 100));
    const maxPages = Math.max(1, Number(options.maxPages || EMPTY_DIR_SCAN_MAX_PAGES_PER_DIR));
    const delayMs = Math.max(0, Number(options.delayMs != null ? options.delayMs : CONFIG.batch.delayMs || 0));
    const taskControl = options.taskControl || null;
    const seenIds = new Set();
    const allItems = [];
    let truncated = false;
    let pageCount = 0;
    let hitPageLimit = true;

    for (let pageIndex = 0; pageIndex < maxPages; pageIndex += 1) {
      await waitForTaskControl(taskControl);
      const requestBody = {
        parentId: normalizedParentId,
        pageSize,
      };
      if (pageIndex > 0) {
        requestBody.page = pageIndex + 1;
      }

      const { items } = await requestListBatch(requestBody);
      pageCount = pageIndex + 1;

      const batchItems = dedupeItems(items);
      if (!batchItems.length) {
        hitPageLimit = false;
        break;
      }

      let newCount = 0;
      for (const item of batchItems) {
        const key = String(item?.fileId || '');
        if (!key || seenIds.has(key)) {
          continue;
        }
        seenIds.add(key);
        allItems.push(item);
        newCount += 1;
      }

      if (batchItems.length < pageSize) {
        hitPageLimit = false;
        break;
      }

      if (!newCount) {
        truncated = true;
        break;
      }

      if (delayMs > 0) {
        await controlledDelay(delayMs, taskControl);
      }
    }

    if (hitPageLimit && pageCount >= maxPages) {
      truncated = true;
    }

    const cachedItems = dedupeItems(getCapturedItemsByParentId(normalizedParentId));
    const mergedItems = cachedItems.length > allItems.length
      ? dedupeItems([...allItems, ...cachedItems])
      : allItems;

    return {
      items: mergedItems,
      pageCount,
      truncated,
      cachedCount: cachedItems.length,
      fetchedCount: allItems.length,
    };
  }

  async function fetchDirectoryItems(parentId, options = {}) {
    const candidates = normalizeIdCandidates([
      ...(Array.isArray(options.idCandidates) ? options.idCandidates : []),
      parentId,
    ]);
    const forwardedOptions = { ...options };
    delete forwardedOptions.idCandidates;

    let lastError = null;
    let hadError = false;
    let bestResult = {
      items: [],
      pageCount: 0,
      truncated: false,
      usedParentId: '',
      uncertain: false,
    };

    for (const candidate of candidates) {
      try {
        const result = await fetchDirectoryItemsByParentId(candidate, forwardedOptions);
        if (result.items.length) {
          return {
            ...result,
            usedParentId: candidate,
          };
        }
        if (!bestResult.items.length) {
          bestResult = {
            ...result,
            usedParentId: candidate,
            uncertain: false,
          };
        }
      } catch (err) {
        hadError = true;
        lastError = err;
      }
    }

    if (lastError && !bestResult.usedParentId) {
      throw lastError;
    }

    return {
      ...bestResult,
      uncertain: hadError && !bestResult.items.length,
    };
  }


  function getEmptyScanItemTypeHints(item) {
    const raw = item && item.raw && typeof item.raw === 'object' ? item.raw : {};
    const values = [
      raw.itemType,
      raw.item_type,
      raw.nodeType,
      raw.node_type,
      raw.resourceType,
      raw.resource_type,
      raw.resType,
      raw.res_type,
      raw.fileType,
      raw.file_type,
      raw.type,
      raw.kind,
      raw.bizType,
      raw.biz_type,
    ];

    return values
      .map((value) => String(value == null ? '' : value).trim().toLowerCase())
      .filter((value) => value !== '');
  }

  function getEmptyScanNameExtension(name = '') {
    const normalized = String(getExt(name) || '').replace(/^\./, '').toLowerCase();
    if (normalized) {
      return normalized;
    }

    const fallback = String(name || '').trim().match(/\.([a-z0-9]{1,8})$/i);
    return fallback ? String(fallback[1] || '').toLowerCase() : '';
  }

  function hasPositiveSizeLikeField(raw) {
    const source = raw && typeof raw === 'object' ? raw : {};
    const keys = [
      'size',
      'fileSize',
      'file_size',
      'contentLength',
      'content_length',
      'byteSize',
      'byte_size',
      'bytes',
    ];

    return keys.some((key) => {
      const value = source[key];
      if (typeof value === 'number') {
        return Number.isFinite(value) && value > 0;
      }
      if (typeof value === 'string') {
        const text = value.trim();
        if (!text) {
          return false;
        }
        if (/^\d+$/u.test(text)) {
          return Number(text) > 0;
        }
        return /^\d+(?:\.\d+)?\s*(?:b|kb|mb|gb|tb)$/iu.test(text);
      }
      return false;
    });
  }

  function hasMeaningfulDirectoryValue(value) {
    if (value == null) {
      return false;
    }
    if (typeof value === 'string') {
      return value.trim() !== '';
    }
    return true;
  }

  function hasDirectoryCountHint(value) {
    const numeric = toFiniteNumberOrNull(value);
    if (numeric != null) {
      return numeric >= 0;
    }
    return hasMeaningfulDirectoryValue(value);
  }

  function isStrongFileLikeItem(item) {
    if (!item) {
      return false;
    }

    const raw = item && item.raw && typeof item.raw === 'object' ? item.raw : {};
    const explicitFlags = [
      raw.isDir,
      raw.is_dir,
      raw.isFolder,
      raw.is_folder,
      raw.folder,
      raw.directory,
      raw.dir,
    ].map((value) => normalizeBooleanish(value)).find((value) => value != null);
    if (explicitFlags === true) {
      return false;
    }
    if (explicitFlags === false) {
      return true;
    }

    const ext = getEmptyScanNameExtension(item.name || '');
    if (ext && (
      KNOWN_FILE_EXTENSIONS.has(ext)
      || CLOUD_VIDEO_EXTENSIONS.has(ext)
      || CLOUD_JUNK_EXTENSIONS.has(ext)
      || EMPTY_SCAN_EXTRA_FILE_EXTENSIONS.has(ext)
    )) {
      return true;
    }

    const typeHints = getEmptyScanItemTypeHints(item);
    if (typeHints.some((value) => /(dir|folder|directory|catalog)/i.test(value))) {
      return false;
    }
    if (typeHints.some((value) => /(file|video|image|audio|doc|text|subtitle|torrent)/i.test(value))) {
      return true;
    }
    // 光鸭返回的 type / fileType 有时目录和文件都会是数字,不能只凭数字就判死为文件。

    if (hasPositiveSizeLikeField(raw)) {
      return true;
    }

    return false;
  }

  function getEmptyScanVisibleDirNameSet(rows = []) {
    return new Set(
      (rows || [])
        .filter((item) => item && item.isDir)
        .map((item) => normalizeDomName(item.name))
        .filter(Boolean)
    );
  }

  function getEmptyScanDirectoryHintLevel(item, visibleDirNameSet = null) {
    if (!item) {
      return 0;
    }

    const id = String(item.fileId || item.dirId || '').trim();
    if (!id || isSyntheticDomId(id)) {
      return 0;
    }

    const raw = item && item.raw && typeof item.raw === 'object' ? item.raw : {};
    const explicitFlags = [
      raw.isDir,
      raw.is_dir,
      raw.isFolder,
      raw.is_folder,
      raw.folder,
      raw.directory,
      raw.dir,
    ].map((value) => normalizeBooleanish(value)).find((value) => value != null);
    if (explicitFlags === true) {
      return 2;
    }
    if (explicitFlags === false) {
      return -1;
    }

    if (isStrongFileLikeItem(item)) {
      return -1;
    }

    if (item.isDir === true || raw.domIsDir) {
      return 2;
    }

    const nameKey = normalizeDomName(item.name || '');
    if (visibleDirNameSet instanceof Set && nameKey && visibleDirNameSet.has(nameKey)) {
      return 2;
    }

    if (shouldTreatItemAsDirectory(item)) {
      return 2;
    }

    const hasDirStructure = Boolean(
      hasMeaningfulDirectoryValue(raw.dirName)
      || hasMeaningfulDirectoryValue(raw.dir_name)
      || hasMeaningfulDirectoryValue(raw.folderName)
      || hasMeaningfulDirectoryValue(raw.folder_name)
      || hasMeaningfulDirectoryValue(raw.folderId)
      || hasMeaningfulDirectoryValue(raw.folder_id)
      || hasDirectoryCountHint(raw.childCount)
      || hasDirectoryCountHint(raw.childrenCount)
      || hasDirectoryCountHint(raw.children_count)
      || hasDirectoryCountHint(raw.dirCount)
      || hasDirectoryCountHint(raw.dir_count)
      || hasDirectoryCountHint(raw.folderCount)
      || hasDirectoryCountHint(raw.folder_count)
      || hasDirectoryCountHint(raw.subCount)
      || hasDirectoryCountHint(raw.sub_count)
      || hasMeaningfulDirectoryValue(raw.dirId)
      || hasMeaningfulDirectoryValue(raw.dir_id)
    );
    if (hasDirStructure) {
      return 2;
    }

    const hasTypeHints = getEmptyScanItemTypeHints(item).length > 0;
    const hasFileExtension = Boolean(getEmptyScanNameExtension(item.name || ''));
    if (!hasTypeHints && !hasFileExtension && !hasPositiveSizeLikeField(raw)) {
      return 1;
    }

    return 0;
  }

  async function fetchDirectoryListingForEmptyScan(parentId, options = {}) {
    const result = await fetchDirectoryItems(parentId, options);
    let items = Array.isArray(result.items) ? result.items : [];

    if (options.includeCurrentSnapshot) {
      const snapshotItems = buildCurrentDirectoryItemsSnapshot(parentId);
      items = dedupeItems([...(Array.isArray(items) ? items : []), ...snapshotItems]);
    }

    return {
      ...result,
      items,
    };
  }


  async function scanEmptyLeafDirectories(options = {}) {
    const rootSnapshot = getDirectoryContextSnapshot();
    const rootParentId = String(options.parentId || rootSnapshot.parentId || CONFIG.request.manualListBody.parentId || '').trim();
    if (!rootParentId) {
      throw new Error('没有拿到 parentId。请先打开要扫描的目录,或在高级兜底里手填 parentId。');
    }

    const onProgress = typeof options.onProgress === 'function' ? options.onProgress : null;
    const taskControl = options.taskControl || null;
    const maxDirs = Math.max(1, Number(options.maxDirs || EMPTY_DIR_SCAN_MAX_DIRS));
    const rootName = String(options.rootName || rootSnapshot.name || '(当前目录)').trim() || '(当前目录)';
    const rootNode = {
      fileId: rootParentId,
      dirId: rootParentId,
      dirIdCandidates: [rootParentId],
      name: rootName,
      path: rootName,
      depth: 0,
      isRoot: true,
      confidence: 'confirmed',
    };
    const visited = new Set([rootParentId]);
    const emptyDirs = [];
    const rootVisibleDirNameSet = getEmptyScanVisibleDirNameSet(collectVisibleDirectoryRows());
    let scannedDirs = 0;
    let scannedItems = 0;
    let truncated = false;
    let aborted = false;

    async function walkDirectory(current, prefetchedListing = null) {
      if (!current || aborted) {
        return;
      }
      await waitForTaskControl(taskControl);

      if (visited.size > maxDirs) {
        truncated = true;
        aborted = true;
        return;
      }

      if (onProgress) {
        onProgress({
          visible: true,
          percent: Math.min(95, Math.max(1, scannedDirs)),
          indeterminate: true,
          text: `正在扫描空目录:${shortDisplayName(current.path, 42)} | 已扫 ${scannedDirs} 个目录`,
        });
      }
      if (CONFIG.debug) {
        log(`空目录扫描检查目录:${current.path} | dirId=${current.dirId || ''} | fileId=${current.fileId || ''}`);
      }

      let inspection = prefetchedListing;
      try {
        if (!inspection) {
          inspection = await fetchDirectoryListingForEmptyScan(current.dirId || current.fileId, {
            pageSize: options.pageSize,
            maxPages: options.maxPages,
            delayMs: options.delayMs,
            idCandidates: current.dirIdCandidates,
            includeCurrentSnapshot: Boolean(current.isRoot),
            taskControl,
          });
        }
      } catch (err) {
        truncated = true;
        warn('空目录扫描拉取目录失败,已跳过:', {
          path: current.path,
          dirId: current.dirId,
          error: getErrorText(err),
        });
        return;
      }

      scannedDirs += 1;
      scannedItems += inspection.items.length;
      if (inspection.truncated || inspection.uncertain) {
        truncated = true;
      }

      if (!inspection.items.length) {
        emptyDirs.push({
          fileId: String(current.fileId || current.dirId || '').trim(),
          dirId: String(current.dirId || current.fileId || '').trim(),
          dirIdCandidates: normalizeIdCandidates([
            current.dirId,
            current.fileId,
            ...(current.dirIdCandidates || []),
          ]),
          name: current.name || '(当前目录)',
          path: current.path || '(当前目录)',
          depth: current.depth || 0,
          confidence: current.confidence === 'likely' ? 'likely' : 'confirmed',
        });
        return;
      }

      const visibleDirNameSet = current.isRoot ? rootVisibleDirNameSet : null;
      const childItems = Array.isArray(inspection.items) ? inspection.items.filter(Boolean) : [];
      for (const child of childItems) {
        await waitForTaskControl(taskControl);
        if (aborted) {
          break;
        }

        const childFileId = String(child.fileId || '').trim();
        if (!childFileId || isSyntheticDomId(childFileId) || !String(child.name || '').trim()) {
          continue;
        }

        const hintLevel = getEmptyScanDirectoryHintLevel(child, visibleDirNameSet);
        if (hintLevel < 0) {
          continue;
        }

        if (visited.size >= maxDirs) {
          truncated = true;
          aborted = true;
          break;
        }

        let childInspection = null;
        try {
          childInspection = await fetchDirectoryListingForEmptyScan(childFileId, {
            pageSize: options.pageSize,
            maxPages: options.maxPages,
            delayMs: options.delayMs,
            idCandidates: normalizeIdCandidates([
              childFileId,
              child.dirId,
              ...(child.dirIdCandidates || []),
            ]),
            taskControl,
          });
        } catch (err) {
          truncated = true;
          warn('空目录扫描探测子项失败,已跳过:', {
            path: current.path,
            childName: child.name,
            childFileId,
            error: getErrorText(err),
          });
          continue;
        }

        if (childInspection.truncated || childInspection.uncertain) {
          truncated = true;
        }

        const actualChildParentId = String(childInspection.usedParentId || child.dirId || childFileId).trim();
        if (childInspection.items.length) {
          if (!actualChildParentId || visited.has(actualChildParentId)) {
            continue;
          }
          visited.add(actualChildParentId);

          await walkDirectory({
            fileId: childFileId,
            dirId: actualChildParentId,
            dirIdCandidates: normalizeIdCandidates([
              actualChildParentId,
              child.dirId,
              childFileId,
              ...(child.dirIdCandidates || []),
            ]),
            name: String(child.name || actualChildParentId),
            path: current.isRoot ? String(child.name || actualChildParentId) : `${current.path}/${String(child.name || actualChildParentId)}`,
            depth: Number(current.depth || 0) + 1,
            isRoot: false,
            confidence: hintLevel >= 2 ? 'confirmed' : 'likely',
          }, childInspection);
          continue;
        }

        if (hintLevel <= 0) {
          continue;
        }

        emptyDirs.push({
          fileId: childFileId,
          dirId: actualChildParentId || childFileId,
          dirIdCandidates: normalizeIdCandidates([
            actualChildParentId,
            child.dirId,
            childFileId,
            ...(child.dirIdCandidates || []),
          ]),
          name: String(child.name || childFileId),
          path: current.isRoot ? String(child.name || childFileId) : `${current.path}/${String(child.name || childFileId)}`,
          depth: Number(current.depth || 0) + 1,
          confidence: hintLevel >= 2 ? 'confirmed' : 'likely',
        });
      }
    }

    await waitForTaskControl(taskControl);
    const rootInspection = await fetchDirectoryListingForEmptyScan(rootParentId, {
      pageSize: options.pageSize,
      maxPages: options.maxPages,
      delayMs: options.delayMs,
      idCandidates: [rootParentId],
      includeCurrentSnapshot: true,
      taskControl,
    });
    await walkDirectory(rootNode, rootInspection);

    const summary = {
      rootParentId,
      scannedDirs,
      scannedItems,
      emptyDirs,
      truncated,
      scannedAt: new Date().toISOString(),
    };
    setEmptyDirScanResult(summary);
    if (UI.emptyDirDetails) {
      UI.emptyDirDetails.open = true;
    }

    if (onProgress) {
      onProgress({
        visible: true,
        percent: 100,
        indeterminate: false,
        text: truncated
          ? `空目录扫描完成:找到 ${emptyDirs.length} 个空目录,已扫 ${scannedDirs} 个目录,结果可能未扫全`
          : `空目录扫描完成:找到 ${emptyDirs.length} 个空目录,已扫 ${scannedDirs} 个目录`,
      });
    }

    console.table(emptyDirs.map((item) => ({
      path: item.path,
      dirId: item.fileId,
      depth: item.depth,
      confidence: item.confidence || 'confirmed',
    })));

    return summary;
  }


  function getCloudImportParentId() {
    const context = getCurrentListContext();
    return String(context.parentId || CONFIG.request.manualListBody.parentId || '').trim();
  }

  function buildSourceImportDirName(fileName, runToken) {
    const prefix = sanitizeCloudDirName(CONFIG.cloud.sourceDirPrefix || '磁力导入', '磁力导入');
    const base = sanitizeCloudDirName(stripGenericExtension(fileName), '磁力文本');
    return `${prefix}-${base}-${runToken}`;
  }

  function buildMagnetImportDirName(magnetUrl, payload, magnetIndex) {
    return extractResolvedResourceName(payload, magnetUrl, `磁力_${magnetIndex}`);
  }

  async function importMagnetTextFiles(options = {}) {
    const onProgress = typeof options.onProgress === 'function' ? options.onProgress : null;
    const taskControl = options.taskControl || null;
    const sourceFiles = (STATE.magnetImportFiles || []).filter((item) => Array.isArray(item.magnets) && item.magnets.length);

    if (!sourceFiles.length) {
      throw new Error('还没有选择可导入的磁力 txt/json 文件。请先点“选择TXT/JSON”。');
    }

    const batchLimit = Math.max(1, Number(options.batchLimit || CONFIG.cloud.maxFilesPerTask || 500));
    const requestDelayMs = Math.max(0, Number(options.delayMs ?? CONFIG.batch.delayMs ?? 300));
    const parentId = String(options.parentId != null ? options.parentId : getCloudImportParentId());
    const runToken = buildTimestampToken();
    const totalMagnets = sourceFiles.reduce((sum, item) => sum + item.magnets.length, 0);
    const summary = {
      parentId,
      batchLimit,
      sourceFileCount: sourceFiles.length,
      totalMagnets,
      submittedMagnets: 0,
      skippedMagnets: 0,
      skippedExistingMagnets: 0,
      skippedDuplicateMagnets: 0,
      failedMagnets: 0,
      totalResolvedFiles: 0,
      totalTaskBatches: 0,
      submittedTaskBatches: 0,
      failedTaskBatches: 0,
      taskIds: [],
      skipped: [],
      failures: [],
      sourceDirs: [],
      existingTaskCount: 0,
      startedAt: new Date().toISOString(),
      finishedAt: '',
    };

    const existingTaskKeys = new Set();
    const inputSeenMagnetKeys = new Set();
    let processedMagnets = 0;

    try {
      await waitForTaskControl(taskControl);
      if (onProgress) {
        onProgress({
          visible: true,
          percent: 1,
          indeterminate: true,
          text: '正在读取云任务历史,用于识别已添加磁力...',
        });
      }

      const existingTasks = await listCloudTasks({
        pageSize: CONFIG.cloud.listTaskPageSize || 50,
      });
      if (existingTasks.ok && isProbablySuccess(existingTasks.payload, existingTasks)) {
        const rows = extractCloudTaskRows(existingTasks.payload);
        summary.existingTaskCount = rows.length;
        for (const row of rows) {
          const key = getMagnetIdentityKey(row.url);
          if (key) {
            existingTaskKeys.add(key);
          }
        }
      }
    } catch (err) {
      warn('读取云任务历史失败,将仅对本次导入内容做去重:', err);
    }

    for (const sourceFile of sourceFiles) {
      await waitForTaskControl(taskControl);
      const pendingMagnets = [];
      for (const magnetUrl of sourceFile.magnets) {
        await waitForTaskControl(taskControl);
        const magnetKey = getMagnetIdentityKey(magnetUrl);

        if (magnetKey && existingTaskKeys.has(magnetKey)) {
          summary.skippedMagnets += 1;
          summary.skippedExistingMagnets += 1;
          if (summary.skipped.length < 200) {
            summary.skipped.push({
              sourceFile: sourceFile.name,
              magnet: magnetUrl,
              reason: '历史云任务中已存在',
            });
          }
          processedMagnets += 1;
          continue;
        }

        if (magnetKey && inputSeenMagnetKeys.has(magnetKey)) {
          summary.skippedMagnets += 1;
          summary.skippedDuplicateMagnets += 1;
          if (summary.skipped.length < 200) {
            summary.skipped.push({
              sourceFile: sourceFile.name,
              magnet: magnetUrl,
              reason: '本次导入文件中重复',
            });
          }
          processedMagnets += 1;
          continue;
        }

        if (magnetKey) {
          inputSeenMagnetKeys.add(magnetKey);
        }
        pendingMagnets.push({
          magnetUrl,
          magnetKey,
        });
      }

      if (!pendingMagnets.length) {
        continue;
      }

      let sourceDir = null;
      try {
        await waitForTaskControl(taskControl);
        sourceDir = await createDirectoryWithFallback(buildSourceImportDirName(sourceFile.name, runToken), parentId, {
          fallbackName: '磁力导入',
        });
        summary.sourceDirs.push({
          name: sourceDir.dirName,
          dirId: sourceDir.dirId,
          sourceFile: sourceFile.name,
        });
      } catch (err) {
        summary.failedMagnets += pendingMagnets.length;
        for (const { magnetUrl } of pendingMagnets) {
          summary.failures.push({
            sourceFile: sourceFile.name,
            magnet: magnetUrl,
            message: `创建导入目录失败:${getErrorText(err)}`,
            submittedTaskBatches: 0,
          });
        }
        processedMagnets += pendingMagnets.length;
        warn('为磁力文本创建父目录失败,已跳过该文件:', {
          sourceFile: sourceFile.name,
          error: err,
        });
        continue;
      }

      for (let magnetIndex = 0; magnetIndex < pendingMagnets.length; magnetIndex += 1) {
        await waitForTaskControl(taskControl);
        const magnetUrl = pendingMagnets[magnetIndex].magnetUrl;
        const currentMagnetNo = processedMagnets + 1;
        let submittedForCurrentMagnet = 0;

        if (onProgress) {
          onProgress({
            visible: true,
            percent: Math.round(((currentMagnetNo - 1) / Math.max(1, totalMagnets)) * 100),
            indeterminate: true,
            text: `正在解析磁力 ${currentMagnetNo}/${totalMagnets}:${shortDisplayName(getMagnetDisplayName(magnetUrl), 42)}`,
          });
        }

        try {
          await waitForTaskControl(taskControl);
          const resolveRes = await resolveCloudResource(magnetUrl);
          if (!resolveRes.ok || !isProbablySuccess(resolveRes.payload, resolveRes)) {
            throw new Error(getErrorText(resolveRes.payload || resolveRes.text || `HTTP ${resolveRes.status}`));
          }

          const resolvedCandidate = pickBestResolvedFileCandidate(resolveRes.payload);
          const resolvedFiles = resolvedCandidate?.entries || [];
          const fileIndexes = Array.from(new Set(
            resolvedFiles
              .map((item) => toFiniteNumberOrNull(item?.index))
              .filter((value) => value != null)
          )).sort((a, b) => a - b);
          if (!fileIndexes.length) {
            throw new Error('resolve_res 没有返回可识别的文件列表,暂时无法自动拆分 fileIndexes');
          }

          log('磁力解析已选文件:', {
            magnet: getMagnetDisplayName(magnetUrl),
            candidateSource: resolvedCandidate?.source || 'unknown',
            candidatePath: resolvedCandidate?.path || '',
            candidateScore: Number(resolvedCandidate?.score || 0),
            fileCount: fileIndexes.length,
            sample: resolvedFiles.slice(0, 12).map((item) => ({
              index: item.index,
              name: item.name,
              sizeBytes: getResolvedEntrySizeBytes(item),
            })),
          });

          summary.totalResolvedFiles += fileIndexes.length;
          const batches = chunkArray(fileIndexes, batchLimit);
          summary.totalTaskBatches += batches.length;

          let taskParentId = sourceDir.dirId;
          if (CONFIG.cloud.createMagnetSubdir !== false) {
            await waitForTaskControl(taskControl);
            const magnetDir = await createDirectoryWithFallback(
              buildMagnetImportDirName(magnetUrl, resolveRes.payload, currentMagnetNo),
              sourceDir.dirId,
              { fallbackName: `磁力_${currentMagnetNo}` }
            );
            taskParentId = magnetDir.dirId;
          }

          for (let batchIndex = 0; batchIndex < batches.length; batchIndex += 1) {
            await waitForTaskControl(taskControl);
            const indexes = batches[batchIndex];
            if (onProgress) {
              const basePercent = ((currentMagnetNo - 1) / Math.max(1, totalMagnets)) * 100;
              const innerPercent = ((batchIndex + 1) / Math.max(1, batches.length)) * (100 / Math.max(1, totalMagnets));
              onProgress({
                visible: true,
                percent: Math.min(99, Math.round(basePercent + innerPercent)),
                indeterminate: false,
                text: `正在提交云添加 ${currentMagnetNo}/${totalMagnets} | ${shortDisplayName(getMagnetDisplayName(magnetUrl), 36)} | 第 ${batchIndex + 1}/${batches.length} 批,文件 ${indexes.length} 个`,
              });
            }

            const taskRes = await createCloudTask(indexes, magnetUrl, taskParentId);
            if (!taskRes.ok || !isProbablySuccess(taskRes.payload, taskRes)) {
              summary.failedTaskBatches += 1;
              throw new Error(getErrorText(taskRes.payload || taskRes.text || `HTTP ${taskRes.status}`));
            }

            const taskId = extractTaskId(taskRes.payload);
            if (taskId) {
              summary.taskIds.push(taskId);
            }
            summary.submittedTaskBatches += 1;
            submittedForCurrentMagnet += 1;

            if (requestDelayMs > 0) {
              await controlledDelay(requestDelayMs, taskControl);
            }
          }

          summary.submittedMagnets += 1;
        } catch (err) {
          summary.failedMagnets += 1;
          summary.failures.push({
            sourceFile: sourceFile.name,
            magnet: magnetUrl,
            message: getErrorText(err),
            submittedTaskBatches: submittedForCurrentMagnet,
          });
          warn('磁力云添加失败:', {
            sourceFile: sourceFile.name,
            magnetUrl,
            error: err,
          });
        } finally {
          processedMagnets += 1;
          STATE.lastCloudImportSummary = { ...summary };
        }
      }
    }

    try {
      await waitForTaskControl(taskControl);
      const cloudTasks = await listCloudTasks({
        pageSize: CONFIG.cloud.listTaskPageSize || 50,
      });
      if (cloudTasks.ok) {
        summary.taskRows = extractCloudTaskRows(cloudTasks.payload);
      }
    } catch (err) {
      warn('读取云添加任务列表失败:', err);
    }

    summary.finishedAt = new Date().toISOString();
    STATE.lastCloudImportSummary = { ...summary };

    if (onProgress) {
      onProgress({
        visible: true,
        percent: 100,
        indeterminate: false,
        text: `云添加提交完成:磁力成功 ${summary.submittedMagnets} 条,跳过 ${summary.skippedMagnets} 条,失败 ${summary.failedMagnets} 条;任务批次成功 ${summary.submittedTaskBatches} 个,失败 ${summary.failedTaskBatches} 个`,
      });
    }

    return summary;
  }

  function extractTaskId(payload) {
    const taskId = findFirstValueByKeys(payload, ['taskId', 'task_id', 'id']);
    return taskId == null ? '' : String(taskId);
  }

  function extractTaskStatus(payload) {
    const raw = findFirstValueByKeys(payload, [
      'taskStatus',
      'task_status',
      'status',
      'state',
      'taskState',
      'task_state',
    ]);
    return raw == null ? '' : String(raw).toUpperCase();
  }

  function getNumericTaskStatus(status) {
    const value = Number(status);
    return Number.isFinite(value) ? value : null;
  }

  function toFiniteNumberOrNull(value) {
    if (value == null || value === '') {
      return null;
    }
    const numeric = Number(value);
    return Number.isFinite(numeric) ? numeric : null;
  }

  function getTaskStatusLabel(status) {
    const code = getNumericTaskStatus(status);
    if (code === null) {
      return status || 'UNKNOWN';
    }
    if (code === 0) {
      return '0(等待中)';
    }
    if (code === 1) {
      return '1(执行中)';
    }
    if (code === 2) {
      return '2(已完成)';
    }
    if (code === 3) {
      return '3(失败)';
    }
    if (code === 4) {
      return '4(已取消)';
    }
    return `${code}(未知状态码)`;
  }

  function normalizeBooleanish(value) {
    if (typeof value === 'boolean') {
      return value;
    }
    if (typeof value === 'number') {
      return value !== 0;
    }
    if (typeof value === 'string') {
      const text = value.trim().toLowerCase();
      if (!text) {
        return null;
      }
      if (['1', 'true', 'yes', 'y', 'dir', 'folder', 'directory'].includes(text)) {
        return true;
      }
      if (['0', 'false', 'no', 'n', 'file'].includes(text)) {
        return false;
      }
    }
    return null;
  }

  function normalizeIdCandidates(values = []) {
    return Array.from(new Set(
      (values || [])
        .map((value) => String(value || '').trim())
        .filter(Boolean)
    ));
  }

  function isLikelyDirectoryIdKey(key) {
    const text = String(key || '').trim();
    if (!text) {
      return false;
    }
    if (/parentid/i.test(text)) {
      return false;
    }
    if (/(user|owner|creator|modifier|account|tenant|project|trace|task|category)id$/i.test(text)) {
      return false;
    }
    return /(dir|folder|file|resource|res|biz|obj|share).*id$/i.test(text) || /(^|_|\b)id$/i.test(text);
  }

  function collectIdLikeValues(node, out = [], seen = new WeakSet(), depth = 0) {
    if (!node || typeof node !== 'object' || depth > 3) {
      return out;
    }
    if (seen.has(node)) {
      return out;
    }
    seen.add(node);

    if (Array.isArray(node)) {
      for (const item of node) {
        collectIdLikeValues(item, out, seen, depth + 1);
      }
      return out;
    }

    for (const [key, value] of Object.entries(node)) {
      if (isLikelyDirectoryIdKey(key) && (typeof value === 'string' || typeof value === 'number')) {
        out.push(value);
      }
      if (value && typeof value === 'object') {
        collectIdLikeValues(value, out, seen, depth + 1);
      }
    }

    return out;
  }

  function extractTaskCounts(payload, expectedTotal = 0) {
    const successCount = toFiniteNumberOrNull(findFirstValueByKeys(payload, ['successCount', 'success_count', 'doneCount', 'done_count']));
    const failedCount = toFiniteNumberOrNull(findFirstValueByKeys(payload, ['failedCount', 'failCount', 'failed_count', 'fail_count']));
    const totalCount = toFiniteNumberOrNull(findFirstValueByKeys(payload, ['totalCount', 'total_count', 'count', 'total']));

    const hasSuccessCount = successCount != null;
    const hasFailedCount = failedCount != null;
    const hasTotalCount = totalCount != null && totalCount > 0;
    const success = hasSuccessCount ? successCount : 0;
    const failed = hasFailedCount ? failedCount : 0;
    const total = hasTotalCount ? totalCount : (Number(expectedTotal) > 0 ? Number(expectedTotal) : 0);

    return {
      success,
      failed,
      total,
      processed: success + failed,
      hasSuccessCount,
      hasFailedCount,
      hasTotalCount,
      hasExplicitCounts: hasSuccessCount || hasFailedCount || hasTotalCount,
    };
  }

  function hasUsefulTaskState(payload, expectedTotal = 0) {
    const status = extractTaskStatus(payload);
    const counts = extractTaskCounts(payload, expectedTotal);
    const rawProgress = Number(findFirstValueByKeys(payload, ['progress', 'percent', 'percentage']));
    return Boolean(status || counts.hasExplicitCounts || Number.isFinite(rawProgress));
  }

  function isTaskFinished(payload, options = {}) {
    const status = extractTaskStatus(payload);
    if (status) {
      const numericStatus = getNumericTaskStatus(status);
      if (numericStatus != null) {
        if ([2, 3, 4].includes(numericStatus)) {
          return true;
        }
        if ([0, 1].includes(numericStatus)) {
          return false;
        }
      }

      const doneWords = ['SUCCESS', 'SUCCEEDED', 'DONE', 'FINISH', 'FINISHED', 'COMPLETED', 'FAILED', 'ERROR', 'CANCEL', '成功', '完成', '失败'];
      if (doneWords.some((word) => status.includes(word))) {
        return true;
      }
      const runningWords = ['RUN', 'PROCESS', 'PENDING', 'QUEUE', 'WAIT', '进行', '处理中', '等待'];
      if (runningWords.some((word) => status.includes(word))) {
        return false;
      }
    }

    const finished = findFirstValueByKeys(payload, ['finished', 'done', 'completed', 'isFinished']);
    if (typeof finished === 'boolean') {
      return finished;
    }

    const progress = Number(findFirstValueByKeys(payload, ['progress', 'percent', 'percentage']));
    if (Number.isFinite(progress) && progress >= 100) {
      return true;
    }

    const counts = extractTaskCounts(payload, options.expectedTotal || 0);
    if (counts.total > 0 && counts.processed >= counts.total) {
      return true;
    }
    if (counts.failed === 0 && counts.success > 0 && Number(options.expectedTotal || 0) > 0 && counts.success >= Number(options.expectedTotal || 0)) {
      return true;
    }

    return false;
  }

  function isTaskSuccessful(payload, options = {}) {
    const status = extractTaskStatus(payload);
    if (status) {
      const numericStatus = getNumericTaskStatus(status);
      if (numericStatus != null) {
        if (numericStatus === 2) {
          return true;
        }
        if ([3, 4].includes(numericStatus)) {
          return false;
        }
      }
    }
    if (status && ['FAILED', 'ERROR', 'CANCEL', 'CANCELLED', '失败'].some((word) => status.includes(word))) {
      return false;
    }
    if (status && ['SUCCESS', 'SUCCEEDED', 'DONE', 'FINISH', 'FINISHED', 'COMPLETED', '成功', '完成'].some((word) => status.includes(word))) {
      return true;
    }

    const success = findFirstValueByKeys(payload, ['success', 'ok']);
    if (typeof success === 'boolean') {
      return success;
    }

    const counts = extractTaskCounts(payload, options.expectedTotal || 0);
    if (counts.total > 0 && counts.processed >= counts.total) {
      return counts.failed === 0;
    }
    if (counts.failed > 0) {
      return false;
    }

    return true;
  }

  async function deleteFiles(fileIds) {
    return postJson(getDeleteUrl(), { fileIds }, getRequestHeaders());
  }

  async function moveFiles(fileIds, parentId) {
    const normalizedFileIds = Array.from(new Set((fileIds || []).map((id) => String(id || '').trim()).filter(Boolean)));
    return postJson(getMoveUrl(), { fileIds: normalizedFileIds, parentId: String(parentId || '').trim() }, getRequestHeaders());
  }

  async function getTaskStatus(taskId) {
    return postJson(getTaskStatusUrl(), { taskId }, getRequestHeaders());
  }

  function buildRenamePayload(target) {
    const base =
      STATE.lastRenameRequest?.requestBody && typeof STATE.lastRenameRequest.requestBody === 'object'
        ? JSON.parse(JSON.stringify(STATE.lastRenameRequest.requestBody))
        : {};

    const idKey = pickExistingKey(base, ['fileId', 'id', 'resourceId', 'resId', 'bizId', 'objId'], 'fileId');
    const nameKey = pickExistingKey(base, ['newName', 'name', 'fileName', 'file_name', 'filename', 'title'], 'newName');
    const payload = {};
    const stableExtraKeys = [
      'parentId',
      'shareId',
      'shareFileId',
      'spaceId',
      'driveId',
      'folderId',
      'resourceType',
      'resType',
      'fileType',
      'bizType',
    ];

    for (const key of stableExtraKeys) {
      if (base[key] != null && base[key] !== '') {
        payload[key] = base[key];
      }
    }

    payload[idKey] = target.fileId;
    payload[nameKey] = target.newName;
    return payload;
  }

  function getErrorText(detail) {
    if (!detail) {
      return '';
    }
    if (typeof detail === 'string') {
      return detail;
    }
    if (detail instanceof Error) {
      return detail.message || String(detail);
    }
    if (typeof detail === 'object') {
      return detail.message || detail.error || detail.msg || detail.show_msg || detail.errmsg || detail.err_msg || detail.code || JSON.stringify(detail);
    }
    return String(detail);
  }

  function escapeHtml(text) {
    return String(text || '')
      .replace(/&/g, '&amp;')
      .replace(/</g, '&lt;')
      .replace(/>/g, '&gt;')
      .replace(/"/g, '&quot;')
      .replace(/'/g, '&#39;');
  }

  function shortDisplayName(name, max = 24) {
    const text = String(name || '').trim();
    if (text.length <= max) {
      return text;
    }
    return `${text.slice(0, max)}...`;
  }

  function escapeRegExp(text) {
    return String(text || '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
  }

  function decodeMiaochuanHtmlEntities(text) {
    const source = String(text || '');
    if (!source || !source.includes('&')) {
      return source;
    }
    return source.replace(/&(#x?[0-9a-f]+|[a-z]+);/gi, (matched, token) => {
      const lower = String(token || '').toLowerCase();
      if (lower === 'amp') return '&';
      if (lower === 'lt') return '<';
      if (lower === 'gt') return '>';
      if (lower === 'quot') return '"';
      if (lower === 'apos' || lower === '#39') return '\'';
      if (lower.startsWith('#x')) {
        const code = parseInt(lower.slice(2), 16);
        return Number.isFinite(code) ? String.fromCodePoint(code) : matched;
      }
      if (lower.startsWith('#')) {
        const code = parseInt(lower.slice(1), 10);
        return Number.isFinite(code) ? String.fromCodePoint(code) : matched;
      }
      return matched;
    });
  }

  function normalizeMiaochuanInteger(rawValue) {
    if (typeof rawValue === 'number' && Number.isFinite(rawValue) && rawValue >= 0) {
      return Math.trunc(rawValue);
    }
    const text = String(rawValue == null ? '' : rawValue).trim();
    if (!/^\d+$/u.test(text)) {
      return null;
    }
    const numeric = Number(text);
    return Number.isFinite(numeric) && numeric >= 0 ? Math.trunc(numeric) : null;
  }

  function decodeMiaochuanMd5Token(rawHash) {
    const text = String(rawHash == null ? '' : rawHash).trim().replace(/^"+|"+$/g, '');
    if (!text) {
      return '';
    }
    if (/^[a-f0-9]{32}$/iu.test(text)) {
      return text.toLowerCase();
    }
    if (/^[a-f0-9g-v]{32}$/iu.test(text) && /[g-v]/iu.test(text)) {
      const normalized = text
        .toLowerCase()
        .replace(/[g-v]/g, (ch) => String(ch.charCodeAt(0) - 103).toString(16));
      if (/^[a-f0-9]{32}$/u.test(normalized)) {
        return normalized;
      }
    }
    try {
      const normalized = text.replace(/-/g, '+').replace(/_/g, '/');
      const padded = normalized + '='.repeat((4 - (normalized.length % 4)) % 4);
      const binary = atob(padded);
      if (binary.length !== 16) {
        return '';
      }
      return Array.from(binary, (ch) => ch.charCodeAt(0).toString(16).padStart(2, '0')).join('');
    } catch {
      return '';
    }
  }

  function normalizeMiaochuanMd5(rawHash) {
    const value = String(rawHash || '').trim().replace(/^"+|"+$/g, '');
    const direct = decodeMiaochuanMd5Token(value);
    if (direct) {
      return direct;
    }
    const matched = value.match(/[a-f0-9g-v]{32}/i);
    return matched ? decodeMiaochuanMd5Token(matched[0]) : '';
  }

  function isExactMiaochuanMd5(rawHash) {
    return Boolean(decodeMiaochuanMd5Token(rawHash));
  }

  function formatMiaochuanBytes(bytes) {
    const value = Number(bytes || 0);
    if (!Number.isFinite(value) || value <= 0) {
      return '0 B';
    }
    const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
    let size = value;
    let unitIndex = 0;
    while (size >= 1024 && unitIndex < units.length - 1) {
      size /= 1024;
      unitIndex += 1;
    }
    return `${size.toFixed(size >= 100 || unitIndex === 0 ? 0 : 2)} ${units[unitIndex]}`;
  }

  function normalizeMiaochuanPath(rawPath, options = {}) {
    let value = String(rawPath || '').trim();
    if (!value) {
      return '';
    }
    if (options.decodeHtml !== false) {
      value = decodeMiaochuanHtmlEntities(value);
    }
    value = value.replace(/\\/g, '/');
    value = value.replace(/\/{2,}/g, '/');
    value = value.replace(/^\.\//, '');
    return options.stripLeadingSlash
      ? value.replace(/^\/+/, '').trim()
      : `/${value.replace(/^\/+/, '')}`.trim();
  }

  function splitMiaochuanPathSegments(pathValue) {
    const normalized = String(pathValue || '').replace(/^\/+/, '').replace(/\/+$/, '');
    return normalized ? normalized.split('/').filter(Boolean) : [];
  }

  function computeMiaochuanCommonPath(paths) {
    const validPaths = Array.isArray(paths) ? paths.filter(Boolean) : [];
    if (!validPaths.length) {
      return '';
    }
    const segmentsList = validPaths.map(splitMiaochuanPathSegments);
    if (!segmentsList.length || !segmentsList[0].length) {
      return '';
    }
    const commonSegments = [];
    const first = segmentsList[0];
    for (let index = 0; index < first.length; index += 1) {
      const segment = first[index];
      if (!segmentsList.every((segments) => segments[index] === segment)) {
        break;
      }
      commonSegments.push(segment);
    }
    if (!commonSegments.length) {
      return '';
    }
    if (commonSegments.length === first.length) {
      commonSegments.pop();
    }
    return commonSegments.length ? `${commonSegments.join('/')}/` : '';
  }

  function getMiaochuanFieldValue(raw, names) {
    if (!raw || typeof raw !== 'object') {
      return { value: undefined, key: '' };
    }
    const lowerMap = new Map();
    for (const key of Object.keys(raw)) {
      lowerMap.set(key.toLowerCase(), key);
    }
    for (const name of names) {
      const actualKey = lowerMap.get(String(name).toLowerCase());
      if (actualKey && raw[actualKey] != null && String(raw[actualKey]).trim() !== '') {
        return { value: raw[actualKey], key: actualKey };
      }
    }
    return { value: undefined, key: '' };
  }

  function pickMiaochuanSizeValue(raw) {
    return getMiaochuanFieldValue(raw, ['size', 'fileSize', 'file_size', 'bytes', 'length']);
  }

  function pickMiaochuanPathValue(raw) {
    const direct = getMiaochuanFieldValue(raw, [
      'path',
      '__gypPath',
      '__gyp_path',
      'filePath',
      'file_path',
      'fullPath',
      'full_path',
      'localPath',
      'local_path',
      'server_path',
    ]);
    if (direct.key) {
      return { ...direct, pathFromNameOnly: false };
    }
    const nameOnly = getMiaochuanFieldValue(raw, [
      'name',
      'fileName',
      'filename',
      'server_filename',
      'file_name',
      'displayName',
      'display_name',
    ]);
    return nameOnly.key ? { ...nameOnly, pathFromNameOnly: true } : { value: undefined, key: '', pathFromNameOnly: false };
  }

  function pickMiaochuanHashValue(raw) {
    const gcid = getMiaochuanFieldValue(raw, ['gcid']);
    if (gcid.key) {
      const text = String(gcid.value || '').trim();
      if (/^[a-f0-9]{40}$/iu.test(text)) {
        return { ...gcid, hashKind: 'gcid', supported: true };
      }
      return {
        ...gcid,
        hashKind: 'gcid',
        supported: false,
        reason: 'gcid 不是 40 位迅雷 GCID,不能用于光鸭 GCID 秒传',
      };
    }

    const md5Fields = [
      'etag',
      'md5',
      'fileMd5',
      'file_md5',
      'contentMd5',
      'content_md5',
      'sourceMd5',
      'source_md5',
      'block_list_md5',
    ];
    const md5 = getMiaochuanFieldValue(raw, md5Fields);
    if (md5.key) {
      return { ...md5, hashKind: 'md5', supported: true };
    }

    const contentHash = getMiaochuanFieldValue(raw, ['content_hash', 'contentHash']);
    if (contentHash.key) {
      const hashName = getMiaochuanFieldValue(raw, ['content_hash_name', 'contentHashName']);
      const kind = String(hashName.value || '').toLowerCase();
      if (kind === 'md5' || (isExactMiaochuanMd5(contentHash.value) && !kind)) {
        return { ...contentHash, hashKind: 'md5', supported: true };
      }
      return {
        ...contentHash,
        hashKind: kind || (String(contentHash.value || '').trim().length === 40 ? 'sha1' : 'unknown'),
        supported: false,
        reason: `content_hash 是 ${kind || '非 MD5'},光鸭秒传需要完整 MD5`,
      };
    }

    const genericHash = getMiaochuanFieldValue(raw, ['hash', 'sha1', 'fileSha1', 'file_sha1', 'quick_hash']);
    if (genericHash.key) {
      const text = String(genericHash.value || '').trim();
      const providerText = String(raw.__gypProvider || raw.provider || raw.source || '').toLowerCase();
      if (/xunlei|迅雷/u.test(providerText) && /^[a-f0-9]{40}$/iu.test(text)) {
        return { ...genericHash, hashKind: 'gcid', supported: true };
      }
      if (isExactMiaochuanMd5(text)) {
        return { ...genericHash, hashKind: 'md5-like', supported: true };
      }
      return {
        ...genericHash,
        hashKind: text.length === 40 ? 'sha1' : 'unknown',
        supported: false,
        reason: `${genericHash.key} 不是 32 位 MD5,不能直接作为光鸭 etag`,
      };
    }

    return { value: undefined, key: '', hashKind: 'missing', supported: false, reason: '没有找到可用于光鸭秒传的 MD5 字段' };
  }

  function getNestedMiaochuanCandidateArrays(payload) {
    const candidates = [];
    const pushArray = (value, label) => {
      if (Array.isArray(value)) {
        candidates.push({ value, label });
      }
    };
    if (!payload || typeof payload !== 'object') {
      return candidates;
    }
    pushArray(payload.files, 'files');
    pushArray(payload.fileList, 'fileList');
    pushArray(payload.file_list, 'file_list');
    pushArray(payload.items, 'items');
    pushArray(payload.list, 'list');
    pushArray(payload.resources, 'resources');
    pushArray(payload.records, 'records');
    const data = payload.data;
    if (data && typeof data === 'object') {
      pushArray(data.files, 'data.files');
      pushArray(data.fileList, 'data.fileList');
      pushArray(data.file_list, 'data.file_list');
      pushArray(data.items, 'data.items');
      pushArray(data.list, 'data.list');
      pushArray(data.resources, 'data.resources');
      pushArray(data.records, 'data.records');
    }
    return candidates;
  }

  function collectMiaochuanRawFiles(payload) {
    if (Array.isArray(payload)) {
      return { files: payload, sourcePath: 'root[]' };
    }
    const candidates = getNestedMiaochuanCandidateArrays(payload);
    if (candidates.length) {
      const scored = candidates
        .map((item) => ({
          ...item,
          score: item.value.reduce((sum, raw) => {
            if (!raw || typeof raw !== 'object') {
              return sum;
            }
            const hash = pickMiaochuanHashValue(raw);
            const size = pickMiaochuanSizeValue(raw);
            const path = pickMiaochuanPathValue(raw);
            return sum + (hash.key ? 3 : 0) + (size.key ? 2 : 0) + (path.key ? 2 : 0);
          }, 0),
        }))
        .sort((left, right) => right.score - left.score);
      return { files: scored[0].value, sourcePath: scored[0].label };
    }
    if (payload && typeof payload === 'object') {
      const hash = pickMiaochuanHashValue(payload);
      const size = pickMiaochuanSizeValue(payload);
      const path = pickMiaochuanPathValue(payload);
      if (hash.key || size.key || path.key) {
        return { files: [payload], sourcePath: 'root{}' };
      }
    }
    return { files: null, sourcePath: '' };
  }

  function isMiaochuanCandidateObject(raw) {
    if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
      return false;
    }
    const hash = pickMiaochuanHashValue(raw);
    const size = pickMiaochuanSizeValue(raw);
    const path = pickMiaochuanPathValue(raw);
    if (hash.key && (size.key || path.key)) {
      return true;
    }
    if (path.key && size.key) {
      return true;
    }
    return Boolean(hash.key && path.key);
  }

  function collectMiaochuanCandidateObjects(node, out = [], seen = new WeakSet(), depth = 0) {
    if (!node || typeof node !== 'object' || depth > 8) {
      return out;
    }
    if (seen.has(node)) {
      return out;
    }
    seen.add(node);

    if (Array.isArray(node)) {
      for (const item of node) {
        collectMiaochuanCandidateObjects(item, out, seen, depth + 1);
      }
      return out;
    }

    if (isMiaochuanCandidateObject(node)) {
      out.push(node);
    }

    for (const value of Object.values(node)) {
      if (value && typeof value === 'object') {
        collectMiaochuanCandidateObjects(value, out, seen, depth + 1);
      }
    }
    return out;
  }

  function getMiaochuanCandidateKey(raw) {
    const hash = pickMiaochuanHashValue(raw);
    const size = pickMiaochuanSizeValue(raw);
    const path = pickMiaochuanPathValue(raw);
    const id = getMiaochuanFieldValue(raw, [
      'fid',
      'fileId',
      'file_id',
      'fs_id',
      'pickcode',
      'id',
      'share_fid',
      'shareFileId',
      'share_file_id',
    ]);
    const hashText = hash.key ? String(hash.value || '').trim().toLowerCase() : '';
    const sizeText = size.key ? String(size.value || '').trim() : '';
    const pathText = path.key ? normalizeMiaochuanPath(path.value, { decodeHtml: true, stripLeadingSlash: true }).toLowerCase() : '';
    const idText = id.key ? String(id.value || '').trim().toLowerCase() : '';
    return [hashText, sizeText, pathText, idText].filter(Boolean).join('__') || JSON.stringify(raw).slice(0, 200);
  }

  function getMiaochuanCurrentSourceName() {
    const host = window.location.hostname || '网页';
    if (/quark\.cn$/i.test(host)) return '夸克网盘当前页面';
    if (/123pan\.(com|cn)$/i.test(host)) return '123 网盘当前页面';
    if (/189\.cn$/i.test(host)) return '天翼云盘当前页面';
    if (isBaiduSharePage()) return '百度网盘分享页(不支持直接取 MD5)';
    if (/(pan|yun)\.baidu\.com$/i.test(host)) return '百度网盘当前页面';
    if (/pan\.xunlei\.com$/i.test(host)) return '迅雷云盘当前页面';
    if (/guangyapan\.com$/i.test(host)) return '光鸭云盘当前页面';
    return `${host} 当前页面`;
  }

  function captureMiaochuanSourcePayload(url, requestBody, responseBody) {
    if (!responseBody || typeof responseBody !== 'object') {
      return 0;
    }
    const rows = collectMiaochuanCandidateObjects(responseBody);
    if (!rows.length) {
      return 0;
    }

    const map = STATE.miaochuanCapturedMap || {};
    let added = 0;
    rows.forEach((row) => {
      const key = getMiaochuanCandidateKey(row);
      if (!key || map[key]) {
        return;
      }
      const copy = { ...row };
      copy.__gypCaptureUrl = url;
      copy.__gypCapturedAt = Date.now();
      if (requestBody && typeof requestBody === 'object') {
        copy.__gypRequestHint = {
          parentId: requestBody.parentId || requestBody.pdir_fid || requestBody.dirId || requestBody.dir_id || requestBody.folderId || requestBody.folder_id || '',
          page: requestBody.page || requestBody.pageNo || requestBody.page_no || requestBody.pn || '',
        };
      }
      map[key] = copy;
      added += 1;
    });

    if (added) {
      STATE.miaochuanCapturedMap = map;
      STATE.miaochuanCapturedRows = Object.values(map).slice(-3000);
      STATE.lastMiaochuanCaptureAt = Date.now();
      STATE.lastMiaochuanCaptureUrl = url;
      renderMiaochuanCaptureStatus();
    }
    return added;
  }

  function isQuarkPageHost() {
    return /(^|\.)quark\.cn$/i.test(window.location.hostname || '');
  }

  const QUARK_PC_USER_AGENT =
    'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) quark-cloud-drive/3.14.2 Chrome/112.0.5615.165 Electron/24.3.1.3 Safari/537.36 Channel/pckk_other_ch';

  function getStoredQuarkCookie() {
    try {
      if (typeof GM_getValue === 'function') {
        const value = GM_getValue(QUARK_COOKIE_STORAGE_KEY, '');
        if (typeof value === 'string' && value.trim()) {
          return value.trim();
        }
      }
    } catch {
      /* ignore */
    }
    try {
      return String(window.localStorage.getItem(QUARK_COOKIE_STORAGE_KEY) || '').trim();
    } catch {
      return '';
    }
  }

  function setStoredQuarkCookie(cookie) {
    const value = String(cookie || '').trim();
    if (!value) {
      return;
    }
    try {
      if (typeof GM_setValue === 'function') {
        GM_setValue(QUARK_COOKIE_STORAGE_KEY, value);
      }
    } catch {
      /* ignore */
    }
    try {
      window.localStorage.setItem(QUARK_COOKIE_STORAGE_KEY, value);
    } catch {
      /* ignore */
    }
  }

  function getQuarkCookieForMiaochuan(options = {}) {
    const manual = String(UI.fields.miaochuanQuarkCookie?.value || '').trim();
    if (manual) {
      setStoredQuarkCookie(manual);
      return manual;
    }
    const stored = getStoredQuarkCookie();
    if (stored) {
      if (UI.fields.miaochuanQuarkCookie && !UI.fields.miaochuanQuarkCookie.value) {
        UI.fields.miaochuanQuarkCookie.value = stored;
      }
      return stored;
    }
    const pageCookie = isQuarkPageHost() ? String(document.cookie || '').trim() : '';
    if (pageCookie) {
      return pageCookie;
    }
    if (options.required) {
      throw new Error('夸克分享页主动获取 MD5 需要 Cookie。请在“秒传 JSON”分区粘贴夸克请求里的完整 Cookie 后重试。');
    }
    return '';
  }

  function getQuarkSharePwdIdFromLocation() {
    const href = String(window.location.href || '');
    const matched = href.match(/\/s\/([^/?#]+)/i);
    return matched ? decodeUrlParam(matched[1]) : '';
  }

  function getQuarkSharePasscodeFromLocation() {
    const href = String(window.location.href || '');
    try {
      const url = new URL(href);
      return url.searchParams.get('pwd') || url.searchParams.get('passcode') || url.searchParams.get('code') || '';
    } catch {
      const matched = href.match(/[?&#](?:pwd|passcode|code)=([^&#]+)/i);
      return matched ? decodeUrlParam(matched[1]) : '';
    }
  }

  function extractFirstUrlFromText(text = '') {
    const matched = String(text || '').match(/https?:\/\/[^\s"'<>]+/iu);
    return matched ? String(matched[0] || '').trim() : '';
  }

  function extractSharePasscodeFromText(text = '') {
    const matched = String(text || '').match(/(?:提取码|访问码|密码|code|pwd|passcode)\s*[::]?\s*([a-z0-9]{4,8})/iu);
    return matched ? String(matched[1] || '').trim() : '';
  }

  function is123PanShareHost(host = '') {
    return /(^|\.)123pan\.(com|cn)$/i.test(host)
      || /(^|\.)123(865|684|912)\.com$/i.test(host);
  }

  function is123PanPageHost() {
    return is123PanShareHost(window.location.hostname || '');
  }

  function extract123PanShareKeyFromUrl(parsedUrl) {
    if (!parsedUrl) {
      return '';
    }
    const direct = parsedUrl.searchParams.get('shareKey')
      || parsedUrl.searchParams.get('share_key')
      || parsedUrl.searchParams.get('sharekey')
      || '';
    if (direct) {
      return decodeUrlParam(direct).trim();
    }
    const pathname = decodeUrlParam(parsedUrl.pathname || '');
    const matched = pathname.match(/\/(?:s|ps|123pan)\/([^/?#]+)/i)
      || pathname.match(/\/([A-Za-z0-9][A-Za-z0-9_-]*-[A-Za-z0-9_-]+)(?:\/|$)/);
    return matched ? String(matched[1] || '').trim() : '';
  }

  function get123PanShareKeyFromLocation() {
    try {
      return extract123PanShareKeyFromUrl(new URL(window.location.href || ''));
    } catch {
      return '';
    }
  }

  function get123PanSharePasscodeFromLocation() {
    try {
      const url = new URL(window.location.href || '');
      return url.searchParams.get('pwd')
        || url.searchParams.get('passcode')
        || url.searchParams.get('code')
        || url.searchParams.get('SharePwd')
        || '';
    } catch {
      const matched = String(window.location.href || '').match(/[?&#](?:pwd|passcode|code|SharePwd)=([^&#]+)/i);
      return matched ? decodeUrlParam(matched[1]) : '';
    }
  }

  function extractTianyiShareCodeFromUrl(parsedUrl) {
    if (!parsedUrl) {
      return '';
    }
    const direct = parsedUrl.searchParams.get('shareCode')
      || parsedUrl.searchParams.get('share_code')
      || parsedUrl.searchParams.get('code')
      || '';
    if (direct) {
      return decodeUrlParam(direct).trim();
    }
    const href = parsedUrl.href || '';
    const queryMatched = href.match(/[?&#](?:shareCode|share_code|code)=([^&#]+)/i);
    if (queryMatched) {
      return decodeUrlParam(queryMatched[1]).trim();
    }
    const pathMatched = decodeUrlParam(parsedUrl.pathname || '').match(/\/t\/([^/?#]+)/i);
    return pathMatched ? String(pathMatched[1] || '').trim() : '';
  }

  function extractXunleiShareIdFromUrl(parsedUrl) {
    if (!parsedUrl) {
      return '';
    }
    const direct = parsedUrl.searchParams.get('share_id')
      || parsedUrl.searchParams.get('shareId')
      || '';
    if (direct) {
      return decodeUrlParam(direct).trim();
    }
    const matched = decodeUrlParam(parsedUrl.pathname || '').match(/^\/s\/([^/?#]+)/i);
    return matched ? String(matched[1] || '').trim() : '';
  }

  function detectShareLinkProvider(rawInput = '', options = {}) {
    const text = String(rawInput || '').trim();
    const manualPasscode = String(options.manualPasscode || '').trim();
    const urlText = extractFirstUrlFromText(text) || text;
    if (!urlText) {
      return {
        provider: '',
        label: '',
        supported: false,
        shareUrl: '',
        passcode: manualPasscode || '',
        message: '请先粘贴分享链接。',
      };
    }

    let parsedUrl;
    try {
      parsedUrl = new URL(urlText);
    } catch {
      return {
        provider: '',
        label: '',
        supported: false,
        shareUrl: urlText,
        passcode: manualPasscode || '',
        message: '分享链接格式不正确,请粘贴完整的 http(s) 链接。',
      };
    }

    const host = String(parsedUrl.hostname || '').toLowerCase();
    const passcode = manualPasscode
      || parsedUrl.searchParams.get('pwd')
      || parsedUrl.searchParams.get('passcode')
      || parsedUrl.searchParams.get('accessCode')
      || parsedUrl.searchParams.get('access_code')
      || parsedUrl.searchParams.get('pickupCode')
      || parsedUrl.searchParams.get('pickup_code')
      || extractSharePasscodeFromText(text);

    if (/(^|\.)quark\.cn$/i.test(host)) {
      const matched = parsedUrl.pathname.match(/\/s\/([^/?#]+)/i);
      const pwdId = matched ? decodeUrlParam(matched[1]) : '';
      return {
        provider: 'quark',
        label: '夸克网盘分享链接',
        supported: Boolean(pwdId),
        shareUrl: parsedUrl.toString(),
        passcode: String(passcode || '').trim(),
        pwdId,
        message: pwdId ? '' : '没有在链接里识别到夸克分享 ID。',
      };
    }

    if (is123PanShareHost(host)) {
      const shareKey = extract123PanShareKeyFromUrl(parsedUrl);
      return {
        provider: '123pan',
        label: '123 网盘分享链接',
        supported: Boolean(shareKey),
        shareUrl: parsedUrl.toString(),
        passcode: String(passcode || '').trim(),
        shareKey,
        message: shareKey ? '' : '没有在链接里识别到 123 网盘分享 Key。',
      };
    }

	    if (/cloud\.189\.cn$/i.test(host)) {
	      const shareCode = extractTianyiShareCodeFromUrl(parsedUrl);
	      return {
        provider: 'tianyiyun',
        label: '天翼云盘分享链接',
        supported: Boolean(shareCode),
        shareUrl: parsedUrl.toString(),
        passcode: String(passcode || '').trim(),
        shareCode,
        message: shareCode ? '' : '没有在链接里识别到天翼云盘分享 Code。',
	      };
	    }

    if (/^pan\.xunlei\.com$/i.test(host)) {
      const shareId = extractXunleiShareIdFromUrl(parsedUrl);
      return {
        provider: 'xunlei',
        label: '迅雷云盘分享链接',
        supported: Boolean(shareId),
        shareUrl: parsedUrl.toString(),
        passcode: String(passcode || '').trim(),
        shareId,
        message: shareId ? '' : '没有在链接里识别到迅雷分享 ID。',
      };
    }

    return {
      provider: 'unknown',
      label: host || '未知来源',
      supported: false,
      shareUrl: parsedUrl.toString(),
      passcode: String(passcode || '').trim(),
      message: `暂未识别或支持这个分享链接来源:${host || '未知域名'}` ,
    };
  }

  function getShareLinkQuarkCookie() {
    const manual = String(UI.fields.shareLinkQuarkCookie?.value || '').trim();
    if (manual) {
      setStoredQuarkCookie(manual);
      return manual;
    }
    if (UI.fields.miaochuanQuarkCookie && UI.fields.miaochuanQuarkCookie.value) {
      const fromMiaochuan = String(UI.fields.miaochuanQuarkCookie.value || '').trim();
      if (fromMiaochuan) {
        setStoredQuarkCookie(fromMiaochuan);
        return fromMiaochuan;
      }
    }
    return getQuarkCookieForMiaochuan();
  }

  function isBaiduPageHost() {
    return /^(pan|yun)\.baidu\.com$/i.test(window.location.hostname || '');
  }

  function isBaiduSharePage() {
    return isBaiduPageHost() && /^\/(?:s|share)\//i.test(window.location.pathname || '');
  }

  function decodeBaiduMd5(encrypted) {
    const text = String(encrypted || '').trim();
    if (!text || text.length !== 32) {
      return text.toLowerCase();
    }
    if (/^[a-f0-9]{32}$/iu.test(text)) {
      return text.toLowerCase();
    }
    const offset = text.charAt(9).charCodeAt(0) - 'g'.charCodeAt(0);
    if (offset < 0 || offset > 15) {
      return /^[0-9a-z]{32}$/iu.test(text) ? text.toLowerCase() : '';
    }
    const replaced = text.toLowerCase().split('');
    replaced[9] = offset.toString(16);
    const decoded = [];
    for (let index = 0; index < 32; index += 1) {
      const ch = replaced[index];
      if (!/^[a-f0-9]$/u.test(ch)) {
        return text.toLowerCase();
      }
      decoded[index] = (parseInt(ch, 16) ^ (15 & index)).toString(16);
    }
    const original =
      decoded.slice(8, 16).join('') +
      decoded.slice(0, 8).join('') +
      decoded.slice(24, 32).join('') +
      decoded.slice(16, 24).join('');
    return /^[a-f0-9]{32}$/u.test(original) ? original : text.toLowerCase();
  }

  function getBaiduBdstoken() {
    try {
      const params = new URLSearchParams(window.location.search || '');
      const direct = params.get('bdstoken') || params.get('token') || '';
      if (direct && direct.length > 10) {
        return direct;
      }
    } catch {
      /* ignore */
    }

    const page = getPageWindowObject();
    const candidates = [
      page?.yunData?.MYBDSTOKEN,
      page?.yunData?.bdstoken,
      page?.locals?.bdstoken,
      page?.context?.bdstoken,
      page?.config?.bdstoken,
    ];
    for (const value of candidates) {
      const text = String(value || '').trim();
      if (text.length > 10) {
        return text;
      }
    }

    try {
      const stores = [page?.__redux_store__, page?.store, page?.reduxStore, page?.__NEXT_Redux_STORE__];
      const findToken = (node, depth = 0, seen = new WeakSet()) => {
        if (!node || typeof node !== 'object' || depth > 6 || seen.has(node)) {
          return '';
        }
        seen.add(node);
        for (const [key, value] of Object.entries(node)) {
          if (/bdstoken/i.test(key) && typeof value === 'string' && value.length > 10) {
            return value;
          }
          if (/^(token|access_token|authToken)$/i.test(key) && typeof value === 'string' && value.length > 20) {
            return value;
          }
          const nested = findToken(value, depth + 1, seen);
          if (nested) {
            return nested;
          }
        }
        return '';
      };
      for (const store of stores) {
        if (store && typeof store.getState === 'function') {
          const token = findToken(store.getState());
          if (token) {
            return String(token);
          }
        }
      }
    } catch {
      /* ignore */
    }

    for (const script of Array.from(document.querySelectorAll('script'))) {
      const source = script.textContent || '';
      const matched = source.match(/["']bdstoken["']\s*[:=]\s*["']([^"']{10,100})["']/i)
        || source.match(/bdstoken["'\s]*[:=]["'\s]*([a-f0-9]{20,100})/i);
      if (matched?.[1]) {
        return matched[1];
      }
    }
    return '';
  }

  function getBaiduCurrentDir() {
    const source = `${window.location.hash || ''}${window.location.search || ''}`;
    const pathMatched = source.match(/[?&#]path=([^&#]+)/i);
    if (pathMatched) {
      return decodeUrlParam(pathMatched[1]).split('?')[0].split('#')[0] || '/';
    }
    const page = getPageWindowObject();
    const candidates = [
      page?.yunData?.path,
      page?.yunData?.currentDir,
      page?.yunData?.curPath,
      page?.yunData?.request?.params?.path,
    ];
    for (const value of candidates) {
      const text = String(value || '').trim();
      if (text) {
        return text.split('?')[0].split('#')[0] || '/';
      }
    }
    return '/';
  }

  function extractBaiduSelectedFsIdsFromDom() {
    const ids = new Set();
    const selectedRows = document.querySelectorAll([
      '.mouse-choose-item.selected',
      '.wp-s-pan-table__body-row.selected',
      '.ant-table-row-selected',
      '[aria-selected="true"]',
      '[class*="selected"][data-id]',
      '[class*="selected"][data-fs-id]',
      '[class*="selected"][data-fsid]',
    ].join(', '));
    const getIdFromFiber = (el) => {
      const fiberKey = Object.keys(el || {}).find((key) => key.startsWith('__reactFiber$') || key.startsWith('__reactInternalInstance$'));
      let fiber = fiberKey ? el[fiberKey] : null;
      for (let index = 0; index < 30 && fiber; index += 1) {
        const props = fiber.memoizedProps || fiber.pendingProps || {};
        const item = props.item || props.file || props.fileInfo || props.data || props.fileItem || props.record || {};
        const id = String(item.fs_id || item.fsId || item.id || props.fs_id || props.fsId || '').trim();
        if (/^\d+$/u.test(id)) {
          return id;
        }
        fiber = fiber.return;
      }
      return '';
    };
    selectedRows.forEach((row) => {
      if (!row || row.tagName === 'INPUT' || row.tagName === 'BUTTON' || row.closest('thead')) {
        return;
      }
      for (const attr of ['data-id', 'data-fs-id', 'data-fsid', 'data-key', 'data-row-key']) {
        const id = String(row.getAttribute(attr) || '').trim();
        if (/^\d+$/u.test(id)) {
          ids.add(id);
        }
      }
      const fiberId = getIdFromFiber(row);
      if (fiberId) {
        ids.add(fiberId);
      }
    });
    return Array.from(ids);
  }

  function extractBaiduSelectedFsIdsFromState() {
    const ids = new Set();
    const page = getPageWindowObject();
    try {
      const stores = [page?.__redux_store__, page?.store, page?.reduxStore];
      const visit = (node, depth = 0, seen = new WeakSet()) => {
        if (!node || typeof node !== 'object' || depth > 7 || seen.has(node)) {
          return;
        }
        seen.add(node);
        if (Array.isArray(node)) {
          return;
        }
        for (const [key, value] of Object.entries(node)) {
          if (/^(selectedList|checkedList|selectedFiles|selectedFsIds|selectedRowKeys|selectedKeys|checkList)$/i.test(key) && Array.isArray(value)) {
            value.slice(0, 2000).forEach((item) => {
              const id = typeof item === 'object'
                ? String(item?.fs_id || item?.fsId || item?.id || '').trim()
                : String(item || '').trim();
              if (/^\d+$/u.test(id)) {
                ids.add(id);
              }
            });
          } else if (value && typeof value === 'object' && !Array.isArray(value)) {
            visit(value, depth + 1, seen);
          }
        }
      };
      for (const store of stores) {
        if (store && typeof store.getState === 'function') {
          visit(store.getState());
        }
      }
      const yunSelected = page?.yunData?.selectedFsIds;
      if (Array.isArray(yunSelected)) {
        yunSelected.forEach((id) => {
          if (/^\d+$/u.test(String(id))) {
            ids.add(String(id));
          }
        });
      }
    } catch {
      /* ignore */
    }
    return Array.from(ids);
  }

  function cleanBaiduSelectedName(rawName) {
    let value = String(rawName || '').replace(/[\r\n\t]+/g, ' ').replace(/\s{2,}/g, ' ').trim();
    if (!value || value.length > 500 || /^https?:\/\//iu.test(value)) {
      return '';
    }
    if (/^(操作|下载|分享|更多|删除|重命名|移动|复制|收藏)$/u.test(value)) {
      return '';
    }
    const half = Math.floor(value.length / 2);
    if (half > 3 && value.slice(0, half).trim() === value.slice(half).trim()) {
      value = value.slice(0, half).trim();
    }
    return value;
  }

  function extractBaiduSelectedFileNames() {
    const names = new Set();
    const rows = document.querySelectorAll([
      '.mouse-choose-item.selected',
      '.wp-s-pan-table__body-row.selected',
      '.ant-table-row-selected',
      '[aria-selected="true"]',
      '[class*="selected"][class*="file"]',
      '[class*="item-active"]',
    ].join(', '));
    rows.forEach((row) => {
      if (!row || row.tagName === 'INPUT' || row.tagName === 'BUTTON' || row.closest('thead')) {
        return;
      }
      const filenameSelectors = [
        '.file-name',
        '.filename',
        '[class*="file-name"]',
        '[class*="fileName"]',
        '[class*="name-col"]',
        'td[class*="name"]',
        'a[title]',
        'span[title]',
      ];
      for (const selector of filenameSelectors) {
        const el = row.querySelector(selector);
        const title = cleanBaiduSelectedName(el?.getAttribute?.('title'));
        const text = cleanBaiduSelectedName(el?.textContent);
        const name = title || text;
        if (name) {
          names.add(name);
          return;
        }
      }
      const ownTitle = cleanBaiduSelectedName(row.getAttribute('title'));
      if (ownTitle) {
        names.add(ownTitle);
      }
    });
    return Array.from(names);
  }

  function getBaiduItemName(item) {
    return chooseBestNameCandidate([
      item?.server_filename,
      item?.filename,
      item?.fileName,
      item?.name,
    ]);
  }

  function isBaiduDirectoryItem(item) {
    return Number(item?.isdir ?? item?.isDir ?? item?.dir) === 1;
  }

  async function fetchBaiduListPage({ dir = '/', page = 1, num = 1000, bdstoken = '' }) {
    const url = new URL('https://pan.baidu.com/api/list');
    const params = {
      dir,
      order: 'name',
      desc: '0',
      showempty: '0',
      web: '1',
      page,
      num,
      channel: 'chunlei',
      app_id: '250528',
      bdstoken,
    };
    Object.entries(params).forEach(([key, value]) => {
      if (value != null && String(value) !== '') {
        url.searchParams.set(key, String(value));
      }
    });
    const response = await requestMiaochuanJson(url.toString(), {
      method: 'GET',
      headers: {
        Accept: 'application/json, text/plain, */*',
        Referer: 'https://pan.baidu.com/disk/main',
      },
      timeout: 45000,
    });
    const payload = response.payload;
    const errno = Number(payload?.errno ?? 0);
    if (!response.ok || errno !== 0 || !Array.isArray(payload?.list)) {
      if (errno === -7) {
        throw new Error('百度 bdstoken 已过期或无效,请刷新百度网盘页面并重新登录后再试。');
      }
      throw new Error(`百度网盘目录读取失败:${getErrorText(payload || response.text || `HTTP ${response.status}`)}`);
    }
    captureMiaochuanSourcePayload(`baidu-list:${dir}:${page}`, { dir, page }, payload);
    return payload;
  }

  async function fetchBaiduFileMetas(fsIds, bdstoken) {
    const map = new Map();
    const ids = Array.from(new Set((fsIds || []).map((id) => String(id || '').trim()).filter((id) => /^\d+$/u.test(id))));
    for (let offset = 0; offset < ids.length; offset += 100) {
      const batch = ids.slice(offset, offset + 100);
      const url = new URL('https://pan.baidu.com/api/filemetas');
      url.searchParams.set('fsids', JSON.stringify(batch.map(Number)));
      url.searchParams.set('dlink', '0');
      url.searchParams.set('thumb', '0');
      url.searchParams.set('extra', '0');
      url.searchParams.set('needmedia', '0');
      url.searchParams.set('detail', '1');
      url.searchParams.set('channel', 'chunlei');
      url.searchParams.set('web', '1');
      url.searchParams.set('app_id', '250528');
      url.searchParams.set('bdstoken', bdstoken);
      const response = await requestMiaochuanJson(url.toString(), {
        method: 'GET',
        headers: {
          Accept: 'application/json, text/plain, */*',
          Referer: 'https://pan.baidu.com/disk/main',
        },
        timeout: 45000,
      });
      const payload = response.payload;
      const errno = Number(payload?.errno ?? 0);
      if (!response.ok || errno !== 0 || !Array.isArray(payload?.info)) {
        if (errno === -7) {
          throw new Error('百度 bdstoken 已过期或无效,请刷新百度网盘页面并重新登录后再试。');
        }
        throw new Error(`百度网盘文件信息读取失败:${getErrorText(payload || response.text || `HTTP ${response.status}`)}`);
      }
      payload.info.forEach((item) => {
        map.set(String(item.fs_id || ''), item);
      });
      await sleep(120);
    }
    return map;
  }

  async function collectBaiduDirectoryFiles(baiduPath, pathPrefix, bdstoken, options = {}) {
    const rows = [];
    let page = 1;
    const pageSize = 100;
    while (page <= 500) {
      await waitForTaskControl(options.taskControl || null);
      if (typeof options.onProgress === 'function') {
        options.onProgress({
          visible: true,
          percent: 0,
          indeterminate: true,
          text: `正在读取百度目录:${pathPrefix || baiduPath || '/'} | 已收集 ${rows.length} 个文件`,
        });
      }
      const payload = await fetchBaiduListPage({ dir: baiduPath || '/', page, num: pageSize, bdstoken });
      const list = Array.isArray(payload.list) ? payload.list : [];
      for (const item of list) {
        const name = sanitizeCloudDirName(getBaiduItemName(item), '未命名');
        const itemPath = `${pathPrefix || ''}/${name}`.replace(/\/{2,}/g, '/');
        if (isBaiduDirectoryItem(item)) {
          rows.push(...(await collectBaiduDirectoryFiles(String(item.path || ''), itemPath, bdstoken, options)));
        } else {
          const etag = decodeBaiduMd5(item.md5 || item.file_md5 || item.etag || '');
          rows.push({
            path: itemPath,
            etag,
            size: String(normalizeMiaochuanInteger(item.size) || 0),
            __gypSource: 'baidu-active',
            __gypProvider: 'baidu',
            __gypFsId: String(item.fs_id || ''),
            __gypHasMd5: Boolean(normalizeMiaochuanMd5(etag)),
          });
        }
        if (rows.length > SHARE_LINK_MAX_FILES) {
          throw new Error(`百度网盘文件过多,已超过安全上限 ${SHARE_LINK_MAX_FILES} 个文件。请进入更小的子目录后再生成。`);
        }
      }
      if (!list.length || list.length < pageSize) {
        break;
      }
      page += 1;
      await sleep(150);
    }
    return rows;
  }

  async function collectBaiduSelectedRows(options = {}) {
    if (!isBaiduPageHost()) {
      throw new Error('当前页面不是百度网盘。');
    }
    if (isBaiduSharePage()) {
      throw new Error('百度分享页暂时不能直接生成光鸭秒传 JSON,因为分享页通常不返回真实 MD5。请先把文件保存到自己的百度网盘,再到“我的网盘/文件列表”里勾选后使用“网盘互通(百度)”。');
    }
    const bdstoken = getBaiduBdstoken();
    if (!bdstoken) {
      throw new Error('没有获取到百度 bdstoken。请刷新百度网盘页面,等待列表加载完成后再试。');
    }
    const selectedIds = Array.from(new Set([
      ...extractBaiduSelectedFsIdsFromDom(),
      ...extractBaiduSelectedFsIdsFromState(),
    ]));
    const rows = [];
    const folders = [];
    if (selectedIds.length) {
      if (typeof options.onProgress === 'function') {
        options.onProgress({ visible: true, percent: 0, indeterminate: true, text: `正在读取百度勾选项信息:${selectedIds.length} 项` });
      }
      const metas = await fetchBaiduFileMetas(selectedIds, bdstoken);
      for (const id of selectedIds) {
        const item = metas.get(String(id));
        if (!item) {
          continue;
        }
        const name = sanitizeCloudDirName(getBaiduItemName(item), '未命名');
        if (isBaiduDirectoryItem(item)) {
          folders.push({ baiduPath: String(item.path || ''), pathPrefix: name });
        } else {
          const etag = decodeBaiduMd5(item.md5 || item.file_md5 || item.etag || '');
          rows.push({
            path: name,
            etag,
            size: String(normalizeMiaochuanInteger(item.size) || 0),
            __gypSource: 'baidu-active',
            __gypProvider: 'baidu',
            __gypFsId: String(item.fs_id || id),
            __gypHasMd5: Boolean(normalizeMiaochuanMd5(etag)),
          });
        }
      }
    }

    if (!rows.length && !folders.length) {
      const names = extractBaiduSelectedFileNames();
      if (!names.length) {
        throw new Error('请先在百度网盘勾选要生成的文件或文件夹。');
      }
      const currentDir = getBaiduCurrentDir();
      const payload = await fetchBaiduListPage({ dir: currentDir, page: 1, num: 1000, bdstoken });
      const fuzzyMatch = (serverName, selectedName) => {
        const normalize = (text) => String(text || '').replace(/[\s\r\n]+/g, ' ').trim();
        const left = normalize(serverName);
        const right = normalize(selectedName);
        if (left === right) {
          return true;
        }
        const half = Math.floor(right.length / 2);
        return half > 3 && left === right.slice(0, half).trim();
      };
      for (const item of payload.list || []) {
        const name = getBaiduItemName(item);
        if (!names.some((selectedName) => fuzzyMatch(name, selectedName))) {
          continue;
        }
        const safeName = sanitizeCloudDirName(name, '未命名');
        if (isBaiduDirectoryItem(item)) {
          folders.push({ baiduPath: String(item.path || ''), pathPrefix: safeName });
        } else {
          const etag = decodeBaiduMd5(item.md5 || item.file_md5 || item.etag || '');
          rows.push({
            path: safeName,
            etag,
            size: String(normalizeMiaochuanInteger(item.size) || 0),
            __gypSource: 'baidu-active',
            __gypProvider: 'baidu',
            __gypFsId: String(item.fs_id || ''),
            __gypHasMd5: Boolean(normalizeMiaochuanMd5(etag)),
          });
        }
      }
    }

    for (const folder of folders) {
      rows.push(...(await collectBaiduDirectoryFiles(folder.baiduPath, folder.pathPrefix, bdstoken, options)));
    }
    if (!rows.length) {
      throw new Error('没有找到可生成的百度网盘文件。');
    }
    return setMiaochuanCapturedRowsFromProvider(rows, 'baidu-active');
  }

  function isXunleiPageHost() {
    return /^pan\.xunlei\.com$/i.test(window.location.hostname || '');
  }

  function isXunleiSharePage() {
    return isXunleiPageHost() && /^\/s\//i.test(window.location.pathname || '');
  }

  function getXunleiCachedAuth() {
    const headers = sanitizeHeaders(STATE.lastXunleiHeaders || {});
    return {
      token: String(headers.authorization || '').replace(/^Bearer\s+/i, '').trim(),
      deviceId: String(headers['x-device-id'] || '').trim(),
      captchaToken: String(headers['x-captcha-token'] || '').trim(),
      clientId: String(headers['x-client-id'] || XUNLEI_CLIENT_ID).trim() || XUNLEI_CLIENT_ID,
    };
  }

  function getXunleiRequestHeaders() {
    const auth = getXunleiCachedAuth();
    const headers = {
      Accept: 'application/json, text/plain, */*',
      'Content-Type': 'application/json',
      Referer: 'https://pan.xunlei.com/',
      'x-client-id': auth.clientId || XUNLEI_CLIENT_ID,
    };
    if (auth.token) {
      headers.Authorization = `Bearer ${auth.token}`;
    }
    if (auth.deviceId) {
      headers['x-device-id'] = auth.deviceId;
    }
    if (auth.captchaToken) {
      headers['x-captcha-token'] = auth.captchaToken;
    }
    return headers;
  }

  function assertXunleiAuthReady() {
    if (!getXunleiCachedAuth().token) {
      throw new Error('没有捕获到迅雷登录态。请在已登录的迅雷云盘页面刷新一次,等待文件列表加载完成或点击一次文件列表后再试。');
    }
  }

  function getXunleiStore() {
    const page = getPageWindowObject();
    const vueRoots = [
      document.querySelector("[class*='SourceListItem__item']"),
      document.querySelector("[class*='Share__']"),
      document.querySelector('#app'),
      document.querySelector('#root'),
      document.body,
    ].filter(Boolean);
    for (const el of vueRoots) {
      const store = el?.__vue__?.$store || el?.__vue__?.$root?.$store;
      if (store) {
        return store;
      }
    }
    return page?.$store || null;
  }

  function getXunleiSelectedIds() {
    const ids = new Set();
    if (isXunleiSharePage()) {
      const store = getXunleiStore();
      const share = store?.state?.share || {};
      for (const key of ['checkedFileIds', 'selectedFileIds', 'selectedIds', 'checkedIds', 'selected']) {
        const value = share[key];
        if (Array.isArray(value)) {
          value.forEach((id) => ids.add(String(id)));
        }
      }
      const listIds = Array.isArray(share.list) ? share.list : [];
      if (!ids.size && listIds.length) {
        const checks = Array.from(document.querySelectorAll("input[type='checkbox']"))
          .filter((cb) => !cb.closest("thead, th, [class*='header' i], [class*='Header']") && cb.getAttribute('aria-label') !== 'Select all');
        checks.forEach((cb, index) => {
          if (cb.checked && listIds[index]) {
            ids.add(String(listIds[index]));
          }
        });
      }
    } else {
      const items = document.querySelectorAll("[class*='SourceListItem__item']");
      for (const el of items) {
        const selected = el?.__vue__?.$props?.selected;
        if (Array.isArray(selected)) {
          selected.forEach((id) => ids.add(String(id)));
        }
      }
    }
    document.querySelectorAll("input[type='checkbox']:checked").forEach((cb) => {
      if (cb.closest("thead, th, [class*='header' i]")) {
        return;
      }
      let el = cb.parentElement;
      for (let depth = 0; depth < 15 && el; depth += 1) {
        const fileId = el.__vue__?.$props?.file?.id || el.__vue__?.$props?.id || el.__vue__?.$data?.file?.id || el.__vue__?.$data?.id || el.dataset?.id || el.dataset?.fileId || el.getAttribute('data-id');
        if (fileId) {
          ids.add(String(fileId));
          break;
        }
        el = el.parentElement;
      }
    });
    return Array.from(ids).filter(Boolean);
  }

  function getXunleiFileById(id) {
    const store = getXunleiStore();
    const state = store?.state || {};
    const maps = [
      state.drive?.all,
      state.share?.files,
      state.share?.fileMap,
    ];
    for (const map of maps) {
      if (map && typeof map === 'object' && !Array.isArray(map) && map[id]) {
        return map[id];
      }
    }
    const arrays = [state.share?.fileList, state.drive?.fileList, state.drive?.list].filter(Array.isArray);
    for (const arr of arrays) {
      const found = arr.find((item) => String(item?.id || '') === String(id));
      if (found) {
        return found;
      }
    }
    return null;
  }

  function normalizeXunleiFileRow(file, pathPrefix = '') {
    const name = sanitizeCloudDirName(file?.name || file?.file_name || file?.filename || '未命名', '未命名');
    const gcid = String(file?.hash || file?.gcid || '').trim().toUpperCase();
    return {
      path: `${pathPrefix || ''}/${name}`.replace(/\/{2,}/g, '/'),
      gcid,
      size: String(normalizeMiaochuanInteger(file?.size) || 0),
      __gypSource: isXunleiSharePage() ? 'xunlei-share-active' : 'xunlei-active',
      __gypProvider: 'xunlei',
      __gypFileId: String(file?.id || ''),
      __gypHasGcid: Boolean(/^[a-f0-9]{40}$/iu.test(gcid)),
    };
  }

  async function fetchXunleiFolderRows(parentId, pathPrefix = '', options = {}) {
    assertXunleiAuthReady();
    const rows = [];
    let pageToken = '';
    do {
      await waitForTaskControl(options.taskControl || null);
      if (typeof options.onProgress === 'function') {
        options.onProgress({
          visible: true,
          percent: 0,
          indeterminate: true,
              text: `正在读取迅雷目录:${String(pathPrefix || '/').replace(/^\/+/, '') || '/'} | 已收集 ${rows.length} 个文件`,
        });
      }
      const filters = encodeURIComponent(JSON.stringify({
        phase: { eq: 'PHASE_TYPE_COMPLETE' },
        trashed: { eq: false },
      }));
      const url = `${XUNLEI_API_BASE}/drive/v1/files?parent_id=${encodeURIComponent(parentId || '')}&usage=DISPLAY&filters=${filters}&with_audit=true&thumbnail_size=SIZE_SMALL&limit=100&page_token=${encodeURIComponent(pageToken)}`;
      const response = await requestMiaochuanJson(url, {
        method: 'GET',
        headers: getXunleiRequestHeaders(),
        timeout: 45000,
      });
      if (!response.ok) {
        throw new Error(`迅雷目录读取失败:${getErrorText(response.payload || response.text || `HTTP ${response.status}`)}`);
      }
      const files = Array.isArray(response.payload?.files) ? response.payload.files : [];
      captureMiaochuanSourcePayload(`xunlei-folder:${parentId}:${pageToken || 'first'}`, { parentId, pageToken }, response.payload);
      for (const file of files) {
        if (file?.kind === 'drive#folder') {
          rows.push(...(await fetchXunleiFolderRows(file.id, `${String(pathPrefix || '').replace(/^\/+/, '')}/${sanitizeCloudDirName(file.name, '未命名')}`.replace(/\/{2,}/g, '/').replace(/^\/+/, ''), options)));
        } else {
          const row = normalizeXunleiFileRow(file, pathPrefix);
          if (row.gcid) {
            rows.push(row);
          }
        }
      }
      pageToken = String(response.payload?.next_page_token || '');
      if (rows.length > SHARE_LINK_MAX_FILES) {
        throw new Error(`迅雷云盘文件过多,已超过安全上限 ${SHARE_LINK_MAX_FILES} 个文件。请进入更小的子目录后再生成。`);
      }
    } while (pageToken);
    return rows;
  }

  function getXunleiShareId() {
    const matched = String(window.location.pathname || '').match(/^\/s\/([^/?#]+)/i);
    return matched ? decodeUrlParam(matched[1]) : '';
  }

  function getCurrentXunleiShareIdFromStore() {
    const store = getXunleiStore();
    const info = store?.state?.share?.shareInfo || {};
    return String(info.shareId || info.share_id || info.id || getXunleiShareId() || '').trim();
  }

  function getXunleiPassCodeToken() {
    const store = getXunleiStore();
    return String(store?.state?.share?.shareInfo?.passCodeToken || '').trim();
  }

  async function fetchXunleiShareRootFiles(shareId, passCodeToken) {
    const url = `${XUNLEI_API_BASE}/drive/v1/share/detail?share_id=${encodeURIComponent(shareId)}&parent_id=&pass_code_token=${encodeURIComponent(passCodeToken)}&limit=100&thumbnail_size=SIZE_SMALL`;
    const response = await requestMiaochuanJson(url, {
      method: 'GET',
      headers: getXunleiRequestHeaders(),
      timeout: 45000,
    });
    if (!response.ok) {
      throw new Error(`迅雷分享根目录读取失败:${getErrorText(response.payload || response.text || `HTTP ${response.status}`)}`);
    }
    return Array.isArray(response.payload?.files) ? response.payload.files : [];
  }

  async function fetchXunleiShareFolderRows(shareId, parentId, passCodeToken, pathPrefix = '', options = {}) {
    assertXunleiAuthReady();
    const rows = [];
    let pageToken = '';
    do {
      await waitForTaskControl(options.taskControl || null);
      if (typeof options.onProgress === 'function') {
        options.onProgress({
          visible: true,
          percent: 0,
          indeterminate: true,
              text: `正在读取迅雷分享目录:${String(pathPrefix || '/').replace(/^\/+/, '') || '/'} | 已收集 ${rows.length} 个文件`,
        });
      }
      const url = `${XUNLEI_API_BASE}/drive/v1/share/detail?share_id=${encodeURIComponent(shareId)}&parent_id=${encodeURIComponent(parentId || '')}&pass_code_token=${encodeURIComponent(passCodeToken)}&limit=100&page_token=${encodeURIComponent(pageToken)}&thumbnail_size=SIZE_SMALL`;
      const response = await requestMiaochuanJson(url, {
        method: 'GET',
        headers: getXunleiRequestHeaders(),
        timeout: 45000,
      });
      if (!response.ok) {
        throw new Error(`迅雷分享目录读取失败:${getErrorText(response.payload || response.text || `HTTP ${response.status}`)}`);
      }
      const files = Array.isArray(response.payload?.files) ? response.payload.files : [];
      captureMiaochuanSourcePayload(`xunlei-share:${shareId}:${parentId}:${pageToken || 'first'}`, { parentId, pageToken }, response.payload);
      for (const file of files) {
        if (file?.kind === 'drive#folder') {
          rows.push(...(await fetchXunleiShareFolderRows(shareId, file.id, passCodeToken, `${String(pathPrefix || '').replace(/^\/+/, '')}/${sanitizeCloudDirName(file.name, '未命名')}`.replace(/\/{2,}/g, '/').replace(/^\/+/, ''), options)));
        } else {
          const row = normalizeXunleiFileRow(file, pathPrefix);
          if (row.gcid) {
            rows.push(row);
          }
        }
      }
      pageToken = String(response.payload?.next_page_token || '');
      if (rows.length > SHARE_LINK_MAX_FILES) {
        throw new Error(`迅雷分享文件过多,已超过安全上限 ${SHARE_LINK_MAX_FILES} 个文件。请进入更小的子目录后再生成。`);
      }
    } while (pageToken);
    return rows;
  }

  async function fetchXunleiShareRowsFromShareInfo(shareInfo = {}, options = {}) {
    const shareId = String(shareInfo.shareId || '').trim();
    if (!shareId) {
      throw new Error('没有拿到迅雷分享 ID。');
    }
    assertXunleiAuthReady();
    const currentShareId = getCurrentXunleiShareIdFromStore();
    const passCodeToken = getXunleiPassCodeToken();
    if (!passCodeToken || (currentShareId && currentShareId !== shareId && getXunleiShareId() !== shareId)) {
      throw new Error('迅雷分享直读需要先打开并加载这个迅雷分享页一次,再回到面板点“读取链接目录”。');
    }
    const rootFiles = await fetchXunleiShareRootFiles(shareId, passCodeToken);
    const rows = [];
    for (const file of rootFiles) {
      await waitForTaskControl(options.taskControl || null);
      if (file?.kind === 'drive#folder') {
        rows.push(...(await fetchXunleiShareFolderRows(shareId, file.id, passCodeToken, sanitizeCloudDirName(file.name, '未命名'), options)));
      } else {
        const row = normalizeXunleiFileRow(file);
        if (row.gcid) {
          rows.push(row);
        }
      }
      if (rows.length > SHARE_LINK_MAX_FILES) {
        throw new Error(`迅雷分享文件过多,已超过安全上限 ${SHARE_LINK_MAX_FILES} 个文件。请进入更小的子目录后再生成。`);
      }
    }
    if (!rows.length) {
      throw new Error('迅雷分享里没有可生成的文件(文件夹为空或文件没有 GCID)。');
    }
    setMiaochuanCapturedRowsFromProvider(rows, `xunlei-share:${shareId}`);
    return rows;
  }

  async function collectXunleiSelectedRows(options = {}) {
    if (!isXunleiPageHost()) {
      throw new Error('当前页面不是迅雷云盘。');
    }
    assertXunleiAuthReady();
    const selectedIds = getXunleiSelectedIds();
    if (!selectedIds.length) {
      throw new Error('请先在迅雷云盘勾选要生成的文件或文件夹。');
    }
    const rows = [];
    if (isXunleiSharePage()) {
      const shareId = getXunleiShareId();
      const passCodeToken = getXunleiPassCodeToken();
      if (!shareId || !passCodeToken) {
        throw new Error('没有拿到迅雷分享 ID 或 pass_code_token,请等待分享页加载完成后重试。');
      }
      let rootFiles = null;
      for (const id of selectedIds) {
        let file = getXunleiFileById(id);
        if (!file) {
          rootFiles = rootFiles || await fetchXunleiShareRootFiles(shareId, passCodeToken);
          file = rootFiles.find((item) => String(item?.id || '') === String(id));
        }
        if (!file) {
          continue;
        }
        if (file.kind === 'drive#folder') {
          rows.push(...(await fetchXunleiShareFolderRows(shareId, file.id, passCodeToken, sanitizeCloudDirName(file.name, '未命名'), options)));
        } else {
          const row = normalizeXunleiFileRow(file);
          if (row.gcid) {
            rows.push(row);
          }
        }
      }
    } else {
      for (const id of selectedIds) {
        const file = getXunleiFileById(id);
        if (!file) {
          continue;
        }
        if (file.kind === 'drive#folder') {
          rows.push(...(await fetchXunleiFolderRows(file.id, sanitizeCloudDirName(file.name, '未命名'), options)));
        } else {
          const row = normalizeXunleiFileRow(file);
          if (row.gcid) {
            rows.push(row);
          }
        }
      }
    }
    if (!rows.length) {
      throw new Error('所选迅雷项目中没有可生成的文件(文件夹为空或文件没有 GCID)。');
    }
    return setMiaochuanCapturedRowsFromProvider(rows, isXunleiSharePage() ? 'xunlei-share-active' : 'xunlei-active');
  }

  function quarkApiUrl(path, params = {}) {
    const url = new URL(`https://drive-pc.quark.cn${path}`);
    const baseParams = {
      pr: 'ucpro',
      fr: 'pc',
      uc_param_str: '',
      ...params,
    };
    for (const [key, value] of Object.entries(baseParams)) {
      if (value == null) {
        continue;
      }
      url.searchParams.set(key, String(value));
    }
    return url.toString();
  }

  function quarkUcApiUrl(path, params = {}) {
    const url = new URL(`https://pc-api.uc.cn${path}`);
    for (const [key, value] of Object.entries(params || {})) {
      if (value == null) {
        continue;
      }
      url.searchParams.set(key, String(value));
    }
    return url.toString();
  }

  function getQuarkRequestHeaders(cookie, options = {}) {
    const headers = {
      Accept: 'application/json, text/plain, */*',
      Referer: 'https://pan.quark.cn/',
      Origin: 'https://pan.quark.cn',
      'User-Agent': QUARK_PC_USER_AGENT,
      ...(options.json === false ? {} : { 'Content-Type': 'application/json;charset=utf-8' }),
      ...(options.extra || {}),
    };
    if (cookie) {
      headers.Cookie = cookie;
    }
    return headers;
  }

  async function requestMiaochuanJson(url, options = {}) {
    const method = String(options.method || 'GET').toUpperCase();
    const headers = options.headers || {};
    const data = options.body == null
      ? undefined
      : typeof options.body === 'string'
        ? options.body
        : JSON.stringify(options.body);

    return new Promise((resolve, reject) => {
      if (typeof GM_xmlhttpRequest !== 'function') {
        reject(new Error(`未检测到 GM_xmlhttpRequest 权限 | ${method} ${url}`));
        return;
      }
      try {
        GM_xmlhttpRequest({
          method,
          url: String(url),
          headers,
          data,
          timeout: options.timeout || 30000,
          anonymous: false,
          onload: (res) => {
            const text = typeof res.responseText === 'string' ? res.responseText : '';
            resolve({
              ok: res.status >= 200 && res.status < 300,
              status: res.status,
              text,
              payload: safeJsonParse(text),
            });
          },
          onerror: (err) => reject(new Error(`秒传接口请求异常:${getErrorText(err) || '未知错误'} | ${method} ${url}`)),
          ontimeout: () => reject(new Error(`秒传接口请求超时 | ${method} ${url}`)),
          onabort: () => reject(new Error(`秒传接口请求被中止 | ${method} ${url}`)),
        });
      } catch (err) {
        reject(new Error(`${getErrorText(err) || '秒传接口调用失败'} | ${method} ${url}`));
      }
    });
  }

  function setMiaochuanCapturedRowsFromShareRows(rows, sourceKey) {
    return setMiaochuanCapturedRowsFromProvider(rows, sourceKey || 'share-link');
  }

  function get123PanRequestHeaders(shareKey = '', options = {}) {
    const headers = {
      Accept: 'application/json, text/plain, */*',
      Referer: shareKey ? `https://www.123pan.com/s/${encodeURIComponent(shareKey)}` : 'https://www.123pan.com/',
      Origin: 'https://www.123pan.com',
      Platform: 'web',
      'App-Version': '3',
      ...(options.json === false ? {} : { 'Content-Type': 'application/json;charset=UTF-8' }),
      ...(options.extra || {}),
    };
    return headers;
  }

  function get123PanItemName(item) {
    return chooseBestNameCandidate([
      item?.FileName,
      item?.filename,
      item?.fileName,
      item?.Name,
      item?.name,
    ]);
  }

  function get123PanItemId(item) {
    return String(item?.FileId ?? item?.fileId ?? item?.id ?? item?.Id ?? '').trim();
  }

  function get123PanItemSize(item) {
    const size = normalizeMiaochuanInteger(item?.Size ?? item?.BaseSize ?? item?.LiveSize ?? item?.size ?? item?.fileSize);
    return size == null ? 0 : size;
  }

  function is123PanDirectoryItem(item) {
    if (!item || typeof item !== 'object') {
      return false;
    }
    const type = item.Type ?? item.type ?? item.FileType ?? item.fileType;
    if (String(type) === '1') {
      return true;
    }
    const bool = normalizeBooleanish(item.isFolder ?? item.IsFolder ?? item.dir ?? item.is_dir);
    if (bool != null) {
      return bool;
    }
    const typeText = String(item.kind ?? item.categoryText ?? item.ContentType ?? '').toLowerCase();
    return /folder|directory|dir/u.test(typeText);
  }

  function extract123PanShareList(payload) {
    const data = payload && typeof payload === 'object' ? (payload.data || payload.Data || payload) : {};
    const list = data.InfoList || data.infoList || data.list || data.items || [];
    return Array.isArray(list) ? list : [];
  }

  function extract123PanShareNext(payload) {
    const data = payload && typeof payload === 'object' ? (payload.data || payload.Data || payload) : {};
    return String(data.Next ?? data.next ?? data.NextPage ?? data.nextPage ?? '').trim();
  }

  async function fetch123PanShareGetPage({ shareKey, parentFileId = '0', page = 1, next = '0', limit = 100, passcode = '' }) {
    const url = new URL('https://www.123pan.com/b/api/share/get');
    const params = {
      limit,
      next,
      orderBy: 'share_id',
      orderDirection: 'desc',
      shareKey,
      ParentFileId: parentFileId || '0',
      Page: page,
      operateType: 1,
    };
    if (passcode) {
      params.SharePwd = passcode;
    }
    Object.entries(params).forEach(([key, value]) => {
      if (value != null && String(value) !== '') {
        url.searchParams.set(key, String(value));
      }
    });
    const response = await requestMiaochuanJson(url.toString(), {
      method: 'GET',
      headers: get123PanRequestHeaders(shareKey, { json: false }),
      timeout: 45000,
    });
    const code = response.payload && typeof response.payload === 'object' ? String(response.payload.code ?? response.payload.Code ?? '0') : '0';
    if (!response.ok || code !== '0') {
      const passwordHint = passcode ? '' : ';如果这个分享需要提取码,请填入提取码后重试';
      throw new Error(`123 网盘目录读取失败:${getErrorText(response.payload || response.text || `HTTP ${response.status}`)}${passwordHint}`);
    }
    return response.payload;
  }

  async function fetch123PanShareRowsFromShareInfo(shareInfo = {}, options = {}) {
    const shareKey = String(shareInfo.shareKey || '').trim();
    if (!shareKey) {
      throw new Error('没有拿到 123 网盘分享 Key。');
    }
    const passcode = String(shareInfo.passcode || '').trim();
    const rows = [];
    const queue = [{ fileId: '0', path: '' }];
    const visited = new Set();
    const pageSize = 100;
    const maxDirs = SHARE_LINK_MAX_DIRS;
    const maxFiles = SHARE_LINK_MAX_FILES;

    while (queue.length) {
      const current = queue.shift();
      const dirId = String(current.fileId || '0');
      const visitKey = `${dirId}:${current.path || ''}`;
      if (visited.has(visitKey)) {
        continue;
      }
      visited.add(visitKey);
      if (visited.size > maxDirs) {
        throw new Error(`123 网盘目录过多,已超过安全上限 ${maxDirs} 个目录。请进入更小的子目录后再生成。`);
      }

      let page = 1;
      let next = '0';
      while (page <= 500) {
        if (typeof options.onProgress === 'function') {
          options.onProgress({
            visible: true,
            percent: 0,
            indeterminate: true,
            text: `正在读取 123 网盘目录:${current.path || '/'} | 已收集 ${rows.length} 个文件`,
          });
        }
        const payload = await fetch123PanShareGetPage({
          shareKey,
          parentFileId: dirId,
          page,
          next,
          limit: pageSize,
          passcode,
        });
        const list = extract123PanShareList(payload);
        captureMiaochuanSourcePayload(`123pan-share:${shareKey}:${dirId}:${page}`, { ParentFileId: dirId, Page: page, next }, payload);

        for (const item of list) {
          const name = sanitizeCloudDirName(get123PanItemName(item), '未命名');
          const itemPath = `${current.path}/${name}`.replace(/\/{2,}/g, '/');
          if (is123PanDirectoryItem(item)) {
            const fileId = get123PanItemId(item);
            if (fileId) {
              queue.push({ fileId, path: itemPath });
            }
            continue;
          }
          const etag = normalizeMiaochuanMd5(item?.Etag || item?.etag || item?.MD5 || item?.md5 || item?.Hash || item?.hash || '');
          rows.push({
            path: itemPath,
            etag,
            size: String(get123PanItemSize(item)),
            __gypSource: '123pan-share-active',
            __gypProvider: '123pan',
            __gypFileId: get123PanItemId(item),
            __gypHasMd5: Boolean(etag),
          });
          if (rows.length > maxFiles) {
            throw new Error(`123 网盘文件过多,已超过安全上限 ${maxFiles} 个文件。请进入更小的子目录后再生成。`);
          }
        }

        const nextValue = extract123PanShareNext(payload);
        if (!list.length || !nextValue || nextValue === '-1') {
          break;
        }
        if (nextValue === next && list.length < pageSize) {
          break;
        }
        next = nextValue;
        page += 1;
      }
    }

    setMiaochuanCapturedRowsFromShareRows(rows, `123pan-share:${shareKey}`);
    return rows;
  }

  function getTianyiRequestHeaders(shareCode = '', options = {}) {
    return {
      Accept: 'application/json;charset=UTF-8',
      Referer: shareCode ? `https://cloud.189.cn/web/share?code=${encodeURIComponent(shareCode)}` : 'https://cloud.189.cn/web/',
      Origin: 'https://cloud.189.cn',
      'Sign-Type': '1',
      ...(options.form ? { 'Content-Type': 'application/x-www-form-urlencoded' } : {}),
      ...(options.extra || {}),
    };
  }

  function assertTianyiSuccess(response, action) {
    const payload = response.payload;
    const code = payload && typeof payload === 'object' ? String(payload.res_code ?? payload.resCode ?? payload.code ?? '0') : '0';
    if (!response.ok || code !== '0') {
      throw new Error(`${action}失败:${getErrorText(payload || response.text || `HTTP ${response.status}`)}`);
    }
    return payload && typeof payload === 'object' ? payload : {};
  }

  function getTianyiItemName(item) {
    return chooseBestNameCandidate([
      item?.name,
      item?.fileName,
      item?.filename,
      item?.file_name,
    ]);
  }

  function getTianyiItemId(item) {
    return String(item?.id ?? item?.fileId ?? item?.fileID ?? item?.file_id ?? '').trim();
  }

  function getTianyiItemSize(item) {
    const size = normalizeMiaochuanInteger(item?.size ?? item?.fileSize ?? item?.file_size ?? item?.bytes);
    return size == null ? 0 : size;
  }

  async function fetchTianyiShareInfo(shareCode) {
    const body = new URLSearchParams({ shareCode }).toString();
    const response = await requestMiaochuanJson('https://cloud.189.cn/api/open/share/getShareInfoByCodeV2.action', {
      method: 'POST',
      headers: getTianyiRequestHeaders(shareCode, { form: true }),
      body,
      timeout: 45000,
    });
    return assertTianyiSuccess(response, '天翼云盘分享信息读取');
  }

  async function fetchTianyiShareId({ shareCode, passcode = '', shareInfo = {} }) {
    const direct = String(shareInfo.shareId ?? shareInfo.shareID ?? shareInfo.ShareId ?? '').trim();
    if (direct) {
      return direct;
    }
    const needAccessCode = String(shareInfo.needAccessCode ?? shareInfo.need_access_code ?? '') === '1'
      || shareInfo.needAccessCode === true;
    const accessCode = String(passcode || shareInfo.accessCode || shareInfo.access_code || '').trim();
    if (needAccessCode && !accessCode) {
      throw new Error('这个天翼云盘分享需要访问码,请填写访问码后重试。');
    }
    const url = new URL('https://cloud.189.cn/api/open/share/checkAccessCode.action');
    url.searchParams.set('shareCode', shareCode);
    url.searchParams.set('accessCode', accessCode);
    const response = await requestMiaochuanJson(url.toString(), {
      method: 'GET',
      headers: getTianyiRequestHeaders(shareCode, { json: false }),
      timeout: 45000,
    });
    const payload = assertTianyiSuccess(response, '天翼云盘访问码校验');
    const shareId = String(payload.shareId ?? payload.shareID ?? payload.data?.shareId ?? '').trim();
    if (!shareId) {
      throw new Error('天翼云盘访问码校验成功,但没有返回 shareId。');
    }
    return shareId;
  }

  async function fetchTianyiShareDirPage({ shareCode, shareId, shareMode = 1, fileId, pageNum = 1, pageSize = 100, accessCode = '' }) {
    const url = new URL('https://cloud.189.cn/api/open/share/listShareDir.action');
    const params = {
      pageNum,
      pageSize,
      fileId,
      shareDirFileId: fileId,
      isFolder: true,
      shareId,
      shareMode: shareMode || 1,
      iconOption: 5,
      orderBy: 'lastOpTime',
      descending: true,
      accessCode,
    };
    Object.entries(params).forEach(([key, value]) => {
      if (value != null && String(value) !== '') {
        url.searchParams.set(key, String(value));
      }
    });
    const response = await requestMiaochuanJson(url.toString(), {
      method: 'GET',
      headers: getTianyiRequestHeaders(shareCode, { json: false }),
      timeout: 45000,
    });
    return assertTianyiSuccess(response, '天翼云盘目录读取');
  }

  function extractTianyiFileListAO(payload) {
    const root = payload && typeof payload === 'object' ? (payload.fileListAO || payload.data?.fileListAO || payload.data || payload) : {};
    return root && typeof root === 'object' ? root : {};
  }

  async function fetchTianyiShareRowsFromShareInfo(shareInfo = {}, options = {}) {
    const shareCode = String(shareInfo.shareCode || '').trim();
    if (!shareCode) {
      throw new Error('没有拿到天翼云盘分享 Code。');
    }
    const passcode = String(shareInfo.passcode || '').trim();
    const infoPayload = await fetchTianyiShareInfo(shareCode);
    const info = infoPayload.data && typeof infoPayload.data === 'object' ? infoPayload.data : infoPayload;
    const shareId = await fetchTianyiShareId({ shareCode, passcode, shareInfo: info });
    const accessCode = String(passcode || info.accessCode || info.access_code || '').trim();
    const shareMode = normalizeMiaochuanInteger(info.shareMode ?? info.share_mode) || 1;
    const shareName = sanitizeCloudDirName(info.fileName || info.name || '天翼云盘分享', '天翼云盘分享');
    const rows = [];
    const isRootFolder = normalizeBooleanish(info.isFolder ?? info.is_folder);
    if (isRootFolder === false) {
      const etag = normalizeMiaochuanMd5(info.md5 || info.fileMd5 || info.file_md5 || info.etag || '');
      const filePath = `/${shareName}`.replace(/\/{2,}/g, '/');
      rows.push({
        path: filePath,
        etag,
        size: String(getTianyiItemSize(info)),
        __gypSource: 'tianyiyun-share-active',
        __gypProvider: 'tianyiyun',
        __gypFileId: getTianyiItemId(info) || String(info.fileId || ''),
        __gypHasMd5: Boolean(etag),
      });
      setMiaochuanCapturedRowsFromShareRows(rows, `tianyiyun-share:${shareCode}`);
      return rows;
    }

    const rootFileId = String(info.fileId ?? info.id ?? '').trim();
    if (!rootFileId) {
      throw new Error('天翼云盘分享信息里没有根目录 fileId。');
    }
    const queue = [{ fileId: rootFileId, path: `/${shareName}`.replace(/\/{2,}/g, '/') }];
    const visited = new Set();
    const pageSize = 100;
    const maxDirs = SHARE_LINK_MAX_DIRS;
    const maxFiles = SHARE_LINK_MAX_FILES;

    while (queue.length) {
      const current = queue.shift();
      const dirId = String(current.fileId || '');
      if (!dirId || visited.has(dirId)) {
        continue;
      }
      visited.add(dirId);
      if (visited.size > maxDirs) {
        throw new Error(`天翼云盘目录过多,已超过安全上限 ${maxDirs} 个目录。请进入更小的子目录后再生成。`);
      }

      let pageNum = 1;
      while (pageNum <= 500) {
        if (typeof options.onProgress === 'function') {
          options.onProgress({
            visible: true,
            percent: 0,
            indeterminate: true,
            text: `正在读取天翼云盘目录:${current.path || '/'} | 已收集 ${rows.length} 个文件`,
          });
        }
        const payload = await fetchTianyiShareDirPage({
          shareCode,
          shareId,
          shareMode,
          fileId: dirId,
          pageNum,
          pageSize,
          accessCode,
        });
        const fileListAO = extractTianyiFileListAO(payload);
        const folders = Array.isArray(fileListAO.folderList) ? fileListAO.folderList : [];
        const files = Array.isArray(fileListAO.fileList) ? fileListAO.fileList : [];
        const total = normalizeMiaochuanInteger(fileListAO.count ?? fileListAO.total ?? fileListAO.totalCount) || 0;
        captureMiaochuanSourcePayload(`tianyiyun-share:${shareCode}:${dirId}:${pageNum}`, { fileId: dirId, pageNum }, payload);

        for (const folder of folders) {
          const name = sanitizeCloudDirName(getTianyiItemName(folder), '未命名');
          const fileId = getTianyiItemId(folder);
          if (fileId) {
            queue.push({
              fileId,
              path: `${current.path}/${name}`.replace(/\/{2,}/g, '/'),
            });
          }
        }
        for (const file of files) {
          const name = sanitizeCloudDirName(getTianyiItemName(file), '未命名');
          const etag = normalizeMiaochuanMd5(file?.md5 || file?.fileMd5 || file?.file_md5 || file?.etag || '');
          rows.push({
            path: `${current.path}/${name}`.replace(/\/{2,}/g, '/'),
            etag,
            size: String(getTianyiItemSize(file)),
            __gypSource: 'tianyiyun-share-active',
            __gypProvider: 'tianyiyun',
            __gypFileId: getTianyiItemId(file),
            __gypHasMd5: Boolean(etag),
          });
          if (rows.length > maxFiles) {
            throw new Error(`天翼云盘文件过多,已超过安全上限 ${maxFiles} 个文件。请进入更小的子目录后再生成。`);
          }
        }

        const itemCount = folders.length + files.length;
        if (!itemCount || (total > 0 && pageNum * pageSize >= total) || itemCount < pageSize) {
          break;
        }
        pageNum += 1;
      }
    }

    setMiaochuanCapturedRowsFromShareRows(rows, `tianyiyun-share:${shareCode}`);
    return rows;
  }

  function extractQuarkStoken(payload) {
    return String(
      findFirstValueByKeys(payload, ['stoken', 'share_token', 'shareToken', 'token'])
      || ''
    ).trim();
  }

  async function getQuarkShareToken(pwdId, passcode = '', cookie = '') {
    const response = await requestMiaochuanJson(
      quarkUcApiUrl('/1/clouddrive/share/sharepage/token'),
      {
        method: 'POST',
        headers: getQuarkRequestHeaders(cookie),
        body: {
          pwd_id: pwdId,
          passcode: passcode || '',
        },
      }
    );
    const stoken = extractQuarkStoken(response.payload);
    if (!response.ok || !stoken) {
      const cookieHint = cookie ? '' : ';如果当前登录态不能跨域使用,请粘贴夸克请求 Cookie 后重试';
      throw new Error(`夸克分享 token 获取失败:${getErrorText(response.payload || response.text || `HTTP ${response.status}`)}${cookieHint}`);
    }
    return stoken;
  }

  function extractQuarkShareList(payload) {
    const data = payload && typeof payload === 'object' ? (payload.data || payload) : {};
    const list = data.list || data.file_list || data.fileList || data.items || data.records || [];
    return Array.isArray(list) ? list : [];
  }

  function extractQuarkShareTotal(payload) {
    const value = findFirstValueByKeys(payload, ['total', 'totalCount', 'total_count', 'count']);
    const total = normalizeMiaochuanInteger(value);
    return total == null ? 0 : total;
  }

  function getQuarkItemName(item) {
    return chooseBestNameCandidate([
      item?.file_name,
      item?.fileName,
      item?.name,
      item?.title,
      item?.server_filename,
      item?.filename,
    ]);
  }

  function getQuarkItemFid(item) {
    return String(
      item?.fid
      || item?.file_id
      || item?.fileId
      || item?.share_fid
      || item?.shareFileId
      || item?.id
      || ''
    ).trim();
  }

  function getQuarkItemFidToken(item) {
    return String(
      item?.share_fid_token
      || item?.fid_token
      || item?.fidToken
      || item?.file_token
      || item?.fileToken
      || item?.token
      || ''
    ).trim();
  }

  function getQuarkItemSize(item) {
    const size = normalizeMiaochuanInteger(item?.size ?? item?.file_size ?? item?.fileSize ?? item?.bytes);
    return size == null ? 0 : size;
  }

  function isQuarkDirectoryItem(item) {
    if (!item || typeof item !== 'object') {
      return false;
    }
    const bool = normalizeBooleanish(item.dir ?? item.isdir ?? item.is_dir ?? item.isDir ?? item.folder ?? item.is_folder ?? item.isFolder);
    if (bool != null) {
      return bool;
    }
    const typeText = String(item.type ?? item.kind ?? item.file_type ?? item.fileType ?? item.obj_category ?? item.category ?? '').toLowerCase();
    if (/dir|folder|directory/u.test(typeText)) {
      return true;
    }
    if (/file|video|audio|image|doc/u.test(typeText)) {
      return false;
    }
    const name = getQuarkItemName(item);
    if (item.dir === 1 || item.file_type === 0 || item.fileType === 0) {
      return !getExt(name);
    }
    return false;
  }

  async function fetchQuarkShareDetailPage({ pwdId, stoken, pdirFid = '0', page = 1, size = 200, cookie = '' }) {
    const response = await requestMiaochuanJson(
      quarkUcApiUrl('/1/clouddrive/share/sharepage/detail', {
        pwd_id: pwdId,
        stoken,
        pdir_fid: pdirFid || '0',
        force: 0,
        _page: page,
        _size: size,
        _fetch_banner: 0,
        _fetch_share: 0,
        _fetch_total: 1,
        sort: 'file_type:asc,file_name:asc',
        pr: 'ucpro',
        fr: 'pc',
      }),
      {
        method: 'GET',
        headers: getQuarkRequestHeaders(cookie, { json: false }),
      }
    );
    if (!response.ok || !isProbablySuccess(response.payload, response)) {
      throw new Error(`夸克目录读取失败:${getErrorText(response.payload || response.text || `HTTP ${response.status}`)}`);
    }
    return response.payload;
  }

  function collectQuarkDownloadInfoObjects(node, out = [], seen = new WeakSet(), depth = 0) {
    if (!node || typeof node !== 'object' || depth > 8) {
      return out;
    }
    if (seen.has(node)) {
      return out;
    }
    seen.add(node);
    if (Array.isArray(node)) {
      for (const item of node) {
        collectQuarkDownloadInfoObjects(item, out, seen, depth + 1);
      }
      return out;
    }
    const fid = getMiaochuanFieldValue(node, ['fid', 'file_id', 'fileId', 'share_fid', 'shareFileId', 'id']);
    const hash = getMiaochuanFieldValue(node, ['md5', 'file_md5', 'fileMd5', 'hash', 'etag', 'content_hash', 'contentHash']);
    if (fid.key || hash.key) {
      out.push(node);
    }
    for (const value of Object.values(node)) {
      if (value && typeof value === 'object') {
        collectQuarkDownloadInfoObjects(value, out, seen, depth + 1);
      }
    }
    return out;
  }

  function getQuarkDownloadInfoMd5(item) {
    const hash = getMiaochuanFieldValue(item, ['md5', 'file_md5', 'fileMd5', 'hash', 'etag', 'content_hash', 'contentHash']);
    return hash.key ? normalizeMiaochuanMd5(hash.value) : '';
  }

  function getQuarkDownloadInfoFid(item) {
    const fid = getMiaochuanFieldValue(item, ['fid', 'file_id', 'fileId', 'share_fid', 'shareFileId', 'id']);
    return fid.key ? String(fid.value || '').trim() : '';
  }

  async function fetchQuarkShareMd5Map(rows, { pwdId, stoken, cookie = '', onProgress } = {}) {
    const map = new Map();
    const files = (rows || []).filter((row) => getQuarkItemFid(row));
    const batchSize = 50;
    for (let offset = 0; offset < files.length; offset += batchSize) {
      const chunk = files.slice(offset, offset + batchSize);
      if (typeof onProgress === 'function') {
        onProgress({
          visible: true,
          percent: files.length ? Math.min(95, Math.round((offset / files.length) * 100)) : 0,
          indeterminate: false,
          text: `正在向夸克请求真实 MD5:${Math.min(offset + chunk.length, files.length)}/${files.length}`,
        });
      }
      const fids = chunk.map((row) => getQuarkItemFid(row));
      const fidsToken = chunk.map((row) => getQuarkItemFidToken(row));
      const response = await requestMiaochuanJson(
        quarkUcApiUrl('/1/clouddrive/file/download', {
          pr: 'ucpro',
          fr: 'pc',
          uc_param_str: '',
          __dt: Math.floor(Math.random() * 900 + 100),
          __t: Date.now(),
        }),
        {
          method: 'POST',
          headers: getQuarkRequestHeaders(cookie),
          body: {
            fids,
            pwd_id: pwdId,
            stoken,
            fids_token: fidsToken,
          },
          timeout: 45000,
        }
      );
      if (!response.ok || !isProbablySuccess(response.payload, response)) {
        throw new Error(`夸克 MD5 获取失败:${getErrorText(response.payload || response.text || `HTTP ${response.status}`)}`);
      }
      const infoList = collectQuarkDownloadInfoObjects(response.payload);
      const md5InfoList = infoList.filter((info) => getQuarkDownloadInfoMd5(info));
      const usedIndexes = new Set();
      for (const info of md5InfoList) {
        const md5 = getQuarkDownloadInfoMd5(info);
        if (!md5) {
          continue;
        }
        const fid = getQuarkDownloadInfoFid(info);
        if (fid) {
          map.set(fid, md5);
          continue;
        }
        const idx = md5InfoList.indexOf(info);
        if (chunk[idx] && !usedIndexes.has(idx)) {
          map.set(getQuarkItemFid(chunk[idx]), md5);
          usedIndexes.add(idx);
        }
      }
      for (let index = 0; index < chunk.length; index += 1) {
        const fid = getQuarkItemFid(chunk[index]);
        if (map.has(fid)) {
          continue;
        }
        const info = md5InfoList[index];
        const md5 = info ? getQuarkDownloadInfoMd5(info) : '';
        if (md5) {
          map.set(fid, md5);
        }
      }
    }
    return map;
  }

  async function fetchQuarkShareRowsFromShareInfo(shareInfo = {}, options = {}) {
    const pwdId = String(shareInfo.pwdId || '').trim();
    if (!pwdId) {
      throw new Error('没有拿到夸克分享 ID。');
    }
    const cookie = String(shareInfo.cookie || '').trim();
    const passcode = String(shareInfo.passcode || '').trim();
    const stoken = await getQuarkShareToken(pwdId, passcode, cookie);
    const rows = [];
    const queue = [{ fid: '0', path: '' }];
    const visited = new Set();
    const pageSize = 200;
    const maxDirs = SHARE_LINK_MAX_DIRS;
    const maxFiles = SHARE_LINK_MAX_FILES;

    while (queue.length) {
      const current = queue.shift();
      const dirFid = String(current.fid || '0');
      if (visited.has(dirFid)) {
        continue;
      }
      visited.add(dirFid);
      if (visited.size > maxDirs) {
        throw new Error(`夸克目录过多,已超过安全上限 ${maxDirs} 个目录。请进入更小的子目录后再生成。`);
      }

      let page = 1;
      let total = 0;
      let observedPageSize = 0;
      while (page <= 50) {
        if (typeof options.onProgress === 'function') {
          options.onProgress({
            visible: true,
            percent: 0,
            indeterminate: true,
            text: `正在读取夸克目录:${current.path || '/'} | 已读目录 ${visited.size}/${maxDirs} | 待读目录 ${queue.length} | 已收集 ${rows.length} 个文件`,
          });
        }
        const payload = await fetchQuarkShareDetailPage({ pwdId, stoken, pdirFid: dirFid, page, size: pageSize, cookie });
        const list = extractQuarkShareList(payload);
        total = extractQuarkShareTotal(payload);
        observedPageSize = Math.max(observedPageSize, list.length || 0);
        captureMiaochuanSourcePayload(`quark-share:${pwdId}:${dirFid}:${page}`, { pdir_fid: dirFid, page }, payload);

        for (const item of list) {
          const name = sanitizeCloudDirName(getQuarkItemName(item), '未命名');
          const itemPath = `${current.path}/${name}`.replace(/\/{2,}/g, '/');
          if (isQuarkDirectoryItem(item)) {
            const fid = getQuarkItemFid(item);
            if (fid) {
              queue.push({ fid, path: itemPath });
            }
            continue;
          }
          rows.push({
            ...item,
            __gypPath: itemPath,
            __gypFid: getQuarkItemFid(item),
            __gypFidToken: getQuarkItemFidToken(item),
            __gypSource: 'quark-share-active',
          });
          if (rows.length > maxFiles) {
            throw new Error(`夸克文件过多,已超过安全上限 ${maxFiles} 个文件。请进入更小的子目录后再生成。`);
          }
        }

        const effectivePageSize = observedPageSize || pageSize;
        const fetchedEnough = total > 0 && page * effectivePageSize >= total;
        const likelyLastPageWithoutTotal = total <= 0 && page > 1 && list.length > 0 && list.length < effectivePageSize;
        if (!list.length || fetchedEnough || likelyLastPageWithoutTotal) {
          break;
        }
        page += 1;
      }
    }

    const md5Map = rows.length
      ? await fetchQuarkShareMd5Map(rows, {
          pwdId,
          stoken,
          cookie,
          onProgress: options.onProgress,
        })
      : new Map();
    const enrichedRows = rows.map((row) => {
      const fid = getQuarkItemFid(row);
      const etag = md5Map.get(fid) || normalizeMiaochuanMd5(row.md5 || row.hash || row.etag || '');
      return {
        path: row.__gypPath || getQuarkItemName(row),
        etag,
        size: String(getQuarkItemSize(row)),
        __gypSource: row.__gypSource,
        __gypProvider: 'quark',
        __gypFid: fid,
        __gypHasMd5: Boolean(etag),
      };
    });

    STATE.miaochuanCapturedRows = enrichedRows;
    STATE.miaochuanCapturedMap = {};
    enrichedRows.forEach((row) => {
      STATE.miaochuanCapturedMap[getMiaochuanCandidateKey(row)] = row;
    });
    STATE.lastMiaochuanCaptureAt = Date.now();
    STATE.lastMiaochuanCaptureUrl = `quark-share:${pwdId}`;
    renderMiaochuanCaptureStatus();
    return enrichedRows;
  }

  async function fetchQuarkShareRowsFromCurrentPage(options = {}) {
    const pwdId = getQuarkSharePwdIdFromLocation();
    if (!pwdId) {
      throw new Error('当前页面不是夸克分享链接,无法解析 /s/ 后面的分享 ID。');
    }
    return fetchQuarkShareRowsFromShareInfo({
      pwdId,
      passcode: getQuarkSharePasscodeFromLocation(),
      cookie: getQuarkCookieForMiaochuan(),
    }, options);
  }


  function collectMiaochuanKeys(payload, limit = 400) {
    const keys = new Set();
    const queue = [payload];
    while (queue.length && keys.size < limit) {
      const current = queue.shift();
      if (!current || typeof current !== 'object') {
        continue;
      }
      if (Array.isArray(current)) {
        queue.push(...current.slice(0, 30));
        continue;
      }
      for (const key of Object.keys(current)) {
        keys.add(key.toLowerCase());
        const value = current[key];
        if (value && typeof value === 'object') {
          queue.push(value);
        }
      }
    }
    return keys;
  }

  function detectMiaochuanSourceFormat(payload, rawFiles) {
    const keys = collectMiaochuanKeys(payload);
    const has = (...names) => names.some((name) => keys.has(String(name).toLowerCase()));
    const labels = [];
    const notes = [];
    const blockers = [];
    const topSource = [payload?.source, payload?.platform, payload?.provider].filter(Boolean).join(' ').toLowerCase();

    if (/quark|夸克/u.test(topSource)) {
      labels.push('夸克网盘');
      if (/链接|直读|share/u.test(topSource)) {
        notes.push('来源由夸克分享链接直读生成;工具已递归读取目录并尽量补齐真实 MD5。');
      }
    }
    if (/xunlei|迅雷/u.test(topSource)) {
      labels.push('迅雷云盘');
      notes.push('来源包含迅雷 GCID;直接导入光鸭时会走 GCID 秒传检测,不按 32 位 MD5 处理。');
    }
    if (/baidu|百度/u.test(topSource)) {
      labels.push('百度网盘');
      notes.push('来源由百度网盘当前勾选生成;工具会尝试解出百度返回的 MD5 并转换成光鸭 etag。');
    }

    if (has('etag') && has('files')) {
      labels.push('光鸭/通用秒传 JSON');
      notes.push('检测到 files + etag 结构,通常可直接转换;是否成功取决于 etag 是否为真实完整 MD5,以及光鸭库存是否命中。');
    }
    if (has('gcid') && has('files')) {
      labels.push('迅雷云盘');
      notes.push('检测到 files + gcid 结构,直接导入光鸭时会使用 GCID 秒传路径。');
    }
    if (has('fid', 'pdir_fid', 'file_name', 'obj_category')) {
      labels.push('夸克网盘');
      notes.push('夸克常见 fid/file_name 是内部资源标识,不等于文件 MD5;只有同时存在真实 md5 和 size 时才适合转光鸭。');
    }
    if (has('drive_id', 'file_id', 'content_hash', 'content_hash_name', 'proof_code')) {
      labels.push('阿里云盘');
      notes.push('阿里云盘 content_hash 通常是 SHA1;如果 content_hash_name 不是 md5,就不能直接转成光鸭 etag。');
    }
    if (has('pickcode', 'sha1', 'preid')) {
      labels.push('115 网盘');
      notes.push('115 常见秒传依据是 SHA1 / pickcode,不是 MD5;缺少完整 MD5 时不能直接转光鸭。');
    }
    if (!labels.length) {
      labels.push('未知/通用 JSON');
      notes.push('未识别到明确网盘来源,按通用字段 md5/etag/hash + size + path/name 尝试转换。');
    }

    const rows = Array.isArray(rawFiles) ? rawFiles : [];
    const validHashRows = rows.filter((raw) => {
      const hash = pickMiaochuanHashValue(raw);
      return hash.supported && (hash.hashKind === 'gcid' || isExactMiaochuanMd5(hash.value));
    });
    const unsupportedHashRows = rows.filter((raw) => {
      const hash = pickMiaochuanHashValue(raw);
      return hash.key && !hash.supported;
    });
    const missingHashRows = rows.filter((raw) => !pickMiaochuanHashValue(raw).key);
    if (unsupportedHashRows.length) {
      blockers.push(`有 ${unsupportedHashRows.length} 项只有 SHA1/内部 hash,不能直接作为光鸭 etag。`);
    }
    if (missingHashRows.length) {
      blockers.push(`有 ${missingHashRows.length} 项缺少 MD5/etag 字段。`);
    }
    const confidence = rows.length && validHashRows.length === rows.length
      ? '高:每项都有 32 位 MD5 候选'
      : validHashRows.length
        ? `中:${validHashRows.length}/${rows.length || 0} 项有 32 位 MD5 候选`
        : '低:未发现可直接用于光鸭 etag 的完整 MD5';

    return {
      labels: Array.from(new Set(labels)),
      notes,
      blockers,
      confidence,
      summary: `${Array.from(new Set(labels)).join(' / ')} | 转换可信度:${confidence}`,
    };
  }

  function parseMiaochuanNumberFromText(text, regex) {
    const matched = String(text || '').match(regex);
    return matched ? normalizeMiaochuanInteger(matched[1]) : null;
  }

  function extractMiaochuanLogSection(text, title) {
    const escapedTitle = escapeRegExp(title);
    const regex = new RegExp(`={4,}\\s*${escapedTitle}\\s*={4,}\\s*([\\s\\S]*?)(?=\\n\\s*={4,}\\s*[^=\\n]+\\s*={4,}|$)`, 'u');
    const matched = String(text || '').match(regex);
    return matched ? String(matched[1] || '').trim() : '';
  }

  function parseMiaochuanImportLog(logText) {
    const text = String(logText || '').trim();
    if (!text) {
      return null;
    }
    const apiSection = extractMiaochuanLogSection(text, '接口调用失败');
    const transferFailSection = extractMiaochuanLogSection(text, '秒传失败');
    const validationSection = extractMiaochuanLogSection(text, '校验失败');
    const failedPaths = transferFailSection
      .split(/\n+/u)
      .map((line) => line.trim())
      .filter((line) => line && !/^[((]?无[))]?$/u.test(line));
    const apiSectionIsEmpty = !apiSection || /^[((]?无[))]?/u.test(apiSection);

    return {
      hasHttpBizSuccess: /HTTP\s*状态与业务\s*code\s*均成功/iu.test(text),
      createDirFailed: parseMiaochuanNumberFromText(text, /创建目录失败[^\d]*(\d+)\s*条/u),
      enteredTransfer: parseMiaochuanNumberFromText(text, /进入秒传阶段[^\d]*(\d+)\s*条/u),
      total: parseMiaochuanNumberFromText(text, /秒传结果[^\n]*共\s*(\d+)\s*条/u),
      success: parseMiaochuanNumberFromText(text, /秒传结果[^\n]*成功\s*(\d+)\s*条/u),
      fail: parseMiaochuanNumberFromText(text, /秒传结果[^\n]*失败\s*(\d+)\s*条/u),
      apiSection,
      transferFailSection,
      validationSection,
      failedPaths,
      apiHasRealFailures: Boolean(apiSection && !apiSectionIsEmpty && !/HTTP\s*状态与业务\s*code\s*均成功/iu.test(apiSection)),
    };
  }

  function analyzeMiaochuanTopLevelMetadata(payload, rawFiles, normalized) {
    const warnings = [];
    const notes = [];
    if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
      notes.push('输入是顶层数组,工具已按明细重新生成光鸭秒传 JSON 外层字段。');
      return { warnings, notes };
    }
    const rawCount = normalizeMiaochuanInteger(payload.totalFilesCount);
    if (payload.totalFilesCount != null && rawCount !== rawFiles.length) {
      warnings.push(`totalFilesCount=${payload.totalFilesCount},但源数组实际有 ${rawFiles.length} 项。`);
    }
    if (rawCount != null && rawCount !== normalized.files.length) {
      warnings.push(`totalFilesCount=${payload.totalFilesCount},但转换后有效项是 ${normalized.files.length} 项。`);
    }
    const rawTotalSize = normalizeMiaochuanInteger(payload.totalSize);
    if (payload.totalSize != null && rawTotalSize !== normalized.totalSize) {
      warnings.push(`totalSize=${payload.totalSize},但有效明细累加是 ${normalized.totalSize}。`);
    }
    if (payload.formattedTotalSize != null && String(payload.formattedTotalSize).trim() !== normalized.formattedTotalSize) {
      notes.push(`formattedTotalSize 原值为 ${payload.formattedTotalSize},工具重算为 ${normalized.formattedTotalSize}。`);
    }
    notes.push('真正影响秒传命中的字段是 files[*].etag + files[*].size;path 只决定导入后的目录位置。');
    return { warnings, notes };
  }

  function buildMiaochuanDiagnosisText(context) {
    const { normalized, errors, warnings, metadata, sourceInfo, logAnalysis } = context;
    const lines = [];
    if (errors.length) {
      lines.push('诊断结论:当前 JSON 不能完整转成光鸭秒传 JSON。');
      lines.push(`发现 ${errors.length} 条字段错误,优先处理 etag/MD5、size、path/name。`);
    } else {
      lines.push('诊断结论:已生成可导入的光鸭秒传 JSON。');
      lines.push(`有效文件:${normalized.files.length} 项,总大小:${normalized.formattedTotalSize}。`);
    }
    lines.push(`来源识别:${sourceInfo.summary}`);
    if (sourceInfo.blockers.length) {
      lines.push(`来源限制:${sourceInfo.blockers.join(';')}`);
    }
    if (metadata.warnings.length) {
      lines.push('外层统计字段有不一致,建议使用本工具右侧生成的 JSON。');
    }
    if (warnings.some((item) => /HTML|反斜杠|重复斜杠|path/u.test(item))) {
      lines.push('路径字段被自动修正过,建议用生成后的 JSON 再导入一次。');
    }
    if (warnings.some((item) => /etag|MD5|hash/i.test(item))) {
      lines.push('hash 字段存在提醒:如果导入后失败,优先核对原始文件真实完整 MD5。');
    }
    if (logAnalysis) {
      if (logAnalysis.validationSection) {
        lines.push('导入日志显示校验阶段已有失败,请先看“校验失败”明细。');
      }
      if (logAnalysis.createDirFailed && logAnalysis.createDirFailed > 0) {
        lines.push(`导入日志显示有 ${logAnalysis.createDirFailed} 条创建目录失败,更像目录名、层级或权限问题。`);
      }
      if (logAnalysis.apiHasRealFailures) {
        lines.push('导入日志显示接口调用阶段存在失败,优先检查授权、网络、请求频率或接口返回。');
      }
      if (logAnalysis.hasHttpBizSuccess && (logAnalysis.enteredTransfer || 0) > 0 && (logAnalysis.fail || 0) > 0 && !logAnalysis.apiHasRealFailures) {
        lines.push('导入日志显示接口成功且已进入秒传阶段,失败点通常是 MD5/size 不匹配或光鸭库存未命中。');
      }
      if (logAnalysis.failedPaths.length) {
        lines.push(`失败路径:${logAnalysis.failedPaths.slice(0, 3).join(';')}${logAnalysis.failedPaths.length > 3 ? ` 等 ${logAnalysis.failedPaths.length} 条` : ''}`);
      }
    } else if (!errors.length) {
      lines.push('如果光鸭导入结果是“接口成功但秒传失败”,一般不是 JSON 壳子问题,而是 MD5/size 或库存命中问题。');
    }
    return lines.join('\n');
  }

  function buildMiaochuanReport(context) {
    const { normalized, errors, warnings, metadata, sourceInfo, logAnalysis, diagnosisText } = context;
    const lines = [
      '========== 光鸭秒传 JSON 转换报告 ==========',
      `工具版本:${SCRIPT_VERSION}`,
      `来源识别:${sourceInfo.summary}`,
      `原始条数:${context.originalCount}`,
      `输出条数:${normalized.files.length}`,
      `总大小:${normalized.totalSize} (${normalized.formattedTotalSize})`,
      `commonPath:${normalized.commonPath || '(无)'}`,
      `错误:${errors.length} 条`,
      `字段提醒:${warnings.length} 条`,
      `外层统计提醒:${metadata.warnings.length} 条`,
      '',
      '========== 诊断结论 ==========',
      diagnosisText || '等待分析。',
      '',
      '========== 来源说明 ==========',
      ...sourceInfo.notes,
      ...(sourceInfo.blockers.length ? ['来源限制:', ...sourceInfo.blockers] : []),
      '',
    ];
    if (metadata.notes.length) {
      lines.push('========== 结构说明 ==========');
      metadata.notes.forEach((item) => lines.push(item));
      lines.push('');
    }
    if (metadata.warnings.length) {
      lines.push('========== 外层统计字段提醒 ==========');
      metadata.warnings.forEach((item) => lines.push(item));
      lines.push('');
    }
    if (logAnalysis) {
      lines.push('========== 导入结果日志分析 ==========');
      lines.push(`创建目录失败:${logAnalysis.createDirFailed ?? '未识别'} 条`);
      lines.push(`进入秒传阶段:${logAnalysis.enteredTransfer ?? '未识别'} 条`);
      lines.push(`秒传总数:${logAnalysis.total ?? '未识别'} 条`);
      lines.push(`秒传成功:${logAnalysis.success ?? '未识别'} 条`);
      lines.push(`秒传失败:${logAnalysis.fail ?? '未识别'} 条`);
      lines.push(`接口状态:${logAnalysis.hasHttpBizSuccess ? 'HTTP 状态与业务 code 均成功' : logAnalysis.apiHasRealFailures ? '接口阶段存在失败' : '未识别'}`);
      if (logAnalysis.failedPaths.length) {
        lines.push('失败路径:');
        logAnalysis.failedPaths.forEach((item) => lines.push(item));
      }
      lines.push('');
    }
    if (errors.length) {
      lines.push('========== 错误 ==========');
      errors.forEach((item) => lines.push(item));
      lines.push('');
    }
    if (warnings.length) {
      lines.push('========== 字段提醒 ==========');
      warnings.forEach((item) => lines.push(item));
      lines.push('');
    }
    if (!errors.length && !warnings.length && !metadata.warnings.length) {
      lines.push('未发现明显格式问题。');
    }
    return lines.join('\n');
  }

  function normalizeMiaochuanPayload(text, options = {}) {
    const payload = safeJsonParse(text);
    if (!payload) {
      throw new Error('JSON 解析失败,请确认内容完整且格式正确。');
    }
    const collected = collectMiaochuanRawFiles(payload);
    const rawFiles = collected.files;
    if (!Array.isArray(rawFiles)) {
      throw new Error('没有找到可转换的文件数组。支持 files、fileList、items、list、data.files、data.list 等结构。');
    }

    const errors = [];
    const warnings = [];
    const normalizedFiles = [];
    const sourceInfo = detectMiaochuanSourceFormat(payload, rawFiles);

    rawFiles.forEach((item, index) => {
      const raw = item && typeof item === 'object' ? item : {};
      const hashPick = pickMiaochuanHashValue(raw);
      const sizePick = pickMiaochuanSizeValue(raw);
      const pathPick = pickMiaochuanPathValue(raw);
      const etag = hashPick.supported ? normalizeMiaochuanMd5(hashPick.value) : '';
      const size = normalizeMiaochuanInteger(sizePick.value);
      const pathValue = normalizeMiaochuanPath(pathPick.value, options);
      const rawPathText = String(pathPick.value == null ? '' : pathPick.value);
      const label = `第 ${index + 1} 项`;
      const gcidValue = hashPick.hashKind === 'gcid' && /^[a-f0-9]{40}$/iu.test(String(hashPick.value || '').trim())
        ? String(hashPick.value || '').trim().toUpperCase()
        : '';

      if (!etag && !gcidValue) {
        const pathHint = pathValue || normalizeMiaochuanPath(pathPick.value, { ...options, decodeHtml: true, stripLeadingSlash: false }) || String(raw.path || raw.name || raw.server_filename || '').trim();
        if (hashPick.key) {
          errors.push(`${label} 缺少可用于光鸭 etag 的 32 位 MD5 或迅雷 GCID。文件:${pathHint || '(路径未识别)'};字段 ${hashPick.key} 原值:${JSON.stringify(hashPick.value ?? '')}`);
        } else {
          errors.push(`${label} 缺少可用于光鸭 etag 的 32 位 MD5 或迅雷 GCID。文件:${pathHint || '(路径未识别)'}。${hashPick.reason || ''}`);
        }
      } else if (!gcidValue && !isExactMiaochuanMd5(hashPick.value)) {
        warnings.push(`${label} ${hashPick.key} 已提取为 ${etag},但原始值不是纯 32 位 MD5:${JSON.stringify(hashPick.value ?? '')}`);
      }
      if (size == null) {
        errors.push(`${label} size 无效或缺失:${JSON.stringify(sizePick.value ?? '')}`);
      }
      if (!pathValue) {
        errors.push(`${label} path/name 为空或无效:${JSON.stringify(pathPick.value ?? '')}`);
      } else {
        if (pathPick.pathFromNameOnly) {
          warnings.push(`${label} 只找到文件名字段 ${pathPick.key},已按根目录路径生成:${pathValue}`);
        }
        if (rawPathText !== rawPathText.trim()) {
          warnings.push(`${label} path 前后有空格,已自动去掉。`);
        }
        if (/\\/u.test(rawPathText)) {
          warnings.push(`${label} path 包含反斜杠,已统一为 /。`);
        }
        if (/&(#x?[0-9a-f]+|[a-z]+);/iu.test(rawPathText)) {
          warnings.push(`${label} path 包含 HTML 实体,已自动解码。`);
        }
        if (/\/{2,}/u.test(rawPathText.replace(/^https?:\/\//iu, ''))) {
          warnings.push(`${label} path 包含重复斜杠,已自动合并。`);
        }
      }

      if ((!etag && !gcidValue) || size == null || !pathValue) {
        return;
      }
      const normalizedFile = {
        size: options.sizeAsNumber ? size : String(size),
        path: pathValue,
      };
      if (gcidValue) {
        normalizedFile.gcid = gcidValue;
      } else {
        normalizedFile.etag = etag;
      }
      normalizedFiles.push(normalizedFile);
    });

    if (options.sortByPath !== false) {
      normalizedFiles.sort((left, right) => String(left.path).localeCompare(String(right.path), 'zh-Hans-CN'));
    }

    const duplicateMap = new Map();
    normalizedFiles.forEach((item) => {
      const key = `${item.etag || item.gcid || ''}__${String(item.size)}__${item.path}`;
      duplicateMap.set(key, (duplicateMap.get(key) || 0) + 1);
    });
    duplicateMap.forEach((count, key) => {
      if (count > 1) {
        warnings.push(`发现重复明细 ${count} 次:${key.split('__').slice(2).join('__')}`);
      }
    });

    const totalSize = normalizedFiles.reduce((sum, item) => sum + Number(item.size || 0), 0);
    const normalized = {
      scriptVersion: new Date().toISOString().slice(0, 10).replace(/-/g, '.'),
      totalFilesCount: normalizedFiles.length,
      totalSize,
      formattedTotalSize: formatMiaochuanBytes(totalSize),
      files: normalizedFiles,
      commonPath: computeMiaochuanCommonPath(normalizedFiles.map((item) => item.path)),
    };
    const metadata = analyzeMiaochuanTopLevelMetadata(payload, rawFiles, normalized);
    const logAnalysis = parseMiaochuanImportLog(options.importLogText || '');
    const diagnosisText = buildMiaochuanDiagnosisText({
      normalized,
      errors,
      warnings,
      metadata,
      sourceInfo,
      logAnalysis,
    });
    const reportText = buildMiaochuanReport({
      normalized,
      errors,
      warnings,
      metadata,
      sourceInfo,
      logAnalysis,
      diagnosisText,
      originalCount: rawFiles.length,
    });

    return {
      normalized,
      outputText: JSON.stringify(normalized, null, 2),
      reportText,
      diagnosisText,
      sourceInfo,
      errors,
      warnings,
      metadata,
      logAnalysis,
      originalCount: rawFiles.length,
      sourcePath: collected.sourcePath,
    };
  }

  function getShareLinkInputText() {
    return String(UI.fields.shareLinkUrl?.value || '').trim();
  }

  function getShareLinkPasscodeText() {
    return String(UI.fields.shareLinkPasscode?.value || '').trim();
  }

  function getShareLinkRowKey(row) {
    return [row?.path || '', row?.etag || row?.gcid || '', row?.size || ''].join('__');
  }

  function getSelectedShareLinkRows() {
    const rows = Array.isArray(STATE.shareLinkRows) ? STATE.shareLinkRows : [];
    return rows.filter((row) => STATE.shareLinkSelection[getShareLinkRowKey(row)] !== false);
  }

  function renderShareLinkList() {
    if (!UI.shareLinkList || !UI.shareLinkCount || !UI.shareLinkSummary) {
      return;
    }

    const rows = Array.isArray(STATE.shareLinkRows) ? STATE.shareLinkRows : [];
    const meta = STATE.shareLinkMeta || {};
    const selectedRows = getSelectedShareLinkRows();
    const readyCount = rows.filter((row) => Boolean(normalizeMiaochuanMd5(row?.etag || '') || /^[a-f0-9]{40}$/iu.test(String(row?.gcid || '').trim()))).length;
    const totalSize = rows.reduce((sum, row) => sum + Number(normalizeMiaochuanInteger(row?.size) || 0), 0);

    UI.shareLinkCount.textContent = `已选 ${selectedRows.length}/${rows.length}`;
    UI.shareLinkSummary.textContent = rows.length
      ? `${meta.label || '分享链接'} | 文件 ${rows.length} 项 | 已就绪 MD5/GCID ${readyCount} 项 | 总大小 ${formatMiaochuanBytes(totalSize)}`
      : (meta.message || '粘贴分享链接后,先点“读取链接目录”,这里会显示可勾选的文件清单。');

    if (!rows.length) {
      UI.shareLinkList.innerHTML = `<div class="gyp-import-empty">${escapeHtml(meta.message || '粘贴分享链接后,先点“读取链接目录”,这里会显示可勾选的文件清单。')}</div>`;
      return;
    }

    UI.shareLinkList.innerHTML = rows.map((row) => {
      const key = getShareLinkRowKey(row);
      const checked = STATE.shareLinkSelection[key] !== false;
      const path = String(row?.path || '');
      const sizeText = formatMiaochuanBytes(normalizeMiaochuanInteger(row?.size) || 0);
      const md5 = normalizeMiaochuanMd5(row?.etag || '');
      const gcid = /^[a-f0-9]{40}$/iu.test(String(row?.gcid || '').trim()) ? String(row.gcid).trim().toUpperCase() : '';
      const hashText = md5 ? `MD5: ${escapeHtml(md5)}` : (gcid ? `GCID: ${escapeHtml(gcid)}` : 'MD5/GCID 未拿到,导入时会被跳过');
      return `
        <label class="gyp-empty-dir-row">
          <input type="checkbox" data-action="toggle-share-link-file" data-share-link-key="${escapeHtml(key)}" ${checked ? 'checked' : ''} />
          <div class="gyp-empty-dir-main">
            <div class="gyp-empty-dir-path" title="${escapeHtml(path)}">${escapeHtml(path)}</div>
            <div class="gyp-empty-dir-meta">${escapeHtml(sizeText)} | ${hashText}</div>
          </div>
        </label>
      `;
    }).join('');
  }

  function setMiaochuanCapturedRowsFromProvider(rows, sourceKey) {
    const list = Array.isArray(rows) ? rows.filter((row) => row && typeof row === 'object') : [];
    STATE.miaochuanCapturedRows = list;
    STATE.miaochuanCapturedMap = {};
    list.forEach((row) => {
      STATE.miaochuanCapturedMap[getMiaochuanCandidateKey(row)] = row;
    });
    STATE.lastMiaochuanCaptureAt = Date.now();
    STATE.lastMiaochuanCaptureUrl = sourceKey || 'provider-active';
    renderMiaochuanCaptureStatus();
    return list;
  }

  function setShareLinkRows(rows = [], meta = {}) {
    const normalizedRows = Array.isArray(rows) ? rows.filter((row) => row && typeof row === 'object') : [];
    const previous = STATE.shareLinkSelection || {};
    STATE.shareLinkRows = normalizedRows;
    STATE.shareLinkMeta = { ...meta, loadedAt: Date.now() };
    STATE.shareLinkSelection = {};
    normalizedRows.forEach((row) => {
      const key = getShareLinkRowKey(row);
      STATE.shareLinkSelection[key] = Object.prototype.hasOwnProperty.call(previous, key) ? previous[key] !== false : true;
    });
    renderShareLinkList();
  }

  function clearShareLinkPanel() {
    STATE.shareLinkRows = [];
    STATE.shareLinkSelection = {};
    STATE.shareLinkMeta = null;
    renderShareLinkList();
    updatePanelStatus('已清空分享直读结果');
  }

  function setAllShareLinkSelection(checked) {
    const rows = Array.isArray(STATE.shareLinkRows) ? STATE.shareLinkRows : [];
    rows.forEach((row) => {
      STATE.shareLinkSelection[getShareLinkRowKey(row)] = Boolean(checked);
    });
    renderShareLinkList();
  }

  async function readShareLinkFromPanel(options = {}) {
    const detected = detectShareLinkProvider(getShareLinkInputText(), {
      manualPasscode: getShareLinkPasscodeText(),
    });
    if (UI.fields.shareLinkPasscode && detected.passcode && !UI.fields.shareLinkPasscode.value) {
      UI.fields.shareLinkPasscode.value = detected.passcode;
    }
    if (!detected.supported) {
      setShareLinkRows([], detected);
      throw new Error(detected.message || '暂不支持这个分享链接。');
    }

    if (detected.provider === 'quark') {
      const rows = await fetchQuarkShareRowsFromShareInfo({
        pwdId: detected.pwdId,
        passcode: detected.passcode,
        cookie: getShareLinkQuarkCookie(),
      }, options);
      setShareLinkRows(rows, detected);
      updatePanelStatus(`已读取 ${detected.label}:文件 ${rows.length} 项`);
      return rows;
    }

    if (detected.provider === '123pan') {
      const rows = await fetch123PanShareRowsFromShareInfo({
        shareKey: detected.shareKey,
        passcode: detected.passcode,
      }, options);
      setShareLinkRows(rows, detected);
      updatePanelStatus(`已读取 ${detected.label}:文件 ${rows.length} 项`);
      return rows;
    }

    if (detected.provider === 'tianyiyun') {
      const rows = await fetchTianyiShareRowsFromShareInfo({
        shareCode: detected.shareCode,
        passcode: detected.passcode,
      }, options);
      setShareLinkRows(rows, detected);
      updatePanelStatus(`已读取 ${detected.label}:文件 ${rows.length} 项`);
      return rows;
    }

    if (detected.provider === 'xunlei') {
      const rows = await fetchXunleiShareRowsFromShareInfo({
        shareId: detected.shareId,
        passcode: detected.passcode,
      }, options);
      setShareLinkRows(rows, detected);
      updatePanelStatus(`已读取 ${detected.label}:文件 ${rows.length} 项`);
      return rows;
    }

    setShareLinkRows([], detected);
    throw new Error(detected.message || '暂不支持这个分享链接。');
  }

  function buildShareLinkMiaochuanSourcePayload(rows) {
    const meta = STATE.shareLinkMeta || {};
    return {
      source: meta.label ? `${meta.label} 直读结果` : '分享链接直读结果',
      provider: meta.provider || '',
      shareUrl: meta.shareUrl || '',
      capturedAt: new Date().toISOString(),
      files: rows,
    };
  }

  function generateMiaochuanJsonFromShareLinkSelection() {
    const rows = getSelectedShareLinkRows();
    if (!rows.length) {
      throw new Error('当前没有勾选任何可生成的文件。请先读取分享链接,并至少勾选 1 项。');
    }
    const sourcePayload = buildShareLinkMiaochuanSourcePayload(rows);
    const sourceText = JSON.stringify(sourcePayload, null, 2);
    if (UI.fields.miaochuanJsonInput) {
      UI.fields.miaochuanJsonInput.value = sourceText;
    }
    const result = normalizeMiaochuanPayload(sourceText, {
      decodeHtml: true,
      stripLeadingSlash: false,
      sizeAsNumber: false,
      sortByPath: true,
      importLogText: getMiaochuanImportLogText(),
    });
    renderMiaochuanJsonResult(result);
    updatePanelStatus(
      result.errors.length
        ? `已按勾选结果生成:可导入 ${result.normalized.files.length} 项,另有 ${result.errors.length} 项字段不完整`
        : `已按勾选结果生成光鸭 JSON:${result.normalized.files.length} 项,${result.normalized.formattedTotalSize}`
    );
    return result;
  }

  function renderMiaochuanJsonResult(result) {
    STATE.lastMiaochuanJsonResult = result || null;
    if (UI.miaochuanOutput) {
      UI.miaochuanOutput.value = result?.outputText || '';
    }
    if (UI.miaochuanReport) {
      UI.miaochuanReport.value = result?.reportText || '';
    }
    if (UI.miaochuanDiagnosis) {
      UI.miaochuanDiagnosis.textContent = result?.diagnosisText || '等待生成。';
    }
    if (UI.miaochuanSource) {
      UI.miaochuanSource.textContent = result?.sourceInfo?.summary || '来源识别:等待生成。';
    }
    renderMiaochuanCaptureStatus();
  }

  function renderMiaochuanCaptureStatus() {
    if (!UI.miaochuanCapturedCount) {
      return;
    }
    const count = Array.isArray(STATE.miaochuanCapturedRows) ? STATE.miaochuanCapturedRows.length : 0;
    if (!count) {
      UI.miaochuanCapturedCount.textContent = '当前网页已捕获 0 条候选文件;请先让网盘列表加载完成。';
      return;
    }
    const time = STATE.lastMiaochuanCaptureAt ? new Date(STATE.lastMiaochuanCaptureAt).toLocaleTimeString() : '';
    UI.miaochuanCapturedCount.textContent = `当前网页已捕获 ${count} 条候选文件${time ? `,最近 ${time}` : ''}。`;
  }

  function getMiaochuanInputText() {
    return String(UI.fields.miaochuanJsonInput?.value || '').trim();
  }

  function getMiaochuanImportLogText() {
    return String(UI.fields.miaochuanImportLog?.value || '').trim();
  }

  async function generateMiaochuanJsonFromPanel() {
    const text = getMiaochuanInputText();
    if (!text) {
      if (Array.isArray(STATE.miaochuanCapturedRows) && STATE.miaochuanCapturedRows.length) {
        return generateMiaochuanJsonFromCapturedPage();
      }
      throw new Error('还没有可生成的数据。请先让当前网盘页面加载文件列表,然后点“从当前网页抓取生成”;也可以粘贴 JSON 或点击“选择秒传 JSON”。');
    }
    const result = normalizeMiaochuanPayload(text, {
      decodeHtml: true,
      stripLeadingSlash: false,
      sizeAsNumber: false,
      sortByPath: true,
      importLogText: getMiaochuanImportLogText(),
    });
    renderMiaochuanJsonResult(result);
    updatePanelStatus(
      result.errors.length
        ? `秒传 JSON 生成完成,但有 ${result.errors.length} 条错误;已输出可转换的 ${result.normalized.files.length} 项`
        : `秒传 JSON 已生成:${result.normalized.files.length} 项,${result.normalized.formattedTotalSize};${result.sourceInfo.summary}`
    );
    return result;
  }

  async function generateMiaochuanJsonFromCapturedPage(options = {}) {
    let rows = Array.isArray(STATE.miaochuanCapturedRows) ? STATE.miaochuanCapturedRows : [];
    if (isBaiduSharePage()) {
      throw new Error('百度分享页暂时不能直接生成光鸭秒传 JSON。请先点百度页面里的“保存到网盘”,再进入自己的百度网盘目录勾选文件后生成。');
    }
    if (isBaiduPageHost()) {
      updatePanelStatus('正在读取百度网盘勾选项并生成 MD5 秒传数据...');
      rows = await collectBaiduSelectedRows(options);
    } else if (isXunleiPageHost()) {
      updatePanelStatus('正在读取迅雷云盘勾选项并生成 GCID 秒传数据...');
      rows = await collectXunleiSelectedRows(options);
    }
    const quarkSharePwdId = isQuarkPageHost() ? getQuarkSharePwdIdFromLocation() : '';
    if (!rows.length && quarkSharePwdId) {
      updatePanelStatus('正在主动读取夸克分享页文件列表并获取真实 MD5...');
      rows = await fetchQuarkShareRowsFromCurrentPage({
        onProgress: options.onProgress,
      });
    }
    const pan123ShareKey = is123PanPageHost() ? get123PanShareKeyFromLocation() : '';
    if (!rows.length && pan123ShareKey) {
      updatePanelStatus('正在主动读取 123 网盘分享页文件列表...');
      rows = await fetch123PanShareRowsFromShareInfo({
        shareKey: pan123ShareKey,
        passcode: get123PanSharePasscodeFromLocation(),
      }, {
        onProgress: options.onProgress,
      });
    }
    if (!rows.length) {
      throw new Error('当前网页还没有捕获到可生成秒传 JSON 的文件数据。请先打开/刷新网盘目录,让文件列表加载出来;如果是分享页,先进入目录或滚动列表后再点这个按钮。');
    }
    const sourcePayload = {
      source: getMiaochuanCurrentSourceName(),
      capturedAt: new Date().toISOString(),
      files: rows,
    };
    const sourceText = JSON.stringify(sourcePayload, null, 2);
    if (UI.fields.miaochuanJsonInput) {
      UI.fields.miaochuanJsonInput.value = sourceText;
    }
    const result = normalizeMiaochuanPayload(sourceText, {
      decodeHtml: true,
      stripLeadingSlash: false,
      sizeAsNumber: false,
      sortByPath: true,
      importLogText: getMiaochuanImportLogText(),
    });
    renderMiaochuanJsonResult(result);
    updatePanelStatus(
      result.errors.length
        ? `已从当前网页捕获 ${rows.length} 条候选数据;其中 ${result.normalized.files.length} 条可生成,${result.errors.length} 条缺少 MD5/size/path`
        : `已从当前网页生成秒传 JSON:${result.normalized.files.length} 项,${result.normalized.formattedTotalSize}`
    );
    return result;
  }

  function downloadMiaochuanText(filename, text, mimeType = 'application/json;charset=utf-8') {
    const blob = new Blob([text], { type: mimeType || 'application/octet-stream' });
    downloadBlobFile(blob, filename);
  }

  function downloadBlobFile(blob, filename) {
    const url = URL.createObjectURL(blob);
    const anchor = document.createElement('a');
    anchor.href = url;
    anchor.download = filename;
    anchor.click();
    setTimeout(() => URL.revokeObjectURL(url), 1000);
  }

  async function copyMiaochuanText(text) {
    if (!text) {
      throw new Error('当前没有可复制的内容。');
    }
    if (typeof GM_setClipboard === 'function') {
      GM_setClipboard(text, 'text');
      return;
    }
    if (navigator.clipboard?.writeText) {
      await navigator.clipboard.writeText(text);
      return;
    }
    const textarea = document.createElement('textarea');
    textarea.value = text;
    textarea.style.position = 'fixed';
    textarea.style.left = '-9999px';
    document.body.appendChild(textarea);
    textarea.select();
    document.execCommand('copy');
    textarea.remove();
  }

  function isVisibleMiaochuanElement(element) {
    if (!element || !(element instanceof HTMLElement)) {
      return false;
    }
    const rect = element.getBoundingClientRect();
    const style = window.getComputedStyle(element);
    return rect.width > 0 && rect.height > 0 && style.display !== 'none' && style.visibility !== 'hidden';
  }

  function findMiaochuanPageImportTextarea() {
    const candidates = Array.from(document.querySelectorAll('textarea'))
      .filter((element) => !UI.root?.contains(element))
      .filter(isVisibleMiaochuanElement);
    if (!candidates.length) {
      return null;
    }
    return candidates
      .map((element) => {
        const boxText = String(element.closest('[role="dialog"], .ant-modal, .modal, section, div')?.textContent || '');
        const placeholder = String(element.getAttribute('placeholder') || '');
        const value = String(element.value || '');
        let score = 0;
        if (/秒传|导入|JSON|json|etag|files|path|size/u.test(boxText)) score += 5;
        if (/秒传|导入|JSON|json|etag|files|path|size/u.test(placeholder)) score += 5;
        if (/files|etag|path|size/u.test(value)) score += 2;
        return { element, score };
      })
      .sort((left, right) => right.score - left.score)[0].element;
  }

  async function fillMiaochuanPageImportBox() {
    let outputText = STATE.lastMiaochuanJsonResult?.outputText || '';
    if (!outputText) {
      outputText = (await generateMiaochuanJsonFromPanel()).outputText;
    }
    const target = findMiaochuanPageImportTextarea();
    if (!target) {
      throw new Error('没有找到网页里的“导入秒传 JSON”输入框。请先打开光鸭自己的导入 JSON 弹窗,再点“填入页面导入框”。');
    }
    target.focus();
    target.value = outputText;
    target.dispatchEvent(new Event('input', { bubbles: true }));
    target.dispatchEvent(new Event('change', { bubbles: true }));
    updatePanelStatus('已把生成后的秒传 JSON 填入网页导入框,请回到光鸭弹窗确认后点“开始导入”。');
  }

  function clearMiaochuanPanel() {
    if (UI.fields.miaochuanJsonInput) {
      UI.fields.miaochuanJsonInput.value = '';
    }
    if (UI.fields.miaochuanImportLog) {
      UI.fields.miaochuanImportLog.value = '';
    }
    if (UI.miaochuanFileInput) {
      UI.miaochuanFileInput.value = '';
    }
    renderMiaochuanJsonResult(null);
    updatePanelStatus('已清空秒传 JSON 转换内容');
  }

  function getGuangyaResCenterTokenUrl() {
    return `${CONFIG.request.apiHost}/nd.bizuserres.s/v1/get_res_center_token`;
  }

  function getGuangyaCheckFlashUploadUrl() {
    return `${CONFIG.request.apiHost}/nd.bizuserres.s/v1/check_can_flash_upload`;
  }

  function getGuangyaDeleteUploadTaskUrl() {
    return `${CONFIG.request.apiHost}/nd.bizuserres.s/v1/file/delete_upload_task`;
  }

  function getMiaochuanGuangyaAuthorization(options = {}) {
    const manual = normalizeGuangyaAuthorization(options.manualAuthorization || UI.fields.miaochuanGuangyaAuthorization?.value || '');
    if (manual) {
      setStoredGuangyaAuthorization(manual);
      return manual;
    }
    const stored = getStoredGuangyaAuthorization();
    if (stored) {
      if (UI.fields.miaochuanGuangyaAuthorization && !UI.fields.miaochuanGuangyaAuthorization.value) {
        UI.fields.miaochuanGuangyaAuthorization.value = stored;
      }
      return stored;
    }
    const currentPageAuth = isGuangyaPageHost()
      ? normalizeGuangyaAuthorization(STATE.headers.authorization || CONFIG.request.manualHeaders.authorization || '')
      : '';
    if (currentPageAuth) {
      setStoredGuangyaAuthorization(currentPageAuth);
      return currentPageAuth;
    }
    return '';
  }

  function getMiaochuanGuangyaHeaders(auth) {
    return {
      Accept: 'application/json, text/plain, */*',
      'Content-Type': 'application/json;charset=utf-8',
      Authorization: normalizeGuangyaAuthorization(auth),
      dt: '4',
    };
  }

  function isGuangyaBusinessOk(payload, allowedCodes = []) {
    if (!payload || typeof payload !== 'object') {
      return true;
    }
    if (payload.code == null) {
      return true;
    }
    const code = Number(payload.code);
    return code === 0 || allowedCodes.map(Number).includes(code);
  }

  async function postGuangyaMiaochuanJson(url, body, auth, options = {}) {
    const response = await requestMiaochuanJson(url, {
      method: 'POST',
      headers: getMiaochuanGuangyaHeaders(auth),
      body,
      timeout: options.timeout || 45000,
    });
    if (!response.ok || !isGuangyaBusinessOk(response.payload, options.allowedCodes || [])) {
      throw new Error(getErrorText(response.payload || response.text || `HTTP ${response.status}`));
    }
    return response.payload;
  }

  function getMiaochuanPathSegments(filePath) {
    return String(filePath || '')
      .replace(/\\/g, '/')
      .split('/')
      .map((item) => item.trim())
      .filter(Boolean);
  }

  function getMiaochuanBasename(filePath) {
    const segments = getMiaochuanPathSegments(filePath);
    return segments.length ? segments[segments.length - 1] : 'file';
  }

  async function ensureGuangyaMiaochuanDirPath(dirSegments, rootParentId, auth, cache, summary, samplePath) {
    if (!dirSegments.length) {
      return rootParentId;
    }
    let parentId = String(rootParentId || '');
    let fullPath = '';
    for (const segment of dirSegments) {
      fullPath = fullPath ? `${fullPath}/${segment}` : segment;
      const cacheKey = `${parentId}::${fullPath}`;
      if (cache.has(cacheKey)) {
        parentId = String(cache.get(cacheKey) || '');
        continue;
      }
      const payload = await postGuangyaMiaochuanJson(
        getCreateDirUrl(),
        {
          dirName: segment,
          parentId,
          failIfNameExist: false,
        },
        auth,
        { allowedCodes: [GUANGYA_CODE_DIR_EXISTS] }
      );
      const dirId = extractCreatedDirId(payload);
      if (!dirId) {
        summary.mkdirFail += 1;
        summary.failures.push(`${samplePath || fullPath}:创建目录失败(目录=${fullPath},未返回目录 ID)`);
        return null;
      }
      cache.set(cacheKey, dirId);
      parentId = dirId;
    }
    return parentId;
  }

  async function getMiaochuanNormalizedForDirectImport(options = {}) {
    const hasDirectImportFiles = (payload) => Boolean(payload && Array.isArray(payload.files) && payload.files.length);
    if (options.normalized && Array.isArray(options.normalized.files) && options.normalized.files.length) {
      return options.normalized;
    }
    if (hasDirectImportFiles(STATE.lastMiaochuanJsonResult?.normalized)) {
      return STATE.lastMiaochuanJsonResult.normalized;
    }
    const outputText = String(UI.miaochuanOutput?.value || STATE.lastMiaochuanJsonResult?.outputText || '').trim();
    if (outputText) {
      const payload = safeJsonParse(outputText);
      if (hasDirectImportFiles(payload)) {
        return payload;
      }
    }
    if (getMiaochuanInputText()) {
      const result = await generateMiaochuanJsonFromPanel();
      if (hasDirectImportFiles(result.normalized)) {
        return result.normalized;
      }
    }
    const result = options.fromCurrentPage
      ? await generateMiaochuanJsonFromCapturedPage(options)
      : await generateMiaochuanJsonFromPanel();
    return result.normalized;
  }

  function formatMiaochuanDirectImportReport(summary) {
    const lines = [
      '========== 直接导入光鸭结果 ==========',
      `提交文件:${summary.total} 条`,
      `秒传成功:${summary.success} 条`,
      `秒传失败:${summary.transferFail} 条`,
      `创建目录失败:${summary.mkdirFail} 条`,
      `跳过:${summary.skipped} 条`,
      `目标 parentId:${summary.parentId || '根目录'}`,
    ];
    if (summary.failures.length) {
      lines.push('', '失败明细:');
      summary.failures.slice(0, 80).forEach((item) => lines.push(item));
      if (summary.failures.length > 80) {
        lines.push(`... 另有 ${summary.failures.length - 80} 条失败未展开`);
      }
    }
    return lines.join('\n');
  }

  async function importMiaochuanJsonDirectlyToGuangya(options = {}) {
    const auth = getMiaochuanGuangyaAuthorization(options);
    if (!auth) {
      throw new Error('还没有光鸭 Authorization。请先打开光鸭页面让脚本自动捕获一次,或在“秒传 JSON”分区粘贴 Bearer Token 后重试。');
    }
    const normalized = await getMiaochuanNormalizedForDirectImport(options);
    const files = Array.isArray(normalized.files) ? normalized.files : [];
    if (!files.length) {
      throw new Error('当前没有可直接导入的秒传文件,请先生成光鸭秒传 JSON。');
    }
    const parentId = String(options.parentId != null ? options.parentId : (UI.fields.miaochuanGuangyaParentId?.value || '')).trim();
    const summary = {
      total: files.length,
      success: 0,
      transferFail: 0,
      mkdirFail: 0,
      skipped: 0,
      parentId,
      failures: [],
    };
    const dirCache = new Map();
    dirCache.set('', parentId);

    for (let index = 0; index < files.length; index += 1) {
      await waitForTaskControl(options.taskControl || null);
      const file = files[index] || {};
      const path = normalizeMiaochuanPath(file.path || file.name || '', { stripLeadingSlash: false });
      const md5 = normalizeMiaochuanMd5(file.etag || file.md5 || '');
      const gcid = String(file.gcid || '').trim().toUpperCase();
      const fileSize = normalizeMiaochuanInteger(file.size);
      if (!path || (!md5 && !gcid) || fileSize == null) {
        summary.skipped += 1;
        summary.failures.push(`${path || `第 ${index + 1} 项`}:缺少 path / MD5或GCID / size,已跳过`);
        continue;
      }
      const segments = getMiaochuanPathSegments(path);
      const fileName = segments.length ? segments[segments.length - 1] : getMiaochuanBasename(path);
      const dirSegments = segments.slice(0, -1);
      if (typeof options.onProgress === 'function') {
        options.onProgress({
          visible: true,
          percent: Math.max(1, Math.round((index / files.length) * 100)),
          indeterminate: false,
          text: `正在直接导入光鸭:${index + 1}/${files.length} ${fileName}`,
        });
      }
      let targetParentId;
      try {
        targetParentId = await ensureGuangyaMiaochuanDirPath(dirSegments, parentId, auth, dirCache, summary, path);
      } catch (err) {
        summary.mkdirFail += 1;
        summary.failures.push(`${path}:创建目录失败(${getErrorText(err)})`);
        continue;
      }
      if (targetParentId == null) {
        continue;
      }

      try {
        const tokenBody = {
          capacity: 1,
          res: gcid
            ? { fileSize }
            : {
                md5,
                fileSize,
              },
          name: fileName || 'file',
          parentId: String(targetParentId || ''),
        };
        const payload = await postGuangyaMiaochuanJson(
          getGuangyaResCenterTokenUrl(),
          tokenBody,
          auth,
          { allowedCodes: [GUANGYA_CODE_RES_TOKEN_INSTANT] }
        );
        const code = Number(payload && payload.code);
        if (code === GUANGYA_CODE_RES_TOKEN_INSTANT) {
          summary.success += 1;
          continue;
        }
        const taskId = findFirstValueByKeys(payload, ['taskId', 'task_id']);
        if (gcid && taskId) {
          try {
            const checkPayload = await postGuangyaMiaochuanJson(
              getGuangyaCheckFlashUploadUrl(),
              { taskId: String(taskId), gcid },
              auth,
              { allowedCodes: [0] }
            );
            if (checkPayload?.data?.canFlashUpload) {
              summary.success += 1;
              continue;
            }
          } catch (err) {
            summary.failures.push(`${path}:GCID 秒传检测失败(${getErrorText(err)})`);
          }
        }
        if (taskId) {
          postGuangyaMiaochuanJson(getGuangyaDeleteUploadTaskUrl(), { taskIds: [String(taskId)] }, auth).catch(() => {});
        }
        summary.transferFail += 1;
        summary.failures.push(`${path}:未命中秒传库存`);
      } catch (err) {
        summary.transferFail += 1;
        summary.failures.push(`${path}:秒传接口失败(${getErrorText(err)})`);
      }
    }

    const report = formatMiaochuanDirectImportReport(summary);
    if (UI.miaochuanReport) {
      UI.miaochuanReport.value = `${String(UI.miaochuanReport.value || '').trim()}\n\n${report}`.trim();
    }
    return summary;
  }

  function toHalfWidthDigits(text) {
    return String(text || '').replace(/[0-9]/g, (ch) => String(ch.charCodeAt(0) - 65248));
  }

  function normalizeDuplicateName(name) {
    return toHalfWidthDigits(String(name || ''))
      .replace(/[(]/g, '(')
      .replace(/[)]/g, ')')
      .replace(/\u00a0/g, ' ')
      .replace(/[\u200b-\u200d\ufeff]/g, '')
      .replace(/\s+/g, ' ')
      .trim();
  }

  function getConfiguredDuplicateNumbers() {
    const values = String(CONFIG.duplicate.numbers || DEFAULT_DUPLICATE_NUMBERS)
      .split(/[\s,,、]+/)
      .map((x) => toHalfWidthDigits(x).trim())
      .filter(Boolean);
    return new Set(values.length ? values : DEFAULT_DUPLICATE_NUMBERS.split(','));
  }

  function getDuplicateInfo(name) {
    const original = String(name || '');
    const normalized = normalizeDuplicateName(original);
    if (!normalized) {
      return null;
    }

    const configuredNumbers = getConfiguredDuplicateNumbers();
    const directMatch = normalized.match(/^(.*?)[(]([0-9]+)[)]\s*$/u);
    if (directMatch && configuredNumbers.has(directMatch[2])) {
      return {
        matched: true,
        number: directMatch[2],
        baseName: directMatch[1].trim(),
        normalized,
        mode: 'direct-tail',
      };
    }

    const withExtMatch = normalized.match(/^(.*?)[(]([0-9]+)[)](\.[a-z0-9]{1,12})$/iu);
    if (withExtMatch && configuredNumbers.has(withExtMatch[2])) {
      return {
        matched: true,
        number: withExtMatch[2],
        baseName: withExtMatch[1].trim(),
        normalized,
        extension: withExtMatch[3],
        mode: 'before-extension',
      };
    }

    return null;
  }

  function isDuplicateName(name) {
    return Boolean(getDuplicateInfo(name));
  }

  function scoreNameCandidate(name) {
    const text = normalizeDuplicateName(name);
    if (!isProbablyUsefulName(text) || isProbablyMetadataText(text)) {
      return -1;
    }

    let score = text.length;
    if (isDuplicateName(text)) score += 120;
    if (/[((][0-90-9]+[))]/.test(text)) score += 40;
    if (/[.\u4e00-\u9fa5A-Za-z]/.test(text)) score += 20;
    if (/\.[a-z0-9]{1,12}$/i.test(text)) score += 12;
    return score;
  }

  function chooseBestNameCandidate(candidates) {
    const values = Array.from(new Set((candidates || []).map((x) => String(x || '').trim()).filter(Boolean)));
    if (!values.length) {
      return '';
    }

    values.sort((a, b) => scoreNameCandidate(b) - scoreNameCandidate(a) || b.length - a.length);
    return values[0] || '';
  }

  function buildDuplicatePatternFromNumbers(numbersText) {
    const values = String(numbersText || DEFAULT_DUPLICATE_NUMBERS)
      .split(/[\s,,、]+/)
      .map((x) => x.trim())
      .filter(Boolean)
      .map((x) => escapeRegExp(toHalfWidthDigits(x)));

    const group = values.length ? values.join('|') : '1|2|3';
    return `[((]\\s*(?:${group})\\s*[))]\\s*(?:\\.[a-zA-Z0-9]{1,12})?$`;
  }

  function getCurrentRuleMode(firstRule = CONFIG.rename.rules[0] || {}) {
    if (CONFIG.rename.ruleMode) {
      return CONFIG.rename.ruleMode;
    }
    if (firstRule.enabled === false) {
      return 'none';
    }
    if ((firstRule.type || '') === 'text') {
      return 'replace-text';
    }
    if ((firstRule.pattern || '') === DEFAULT_LEADING_BRACKET_PATTERN && (firstRule.flags || '') === 'u') {
      return 'remove-leading-bracket';
    }
    return 'custom-regex';
  }

  function getRuleModeLabel(mode = getCurrentRuleMode()) {
    if (mode === 'remove-leading-bracket') {
      return '删除开头第一个 [] / 【】 段';
    }
    if (mode === 'replace-text') {
      return '按固定文字查找并替换';
    }
    if (mode === 'none') {
      return '不处理前缀';
    }
    if (mode === 'custom-regex') {
      return '自定义正则(高级)';
    }
    return mode || '(未设置)';
  }

  function getRenameOutputModeLabel(mode = (CONFIG.rename.output || {}).mode || 'keep-clean') {
    if (mode === 'keep-clean') {
      return '直接使用处理后的名字';
    }
    if (mode === 'add-text') {
      return '增加文字';
    }
    if (mode === 'replace-text') {
      return '替换文字';
    }
    if (mode === 'format') {
      return '格式命名';
    }
    if (mode === 'custom-template') {
      return '自定义模板(高级)';
    }
    return mode || '(未设置)';
  }

  function splitRecognizedExtension(name) {
    const str = String(name || '');
    if (!str) {
      return {
        base: '',
        ext: '',
      };
    }

    const lower = str.toLowerCase();
    for (const ext of KNOWN_COMPOUND_FILE_EXTENSIONS) {
      if (lower.endsWith(ext) && str.length > ext.length) {
        return {
          base: str.slice(0, -ext.length),
          ext: str.slice(-ext.length),
        };
      }
    }

    const idx = str.lastIndexOf('.');
    if (idx <= 0) {
      return {
        base: str,
        ext: '',
      };
    }

    const extBody = lower.slice(idx + 1);
    if (!KNOWN_FILE_EXTENSIONS.has(extBody)) {
      return {
        base: str,
        ext: '',
      };
    }

    return {
      base: str.slice(0, idx),
      ext: str.slice(idx),
    };
  }

  function getBaseName(name) {
    return splitRecognizedExtension(name).base;
  }

  function getExt(name) {
    return splitRecognizedExtension(name).ext;
  }

  function renderTemplate(template, values) {
    return String(template || '').replace(/\{([a-zA-Z0-9_]+)\}/g, (_, key) => {
      if (Object.prototype.hasOwnProperty.call(values, key)) {
        return values[key] == null ? '' : String(values[key]);
      }
      return '';
    });
  }

  function applyRulesWithRuleSet(name, rules, options = {}) {
    let result = String(name || '');
    const list = Array.isArray(rules) ? rules : [];

    for (const rule of list) {
      if (!rule || rule.enabled === false) {
        continue;
      }

      if (rule.type === 'regex') {
        const pattern = String(rule.pattern || '');
        if (!pattern) {
          continue;
        }
        const re = new RegExp(pattern, rule.flags || '');
        result = result.replace(re, rule.replace ?? '');
        continue;
      }

      if (rule.type === 'text') {
        const search = String(rule.search ?? '');
        if (!search) {
          continue;
        }
        result = result.split(search).join(rule.replace ?? '');
      }
    }

    if (options.trimResult !== false && CONFIG.rename.trimResult) {
      result = result.trim();
    }
    return result;
  }

  function applyRules(name, item) {
    return applyRulesWithRuleSet(name, CONFIG.rename.rules, {
      trimResult: CONFIG.rename.trimResult,
    });
  }

  function getDefaultExampleName() {
    const captured = getCapturedItems().find((item) => item && item.name);
    if (captured && captured.name) {
      return String(captured.name);
    }
    return '[高清剧集网]访达[全12集].2025.2160p.WEB-DL.H265.AAC-ColorTV';
  }

  function getPanelFirstRuleDraft() {
    const ruleMode = UI.fields.ruleMode?.value || getCurrentRuleMode();
    const rule = {
      enabled: ruleMode !== 'none',
      type: 'regex',
      pattern: '',
      flags: '',
      search: '',
      replace: '',
    };

    if (ruleMode === 'remove-leading-bracket') {
      rule.type = 'regex';
      rule.pattern = DEFAULT_LEADING_BRACKET_PATTERN;
      rule.flags = 'u';
      rule.replace = '';
      return rule;
    }

    if (ruleMode === 'replace-text') {
      rule.type = 'text';
      rule.search = String(UI.fields.ruleSearchText?.value || '');
      rule.replace = String(UI.fields.ruleReplaceText?.value || '');
      return rule;
    }

    if (ruleMode === 'custom-regex') {
      rule.type = 'regex';
      rule.pattern = String(UI.fields.rulePattern?.value || '');
      rule.flags = String(UI.fields.ruleFlags?.value || '');
      rule.replace = String(UI.fields.ruleReplace?.value || '');
      return rule;
    }

    return rule;
  }

  function getPanelOutputDraft() {
    const mode = UI.fields.outputMode?.value || ((CONFIG.rename.output || {}).mode || 'keep-clean');
    return {
      mode,
      addText: String(UI.fields.addText?.value || ''),
      addPosition: String(UI.fields.addPosition?.value || 'suffix'),
      addIgnoreExtension: UI.fields.addIgnoreExtension?.checked !== false,
      findText: String(UI.fields.outputFindText?.value || ''),
      replaceText: String(UI.fields.outputReplaceText?.value || ''),
      formatStyle: String(UI.fields.formatStyle?.value || 'text-and-index'),
      formatText: String(UI.fields.formatText?.value || ''),
      formatPosition: String(UI.fields.formatPosition?.value || 'suffix'),
      startIndex: Number(UI.fields.startIndex?.value || 0),
      template: String(UI.fields.template?.value || '{clean}').trim() || '{clean}',
    };
  }

  function buildFinalNameFromDraft(original, clean, fileId, output, renameIndex = 0) {
    const ext = getExt(clean);
    const base = getBaseName(clean);
    const serial = Number(output.startIndex || 0) + Number(renameIndex || 0);

    if (output.mode === 'add-text') {
      if (!output.addText) {
        return clean;
      }
      const ignoreExtension = output.addIgnoreExtension !== false;
      if (ignoreExtension && output.addPosition === 'suffix' && ext) {
        return `${base}${output.addText}${ext}`;
      }
      return output.addPosition === 'prefix' ? `${output.addText}${clean}` : `${clean}${output.addText}`;
    }

    if (output.mode === 'replace-text') {
      if (!output.findText) {
        return clean;
      }
      return clean.split(output.findText).join(output.replaceText || '');
    }

    if (output.mode === 'format') {
      const formatText = String(output.formatText || '').trim() || '文件';
      if (output.formatStyle === 'text-only') {
        return formatText;
      }
      return output.formatPosition === 'prefix' ? `${serial}${formatText}` : `${formatText}${serial}`;
    }

    if (output.mode === 'custom-template') {
      return renderTemplate(output.template || '{clean}', {
        original,
        clean,
        base,
        ext,
        fileId,
        index: serial,
      });
    }

    return clean;
  }

  function getPanelPreviewDraft() {
    return {
      ruleMode: UI.fields.ruleMode?.value || getCurrentRuleMode(),
      outputMode: UI.fields.outputMode?.value || ((CONFIG.rename.output || {}).mode || 'keep-clean'),
      firstRule: getPanelFirstRuleDraft(),
      output: getPanelOutputDraft(),
    };
  }

  function getRenameExampleDescription(draft) {
    if (draft.ruleMode === 'replace-text') {
      const search = String(draft.firstRule.search || '');
      const replace = String(draft.firstRule.replace || '');
      if (search) {
        return replace ? `预处理会把所有“${search}”替换成“${replace}”。` : `预处理会删除所有“${search}”。`;
      }
    }

    if (draft.output.mode === 'add-text') {
      return draft.output.addText
        ? `最终会在名字${draft.output.addPosition === 'prefix' ? '前面' : (draft.output.addIgnoreExtension !== false ? '后面(扩展名前)' : '后面')}增加“${draft.output.addText}”。`
        : '增加文字模式下,先填写要增加的内容。';
    }

    if (draft.output.mode === 'replace-text') {
      return draft.output.findText
        ? `最终会把名字里的所有“${draft.output.findText}”替换成“${draft.output.replaceText || ''}”。`
        : '替换文字模式下,先填写“查找文本”。';
    }

    if (draft.output.mode === 'format') {
      return draft.output.formatStyle === 'text-only'
        ? '格式命名会把名字统一改成你填写的“自定义格式”。'
        : '格式命名会按“自定义格式 + 序号”来生成新名字。';
    }

    if (draft.output.mode === 'custom-template') {
      return '高级模板模式会按你填写的模板生成名字,比如 {clean}、{original}、{index}。';
    }

    return '最终会直接使用“预处理后”的名字。';
  }

  function updateRenameModePreview() {
    if (!UI.root) {
      return;
    }

    const ruleMode = UI.fields.ruleMode?.value || getCurrentRuleMode();
    const outputMode = UI.fields.outputMode?.value || ((CONFIG.rename.output || {}).mode || 'keep-clean');
    const advanced = UI.root.querySelector('[data-role="advanced-details"]');
    if (advanced && (ruleMode === 'custom-regex' || outputMode === 'custom-template')) {
      advanced.open = true;
    }

    UI.root.querySelectorAll('[data-role="rule-text-group"]').forEach((node) => {
      node.style.display = ruleMode === 'replace-text' ? '' : 'none';
    });
    UI.root.querySelectorAll('[data-role="output-add-group"]').forEach((node) => {
      node.style.display = outputMode === 'add-text' ? '' : 'none';
    });
    UI.root.querySelectorAll('[data-role="output-replace-group"]').forEach((node) => {
      node.style.display = outputMode === 'replace-text' ? '' : 'none';
    });
    UI.root.querySelectorAll('[data-role="output-format-group"]').forEach((node) => {
      node.style.display = outputMode === 'format' ? '' : 'none';
    });
    UI.root.querySelectorAll('[data-role="output-template-group"]').forEach((node) => {
      node.style.display = outputMode === 'custom-template' ? '' : 'none';
    });

    const exampleField = UI.fields.exampleName;
    if (exampleField && !String(exampleField.value || '').trim()) {
      exampleField.value = getDefaultExampleName();
    }
    const original = String(exampleField?.value || getDefaultExampleName());
    const draft = getPanelPreviewDraft();
    const clean = applyRulesWithRuleSet(original, [draft.firstRule], {
      trimResult: CONFIG.rename.trimResult,
    });
    const finalName = buildFinalNameFromDraft(original, clean, 'demo', draft.output, 0);

    const desc = UI.root.querySelector('[data-role="rename-example-desc"]');
    const cleanEl = UI.root.querySelector('[data-role="rename-example-clean"]');
    const finalEl = UI.root.querySelector('[data-role="rename-example-final"]');

    if (desc) {
      desc.textContent = getRenameExampleDescription(draft);
    }
    if (cleanEl) {
      cleanEl.textContent = clean || '(空)';
    }
    if (finalEl) {
      finalEl.textContent = finalName || '(空)';
    }
  }

  function guessItemIsDirectory(obj, name = '') {
    if (!obj || typeof obj !== 'object' || Array.isArray(obj)) {
      return false;
    }

    const ext = String(getExt(name) || '').replace(/^\./, '').toLowerCase();
    if (ext && (
      KNOWN_FILE_EXTENSIONS.has(ext)
      || CLOUD_VIDEO_EXTENSIONS.has(ext)
      || CLOUD_JUNK_EXTENSIONS.has(ext)
      || EMPTY_SCAN_EXTRA_FILE_EXTENSIONS.has(ext)
    )) {
      return false;
    }

    const explicit = [
      obj.isDir,
      obj.is_dir,
      obj.isFolder,
      obj.is_folder,
      obj.folder,
      obj.directory,
      obj.dir,
    ].map((value) => normalizeBooleanish(value)).find((value) => value != null);
    if (explicit != null) {
      return explicit;
    }

    const dirType = toFiniteNumberOrNull(obj.dirType ?? obj.dir_type);
    if (dirType != null) {
      return dirType > 0;
    }

    if (
      hasMeaningfulDirectoryValue(obj.dirName)
      || hasMeaningfulDirectoryValue(obj.dir_name)
      || hasMeaningfulDirectoryValue(obj.folderName)
      || hasMeaningfulDirectoryValue(obj.folder_name)
      || hasMeaningfulDirectoryValue(obj.folderId)
      || hasMeaningfulDirectoryValue(obj.folder_id)
      || hasDirectoryCountHint(obj.childCount)
      || hasDirectoryCountHint(obj.childrenCount)
      || hasDirectoryCountHint(obj.children_count)
      || hasDirectoryCountHint(obj.dirCount)
      || hasDirectoryCountHint(obj.dir_count)
      || hasDirectoryCountHint(obj.folderCount)
      || hasDirectoryCountHint(obj.folder_count)
      || hasDirectoryCountHint(obj.subCount)
      || hasDirectoryCountHint(obj.sub_count)
      || hasMeaningfulDirectoryValue(obj.dirId)
      || hasMeaningfulDirectoryValue(obj.dir_id)
    ) {
      return true;
    }

    const typeHints = [
      obj.itemType,
      obj.item_type,
      obj.nodeType,
      obj.node_type,
      obj.resourceType,
      obj.resource_type,
      obj.resType,
      obj.res_type,
      obj.fileType,
      obj.file_type,
      obj.type,
      obj.kind,
      obj.bizType,
      obj.biz_type,
    ];

    for (const hint of typeHints) {
      if (hint == null || hint === '') {
        continue;
      }
      const text = String(hint).trim().toLowerCase();
      if (!text) {
        continue;
      }
      if (/(dir|folder|directory|catalog)/i.test(text)) {
        return true;
      }
      if (/(file|video|image|audio|doc|text|subtitle|torrent)/i.test(text)) {
        return false;
      }
    }

    if (obj.folderId != null || obj.folder_id != null) {
      return !getExt(name);
    }

    return false;
  }

  function extractDirectoryIdCandidates(obj, fallbackId = '') {
    if (!obj || typeof obj !== 'object' || Array.isArray(obj)) {
      return normalizeIdCandidates([fallbackId]);
    }
    return normalizeIdCandidates([
      obj.dirId,
      obj.dir_id,
      obj.folderId,
      obj.folder_id,
      obj.fileId,
      obj.id,
      obj.resourceId,
      obj.resId,
      obj.bizId,
      obj.objId,
      obj.shareFileId,
      obj.share_file_id,
      ...collectIdLikeValues(obj),
      fallbackId,
    ]);
  }

  function isCompactRawScalar(value) {
    return value == null || ['string', 'number', 'boolean'].includes(typeof value);
  }

  function compactNormalizedItemRaw(obj) {
    if (!obj || typeof obj !== 'object' || Array.isArray(obj)) {
      return {};
    }

    const out = {};
    const pickedKeys = [
      'fileId',
      'id',
      'resourceId',
      'resId',
      'bizId',
      'objId',
      'shareFileId',
      'share_file_id',
      'dirId',
      'dir_id',
      'folderId',
      'folder_id',
      'parentId',
      'parent_id',
      'pid',
      'parentFileId',
      'parent_file_id',
      'name',
      'fileName',
      'file_name',
      'filename',
      'resName',
      'resourceName',
      'title',
      'displayName',
      'display_name',
      'originalName',
      'original_name',
      'fileFullName',
      'fullName',
      'path',
      'filePath',
      'file_path',
      'fullPath',
      'full_path',
      'ext',
      'size',
      'fileSize',
      'file_size',
      'resourceSize',
      'resource_size',
      'resSize',
      'res_size',
      'bytes',
      'length',
      'gcid',
      'md5',
      'fileMd5',
      'file_md5',
      'etag',
      'contentMd5',
      'content_md5',
      'content_hash',
      'contentHash',
      'content_hash_name',
      'contentHashName',
      'fileHash',
      'file_hash',
      'resHash',
      'res_hash',
      'resourceHash',
      'resource_hash',
      'hash',
      'digest',
      'checksum',
      'hashType',
      'hash_type',
      'isDir',
      'is_dir',
      'isFolder',
      'is_folder',
      'folder',
      'directory',
      'dir',
      'dirType',
      'dir_type',
      'itemType',
      'item_type',
      'nodeType',
      'node_type',
      'resourceType',
      'resource_type',
      'resType',
      'res_type',
      'fileType',
      'file_type',
      'type',
      'kind',
      'bizType',
      'biz_type',
      'fromDom',
      'domIsDir',
    ];

    for (const key of pickedKeys) {
      if (!Object.prototype.hasOwnProperty.call(obj, key)) {
        continue;
      }
      const value = obj[key];
      if (isCompactRawScalar(value)) {
        out[key] = value;
        continue;
      }
      if (Array.isArray(value) && value.length <= 24 && value.every((entry) => isCompactRawScalar(entry))) {
        out[key] = value.slice();
      }
    }

    return out;
  }

  function normalizeItem(obj) {
    if (!obj || typeof obj !== 'object' || Array.isArray(obj)) {
      return null;
    }

    const fileId = obj.fileId ?? obj.id ?? obj.resourceId ?? obj.resId ?? obj.bizId ?? obj.objId ?? obj.shareFileId ?? obj.share_file_id ?? obj.dirId ?? obj.dir_id ?? obj.folderId ?? obj.folder_id;
    const dirIdCandidates = extractDirectoryIdCandidates(obj, fileId);
    const dirId = dirIdCandidates[0] || fileId;
    const name = chooseBestNameCandidate([
      obj.name,
      obj.fileName,
      obj.file_name,
      obj.filename,
      obj.resName,
      obj.resourceName,
      obj.title,
      obj.displayName,
      obj.display_name,
      obj.originalName,
      obj.original_name,
      obj.fileFullName,
      obj.fullName,
    ]) || chooseBestNameCandidate([
      obj.dirName,
      obj.dir_name,
      obj.folderName,
      obj.folder_name,
    ]);

    if ((typeof fileId === 'string' || typeof fileId === 'number') && typeof name === 'string') {
      return {
        fileId: String(fileId),
        dirId: dirId == null ? String(fileId) : String(dirId),
        dirIdCandidates,
        name,
        parentId: String(obj.parentId ?? obj.parent_id ?? obj.pid ?? obj.parentFileId ?? obj.parent_file_id ?? ''),
        isDir: guessItemIsDirectory(obj, name),
        raw: compactNormalizedItemRaw(obj),
      };
    }

    return null;
  }

  function scanItems(node, out = []) {
    if (!node || typeof node !== 'object') {
      return out;
    }

    if (Array.isArray(node)) {
      for (const item of node) {
        scanItems(item, out);
      }
      return out;
    }

    const normalized = normalizeItem(node);
    if (normalized) {
      out.push(normalized);
    }

    for (const value of Object.values(node)) {
      scanItems(value, out);
    }

    return out;
  }

  function dedupeItems(items) {
    const seen = new Set();
    const out = [];
    for (const item of items) {
      if (!item || seen.has(item.fileId)) {
        continue;
      }
      seen.add(item.fileId);
      out.push(item);
    }
    return out;
  }

  function normalizeItemsFromArray(items = []) {
    return dedupeItems(
      (Array.isArray(items) ? items : [])
        .map((item) => normalizeItem(item))
        .filter(Boolean)
    );
  }

  function getPageWindowObject() {
    try {
      if (typeof unsafeWindow !== 'undefined' && unsafeWindow) {
        return unsafeWindow;
      }
    } catch {}
    return window;
  }

  function getPageRuntimeWindows() {
    return dedupeElements([window, getPageWindowObject()].filter(Boolean));
  }

  function getExplicitSelectedState(obj) {
    if (!obj || typeof obj !== 'object' || Array.isArray(obj)) {
      return null;
    }

    const keys = [
      'selected',
      'isSelected',
      'is_selected',
      'checked',
      'isChecked',
      'is_checked',
      'choose',
      'chosen',
      'isChoose',
      'isChosen',
      'is_choose',
      'is_chosen',
    ];

    for (const key of keys) {
      if (!Object.prototype.hasOwnProperty.call(obj, key)) {
        continue;
      }
      const normalized = normalizeBooleanish(obj[key]);
      if (normalized != null) {
        return normalized;
      }
    }

    return null;
  }

  function isLikelySelectedItemsKey(key = '') {
    const text = String(key || '').trim();
    if (!text) {
      return false;
    }
    return /(?:^|_)(selected|checked)(?:rows?|items?|files?|list)?$/iu.test(text)
      || /^(?:selectedRows|selectedItems|selectedFiles|checkedRows|checkedItems|checkedFiles|selectedList|checkedList|selection)$/iu.test(text)
      || /(?:selected|checked|choose|chosen|selection).*(?:rows?|items?|files?|records?|nodes?|list|data)$/iu.test(text)
      || /^(?:rows?|items?|files?|records?|nodes?)Selected$/iu.test(text);
  }

  function isLikelySelectedIdsKey(key = '') {
    const text = String(key || '').trim();
    if (!text) {
      return false;
    }
    return /^(?:selected(?:Row)?Keys|selectedIds|selectedFileIds|checkedKeys|checkedIds|selectedMap|checkedMap|selectionMap|selectionIds?)$/iu.test(text)
      || /(?:selected|checked|choose|chosen|selection).*(?:ids?|keys?|map|set|list)$/iu.test(text)
      || /^(?:ids?|keys?|map|set|list)(?:Selected|Checked)$/iu.test(text);
  }

  function isDomLikeNode(value) {
    return Boolean(value && typeof value === 'object' && typeof value.nodeType === 'number');
  }

  function scoreLikelyStateKey(key = '') {
    const text = String(key || '').trim();
    if (!text) {
      return Number.NEGATIVE_INFINITY;
    }

    let score = 0;
    if (/(selected|selection|checked|choose|chosen)/iu.test(text)) score += 140;
    if (/(store|state)/iu.test(text)) score += 80;
    if (/(file|list|row|item|record|folder|dir|resource|res|disk|drive)/iu.test(text)) score += 50;
    if (/^__.*__$/.test(text)) score += 25;
    if (text.length > 64) score -= 30;
    return score;
  }

  function collectRelatedFrameworkObjects(seed, out = [], options = {}) {
    const queue = Array.isArray(seed) ? [...seed] : [seed];
    const seen = options.seen || new WeakSet();
    const maxNodes = Math.max(32, Number(options.maxNodes || 240));
    let visited = 0;

    while (queue.length && visited < maxNodes) {
      const current = queue.shift();
      if (!current || (typeof current !== 'object' && typeof current !== 'function') || isDomLikeNode(current)) {
        continue;
      }
      if (seen.has(current)) {
        continue;
      }
      seen.add(current);
      visited += 1;
      out.push(current);

      const nextValues = [
        current.current,
        current._internalRoot,
        current.memoizedProps,
        current.pendingProps,
        current.memoizedState,
        current.baseState,
        current.updateQueue,
        current.dependencies,
        current.queue,
        current.baseQueue,
        current.shared,
        current.child,
        current.sibling,
        current.return,
        current.alternate,
        current.stateNode,
        current.containerInfo,
        current.ctx,
        current.setupState,
        current.data,
        current.props,
        current.proxy,
        current.exposed,
        current.subTree,
        current.vnode,
        current.appContext,
        current.provides,
        current.refs,
        current.renderContext,
        current.root,
        current.store,
      ];

      const keyedCandidates = [];
      try {
        for (const [key, value] of Object.entries(current)) {
          if (
            value
            && (typeof value === 'object' || typeof value === 'function')
            && !isDomLikeNode(value)
            && /(?:selected|selection|checked|choose|chosen|store|state|memoized|props|current|child|sibling|return|alternate|file|list|row|item|record|folder|resource|res)/iu.test(key)
          ) {
            keyedCandidates.push(value);
          }
          if (keyedCandidates.length >= 32) {
            break;
          }
        }
      } catch {}

      for (const value of [...nextValues, ...keyedCandidates]) {
        if (!value || (typeof value !== 'object' && typeof value !== 'function') || isDomLikeNode(value)) {
          continue;
        }
        queue.push(value);
      }
    }

    return out;
  }

  function collectSelectionMarkers(value, marker = null, options = {}) {
    const out = marker || {
      ids: new Set(),
      names: new Set(),
    };
    const seen = options.seen || new WeakSet();
    const depth = Number(options.depth || 0);
    if (value == null || depth > 5) {
      return out;
    }

    if (typeof value === 'string' || typeof value === 'number') {
      const text = String(value).trim();
      if (looksLikeStructuredEntityId(text)) {
        out.ids.add(text);
      } else if (isProbablyUsefulName(text) && !isProbablyMetadataText(text)) {
        out.names.add(normalizeDomName(text));
      }
      return out;
    }

    if (typeof value !== 'object') {
      return out;
    }
    if (seen.has(value)) {
      return out;
    }
    seen.add(value);

    if (value instanceof Set) {
      for (const entry of value.values()) {
        collectSelectionMarkers(entry, out, {
          seen,
          depth: depth + 1,
        });
      }
      return out;
    }

    if (value instanceof Map) {
      for (const [key, entry] of value.entries()) {
        if (looksLikeStructuredEntityId(key) && normalizeBooleanish(entry) === true) {
          out.ids.add(String(key));
          continue;
        }
        collectSelectionMarkers(key, out, {
          seen,
          depth: depth + 1,
        });
        collectSelectionMarkers(entry, out, {
          seen,
          depth: depth + 1,
        });
      }
      return out;
    }

    if (Array.isArray(value)) {
      for (const item of value) {
        collectSelectionMarkers(item, out, {
          seen,
          depth: depth + 1,
        });
      }
      return out;
    }

    const normalized = normalizeItem(value);
    const explicit = getExplicitSelectedState(value);
    if (normalized && explicit === true) {
      out.ids.add(String(normalized.fileId || ''));
      out.dirCandidates = out.dirCandidates || new Set();
      for (const id of normalizeIdCandidates([normalized.fileId, normalized.dirId, ...(normalized.dirIdCandidates || [])])) {
        out.ids.add(String(id || ''));
      }
      out.names.add(normalizeDomName(normalized.name || ''));
    }

    for (const [key, entry] of Object.entries(value)) {
      if (looksLikeStructuredEntityId(key) && normalizeBooleanish(entry) === true) {
        out.ids.add(String(key));
        continue;
      }
      if (/^(?:fileId|id|resourceId|resId|bizId|objId|dirId|folderId)$/iu.test(key) && entry != null) {
        const text = String(entry).trim();
        if (looksLikeStructuredEntityId(text)) {
          out.ids.add(text);
        }
        continue;
      }
      if (/^(?:name|fileName|filename|title|displayName|fullName|originalName)$/iu.test(key) && typeof entry === 'string') {
        const text = normalizeDomName(entry);
        if (text && isProbablyUsefulName(text) && !isProbablyMetadataText(text)) {
          out.names.add(text);
        }
      }
      if (entry && typeof entry === 'object') {
        collectSelectionMarkers(entry, out, {
          seen,
          depth: depth + 1,
        });
      }
    }

    return out;
  }

  function itemMatchesSelectionMarkers(item, marker) {
    if (!item || !marker) {
      return false;
    }
    const candidates = normalizeIdCandidates([item.fileId, item.dirId, ...(item.dirIdCandidates || [])]).map((value) => String(value || ''));
    if (candidates.some((value) => marker.ids.has(value))) {
      return true;
    }
    const nameKey = normalizeDomName(item.name || '');
    return Boolean(nameKey && marker.names.has(nameKey));
  }

  function collectSelectedItemsFromUnknownObject(node, out = [], options = {}) {
    const seen = options.seen || new WeakSet();
    const depth = Number(options.depth || 0);
    if (!node || typeof node !== 'object' || depth > 5) {
      return out;
    }
    if (seen.has(node)) {
      return out;
    }
    seen.add(node);

    if (node instanceof Set) {
      for (const entry of node.values()) {
        if (entry && typeof entry === 'object') {
          collectSelectedItemsFromUnknownObject(entry, out, {
            seen,
            depth: depth + 1,
          });
        }
      }
      return out;
    }

    if (node instanceof Map) {
      for (const [key, value] of node.entries()) {
        if (value && typeof value === 'object') {
          collectSelectedItemsFromUnknownObject(value, out, {
            seen,
            depth: depth + 1,
          });
        }
        if (key && typeof key === 'object') {
          collectSelectedItemsFromUnknownObject(key, out, {
            seen,
            depth: depth + 1,
          });
        }
      }
      return out;
    }

    if (Array.isArray(node)) {
      const directSelected = [];
      for (const entry of node) {
        const normalized = normalizeItem(entry);
        if (!normalized) {
          continue;
        }
        if (getExplicitSelectedState(entry) === true) {
          directSelected.push(normalized);
        }
      }
      if (directSelected.length) {
        out.push(...directSelected);
      }
      for (const entry of node) {
        if (entry && typeof entry === 'object') {
          collectSelectedItemsFromUnknownObject(entry, out, {
            seen,
            depth: depth + 1,
          });
        }
      }
      return out;
    }

    const explicit = getExplicitSelectedState(node);
    const normalized = normalizeItem(node);
    if (normalized && explicit === true) {
      out.push(normalized);
    }

    const marker = {
      ids: new Set(),
      names: new Set(),
    };

    for (const [key, value] of Object.entries(node)) {
      if (isLikelySelectedItemsKey(key)) {
        out.push(...normalizeItemsFromArray(Array.isArray(value) ? value : extractItemsFromUnknownObject(value)));
        collectSelectionMarkers(value, marker, {
          depth: depth + 1,
        });
        continue;
      }
      if (isLikelySelectedIdsKey(key)) {
        collectSelectionMarkers(value, marker, {
          depth: depth + 1,
        });
      }
    }

    if (marker.ids.size || marker.names.size) {
      for (const [key, value] of Object.entries(node)) {
        if (!value || typeof value !== 'object' || !Array.isArray(value) || !value.length) {
          continue;
        }
        if (!isLikelyListArrayKey(key) && !value.some((entry) => normalizeItem(entry))) {
          continue;
        }
        for (const item of normalizeItemsFromArray(value)) {
          if (itemMatchesSelectionMarkers(item, marker)) {
            out.push(item);
          }
        }
      }
    }

    for (const value of Object.values(node)) {
      if (value && typeof value === 'object') {
        collectSelectedItemsFromUnknownObject(value, out, {
          seen,
          depth: depth + 1,
        });
      }
    }

    return out;
  }

  function getWindowSelectionStateCandidates() {
    const candidates = [];
    const exactKeys = [
      '__INITIAL_STATE__',
      '__INITIAL_DATA__',
      '__APP_STATE__',
      '__APP_DATA__',
      '__STORE__',
      '__NEXT_DATA__',
      '__NUXT__',
      'store',
      'appStore',
      'pageStore',
      'listStore',
      'fileStore',
      'diskStore',
      'selectionStore',
      'selectedStore',
      'reduxStore',
      '__REACT_DEVTOOLS_GLOBAL_HOOK__',
    ];

    for (const runtimeWindow of getPageRuntimeWindows()) {
      for (const key of exactKeys) {
        try {
          const value = runtimeWindow[key];
          value && candidates.push(value);
        } catch {}
      }

      try {
        const dynamicKeys = Array.from(new Set([
          ...Object.keys(runtimeWindow),
          ...Object.getOwnPropertyNames(runtimeWindow),
        ]))
          .filter((key) => scoreLikelyStateKey(key) > 0)
          .sort((left, right) => scoreLikelyStateKey(right) - scoreLikelyStateKey(left) || left.length - right.length)
          .slice(0, 240);
        for (const key of dynamicKeys) {
          try {
            const value = runtimeWindow[key];
            value && candidates.push(value);
          } catch {}
        }
      } catch {}

      try {
        const hook = runtimeWindow.__REACT_DEVTOOLS_GLOBAL_HOOK__;
        if (hook) {
          candidates.push(hook);
          hook.renderers && candidates.push(hook.renderers);
          hook._fiberRoots && candidates.push(hook._fiberRoots);
          if (hook.renderers instanceof Map && typeof hook.getFiberRoots === 'function') {
            for (const rendererId of hook.renderers.keys()) {
              try {
                const roots = hook.getFiberRoots(rendererId);
                roots && candidates.push(roots);
              } catch {}
            }
          }
        }
      } catch {}
    }

    return dedupeElements(candidates);
  }

  function getPageFrameworkStateRoots() {
    const appRoots = dedupeElements(Array.from(document.querySelectorAll(
      '#root, #app, #__next, #__nuxt, [data-reactroot], main, [role="main"], [class*="app"]'
    )).filter((node) => node && !isHelperPanelNode(node)).slice(0, 16));
    const roots = dedupeElements([
      ...getListRows().slice(0, 16),
      findScrollableListContainer(),
      findScrollableListContainer()?.parentElement,
      findScrollableListContainer()?.parentElement?.parentElement,
      ...appRoots,
      ...getWindowSelectionStateCandidates(),
    ].filter(Boolean));
    const frameworkRoots = [];
    const relatedSeen = new WeakSet();

    for (const root of roots) {
      frameworkRoots.push(root);
      if (root?.parentElement) {
        frameworkRoots.push(root.parentElement);
      }
      for (const candidate of collectFrameworkObjectCandidatesFromNode(root)) {
        frameworkRoots.push(candidate);
        collectRelatedFrameworkObjects(candidate, frameworkRoots, {
          seen: relatedSeen,
          maxNodes: 320,
        });
      }
    }

    return dedupeElements(frameworkRoots.filter(Boolean));
  }

  function collectSelectionMarkersFromPageFrameworkState() {
    const marker = {
      ids: new Set(),
      names: new Set(),
    };
    const seenObjects = new WeakSet();

    for (const candidate of getPageFrameworkStateRoots()) {
      if (!candidate || (typeof candidate !== 'object' && typeof candidate !== 'function')) {
        continue;
      }
      collectSelectionMarkers(candidate, marker, {
        seen: seenObjects,
      });
      if (typeof candidate.getState === 'function') {
        try {
          const state = candidate.getState();
          state && collectSelectionMarkers(state, marker, {
            seen: seenObjects,
          });
        } catch {}
      }
    }

    return marker;
  }

  function collectSelectedItemsFromPageFrameworkState(options = {}) {
    const frameworkRoots = getPageFrameworkStateRoots();

    const selected = [];
    const seenObjects = new WeakSet();
    for (const candidate of frameworkRoots) {
      if (!candidate || (typeof candidate !== 'object' && typeof candidate !== 'function')) {
        continue;
      }
      collectSelectedItemsFromUnknownObject(candidate, selected, {
        seen: seenObjects,
      });
      if (typeof candidate.getState === 'function') {
        try {
          const state = candidate.getState();
          state && collectSelectedItemsFromUnknownObject(state, selected, {
            seen: seenObjects,
          });
        } catch {}
      }
    }

    const deduped = dedupeItems(selected).filter((item) => item && item.fileId && !isSyntheticDomId(item.fileId));
    if (options.onlyDirectories) {
      return deduped.filter((item) => item.isDir === true || shouldTreatItemAsDirectory(item));
    }
    return deduped;
  }

  function isLikelyListArrayKey(key = '') {
    const text = String(key || '').trim().toLowerCase();
    if (!text) {
      return false;
    }
    return /^(data|list|items|rows|records|files|filelist|file_list|children|child_list|childlist|result)$/i.test(text)
      || /(list|item|row|record|file|child|result)s?$/i.test(text);
  }

  function collectItemArrayCandidates(node, out = [], options = {}) {
    const seen = options.seen || new WeakSet();
    const pathKeys = Array.isArray(options.pathKeys) ? options.pathKeys : [];
    const depth = Number(options.depth || 0);
    if (!node || typeof node !== 'object' || depth > 5) {
      return out;
    }
    if (seen.has(node)) {
      return out;
    }
    seen.add(node);

    if (Array.isArray(node)) {
      const normalizedItems = normalizeItemsFromArray(node);
      const lastKey = String(pathKeys[pathKeys.length - 1] || '').trim();
      const likelyKey = isLikelyListArrayKey(lastKey);
      const pathBonus = pathKeys.some((key) => isLikelyListArrayKey(key)) ? 40 : 0;
      const dataBonus = pathKeys[0] === 'data' ? 30 : 0;
      const sizeBonus = normalizedItems.length;

      if (normalizedItems.length || likelyKey || node.length === 0) {
        out.push({
          items: normalizedItems,
          score: sizeBonus + (likelyKey ? 200 : 0) + pathBonus + dataBonus,
          isExplicitEmpty: node.length === 0 && likelyKey,
          path: pathKeys.join('.'),
        });
      }

      for (const item of node) {
        if (item && typeof item === 'object') {
          collectItemArrayCandidates(item, out, {
            seen,
            pathKeys,
            depth: depth + 1,
          });
        }
      }
      return out;
    }

    for (const [key, value] of Object.entries(node)) {
      if (!value || typeof value !== 'object') {
        continue;
      }
      collectItemArrayCandidates(value, out, {
        seen,
        pathKeys: [...pathKeys, key],
        depth: depth + 1,
      });
    }
    return out;
  }

  function pickBestItemArrayCandidate(payload) {
    const candidates = collectItemArrayCandidates(payload);
    if (!candidates.length) {
      return null;
    }

    candidates.sort((left, right) => {
      if (right.score !== left.score) {
        return right.score - left.score;
      }
      if (left.isExplicitEmpty !== right.isExplicitEmpty) {
        return Number(right.isExplicitEmpty) - Number(left.isExplicitEmpty);
      }
      return String(left.path || '').length - String(right.path || '').length;
    });

    return candidates[0] || null;
  }

  function extractItemsFromPayload(payload) {
    const explicitCandidate = pickBestItemArrayCandidate(payload);
    if (explicitCandidate) {
      return explicitCandidate.items;
    }
    return dedupeItems(scanItems(payload));
  }

  function looksLikeListRequest(url, requestBody) {
    if (typeof url === 'string' && /get_file_list|list|share/i.test(url)) {
      return true;
    }

    if (!requestBody || typeof requestBody !== 'object') {
      return false;
    }

    return ['parentId', 'pageSize', 'pageNum', 'pageNo', 'sortType', 'orderBy'].some((key) =>
      Object.prototype.hasOwnProperty.call(requestBody, key)
    );
  }

  function looksLikeListResponse(payload) {
    if (!payload || typeof payload !== 'object') {
      return false;
    }
    return extractItemsFromPayload(payload).length > 0;
  }

  function isLikelyListCapture(url, requestBody, responseBody) {
    return looksLikeListRequest(url, requestBody) || looksLikeListResponse(responseBody);
  }

  function normalizeDomName(name) {
    return String(name || '').replace(/\s+/g, ' ').trim();
  }

  function getVisibleNodeText(node) {
    if (!node) {
      return '';
    }
    return normalizeDomName(node.textContent || node.innerText || '');
  }

  function cleanDirectoryTitleCandidate(text) {
    const value = normalizeDomName(text)
      .replace(/\s*[-||丨]\s*光鸭云盘.*$/u, '')
      .replace(/\s*[-||丨]\s*www\.guangyapan\.com.*$/iu, '')
      .trim();
    return value;
  }

  function getCurrentDirectoryDisplayName() {
    const selectors = [
      '[aria-label*="breadcrumb" i] [aria-current="page"]',
      '[class*="breadcrumb"] [aria-current="page"]',
      '[class*="crumb"] [aria-current="page"]',
      '[class*="breadcrumb"] [class*="item"]:last-child',
      '[class*="crumb"] [class*="item"]:last-child',
      '[class*="path"] [class*="name"]:last-child',
      '[class*="path"] [class*="item"]:last-child',
      'nav [aria-current="page"]',
    ];

    for (const selector of selectors) {
      const nodes = Array.from(document.querySelectorAll(selector));
      for (const node of nodes.reverse()) {
        const text = cleanDirectoryTitleCandidate(getVisibleNodeText(node));
        if (isProbablyUsefulName(text) && !/^光鸭云盘工具$/u.test(text)) {
          return text;
        }
      }
    }

    const title = cleanDirectoryTitleCandidate(document.title || '');
    if (isProbablyUsefulName(title) && !/^(光鸭云盘|首页|我的网盘)$/u.test(title)) {
      return title;
    }

    return '(当前目录)';
  }

  function isHelperPanelNode(node) {
    return Boolean(node && typeof node.closest === 'function' && node.closest('#gyp-batch-rename-root'));
  }

  function isSyntheticDomId(value) {
    return /^dom(?:dir)?:/u.test(String(value || '').trim());
  }

  function isBreadcrumbContainerNode(node) {
    return Boolean(
      node
      && typeof node.closest === 'function'
      && node.closest('[aria-label*="breadcrumb" i], [class*="breadcrumb"], [class*="crumb"], [class*="path"], nav')
    );
  }

  function isLikelyListHeaderRow(row) {
    const text = normalizeDomName(row?.innerText || row?.textContent || '');
    if (!text) {
      return false;
    }

    if (/^(文件名称|大小|类型|修改时间)(\s+(文件名称|大小|类型|修改时间))+$/u.test(text)) {
      return true;
    }

    return ['文件名称', '大小', '类型', '修改时间'].every((keyword) => text.includes(keyword)) && text.length <= 40;
  }

  function isUsableListRow(row) {
    if (!row || !isVisibleElement(row) || isHelperPanelNode(row) || isBreadcrumbContainerNode(row) || isLikelyListHeaderRow(row)) {
      return false;
    }

    const text = normalizeDomName(row.innerText || row.textContent || '');
    if (!text) {
      return false;
    }

    return true;
  }

  async function waitForCondition(check, options = {}) {
    const timeoutMs = Math.max(200, Number(options.timeoutMs || 3000));
    const intervalMs = Math.max(60, Number(options.intervalMs || 120));
    const deadline = Date.now() + timeoutMs;

    while (Date.now() <= deadline) {
      try {
        const result = check();
        if (result) {
          return result;
        }
      } catch {}
      await sleep(intervalMs);
    }

    return null;
  }

  function getDirectoryContextSnapshot() {
    const context = getCurrentListContext();
    return {
      parentId: String(context.parentId || STATE.lastCapturedParentId || '').trim(),
      name: String(getCurrentDirectoryDisplayName() || '(当前目录)').trim() || '(当前目录)',
      url: String(location.href || ''),
      hash: String(location.hash || ''),
      capturedAt: Number(STATE.lastListCapturedAt || 0),
    };
  }

  function isSameDirectorySnapshot(left, right) {
    if (!left || !right) {
      return false;
    }

    const leftParentId = String(left.parentId || '').trim();
    const rightParentId = String(right.parentId || '').trim();
    if (leftParentId && rightParentId) {
      return leftParentId === rightParentId;
    }

    const leftName = normalizeDomName(left.name);
    const rightName = normalizeDomName(right.name);
    if (leftName && rightName && leftName !== rightName) {
      return false;
    }

    const leftUrl = String(left.url || '');
    const rightUrl = String(right.url || '');
    if (leftUrl && rightUrl) {
      return leftUrl === rightUrl;
    }

    return Boolean(leftName && rightName && leftName === rightName);
  }

  function hasDirectoryContextChanged(previous, current) {
    if (!previous || !current) {
      return false;
    }

    const previousParentId = String(previous.parentId || '').trim();
    const currentParentId = String(current.parentId || '').trim();
    if (previousParentId && currentParentId) {
      return previousParentId !== currentParentId;
    }

    const previousUrl = String(previous.url || '');
    const currentUrl = String(current.url || '');
    const previousName = normalizeDomName(previous.name);
    const currentName = normalizeDomName(current.name);
    const urlChanged = Boolean(previousUrl && currentUrl && previousUrl !== currentUrl);
    const nameChanged = Boolean(previousName && currentName && previousName !== currentName);
    if (!urlChanged && !nameChanged) {
      return false;
    }

    return Number(current.capturedAt || 0) > Number(previous.capturedAt || 0);
  }

  function findVisibleEmptyStateInfo() {
    const selectors = [
      '.ant-empty',
      '.arco-empty',
      '[class*="empty"]',
      '[class*="blank"]',
      '[class*="no-data"]',
      '[class*="nodata"]',
      '[class*="zero-state"]',
      '[data-empty]',
      '[data-status="empty"]',
      '[aria-label*="空"]',
    ];

    const selectorNodes = dedupeElements(Array.from(document.querySelectorAll(selectors.join(', '))))
      .filter((node) => isVisibleElement(node) && !isHelperPanelNode(node));
    for (const node of selectorNodes) {
      const text = normalizeDomName(node.innerText || node.textContent || '');
      const matched = EMPTY_STATE_TEXT_PATTERNS.find((pattern) => pattern.test(text));
      if (matched || !text) {
        return {
          visible: true,
          text: text || '(空态组件)',
          via: 'selector',
        };
      }
    }

    const roots = dedupeElements([
      findScrollableListContainer(),
      findScrollableListContainer()?.parentElement,
      document.body,
    ]).filter((node) => node && !isHelperPanelNode(node));

    for (const root of roots) {
      const text = normalizeDomName(root?.innerText || root?.textContent || '');
      const matched = EMPTY_STATE_TEXT_PATTERNS.find((pattern) => pattern.test(text));
      if (matched) {
        return {
          visible: true,
          text: matched.exec(text)?.[0] || matched.source,
          via: 'text',
        };
      }
    }

    return {
      visible: false,
      text: '',
      via: '',
    };
  }

  async function waitForDirectoryChange(previousSnapshot, options = {}) {
    return waitForCondition(() => {
      const currentSnapshot = getDirectoryContextSnapshot();
      return hasDirectoryContextChanged(previousSnapshot, currentSnapshot) ? currentSnapshot : null;
    }, options);
  }

  async function waitForDirectoryMatch(targetSnapshot, options = {}) {
    return waitForCondition(() => {
      const currentSnapshot = getDirectoryContextSnapshot();
      return isSameDirectorySnapshot(targetSnapshot, currentSnapshot) ? currentSnapshot : null;
    }, options);
  }

  function normalizeHashRoute(hash) {
    const text = String(hash || '').trim();
    if (!text) {
      return '';
    }
    return text.startsWith('#') ? text : `#${text.replace(/^#*/, '')}`;
  }

  function extractDirectoryIdFromHashSegment(segment) {
    const matched = String(segment || '').trim().match(/^(\d{8,})-/u);
    return matched ? String(matched[1] || '') : '';
  }

  function getCurrentDirectoryParentIdFromHash(hash = location.hash) {
    const normalizedHash = normalizeHashRoute(hash).replace(/^#\/?/u, '');
    const segments = normalizedHash.split('/').filter(Boolean);
    const ids = segments.map((segment) => extractDirectoryIdFromHashSegment(segment)).filter(Boolean);
    if (ids.length < 2) {
      return '';
    }
    return String(ids[ids.length - 2] || '').trim();
  }

  function buildChildDirectoryHash(previousSnapshot, childItem) {
    const childId = String(childItem?.fileId || childItem?.dirId || '').trim();
    const childName = String(childItem?.name || '').trim();
    if (!childId || isSyntheticDomId(childId) || !childName) {
      return '';
    }

    const baseHash = normalizeHashRoute(previousSnapshot?.hash || location.hash || '');
    if (!baseHash) {
      return '';
    }

    const cleanBase = baseHash.replace(/\/+$/u, '');
    const encodedName = encodeURIComponent(childName);
    const segment = `${childId}-${encodedName}`;
    return `${cleanBase}/${segment}`;
  }

  async function navigateToDirectoryHash(targetHash, previousSnapshot, options = {}) {
    const normalizedHash = normalizeHashRoute(targetHash);
    if (!normalizedHash || normalizeHashRoute(previousSnapshot?.hash || '') === normalizedHash) {
      return null;
    }

    try {
      location.hash = normalizedHash;
    } catch {
      return null;
    }

    await waitForCondition(() => normalizeHashRoute(location.hash) === normalizedHash, {
      timeoutMs: 800,
      intervalMs: 80,
    });

    return waitForFreshDirectoryContext(previousSnapshot, {
      expectedName: options.expectedName || '',
      timeoutMs: Number(options.timeoutMs || 4200),
      intervalMs: Number(options.intervalMs || 180),
      stableMs: Number(options.stableMs || 420),
    });
  }

  async function waitForFreshDirectoryContext(previousSnapshot, options = {}) {
    const timeoutMs = Math.max(400, Number(options.timeoutMs || 2600));
    const intervalMs = Math.max(80, Number(options.intervalMs || 180));
    const stableMs = Math.max(intervalMs, Number(options.stableMs || 360));
    const expectedName = normalizeDomName(options.expectedName || '');
    const previousParentId = String(previousSnapshot?.parentId || '').trim();
    const previousName = normalizeDomName(previousSnapshot?.name || '');
    const previousHash = normalizeHashRoute(previousSnapshot?.hash || '');
    const deadline = Date.now() + timeoutMs;
    let candidate = null;
    let candidateAt = 0;

    while (Date.now() <= deadline) {
      const snapshot = getDirectoryContextSnapshot();
      const currentParentId = String(snapshot.parentId || '').trim();
      const parentChanged = Boolean(currentParentId && currentParentId !== previousParentId && !isSyntheticDomId(currentParentId));
      const currentName = normalizeDomName(snapshot.name || '');
      const hashChanged = normalizeHashRoute(snapshot.hash || '') !== previousHash;
      const nameChanged = Boolean(currentName && currentName !== previousName);
      const nameMatches = !expectedName
        || !snapshot.name
        || textLooksLikeExpected(snapshot.name, expectedName)
        || textLooksLikeExpected(expectedName, snapshot.name);
      const plausibleChange = parentChanged || hashChanged || (nameChanged && nameMatches);

      if (plausibleChange && nameMatches) {
        const candidateKey = currentParentId || normalizeHashRoute(snapshot.hash || '') || currentName;
        if (!candidate || (candidate.parentId || normalizeHashRoute(candidate.hash || '') || normalizeDomName(candidate.name || '')) !== candidateKey) {
          candidate = snapshot;
          candidateAt = Date.now();
        } else if (Date.now() - candidateAt >= stableMs) {
          return snapshot;
        }
      } else {
        candidate = null;
        candidateAt = 0;
      }

      await sleep(intervalMs);
    }

    return candidate;
  }

  function isProbablyUsefulName(name) {
    const text = normalizeDomName(name);
    if (!text) {
      return false;
    }

    const compact = text.replace(/\s+/gu, '');
    if (compact.length < 2 && !/^[A-Za-z0-9一-龥]$/u.test(compact)) {
      return false;
    }

    const blacklist = ['上传', '新建文件夹', '云添加', '文件', '文件名称', '大小', '类型', '文件夹', '其他', '未知类型', 'typeunknown', '-'];
    if (blacklist.includes(text)) {
      return false;
    }

    return true;
  }

  function isProbablyMetadataText(text) {
    const value = normalizeDomName(text);
    if (!value) {
      return true;
    }

    return (
      /^(type(?:unknown|file|folder|video|audio|image|document|other|torrent)|filetypeunknown)$/i.test(value) ||
      /^(其他|未知类型|文件类型|视频|图片|音频|文档|压缩包|种子|字幕)$/u.test(value) ||
      /^\d+(\.\d+)?\s*(B|KB|MB|GB|TB|PB)$/i.test(value) ||
      /^\d{4}[-/.年]\d{1,2}[-/.月]\d{1,2}/.test(value) ||
      /^(今天|昨天|刚刚|\d{1,2}:\d{2})$/.test(value) ||
      /^\d+$/.test(value)
    );
  }

  function rowHasFileSizeHint(row) {
    const text = normalizeDomName(row?.innerText || row?.textContent || '');
    if (!text) {
      return false;
    }
    return /\b\d+(?:\.\d+)?\s*(B|KB|MB|GB|TB|PB)\b/i.test(text);
  }

  function guessDomRowIsDirectory(row, name = '') {
    if (!row) {
      return false;
    }

    const folderSelectors = [
      '[data-type*="folder" i]',
      '[data-kind*="folder" i]',
      '[data-icon*="folder" i]',
      '[class*="folder"]',
      '[class*="dir-icon"]',
      '[class*="folder-icon"]',
      '[aria-label*="文件夹"]',
      '[title*="文件夹"]',
      'img[alt*="folder" i]',
      'img[alt*="文件夹"]',
      'img[src*="folder" i]',
      'svg[aria-label*="folder" i]',
    ];

    for (const selector of folderSelectors) {
      if (row.querySelector(selector)) {
        return true;
      }
    }

    const textCandidates = collectTextCandidates(row);
    if (textCandidates.some((text) => /^(文件夹|folder|directory)$/iu.test(text))) {
      return true;
    }

    const cleanName = normalizeDomName(name);
    if (cleanName && !getExt(cleanName) && !rowHasFileSizeHint(row)) {
      return true;
    }

    return false;
  }

  function collectTextCandidates(row) {
    const out = new Set();
    const push = (value) => {
      const text = normalizeDomName(value);
      if (!isProbablyUsefulName(text) || isProbablyMetadataText(text)) {
        return;
      }
      out.add(text);
    };

    if (!row) {
      return [];
    }

    push(row.getAttribute && row.getAttribute('title'));
    push(row.getAttribute && row.getAttribute('aria-label'));

    const attrNodes = Array.from(row.querySelectorAll('[title], [aria-label], [data-name], [data-filename]'));
    for (const node of attrNodes) {
      push(node.getAttribute && node.getAttribute('title'));
      push(node.getAttribute && node.getAttribute('aria-label'));
      push(node.getAttribute && node.getAttribute('data-name'));
      push(node.getAttribute && node.getAttribute('data-filename'));
    }

    const leafNodes = Array.from(row.querySelectorAll('span, div, p, a, strong, td'))
      .filter((el) => el && el.childElementCount === 0)
      .map((el) => el.textContent);
    for (const value of leafNodes) {
      push(value);
    }

    const rowText = String(row.innerText || row.textContent || '');
    for (const line of rowText.split(/\n+/)) {
      push(line);
    }

    return Array.from(out);
  }

  function findExpectedNameInRow(row, expectedSet) {
    if (!row || !expectedSet || !expectedSet.size) return '';

    const candidates = collectTextCandidates(row);

    for (const candidate of candidates) {
      const domName = normalizeDomName(candidate);
      for (const expected of expectedSet) {
        if (domName === expected) {
          return expected;
        }

        if (textLooksLikeExpected(domName, expected)) {
          return expected;
        }

        if ((domName.includes('...') || domName.length > 20) && expected.startsWith(domName.replace('...', ''))) {
          return expected;
        }

        if (expected.split('.')[0] === domName) {
          return expected;
        }
      }
    }
    return '';
  }

  function extractNameFromRow(row) {
    if (!row) {
      return '';
    }

    const candidates = collectTextCandidates(row)
      .sort((a, b) => b.length - a.length);

    return candidates[0] || '';
  }

  function collectDomItems() {
    const rows = Array.from(
      document.querySelectorAll('[role="row"], li, tr, [class*="row"], [class*="item"], [class*="file"]')
    );
    const items = [];
    const seen = new Set();

    rows.forEach((row, index) => {
      if (isHelperPanelNode(row)) {
        return;
      }

      const checkbox = row.querySelector(
        'input[type="checkbox"], [role="checkbox"], [class*="checkbox"], [class*="check"]'
      );
      if (!checkbox) {
        return;
      }

      const name = extractNameFromRow(row);
      if (!isProbablyUsefulName(name) || seen.has(name)) {
        return;
      }

      seen.add(name);
      items.push({
        fileId: `dom:${index}:${name}`,
        dirId: `dom:${index}:${name}`,
        dirIdCandidates: [`dom:${index}:${name}`],
        name,
        isDir: guessDomRowIsDirectory(row, name),
        raw: {
          fromDom: true,
          domIsDir: guessDomRowIsDirectory(row, name),
        },
      });
    });

    return items;
  }

  function scanItemsSafe(node, out = [], seen = new WeakSet(), depth = 0) {
    if (!node || typeof node !== 'object' || depth > 5) {
      return out;
    }
    if (seen.has(node)) {
      return out;
    }
    seen.add(node);

    if (Array.isArray(node)) {
      for (const item of node) {
        scanItemsSafe(item, out, seen, depth + 1);
      }
      return out;
    }

    const normalized = normalizeItem(node);
    if (normalized) {
      out.push(normalized);
    }

    for (const value of Object.values(node)) {
      if (value && typeof value === 'object') {
        scanItemsSafe(value, out, seen, depth + 1);
      }
    }

    return out;
  }

  function extractItemsFromUnknownObject(node) {
    const explicitCandidate = pickBestItemArrayCandidate(node);
    if (explicitCandidate) {
      return explicitCandidate.items;
    }
    return dedupeItems(scanItemsSafe(node));
  }

  function looksLikeStructuredEntityId(value) {
    const text = String(value || '').trim();
    if (!text || /^dom(?:dir)?:/u.test(text)) {
      return false;
    }
    return /^\d{6,}$/u.test(text) || /^[A-Za-z0-9_-]{12,}$/u.test(text);
  }

  function toCamelAttrKey(name = '') {
    return String(name || '')
      .replace(/^data[-_:]*/iu, '')
      .replace(/[-_:]+([a-z0-9])/giu, (_, ch) => ch.toUpperCase())
      .replace(/[^a-z0-9]/giu, '');
  }

  function buildItemSnapshotFromDomNode(node, expectedName = '', expectedIsDir = false) {
    if (!node || typeof node !== 'object') {
      return null;
    }

    const snapshot = {};
    const assignValue = (key, value) => {
      if (!key || value == null || value === '') {
        return;
      }
      snapshot[key] = value;
    };

    if (node.dataset && typeof node.dataset === 'object') {
      for (const [key, value] of Object.entries(node.dataset)) {
        if (
          /^(?:file|folder|dir|resource|res|biz|obj|share|parent).*id$/iu.test(key)
          || /^(?:name|fileName|folderName|dirName|resourceType|resType|fileType|type|kind|dirType|isDir|isFolder)$/iu.test(key)
        ) {
          assignValue(key, value);
        }
      }
    }

    if (node.attributes && typeof node.attributes.length === 'number') {
      for (const attr of Array.from(node.attributes)) {
        const attrName = String(attr?.name || '');
        const attrValue = String(attr?.value || '').trim();
        if (!attrName || !attrValue) {
          continue;
        }
        if (/^(title|aria-label|data-name|data-filename)$/iu.test(attrName)) {
          assignValue(toCamelAttrKey(attrName), attrValue);
          continue;
        }
        if (/(?:file|folder|dir|resource|res|biz|obj|share|parent).*id/iu.test(attrName) && looksLikeStructuredEntityId(attrValue)) {
          assignValue(toCamelAttrKey(attrName), attrValue);
        }
      }
    }

    if (expectedName && !snapshot.name && !snapshot.fileName && !snapshot.folderName && !snapshot.dirName) {
      snapshot.name = String(expectedName || '');
      snapshot.fileName = String(expectedName || '');
      if (expectedIsDir) {
        snapshot.folderName = String(expectedName || '');
        snapshot.dirName = String(expectedName || '');
      }
    }

    if (expectedIsDir) {
      snapshot.isDir = true;
      snapshot.isFolder = true;
      snapshot.dirType = snapshot.dirType || 1;
      if (!snapshot.dirId && snapshot.fileId) {
        snapshot.dirId = snapshot.fileId;
      }
      if (!snapshot.folderId && (snapshot.dirId || snapshot.fileId)) {
        snapshot.folderId = snapshot.dirId || snapshot.fileId;
      }
    }

    return Object.keys(snapshot).length ? snapshot : null;
  }

  function collectFrameworkObjectCandidatesFromNode(node) {
    if (!node || typeof node !== 'object') {
      return [];
    }

    const out = [];
    const ownKeys = Object.keys(node);
    for (const key of ownKeys) {
      if (
        key === '__vue__'
        || key === '__vueParentComponent'
        || key === '__vue_app__'
        || key === '_reactRootContainer'
        || key.startsWith('__reactFiber$')
        || key.startsWith('__reactProps$')
        || key.startsWith('__reactContainer$')
        || key.startsWith('__reactInternalInstance$')
      ) {
        collectRelatedFrameworkObjects(node[key], out, {
          maxNodes: 160,
        });
      }
    }

    if (node.__vue__) {
      collectRelatedFrameworkObjects(node.__vue__, out, {
        maxNodes: 160,
      });
    }
    if (node.__vueParentComponent) {
      collectRelatedFrameworkObjects(node.__vueParentComponent, out, {
        maxNodes: 160,
      });
    }
    if (node._reactRootContainer) {
      collectRelatedFrameworkObjects(node._reactRootContainer, out, {
        maxNodes: 160,
      });
    }

    return out.filter(Boolean);
  }

  function extractNormalizedItemsFromRow(row, expectedName = '', expectedIsDir = false) {
    if (!row) {
      return [];
    }

    const aggregated = [];
    const pushItems = (items = []) => {
      for (const item of items || []) {
        if (!item) {
          continue;
        }
        aggregated.push({
          ...item,
          name: String(item.name || expectedName || ''),
          isDir: expectedIsDir ? true : (item.isDir === true || shouldTreatItemAsDirectory(item)),
          raw: item.raw || {},
        });
      }
    };

    const scanNodes = (nodes = []) => {
      for (const node of nodes) {
        const snapshot = buildItemSnapshotFromDomNode(node, expectedName, expectedIsDir);
        if (snapshot) {
          pushItems(normalizeItemsFromArray([snapshot]));
        }

        const candidates = collectFrameworkObjectCandidatesFromNode(node);
        for (const candidate of candidates) {
          pushItems(extractItemsFromUnknownObject(candidate));
        }
      }
    };

    const directNodes = dedupeElements([
      row,
      ...Array.from(row.querySelectorAll('*')).slice(0, 80),
    ]);
    scanNodes(directNodes);

    const hasResolvedDirectItems = aggregated.some((item) => item && item.fileId && !isSyntheticDomId(item.fileId));
    if (!hasResolvedDirectItems) {
      const fallbackNodes = dedupeElements([
        row.parentElement,
        row.parentElement?.parentElement,
        row.previousElementSibling,
        row.nextElementSibling,
      ].filter(Boolean).flatMap((node) => [
        node,
        ...Array.from(node.querySelectorAll ? node.querySelectorAll('*') : []).slice(0, 24),
      ]));
      scanNodes(fallbackNodes);
    }

    return dedupeItems(aggregated);
  }

  function buildNormalizedItemsFromVisibleRows(rows = []) {
    const out = [];

    for (const entry of rows || []) {
      if (!entry?.row) {
        continue;
      }
      const candidates = extractNormalizedItemsFromRow(entry.row, entry.name || '', entry.isDir === true);
      const typed = candidates.filter((candidate) => (candidate.isDir === true || shouldTreatItemAsDirectory(candidate)) === (entry.isDir === true));
      const nameMatched = (typed.length ? typed : candidates).filter((candidate) => {
        const candidateName = String(candidate?.name || '').trim();
        return !entry.name || !candidateName || textLooksLikeExpected(candidateName, entry.name) || textLooksLikeExpected(entry.name, candidateName);
      });
      const chosen = (nameMatched.length ? nameMatched : (typed.length ? typed : candidates))[0];
      if (!chosen || !chosen.fileId || isSyntheticDomId(chosen.fileId)) {
        continue;
      }
      out.push({
        ...chosen,
        name: String(chosen.name || entry.name || ''),
        isDir: entry.isDir === true || chosen.isDir === true || shouldTreatItemAsDirectory(chosen),
        row: entry.row,
      });
    }

    return dedupeItems(out);
  }

  function collectVisibleDirectoryHints(expectedNames = []) {
    const expectedSet = new Set((expectedNames || []).map((name) => normalizeDomName(name)).filter(Boolean));
    if (!expectedSet.size) {
      return new Set();
    }

    const checkboxNodes = Array.from(document.querySelectorAll(
      'input[type="checkbox"], [role="checkbox"], [class*="checkbox"], [class*="check"]'
    ));
    const matched = new Set();

    for (const checkbox of checkboxNodes) {
      if (isHelperPanelNode(checkbox)) {
        continue;
      }

      let node = checkbox;
      let depth = 0;
      while (node && depth < 8) {
        const candidates = collectTextCandidates(node);
        const matchedName = candidates.find((text) => expectedSet.has(normalizeDomName(text)));
        if (matchedName) {
          const normalizedName = normalizeDomName(matchedName);
          if (guessDomRowIsDirectory(node, normalizedName)) {
            matched.add(normalizedName);
          }
          break;
        }
        node = node.parentElement;
        depth += 1;
      }
    }

    return matched;
  }

  function getUtils() {
    return {
      applyRules,
      getBaseName,
      getExt,
      renderTemplate,
    };
  }

  function buildNewName(item, context = {}) {
    const next = CONFIG.rename.buildName(item, getUtils(), context);
    return CONFIG.rename.trimResult ? String(next || '').trim() : String(next || '');
  }

  function buildTargets(items) {
    const targets = [];
    const usedNames = new Set();

    // 第一步:把所有“不改名”的文件名字存起来,作为不可占用的“坑位”
    items.forEach((item, index) => {
      const nextName = buildNewName(item, { renameIndex: index });
      const isSkipped = CONFIG.filter.predicate(item) === false;
      // 如果被过滤了,或者改名后和原名一样,那它现在的名字就是被占用的
      if (isSkipped || !nextName || nextName === item.name) {
        usedNames.add(item.name);
      }
    });

    // 第二步:处理需要改名的文件
    let renameIndex = 0;
    items.forEach((item) => {
      let finalName = buildNewName(item, { renameIndex });
      const isSkipped = CONFIG.filter.predicate(item) === false;

      if (isSkipped || !finalName || finalName === item.name) return;

      // 如果新名字冲突了,就循环加 (1), (2)...
      if (usedNames.has(finalName)) {
        let counter = 1;
        const base = getBaseName(finalName);
        const ext = getExt(finalName);
        let candidate = `${base}(${counter})${ext}`;

        while (usedNames.has(candidate)) {
          counter++;
          candidate = `${base}(${counter})${ext}`;
        }
        finalName = candidate;
      }

      // 把确定要用的新名字也存入占用列表,防止后续文件撞车
      usedNames.add(finalName);

      targets.push({
        fileId: item.fileId,
        oldName: item.name,
        newName: finalName,
        raw: item.raw,
      });
      renameIndex += 1;
    });

    return targets;
  }

  function getDuplicateRegex() {
    if (CONFIG.duplicate.mode === 'numbers') {
      return new RegExp(buildDuplicatePatternFromNumbers(CONFIG.duplicate.numbers), 'u');
    }
    return new RegExp(CONFIG.duplicate.pattern, CONFIG.duplicate.flags || '');
  }

  function buildDuplicateTargets(items) {
    const re = getDuplicateRegex();
    return items.filter((item) => {
      const isMatch = re.test(String(item.name || ''));
      if (isMatch && CONFIG.debug) {
        console.log(LOG_PREFIX, `[重复项匹配成功]: ${item.name}`);
      }
      return isMatch;
    });
  }

  function resolveListBody(overrideBody = {}) {
    // 基础数据优先从上次捕获中拿,保证 parentId 等参数正确
    let body = sanitizeListBody(
      (STATE.lastListBody && Object.keys(STATE.lastListBody).length > 0)
        ? STATE.lastListBody
        : CONFIG.request.manualListBody
    );

    for (const [key, value] of Object.entries(overrideBody || {})) {
      if (value !== '' && value != null) {
        body[key] = value;
      }
    }

    body = sanitizeListBody(body);

    // 刷新预览时要回到当前目录第一页,不能沿用滚动加载时的分页游标。
    const manualSize = Number(UI.fields.pageSize?.value || body.pageSize || CONFIG.request.manualListBody.pageSize || 100);
    if (manualSize > 0) {
      body.pageSize = manualSize;
    }

    if (!body.parentId) {
      body.parentId = normalizeParentId(CONFIG.request.manualListBody.parentId);
    }

    return body;
  }

  function normalizePageRequestOptions(options = {}) {
    const source = options && typeof options === 'object' ? options : {};
    const normalized = {};
    const stringFields = ['method', 'mode', 'credentials', 'cache', 'redirect', 'referrer', 'referrerPolicy'];

    for (const key of stringFields) {
      if (typeof source[key] === 'string' && source[key]) {
        normalized[key] = source[key];
      }
    }

    if (typeof source.keepalive === 'boolean') {
      normalized.keepalive = source.keepalive;
    }

    const headers = sanitizeHeaders(source.headers);
    if (Object.keys(headers).length) {
      normalized.headers = headers;
    }

    if (typeof source.body === 'string') {
      normalized.body = source.body;
    }

    return normalized;
  }

  function pageRequest(url, options = {}) {
    const normalizedOptions = normalizePageRequestOptions(options);

    return new Promise((resolve, reject) => {
      if (typeof GM_xmlhttpRequest !== 'function') {
        reject(new Error(`未检测到 GM_xmlhttpRequest 权限 | ${normalizedOptions.method || 'GET'} ${url}`));
        return;
      }

      try {
        GM_xmlhttpRequest({
          method: normalizedOptions.method || 'GET',
          url: String(url),
          headers: normalizedOptions.headers || {},
          data: typeof normalizedOptions.body === 'string' ? normalizedOptions.body : undefined,
          timeout: 30000,
          anonymous: false,
          onload: (res) => {
            resolve({
              ok: res.status >= 200 && res.status < 300,
              status: res.status,
              text: typeof res.responseText === 'string' ? res.responseText : '',
              via: 'GM_xmlhttpRequest',
            });
          },
          onerror: (err) => {
            reject(
              new Error(
                `GM_xmlhttpRequest 网络请求异常 (${getErrorText(err) || '未知错误'}) | ${normalizedOptions.method || 'GET'} ${url}`
              )
            );
          },
          ontimeout: () => {
            reject(new Error(`GM_xmlhttpRequest 请求超时 | ${normalizedOptions.method || 'GET'} ${url}`));
          },
          onabort: () => {
            reject(new Error(`GM_xmlhttpRequest 请求被中止 | ${normalizedOptions.method || 'GET'} ${url}`));
          },
        });
      } catch (err) {
        reject(new Error(`${getErrorText(err) || 'GM_xmlhttpRequest 调用失败'} | ${normalizedOptions.method || 'GET'} ${url}`));
      }
    });
  }

  async function requestListBatch(overrideBody = {}) {
    const headers = getRequestHeaders();
    const body = resolveListBody(overrideBody);
    if (Object.prototype.hasOwnProperty.call(overrideBody || {}, 'page')) {
      body.page = overrideBody.page;
    }
    const listUrl = STATE.lastListUrl || `${CONFIG.request.apiHost}${CONFIG.request.listPath}`;

    if (!body.parentId) {
      throw new Error('没有拿到 parentId。请先打开目标目录等待列表加载,或在 CONFIG.request.manualListBody.parentId 里手填。');
    }

    const response = await pageRequest(listUrl, {
      method: 'POST',
      headers,
      mode: 'cors',
      credentials: 'include',
      body: JSON.stringify(body),
    });

    if (!response.ok) {
      throw new Error(`获取列表失败:HTTP ${response.status}`);
    }

    const payload = safeJsonParse(response.text);
    if (!payload) {
      throw new Error('获取列表失败:响应不是有效 JSON');
    }

    return {
      headers,
      body,
      listUrl,
      response,
      payload,
      items: extractItemsFromPayload(payload),
    };
  }

  async function fetchCurrentList(overrideBody = {}) {
    const override = overrideBody && typeof overrideBody === 'object' ? { ...overrideBody } : {};
    const returnBatchOnly = Boolean(override.__returnBatchOnly);
    delete override.__returnBatchOnly;

    const { body, listUrl, payload, items } = await requestListBatch(override);
    const merged = mergeCapturedItems(body.parentId, items, {
      listUrl,
      requestBody: body,
    });

    STATE.lastListUrl = listUrl;
    STATE.lastListBody = body;
    STATE.lastListResponse = payload;
    STATE.lastListItems = merged.items;
    STATE.lastCapturedParentId = normalizeParentId(body.parentId);
    STATE.lastItemsSource = merged.batchCount > 1 ? 'api-merged' : 'api';

    log(`重新拉取列表完成:本批 ${items.length} 项,当前目录累计 ${merged.total} 项(共 ${merged.batchCount} 批)。`);
    return returnBatchOnly ? items : merged.items;
  }

  function getCapturedItems() {
    const stats = getCapturedListStats();
    const bucket = getCapturedListBucket(stats.parentId, { create: false });
    if (bucket && Array.isArray(bucket.items) && bucket.items.length) {
      return bucket.items;
    }
    return Array.isArray(STATE.lastListItems) ? STATE.lastListItems : [];
  }

  function buildCurrentDirectoryItemsSnapshot(parentId = '') {
    const currentParentId = String(getCurrentListContext().parentId || '').trim();
    const targetParentId = String(parentId || '').trim();
    if (!currentParentId || !targetParentId || currentParentId !== targetParentId) {
      return [];
    }

    const captured = Array.isArray(getCapturedItems()) ? getCapturedItems() : [];
    if (!captured.length) {
      return [];
    }

    const visibleDirNames = collectVisibleDirectoryHints(captured.map((item) => item?.name || ''));
    const domItems = collectDomItems();
    const domByName = new Map(
      (domItems || [])
        .filter((item) => item && item.name)
        .map((item) => [normalizeDomName(item.name), item])
    );

    return dedupeItems(captured.map((item) => {
      const key = normalizeDomName(item?.name || '');
      const domItem = key ? domByName.get(key) : null;
      if (!visibleDirNames.has(key) && !domItem?.isDir) {
        return item;
      }
      return {
        ...item,
        isDir: true,
        dirIdCandidates: normalizeIdCandidates(item?.dirIdCandidates || [item?.dirId, item?.fileId]),
        raw: {
          ...(item.raw || {}),
          domIsDir: true,
        },
      };
    }));
  }

  function shouldTreatItemAsDirectory(item) {
    if (!item) {
      return false;
    }
    if (item.isDir === true) {
      return true;
    }
    return guessItemIsDirectory(item.raw || {}, item.name || '');
  }

  function getDomItems() {
    const items = collectDomItems();
    if (items.length) {
      STATE.lastItemsSource = 'dom';
    }
    return items;
  }

  async function getPreviewItems(options = {}) {
    const refresh = Boolean(options.refresh);
    const overrideBody = options.listBody || {};

    if (refresh) {
      try {
        const apiItems = await fetchCurrentList(overrideBody);
        if (apiItems.length) {
          return apiItems;
        }
      } catch (err) {
        warn('接口刷新列表失败,改用页面可见项目兜底:', err);
      }
    }

    const captured = getCapturedItems();
    if (captured.length) {
      return captured;
    }

    const domItems = getDomItems();
    if (domItems.length) {
      warn('当前未捕获到接口列表,已退回到页面可见项目模式。此模式可做预览和勾选重复项,但不能直接执行改名。');
      return domItems;
    }

    return [];
  }

  function summarizeTargets(targets) {
    const rows = targets.map((item) => ({
      fileId: item.fileId,
      oldName: item.oldName,
      newName: item.newName,
    }));
    console.table(rows);

    const duplicateNames = {};
    for (const item of targets) {
      duplicateNames[item.newName] = (duplicateNames[item.newName] || 0) + 1;
    }
    const collisions = Object.entries(duplicateNames)
      .filter(([, count]) => count > 1)
      .map(([name, count]) => ({ newName: name, count }));

    if (collisions.length) {
      warn('检测到潜在重名,相关项目可能改名失败:');
      console.table(collisions);
    }
  }

  function logZeroPreviewDiagnostics(items) {
    if (!items.length) {
      return;
    }

    const samples = items.slice(0, 8).map((item, index) => {
      const original = String(item.name || '');
      const clean = applyRules(original, item);
      const finalName = buildNewName(item, { renameIndex: index });
      return {
        fileId: item.fileId,
        original,
        clean,
        finalName,
        startsWithBracket: /^\s*[\[【]/.test(original),
      };
    });

    const leadingBracketCount = items.filter((item) => /^\s*[\[【]/.test(String(item?.name || ''))).length;
    console.table(samples);
    warn(`预览结果为 0。当前目录累计 ${items.length} 项,其中 ${leadingBracketCount} 项以 [] / 【】 开头;已在控制台输出诊断样本。`);
  }

  function isProbablySuccess(payload, response) {
    if (!response.ok) {
      return false;
    }
    if (!payload || typeof payload !== 'object') {
      return true;
    }
    if (payload.success === false) {
      return false;
    }
    if (payload.status === 'error') {
      return false;
    }
    if ('code' in payload) {
      const code = String(payload.code);
      if (!['0', '200', '2000'].includes(code) && !code.startsWith('2')) {
        return false;
      }
    }
    return true;
  }

  function isDirectoryExistsResponse(payload, response) {
    const code = String(payload?.code ?? response?.payload?.code ?? '').trim();
    if (code && code === String(GUANGYA_CODE_DIR_EXISTS)) {
      return true;
    }
    return looksLikeNameExistError(payload || response?.text || response);
  }

  async function renameOne(target) {
    const response = await pageRequest(getRenameUrl(), {
      method: 'POST',
      headers: getRenameHeaders(),
      mode: 'cors',
      credentials: 'include',
      body: JSON.stringify(buildRenamePayload(target)),
    });

    const text = response.text || '';
    const payload = safeJsonParse(text);
    return {
      ok: isProbablySuccess(payload, response),
      status: response.status,
      text,
      payload,
    };
  }

  async function preview(options = {}) {
    const items = await getPreviewItems(options);

    if (!items.length) {
      warn('当前没有拿到可用项目。先刷新当前分享目录,再试一次。');
      return [];
    }

    const targets = buildTargets(items);
    if (!targets.length) {
      logZeroPreviewDiagnostics(items);
    }
    summarizeTargets(targets);
    log(`预览完成:当前共 ${targets.length} 个项目将被改名。`);
    return targets;
  }

  async function previewDuplicates(options = {}) {
    const items = await getPreviewItems(options);

    if (!items.length) {
      warn('当前没有拿到可用项目。先刷新当前分享目录,再试一次。');
      return [];
    }

    const duplicates = buildDuplicateTargets(items).map((item) => ({
      fileId: item.fileId,
      name: item.name,
    }));

    console.table(duplicates);
    log(`重复项预览完成:共 ${duplicates.length} 个项目匹配尾部 (1)/(2)/(3) 规则。`);
    return duplicates;
  }

  function resolveItemsByName(previewItems, sourceItems) {
    const exactMap = new Map();

    for (const item of sourceItems || []) {
      if (!item || !item.fileId || String(item.fileId).startsWith('dom:')) {
        continue;
      }
      const key = normalizeDomName(item.name);
      if (!key) {
        continue;
      }
      if (!exactMap.has(key)) {
        exactMap.set(key, []);
      }
      exactMap.get(key).push(item);
    }

    const merged = [];
    const resolved = [];
    const unresolved = [];

    for (const item of previewItems || []) {
      if (!item) {
        continue;
      }

      if (item.fileId && !String(item.fileId).startsWith('dom:')) {
        const normalized = {
          fileId: String(item.fileId),
          name: String(item.name || ''),
        };
        merged.push(normalized);
        resolved.push(normalized);
        continue;
      }

      const key = normalizeDomName(item.name);
      const matches = key ? (exactMap.get(key) || []) : [];
      if (matches.length === 1) {
        const normalized = {
          fileId: String(matches[0].fileId),
          name: String(matches[0].name || item.name || ''),
        };
        merged.push(normalized);
        resolved.push(normalized);
      } else {
        const fallback = {
          fileId: String(item.fileId || ''),
          name: String(item.name || ''),
        };
        merged.push(fallback);
        unresolved.push(fallback);
      }
    }

    return {
      merged,
      resolved,
      unresolved,
    };
  }

  function updateDuplicatePreviewResolvedItems(items) {
    const selectionByName = new Map(
      (STATE.duplicatePreviewItems || []).map((item) => [
        normalizeDomName(item.name),
        STATE.duplicateSelection[item.fileId] !== false,
      ])
    );

    setDuplicatePreview(
      (items || []).map((item) => ({
        fileId: String(item.fileId),
        name: String(item.name || ''),
      }))
    );

    for (const item of STATE.duplicatePreviewItems || []) {
      const saved = selectionByName.get(normalizeDomName(item.name));
      if (typeof saved === 'boolean') {
        STATE.duplicateSelection[item.fileId] = saved;
      }
    }

    renderDuplicatePreviewList();
  }

  async function ensureDuplicateItemsHaveRealIds(previewItems, options = {}) {
    const domItems = (previewItems || []).filter((item) => String(item?.fileId || '').startsWith('dom:'));
    if (!domItems.length) {
      return {
        mergedItems: (previewItems || []).map((item) => ({
          fileId: String(item.fileId),
          name: String(item.name || ''),
        })),
        resolved: (previewItems || []).map((item) => ({
          fileId: String(item.fileId),
          name: String(item.name || ''),
        })),
        unresolved: [],
        source: 'existing',
      };
    }

    const onProgress = typeof options.onProgress === 'function' ? options.onProgress : null;
    const candidateSources = [];
    const verifiedPageSize = Math.max(
      Number(UI.fields.pageSize?.value || 0),
      Number(CONFIG.request.manualListBody.pageSize || 0),
      Number(getCapturedItems().length || 0),
      Number((previewItems || []).length || 0) + 50,
      200
    );

    if (onProgress) {
      onProgress({
        visible: true,
        percent: 8,
        indeterminate: true,
        text: '正在补齐真实 fileId...',
      });
    }

    try {
      const fetched = await fetchCurrentList({ pageSize: verifiedPageSize });
      if (fetched.length) {
        candidateSources.push({
          source: 'api',
          items: fetched,
        });
      }
    } catch (err) {
      warn('删除前补齐真实 fileId 时,接口拉取列表失败:', err);
    }

    const captured = getCapturedItems();
    if (captured.length) {
      candidateSources.push({
        source: 'captured',
        items: captured,
      });
    }

    for (const entry of candidateSources) {
      const mapping = resolveItemsByName(previewItems, entry.items);
      if (!mapping.unresolved.length) {
        return {
          mergedItems: mapping.merged,
          resolved: mapping.resolved,
          unresolved: [],
          source: entry.source,
        };
      }
    }

    const best = candidateSources.length
      ? resolveItemsByName(previewItems, candidateSources[0].items)
      : {
          merged: (previewItems || []).map((item) => ({
            fileId: String(item.fileId || ''),
            name: String(item.name || ''),
          })),
          resolved: (previewItems || []).filter((item) => !String(item?.fileId || '').startsWith('dom:')).map((item) => ({
            fileId: String(item.fileId || ''),
            name: String(item.name || ''),
          })),
          unresolved: domItems.map((item) => ({
            fileId: String(item.fileId || ''),
            name: String(item.name || ''),
          })),
        };

    return {
      mergedItems: best.merged,
      resolved: best.resolved,
      unresolved: best.unresolved,
      source: candidateSources[0]?.source || 'none',
    };
  }

  function getSelectedDuplicatePreviewItems() {
    return (STATE.duplicatePreviewItems || []).filter((item) => STATE.duplicateSelection[item.fileId] !== false);
  }

  function renderDuplicatePreviewList() {
    if (!UI.duplicateList || !UI.duplicateCount) {
      return;
    }

    const items = STATE.duplicatePreviewItems || [];
    const selected = getSelectedDuplicatePreviewItems();
    UI.duplicateCount.textContent = `删除勾选 ${selected.length}/${items.length}`;

    if (!items.length) {
      UI.duplicateList.innerHTML = '<div class="gyp-duplicate-empty">先点“重复项预览”,再在这里取消不想删的项目。</div>';
      return;
    }

    UI.duplicateList.innerHTML = items.map((item) => `
      <label class="gyp-duplicate-row">
        <input
          type="checkbox"
          data-action="toggle-duplicate"
          data-file-id="${escapeHtml(item.fileId)}"
          ${STATE.duplicateSelection[item.fileId] !== false ? 'checked' : ''}
        />
        <span class="gyp-duplicate-name" title="${escapeHtml(item.name)}">${escapeHtml(item.name)}</span>
      </label>
    `).join('');
  }

  function setDuplicatePreview(items, options = {}) {
    const preserveSelection = Boolean(options.preserveSelection);
    const deduped = [];
    const seen = new Set();
    for (const item of items || []) {
      if (!item || !item.fileId || seen.has(item.fileId)) {
        continue;
      }
      seen.add(item.fileId);
      deduped.push({
        fileId: String(item.fileId),
        name: String(item.name || ''),
      });
    }

    const nextSelection = {};
    for (const item of deduped) {
      if (preserveSelection && Object.prototype.hasOwnProperty.call(STATE.duplicateSelection, item.fileId)) {
        nextSelection[item.fileId] = STATE.duplicateSelection[item.fileId] !== false;
      } else {
        nextSelection[item.fileId] = true;
      }
    }

    STATE.duplicatePreviewItems = deduped;
    STATE.duplicateSelection = nextSelection;
    renderDuplicatePreviewList();
    if (UI.duplicateDetails) {
      UI.duplicateDetails.open = true;
    }
  }

  function buildCheckedPageSelectionPreviewItems(options = {}) {
    const onlyDirectories = Boolean(options.onlyDirectories);
    const visibleEntries = collectVisibleListRowEntries();
    const entries = visibleEntries.some((entry) => entry?.checkbox && isElementChecked(entry.checkbox))
      ? visibleEntries
      : collectCheckedListRowEntries();
    const checkedEntries = [];
    const seenRows = new Set();

    for (const entry of entries) {
      if (!entry?.checkbox || !isElementChecked(entry.checkbox)) {
        continue;
      }
      const rowKey = entry.row || entry.checkbox;
      if (rowKey && seenRows.has(rowKey)) {
        continue;
      }
      rowKey && seenRows.add(rowKey);
      checkedEntries.push(entry);
    }

    if (!checkedEntries.length) {
      return [];
    }

    const resolvedByRow = new Map(
      buildNormalizedItemsFromVisibleRows(checkedEntries)
        .filter((item) => item?.row)
        .map((item) => [item.row, item])
    );
    const seen = new Set();
    const out = [];

    for (const entry of checkedEntries) {
      const resolvedRowItem = entry.row ? resolvedByRow.get(entry.row) : null;
      const effectiveName = String(resolvedRowItem?.name || entry.name || '');
      const effectiveIsDir = resolvedRowItem
        ? (resolvedRowItem.isDir === true || shouldTreatItemAsDirectory(resolvedRowItem))
        : (entry.isDir === true);
      if (onlyDirectories && !effectiveIsDir) {
        continue;
      }

      const normalizedName = normalizeDomName(effectiveName);
      const key = resolvedRowItem && resolvedRowItem.fileId && !isSyntheticDomId(resolvedRowItem.fileId)
        ? `id:${String(resolvedRowItem.fileId)}`
        : `${effectiveIsDir ? 'dir' : 'file'}:${normalizedName}`;
      if (!normalizedName || seen.has(key)) {
        continue;
      }
      seen.add(key);

      if (resolvedRowItem && resolvedRowItem.fileId && !isSyntheticDomId(resolvedRowItem.fileId)) {
        out.push({
          fileId: String(resolvedRowItem.fileId),
          dirId: String(resolvedRowItem.dirId || resolvedRowItem.fileId),
          dirIdCandidates: normalizeIdCandidates(
            resolvedRowItem.dirIdCandidates || [resolvedRowItem.dirId, resolvedRowItem.fileId]
          ),
          name: effectiveName,
          parentId: String(resolvedRowItem.parentId || ''),
          isDir: effectiveIsDir,
          row: entry.row || resolvedRowItem.row || null,
          raw: resolvedRowItem.raw || {},
          resolved: true,
        });
        continue;
      }

      out.push({
        fileId: `dom:checked:${out.length}:${effectiveName}`,
        dirId: `dom:checked:${out.length}:${effectiveName}`,
        dirIdCandidates: [`dom:checked:${out.length}:${effectiveName}`],
        name: effectiveName,
        parentId: '',
        isDir: effectiveIsDir,
        row: entry.row || null,
        resolved: false,
      });
    }

    return out;
  }

  function collectCheckedListRowEntries() {
    const checkboxNodes = Array.from(document.querySelectorAll(
      'input[type="checkbox"], label[role="checkbox"], [role="checkbox"], [aria-label*="选择"], button[aria-label*="选择"], [data-testid*="checkbox"], [class*="checkbox"], [class*="check"]'
    ));
    const seen = new Set();
    const out = [];

    for (const checkbox of checkboxNodes) {
      if (!checkbox || isHelperPanelNode(checkbox) || !isElementChecked(checkbox)) {
        continue;
      }

      const row = getClosestRow(checkbox);
      if (!row || !isUsableListRow(row)) {
        continue;
      }

      const name = extractNameFromRow(row);
      const normalizedName = normalizeDomName(name);
      const checkboxInRow = getCheckboxInRow(row) || checkbox;
      const key = `${guessDomRowIsDirectory(row, name) ? 'dir' : 'file'}:${normalizedName}`;
      if (!normalizedName || !isProbablyUsefulName(name) || isProbablyMetadataText(name) || seen.has(key)) {
        continue;
      }

      seen.add(key);
      out.push({
        row,
        name,
        normalizedName,
        checkbox: checkboxInRow,
        isDir: guessDomRowIsDirectory(row, name),
      });
    }

    return out;
  }

  async function collectCheckedPageSelectionByScrolling(options = {}) {
    const container = options.container || findScrollableListContainer();
    const taskControl = options.taskControl || null;
    const expectedCount = Math.max(0, Number(options.expectedCount || 0));
    const maxRounds = Math.max(1, Number(options.maxRounds || 48));
    const isDocumentScroller =
      container === document.scrollingElement ||
      container === document.documentElement ||
      container === document.body;
    const startScroll = container
      ? (isDocumentScroller ? (window.scrollY || window.pageYOffset || 0) : container.scrollTop)
      : 0;
    const deltaY = Math.max(280, Math.floor((container?.clientHeight || window.innerHeight || 640) * 0.72));
    const seen = new Map();

    const addItems = (items = []) => {
      for (const item of items) {
        if (!item) {
          continue;
        }
        const key = `${item.isDir ? 'dir' : 'file'}:${normalizeDomName(item.name || '')}`;
        if (!key || seen.has(key)) {
          continue;
        }
        seen.set(key, item);
      }
    };

    try {
      if (container) {
        if (isDocumentScroller) {
          window.scrollTo({ top: 0, behavior: 'auto' });
        } else {
          container.scrollTop = 0;
        }
        await sleep(180);
      }

      for (let round = 0; round < maxRounds; round += 1) {
        await waitForTaskControl(taskControl);
        addItems(buildCheckedPageSelectionPreviewItems({ onlyDirectories: false }));

        if (expectedCount > 0 && seen.size >= expectedCount) {
          break;
        }

        const moved = await scrollListContainer(container, deltaY);
        if (!moved) {
          break;
        }
      }
    } finally {
      if (container) {
        if (isDocumentScroller) {
          window.scrollTo({ top: startScroll, behavior: 'auto' });
        } else {
          container.scrollTop = startScroll;
        }
      }
    }

    return Array.from(seen.values());
  }

  function getPageSelectedCount() {
    const patterns = [
      /(?:已选(?:择)?|已勾选|selected)\s*[::]?\s*(\d+)\s*(?:项|个\s*(?:文件(?:\s*\/\s*文件夹)?|文件夹|项目)?|items?|files?(?:\s*\/\s*folders?)?)?/iu,
      /(\d+)\s*(?:项|个\s*(?:文件(?:\s*\/\s*文件夹)?|文件夹|项目)?|items?|files?(?:\s*\/\s*folders?)?)\s*(?:已选(?:择)?|已勾选|selected)/iu,
    ];
    let maxCount = 0;
    const nodes = Array.from(document.querySelectorAll('span, div, p, strong, b, em, button'));

    for (const node of nodes) {
      if (!node || !isVisibleElement(node) || isHelperPanelNode(node)) {
        continue;
      }
      const text = normalizeDomName(node.textContent || node.innerText || '');
      if (!text || text.length > 40) {
        continue;
      }
      const matched = patterns.map((pattern) => text.match(pattern)).find(Boolean);
      if (!matched) {
        continue;
      }
      const count = Number(matched[1] || 0);
      if (Number.isFinite(count) && count > maxCount) {
        maxCount = count;
      }
    }

    return maxCount;
  }

  async function collectCheckedPageSelectionPreviewItems(options = {}) {
    const onlyDirectories = Boolean(options.onlyDirectories);
    const taskControl = options.taskControl || null;
    const disableFullSelectionFallback = Boolean(options.disableFullSelectionFallback);
    const allowExactCapturedSelectionFallback = Boolean(options.allowExactCapturedSelectionFallback);
    const ignorePageSelectedCount = Boolean(options.ignorePageSelectedCount);
    const avoidSlowScrollScan = Boolean(options.avoidSlowScrollScan);
    const visibleAllItems = buildCheckedPageSelectionPreviewItems({ onlyDirectories: false });
    const visibleItems = onlyDirectories ? visibleAllItems.filter((item) => item.isDir) : visibleAllItems;
    const pageSelectedCount = ignorePageSelectedCount
      ? 0
      : Math.max(0, Number(options.pageSelectedCount != null ? options.pageSelectedCount : getPageSelectedCount()) || 0);
    const currentParentId = String(getCurrentListContext().parentId || CONFIG.request.manualListBody.parentId || '').trim();
    const capturedItems = dedupeItems(getCapturedItems() || []);
    const capturedCount = capturedItems.length;
    const visibleCount = visibleAllItems.length;
    if (pageSelectedCount > Math.max(visibleCount, 1)) {
      await waitForUiPaint(1);
    }
    const frameworkSelectedAllItemsRaw = pageSelectedCount > Math.max(visibleCount, 1)
      ? collectSelectedItemsFromPageFrameworkState({ onlyDirectories: false })
      : [];
    const frameworkSelectedAllItems = currentParentId
      ? frameworkSelectedAllItemsRaw.filter((item) => !String(item?.parentId || '').trim() || String(item?.parentId || '').trim() === currentParentId)
      : frameworkSelectedAllItemsRaw;
    const frameworkSelectedItems = onlyDirectories
      ? frameworkSelectedAllItems.filter((item) => item.isDir === true || shouldTreatItemAsDirectory(item))
      : frameworkSelectedAllItems;
    const frameworkSelectedCount = frameworkSelectedAllItems.length;
    if (pageSelectedCount > Math.max(visibleCount, 1)) {
      await waitForUiPaint(1);
    }
    const frameworkSelectionMarker = pageSelectedCount > Math.max(visibleCount, 1)
      ? collectSelectionMarkersFromPageFrameworkState()
      : { ids: new Set(), names: new Set() };
    const markerMatchedCapturedAllItems = (frameworkSelectionMarker.ids.size || frameworkSelectionMarker.names.size)
      ? capturedItems.filter((item) => itemMatchesSelectionMarkers(item, frameworkSelectionMarker))
      : [];
    const markerMatchedCapturedItems = onlyDirectories
      ? markerMatchedCapturedAllItems.filter((item) => item.isDir === true || shouldTreatItemAsDirectory(item))
      : markerMatchedCapturedAllItems;
    const markerMatchedCapturedCount = markerMatchedCapturedAllItems.length;
    const frameworkMarkerIdCount = frameworkSelectionMarker.ids.size;
    const frameworkMarkerNameCount = frameworkSelectionMarker.names.size;
    const selectionDiagnostics = pageSelectedCount > Math.max(visibleCount, 1)
      ? `框架 ${frameworkSelectedCount} / marker id ${frameworkMarkerIdCount} / name ${frameworkMarkerNameCount} / 命中 ${markerMatchedCapturedCount}`
      : '';

    if (allowExactCapturedSelectionFallback && pageSelectedCount > 0 && capturedCount && capturedCount === pageSelectedCount) {
      const normalizedCapturedItems = capturedItems.map((item) => ({
        ...item,
        resolved: true,
      }));
      const matchedItems = onlyDirectories
        ? normalizedCapturedItems.filter((item) => item.isDir === true || shouldTreatItemAsDirectory(item))
        : normalizedCapturedItems;
      return {
        items: matchedItems,
        meta: {
          expectedCount: pageSelectedCount,
          visibleCount: Math.max(visibleCount, matchedItems.length),
          partial: false,
          source: 'captured-exact-selected-count',
          diagnostics: selectionDiagnostics,
          warning: onlyDirectories
            ? `页面已选 ${pageSelectedCount} 项,且当前目录接口累计也为 ${capturedCount} 项;已优先使用光鸭接口累计列表补齐,其中可操作文件夹 ${matchedItems.length} 项。`
            : `页面已选 ${pageSelectedCount} 项,且当前目录接口累计也为 ${capturedCount} 项;已优先使用光鸭接口累计列表补齐全部勾选项。`,
        },
      };
    }

    if (pageSelectedCount > 0 && frameworkSelectedCount === pageSelectedCount) {
      const normalizedFrameworkItems = frameworkSelectedItems.map((item) => ({
        ...item,
        resolved: true,
      }));
      return {
        items: normalizedFrameworkItems,
        meta: {
          expectedCount: pageSelectedCount,
          visibleCount: Math.max(visibleCount, normalizedFrameworkItems.length),
          partial: false,
          source: 'framework-selected-state',
          diagnostics: selectionDiagnostics,
          warning: onlyDirectories
            ? `页面已选 ${pageSelectedCount} 项,已直接从页面内部状态读取勾选文件夹 ${normalizedFrameworkItems.length} 项。`
            : `页面已选 ${pageSelectedCount} 项,已直接从页面内部状态读取全部勾选项。`,
        },
      };
    }

    if (pageSelectedCount > 0 && markerMatchedCapturedCount === pageSelectedCount) {
      const normalizedMarkerMatchedItems = markerMatchedCapturedItems.map((item) => ({
        ...item,
        resolved: true,
      }));
      return {
        items: normalizedMarkerMatchedItems,
        meta: {
          expectedCount: pageSelectedCount,
          visibleCount: Math.max(visibleCount, normalizedMarkerMatchedItems.length),
          partial: false,
          source: 'framework-selection-marker-captured',
          diagnostics: selectionDiagnostics,
          warning: onlyDirectories
            ? `页面已选 ${pageSelectedCount} 项,已通过页面内部选中标记与当前目录累计列表匹配出 ${normalizedMarkerMatchedItems.length} 个文件夹。`
            : `页面已选 ${pageSelectedCount} 项,已通过页面内部选中标记与当前目录累计列表匹配出全部勾选项。`,
        },
      };
    }

    if (pageSelectedCount > 0 && visibleCount > pageSelectedCount) {
      const limitedItems = visibleItems.slice(0, pageSelectedCount);
      return {
        items: limitedItems,
        meta: {
          expectedCount: pageSelectedCount,
          visibleCount,
          partial: false,
          source: 'visible-limited',
          diagnostics: selectionDiagnostics,
          warning: `页面显示已选 ${pageSelectedCount} 项,但脚本识别到 ${visibleCount} 个疑似勾选项;已按页面已选数量截断,避免误导出全目录。`,
        },
      };
    }

    if (!pageSelectedCount || visibleCount >= pageSelectedCount) {
      return {
        items: visibleItems,
        meta: {
          expectedCount: Math.max(pageSelectedCount, visibleCount, visibleItems.length),
          visibleCount,
          partial: false,
          source: 'visible',
          diagnostics: selectionDiagnostics,
          warning: '',
        },
      };
    }

    if (avoidSlowScrollScan) {
      const bestFrameworkItems = markerMatchedCapturedItems.length > frameworkSelectedItems.length
        ? markerMatchedCapturedItems
        : frameworkSelectedItems;
      const recognizedItems = bestFrameworkItems.length > visibleItems.length ? bestFrameworkItems : visibleItems;
      return {
        items: recognizedItems,
        meta: {
          expectedCount: pageSelectedCount,
          visibleCount: Math.max(visibleCount, frameworkSelectedCount, markerMatchedCapturedCount, recognizedItems.length),
          partial: recognizedItems.length < pageSelectedCount,
          source: markerMatchedCapturedItems.length > frameworkSelectedItems.length
            ? 'framework-marker-fast-no-scroll'
            : (frameworkSelectedCount ? 'framework-fast-no-scroll' : 'visible-fast-no-scroll'),
          diagnostics: selectionDiagnostics,
          warning: `页面显示已选 ${pageSelectedCount} 项;为避免长时间滚动扫描,本次先按已确认的 ${recognizedItems.length} 项处理${selectionDiagnostics ? `(${selectionDiagnostics})` : ''}。`,
        },
      };
    }

    if (disableFullSelectionFallback && pageSelectedCount > 0 && frameworkSelectedCount > 0) {
      const bestFrameworkItems = markerMatchedCapturedItems.length > frameworkSelectedItems.length
        ? markerMatchedCapturedItems
        : frameworkSelectedItems;
      const recognizedItems = bestFrameworkItems.length >= visibleItems.length ? bestFrameworkItems : visibleItems;
      return {
        items: recognizedItems,
        meta: {
          expectedCount: pageSelectedCount,
          visibleCount: Math.max(visibleCount, frameworkSelectedCount, markerMatchedCapturedCount, recognizedItems.length),
          partial: recognizedItems.length < pageSelectedCount,
          source: markerMatchedCapturedItems.length > frameworkSelectedItems.length ? 'framework-marker-fast-partial' : 'framework-fast-partial',
          diagnostics: selectionDiagnostics,
          warning: recognizedItems.length < pageSelectedCount
            ? `页面显示已选 ${pageSelectedCount} 项;已优先读取页面内部勾选状态并快速确认 ${recognizedItems.length} 项,未再执行慢速滚动扫描${selectionDiagnostics ? `(${selectionDiagnostics})` : ''}。`
            : '',
        },
      };
    }

    const scannedAllItems = await collectCheckedPageSelectionByScrolling({
      taskControl,
      expectedCount: pageSelectedCount,
      maxRounds: Math.max(24, Math.min(96, pageSelectedCount + 18)),
    });
    const scannedAllCount = scannedAllItems.length;
    const scannedItems = onlyDirectories
      ? scannedAllItems.filter((item) => item.isDir === true)
      : scannedAllItems;

    if (scannedAllCount >= pageSelectedCount) {
      return {
        items: scannedItems,
        meta: {
          expectedCount: pageSelectedCount,
          visibleCount: scannedAllCount,
          partial: false,
          source: 'scroll-scan',
          diagnostics: selectionDiagnostics,
          warning: onlyDirectories
            ? `页面已选 ${pageSelectedCount} 项,已滚动扫描完整个列表;其中勾选文件夹 ${scannedItems.length} 项。`
            : `页面已选 ${pageSelectedCount} 项,已滚动扫描完整个列表并补齐全部勾选项。`,
        },
      };
    }

    if (allowExactCapturedSelectionFallback && capturedCount && capturedCount === pageSelectedCount) {
      const normalizedCapturedItems = capturedItems.map((item) => ({
        ...item,
        resolved: true,
      }));
      const matchedItems = onlyDirectories
        ? normalizedCapturedItems.filter((item) => item.isDir === true || shouldTreatItemAsDirectory(item))
        : normalizedCapturedItems;
      return {
        items: matchedItems,
        meta: {
          expectedCount: pageSelectedCount,
          visibleCount: Math.max(visibleCount, scannedAllCount, matchedItems.length),
          partial: false,
          source: 'captured-exact-selected-count',
          diagnostics: selectionDiagnostics,
          warning: onlyDirectories
            ? `页面已选 ${pageSelectedCount} 项,且当前目录接口累计也为 ${capturedCount} 项;已用光鸭接口累计列表补齐,其中可操作文件夹 ${matchedItems.length} 项。`
            : `页面已选 ${pageSelectedCount} 项,且当前目录接口累计也为 ${capturedCount} 项;已用光鸭接口累计列表补齐全部勾选项。`,
        },
      };
    }

    if (disableFullSelectionFallback) {
      const recognizedItems = scannedItems.length > visibleItems.length ? scannedItems : visibleItems;
      const recognizedCount = Math.max(visibleCount, scannedAllCount, recognizedItems.length);
      return {
        items: recognizedItems,
        meta: {
          expectedCount: pageSelectedCount,
          visibleCount: recognizedCount,
          partial: true,
          source: 'scroll-partial-no-full-fallback',
          diagnostics: selectionDiagnostics,
          warning: `页面显示已选 ${pageSelectedCount} 项,但当前只确认识别到 ${recognizedCount} 项${selectionDiagnostics ? `(${selectionDiagnostics})` : ''};已关闭“按全目录补齐”,避免误导出当前目录全部文件。`,
        },
      };
    }

    if (capturedCount && capturedCount === pageSelectedCount) {
      const normalizedCapturedItems = capturedItems.map((item) => ({
        ...item,
        resolved: true,
      }));
      const matchedItems = onlyDirectories
        ? normalizedCapturedItems.filter((item) => item.isDir === true || shouldTreatItemAsDirectory(item))
        : normalizedCapturedItems;
      return {
        items: matchedItems,
        meta: {
          expectedCount: pageSelectedCount,
          visibleCount,
          partial: false,
          source: 'captured-all-current-dir',
          diagnostics: selectionDiagnostics,
          warning: onlyDirectories
            ? `页面已选 ${pageSelectedCount} 项,已使用脚本累计识别的当前目录列表补齐;其中可操作文件夹 ${matchedItems.length} 项。`
            : `页面已选 ${pageSelectedCount} 项,已使用脚本累计识别的当前目录列表补齐全部勾选项。`,
        },
      };
    }

    if (!currentParentId) {
      return {
        items: visibleItems,
        meta: {
          expectedCount: pageSelectedCount,
          visibleCount: Math.max(visibleCount, scannedAllCount),
          partial: true,
          source: 'scroll-partial',
          diagnostics: selectionDiagnostics,
          warning: `页面显示已选 ${pageSelectedCount} 项,脚本已尝试滚动扫描整页,但目前只识别到 ${Math.max(visibleCount, scannedAllCount)} 项${selectionDiagnostics ? `(${selectionDiagnostics})` : ''};还没拿到当前目录 parentId,暂时无法继续补齐。`,
        },
      };
    }

    const verifiedPageSize = Math.max(
      Number(UI.fields.pageSize?.value || 0),
      Number(CONFIG.request.manualListBody.pageSize || 0),
      Number(getCurrentListContext().capturedCount || 0),
      pageSelectedCount,
      200
    );

    try {
      const listing = await fetchDirectoryItemsByParentId(currentParentId, {
        pageSize: verifiedPageSize,
        maxPages: Math.max(2, Math.ceil(pageSelectedCount / Math.max(1, verifiedPageSize)) + 2),
        delayMs: 0,
        taskControl,
      });
      const fullItems = dedupeItems(listing.items || []);

      if (!listing.truncated && fullItems.length === pageSelectedCount) {
        const normalizedFullItems = fullItems.map((item) => ({
          ...item,
          resolved: true,
        }));
        return {
          items: onlyDirectories
            ? normalizedFullItems.filter((item) => item.isDir === true || shouldTreatItemAsDirectory(item))
            : normalizedFullItems,
          meta: {
            expectedCount: pageSelectedCount,
            visibleCount,
            partial: false,
            source: 'all-current-dir',
            diagnostics: selectionDiagnostics,
            warning: onlyDirectories
              ? `页面已选 ${pageSelectedCount} 项,已按“当前目录全选”补齐;其中可操作文件夹 ${normalizedFullItems.filter((item) => item.isDir === true || shouldTreatItemAsDirectory(item)).length} 项。`
              : `页面已选 ${pageSelectedCount} 项,已按“当前目录全选”自动补齐全部勾选项。`,
          },
        };
      }

      return {
        items: scannedItems.length > visibleItems.length ? scannedItems : visibleItems,
        meta: {
          expectedCount: pageSelectedCount,
          visibleCount: Math.max(visibleCount, scannedAllCount),
          partial: true,
          source: 'scroll-partial',
          diagnostics: selectionDiagnostics,
          warning: `页面显示已选 ${pageSelectedCount} 项,脚本已尝试滚动扫描整页,但目前只识别到 ${Math.max(visibleCount, scannedAllCount)} 项${selectionDiagnostics ? `(${selectionDiagnostics})` : ''}。请稍等列表继续加载,或上下滚动一次后再试。`,
        },
      };
    } catch (err) {
      return {
        items: scannedItems.length > visibleItems.length ? scannedItems : visibleItems,
        meta: {
          expectedCount: pageSelectedCount,
          visibleCount: Math.max(visibleCount, scannedAllCount),
          partial: true,
          source: 'scroll-partial',
          diagnostics: selectionDiagnostics,
          warning: `页面显示已选 ${pageSelectedCount} 项,滚动扫描后识别到 ${Math.max(visibleCount, scannedAllCount)} 项${selectionDiagnostics ? `(${selectionDiagnostics})` : ''};继续补齐失败:${getErrorText(err) || '未知错误'}。`,
        },
      };
    }
  }

  function renderMoveSelectionList() {
    if (!UI.moveSelectionList || !UI.moveSelectionCount) {
      return;
    }

    const items = Array.isArray(STATE.moveSelectionPreviewItems) ? STATE.moveSelectionPreviewItems : [];
    const expectedCount = Math.max(0, Number(STATE.moveSelectionExpectedCount || 0));
    const warning = String(STATE.moveSelectionWarning || '').trim();
    UI.moveSelectionCount.textContent =
      expectedCount > items.length
        ? `当前勾选 ${items.length}/${expectedCount} 项`
        : `当前勾选 ${Math.max(items.length, expectedCount)} 项`;

    if (!items.length) {
      UI.moveSelectionList.innerHTML = `
        ${warning ? `<div class="gyp-import-empty">${escapeHtml(warning)}</div>` : ''}
        <div class="gyp-import-empty">点“读取当前勾选”后,这里会显示当前页面已勾选的文件 / 文件夹。</div>
      `;
      return;
    }

    UI.moveSelectionList.innerHTML = `
      ${warning ? `<div class="gyp-import-empty">${escapeHtml(warning)}</div>` : ''}
      ${items.map((item) => `
      <div class="gyp-import-row">
        <div class="gyp-import-name" title="${escapeHtml(String(item.name || ''))}">${escapeHtml(String(item.name || ''))}</div>
        <div class="gyp-import-meta">${item.isDir ? '文件夹' : '文件'} | ${item.resolved ? `已识别 fileId: ${escapeHtml(String(item.fileId || ''))}` : '仅识别到当前页面勾选,执行前会自动补齐真实 fileId'}</div>
      </div>
      `).join('')}
    `;
  }

  function setMoveSelectionPreview(items = [], meta = {}) {
    STATE.moveSelectionPreviewItems = (items || []).filter(Boolean).map((item) => ({
      fileId: String(item.fileId || ''),
      name: String(item.name || ''),
      isDir: item.isDir === true,
      resolved: Boolean(item.resolved || (item.fileId && !String(item.fileId).startsWith('dom:'))),
      parentId: String(item.parentId || ''),
      dirId: String(item.dirId || item.fileId || ''),
      dirIdCandidates: normalizeIdCandidates(item.dirIdCandidates || [item.dirId, item.fileId]),
    }));
    STATE.moveSelectionExpectedCount = Math.max(0, Number(meta.expectedCount || 0));
    STATE.moveSelectionSource = String(meta.source || 'visible');
    STATE.moveSelectionWarning = String(meta.warning || '');
    renderMoveSelectionList();
    if (UI.moveDetails) {
      UI.moveDetails.open = true;
    }
  }

  function renderDirectDownloadList() {
    if (!UI.directDownloadList || !UI.directDownloadCount || !UI.directDownloadSummary) {
      return;
    }

    const items = Array.isArray(STATE.directDownloadPreviewItems) ? STATE.directDownloadPreviewItems : [];
    const expectedCount = Math.max(0, Number(STATE.directDownloadExpectedCount || 0));
    const warning = String(STATE.directDownloadWarning || '').trim();
    const summary = STATE.lastDirectDownloadSummary || null;
    const failedCount = Math.max(0, Number(summary?.failedCount || 0));
    const failedSample = getDirectDownloadFailureSample(summary?.failedItems || []);
    UI.directDownloadCount.textContent =
      expectedCount > items.length
        ? `待下载 ${items.length}/${expectedCount} 项`
        : `待下载 ${Math.max(items.length, expectedCount)} 项`;
    UI.directDownloadSummary.textContent = summary
      ? `${summary.mode === 'triggered' ? '已触发浏览器/下载器任务' : '已生成直链'} ${summary.linkCount || 0} ${summary.mode === 'triggered' ? '个' : '条'},总大小 ${summary.formattedTotalSize || '0 B'}${failedCount ? `;另有 ${failedCount} 项取链失败` : ''}${failedSample ? `;示例:${failedSample}` : ''}${summary.mode === 'triggered' ? '。' : ';点“下载勾选”会触发浏览器下载。'}`
      : '请先打开文件夹后勾选里面的文件;点“读取当前勾选并展开”后,这里会显示最终要下载的文件列表。';

    if (!items.length) {
      UI.directDownloadList.innerHTML = `
        ${warning ? `<div class="gyp-import-empty">${escapeHtml(warning)}</div>` : ''}
        <div class="gyp-import-empty">请先打开文件夹后勾选里面的文件;点“读取当前勾选并展开”后,这里会显示最终要下载的文件列表。</div>
      `;
      return;
    }

    UI.directDownloadList.innerHTML = `
      ${warning ? `<div class="gyp-import-empty">${escapeHtml(warning)}</div>` : ''}
      ${failedCount ? `<div class="gyp-import-empty">最近有 ${failedCount} 项取链失败。${failedSample ? `示例:${escapeHtml(failedSample)}` : ''} 如果“源已修改”较多,建议把下面的“每批并发取链数”降到 1 或 2 后重试。</div>` : ''}
      ${items.map((item) => `
      <div class="gyp-import-row">
        <div class="gyp-import-name" title="${escapeHtml(String(item.path || item.name || ''))}">${escapeHtml(String(item.path || item.name || ''))}</div>
        <div class="gyp-import-meta">${item.isDir ? '文件夹' : '文件'} | ${item.fileId ? `fileId: ${escapeHtml(String(item.fileId || ''))}` : '未识别 fileId'}${item.sizeText ? ` | ${escapeHtml(item.sizeText)}` : ''}${item.md5 ? ` | MD5: ${escapeHtml(item.md5)}` : ''}</div>
      </div>
      `).join('')}
    `;
  }

  function setDirectDownloadPreview(items = [], meta = {}) {
    STATE.directDownloadPreviewItems = (items || []).filter(Boolean).map((item) => {
      const size = normalizeMiaochuanInteger(item.size ?? item.fileSize ?? item.bytes);
      const sizeRaw = getGuangyaDirectFileSizeText(item) || String(size || 0);
      const hash = getGuangyaDirectFileHashInfo(item);
      return {
        fileId: String(item.fileId || ''),
        name: String(item.name || ''),
        path: String(item.path || item.name || ''),
        isDir: item.isDir === true,
        parentId: String(item.parentId || ''),
        dirId: String(item.dirId || item.fileId || ''),
        dirIdCandidates: normalizeIdCandidates(item.dirIdCandidates || [item.dirId, item.fileId]),
        size: size == null ? 0 : size,
        sizeRaw,
        sizeText: formatMiaochuanBytes(size || 0),
        md5: hash.value,
        hashSource: hash.source,
        hashKind: hash.kind,
      };
    });
    STATE.directDownloadExpectedCount = Math.max(0, Number(meta.expectedCount || 0));
    STATE.directDownloadWarning = String(meta.warning || '');
    if (!meta.preserveSummary) {
      STATE.lastDirectDownloadSummary = null;
    }
    renderDirectDownloadList();
    if (UI.directDownloadDetails) {
      UI.directDownloadDetails.open = true;
    }
  }

  function clearDirectDownloadPanel() {
    STATE.directDownloadPreviewItems = [];
    STATE.directDownloadExpectedCount = 0;
    STATE.directDownloadWarning = '';
    STATE.lastDirectDownloadSummary = null;
    renderDirectDownloadList();
    updatePanelStatus('已清空批量直链下载结果');
  }

  function getDirectDownloadExportFormat() {
    return normalizeDirectDownloadExportFormat(
      UI.fields.directDownloadExportFormat?.value || CONFIG.download.exportFormat || 'aria2'
    );
  }

  function getDirectDownloadExportMimeType(format) {
    return 'text/plain;charset=utf-8';
  }

  function getDirectDownloadExportFilename(format) {
    const normalizedFormat = normalizeDirectDownloadExportFormat(format);
    const stamp = new Date().toISOString().slice(0, 19).replace(/[T:]/g, '-');
    return `光鸭批量直链_${normalizedFormat}_${stamp}.txt`;
  }

  function getDirectDownloadMd5SizeFilename() {
    const stamp = new Date().toISOString().slice(0, 19).replace(/[T:]/g, '-');
    return `光鸭勾选文件_MD5_Size_${stamp}.json`;
  }

  function getGuangyaDirectFileRaw(item) {
    return item?.raw && typeof item.raw === 'object' ? item.raw : (item && typeof item === 'object' ? item : {});
  }

  function normalizeGuangyaSizeText(value) {
    if (value == null || value === '') {
      return '';
    }
    const text = String(value).trim();
    if (/^\d+$/u.test(text)) {
      return text.replace(/^0+(?=\d)/u, '') || '0';
    }
    const normalized = normalizeMiaochuanInteger(value);
    return normalized == null ? '' : String(normalized);
  }

  function isDeniedGuangyaHashKey(key = '') {
    const text = String(key || '');
    if (/^gcid$/iu.test(text)) {
      return false;
    }
    return /(url|link|path|name|token|sign|proof|request|session|cookie|auth|id)$/iu.test(text);
  }

  function isLikelyGuangyaMd5Key(key = '') {
    const text = String(key || '').toLowerCase();
    if (!text || isDeniedGuangyaHashKey(text)) {
      return false;
    }
    return /(gcid|md5|etag|hash|digest|checksum)/iu.test(text) && !/(sha1|sha256|sha512)/iu.test(text);
  }

  function normalizeGuangyaMd5FieldValue(value, key = '') {
    const keyText = String(key || '');
    const text = String(value == null ? '' : value).trim().replace(/^"+|"+$/g, '');
    if (/^gcid$/iu.test(keyText) && /^[a-f0-9]{40}$/iu.test(text)) {
      return text.toUpperCase();
    }
    const direct = decodeMiaochuanMd5Token(value);
    if (direct) {
      return direct;
    }
    if (/(md5|etag)/iu.test(keyText)) {
      return normalizeMiaochuanMd5(value);
    }
    return '';
  }

  function getGuangyaHashKind(key = '') {
    return /^gcid$/iu.test(String(key || '')) ? 'gcid' : 'md5';
  }

  function findGuangyaMd5Candidate(node, options = {}) {
    const seen = options.seen || new WeakSet();
    const depth = Number(options.depth || 0);
    const path = Array.isArray(options.path) ? options.path : [];
    if (!node || typeof node !== 'object' || depth > 7) {
      return null;
    }
    if (seen.has(node)) {
      return null;
    }
    seen.add(node);

    if (Array.isArray(node)) {
      for (let index = 0; index < node.length; index += 1) {
        const found = findGuangyaMd5Candidate(node[index], {
          seen,
          depth: depth + 1,
          path: [...path, String(index)],
        });
        if (found) {
          return found;
        }
      }
      return null;
    }

    const contentHashName = getMiaochuanFieldValue(node, ['content_hash_name', 'contentHashName', 'hashType', 'hash_type']);
    const contentHash = getMiaochuanFieldValue(node, ['content_hash', 'contentHash']);
    if (contentHash.key && String(contentHashName.value || '').toLowerCase() === 'md5') {
      const md5 = normalizeGuangyaMd5FieldValue(contentHash.value, contentHash.key);
      if (md5) {
        return {
          value: md5,
          key: contentHash.key,
          path: [...path, contentHash.key].join('.'),
          kind: getGuangyaHashKind(contentHash.key),
        };
      }
    }

    for (const [key, value] of Object.entries(node)) {
      if (!isLikelyGuangyaMd5Key(key)) {
        continue;
      }
      const md5 = normalizeGuangyaMd5FieldValue(value, key);
      if (md5) {
        return {
          value: md5,
          key,
          path: [...path, key].join('.'),
          kind: getGuangyaHashKind(key),
        };
      }
    }

    for (const [key, value] of Object.entries(node)) {
      if (!value || typeof value !== 'object' || isDeniedGuangyaHashKey(key)) {
        continue;
      }
      const found = findGuangyaMd5Candidate(value, {
        seen,
        depth: depth + 1,
        path: [...path, key],
      });
      if (found) {
        return found;
      }
    }

    return null;
  }

  function isLikelyGuangyaSizeKey(key = '') {
    const text = String(key || '').toLowerCase();
    return /(size|bytes|length)$/iu.test(text) && !/(page|total|count|chunk|block|part|limit)/iu.test(text);
  }

  function findGuangyaSizeCandidate(node, options = {}) {
    const seen = options.seen || new WeakSet();
    const depth = Number(options.depth || 0);
    const path = Array.isArray(options.path) ? options.path : [];
    if (!node || typeof node !== 'object' || depth > 7) {
      return null;
    }
    if (seen.has(node)) {
      return null;
    }
    seen.add(node);

    if (Array.isArray(node)) {
      for (let index = 0; index < node.length; index += 1) {
        const found = findGuangyaSizeCandidate(node[index], {
          seen,
          depth: depth + 1,
          path: [...path, String(index)],
        });
        if (found) {
          return found;
        }
      }
      return null;
    }

    for (const [key, value] of Object.entries(node)) {
      if (!isLikelyGuangyaSizeKey(key)) {
        continue;
      }
      const size = normalizeGuangyaSizeText(value);
      if (size) {
        return {
          value: size,
          key,
          path: [...path, key].join('.'),
        };
      }
    }

    for (const [key, value] of Object.entries(node)) {
      if (!value || typeof value !== 'object' || isDeniedGuangyaHashKey(key)) {
        continue;
      }
      const found = findGuangyaSizeCandidate(value, {
        seen,
        depth: depth + 1,
        path: [...path, key],
      });
      if (found) {
        return found;
      }
    }

    return null;
  }

  function getGuangyaDirectFileHashInfo(item, options = {}) {
    const raw = getGuangyaDirectFileRaw(item);
    const picked = getMiaochuanFieldValue(raw, [
      'gcid',
      'md5',
      'fileMd5',
      'file_md5',
      'etag',
      'contentMd5',
      'content_md5',
      'resMd5',
      'res_md5',
      'resourceMd5',
      'resource_md5',
      'fileHash',
      'file_hash',
      'resHash',
      'res_hash',
      'resourceHash',
      'resource_hash',
      'hash',
      'digest',
      'checksum',
    ]);
    const direct = normalizeGuangyaMd5FieldValue(picked.value, picked.key)
      || normalizeGuangyaMd5FieldValue(item?.gcid, 'gcid')
      || normalizeGuangyaMd5FieldValue(item?.md5, 'md5')
      || normalizeGuangyaMd5FieldValue(item?.etag, 'etag')
      || normalizeGuangyaMd5FieldValue(item?.hash, 'hash');
    if (direct) {
      const key = picked.key || (item?.gcid ? 'gcid' : (item?.md5 ? 'md5' : (item?.etag ? 'etag' : 'hash')));
      return {
        value: direct,
        source: key,
        kind: getGuangyaHashKind(key),
      };
    }
    if (options.deepScan === false) {
      return {
        value: '',
        source: '',
        kind: '',
      };
    }
    const nested = findGuangyaMd5Candidate(raw);
    return {
      value: nested?.value || '',
      source: nested?.path || nested?.key || '',
      kind: nested?.kind || (nested?.key ? getGuangyaHashKind(nested.key) : ''),
    };
  }

  function getGuangyaDirectFileMd5(item) {
    return getGuangyaDirectFileHashInfo(item).value || '';
  }

  function getGuangyaDirectFileSizeText(item, options = {}) {
    const raw = getGuangyaDirectFileRaw(item);
    const picked = getMiaochuanFieldValue(raw, [
      'size',
      'fileSize',
      'file_size',
      'resSize',
      'res_size',
      'resourceSize',
      'resource_size',
      'bytes',
      'length',
    ]);
    const direct = normalizeGuangyaSizeText(picked.value)
      || normalizeGuangyaSizeText(item?.sizeRaw)
      || normalizeGuangyaSizeText(item?.size)
      || normalizeGuangyaSizeText(item?.fileSize)
      || normalizeGuangyaSizeText(item?.bytes);
    if (direct || options.deepScan === false) {
      return direct || '';
    }
    return findGuangyaSizeCandidate(raw)?.value || '';
  }

  function buildDirectDownloadMd5SizePayload(files = []) {
    const rows = (files || []).filter(Boolean).map((file) => {
      const sizeText = getGuangyaDirectFileSizeText(file);
      const hash = getGuangyaDirectFileHashInfo(file);
      return {
        path: String(file.path || file.name || '').replace(/^\/+/, ''),
        name: String(file.name || ''),
        fileId: String(file.fileId || ''),
        md5: hash.value,
        hashSource: hash.source,
        hashKind: hash.kind,
        size: sizeText,
      };
    });
    const missingMd5 = rows.filter((row) => !row.md5).length;
    const missingSize = rows.filter((row) => !row.size).length;
    return {
      source: 'guangyapan-current-file-list',
      note: 'md5 字段优先导出光鸭 get_file_list 的 gcid;若拿到传统 32 位 MD5/etag 也会导出。size 来自光鸭 fileSize/size 字段,不来自夸克、分享链接或直链下载 URL。',
      generatedAt: new Date().toISOString(),
      count: rows.length,
      missingMd5,
      missingSize,
      files: rows,
    };
  }

  async function getGuangyaDownloadMetadataByFileId(fileId) {
    const normalizedFileId = String(fileId || '').trim();
    if (!normalizedFileId) {
      return { md5: '', size: '', payload: null };
    }
    try {
      const response = await postJson(getDownloadUrl(), { fileId: normalizedFileId }, getRequestHeaders());
      const payload = response.payload || {};
      if (!response.ok || !isProbablySuccess(payload, response)) {
        return { md5: '', size: '', payload };
      }
      return {
        md5: getGuangyaDirectFileMd5({ raw: payload }),
        size: getGuangyaDirectFileSizeText({ raw: payload }),
        payload,
      };
    } catch (err) {
      warn('读取光鸭下载元数据失败:', err);
      return { md5: '', size: '', payload: null };
    }
  }

  async function enrichDirectDownloadMd5SizePayload(payload, options = {}) {
    const taskControl = options.taskControl || null;
    const onProgress = typeof options.onProgress === 'function' ? options.onProgress : null;
    const files = Array.isArray(payload?.files) ? payload.files : [];
    const needs = files.filter((row) => row && row.fileId && (!row.md5 || !row.size));
    if (!needs.length) {
      return payload;
    }

    for (let index = 0; index < needs.length; index += 1) {
      await waitForTaskControl(taskControl);
      const row = needs[index];
      if (onProgress) {
        onProgress({
          visible: true,
          percent: Math.min(95, Math.round((index / Math.max(1, needs.length)) * 100)),
          indeterminate: false,
          text: `正在从光鸭接口补齐 MD5/Size:${index + 1}/${needs.length} | ${shortDisplayName(row.path || row.name, 42)}`,
        });
      }
      const meta = await getGuangyaDownloadMetadataByFileId(row.fileId);
      if (!row.md5 && meta.md5) {
        row.md5 = meta.md5;
        row.hashSource = row.hashSource || 'get_res_download_url';
        row.hashKind = row.hashKind || (meta.md5.length === 40 ? 'gcid' : 'md5');
      }
      if (!row.size && meta.size) {
        row.size = meta.size;
      }
    }

    payload.missingMd5 = files.filter((row) => !row.md5).length;
    payload.missingSize = files.filter((row) => !row.size).length;
    payload.note = `${payload.note} 若列表字段缺失,已额外尝试读取光鸭 get_res_download_url 响应 payload 中的元数据;未解析签名 URL 字符串。`;
    return payload;
  }

  function formatDirectDownloadEntries(entries = [], format = 'aria2') {
    const normalizedFormat = normalizeDirectDownloadExportFormat(format);
    const rows = (entries || []).filter((entry) => String(entry?.url || '').trim());
    if (!rows.length) {
      return '';
    }

    if (normalizedFormat === 'url') {
      return rows.map((entry) => String(entry.url || '').trim()).join('\n');
    }

    return rows.map((entry) => {
      const url = String(entry.url || '').trim();
      const outPath = String(entry.outPath || entry.path || entry.name || '').replace(/^\/+/, '').trim();
      const segments = getMiaochuanPathSegments(outPath);
      const fileName = segments.pop() || String(entry.name || '').trim();
      const dir = segments.join('/');
      const bits = [url];
      if (dir) {
        bits.push(`  dir=${dir}`);
      }
      if (fileName) {
        bits.push(`  out=${fileName}`);
      }
      return bits.join('\n');
    }).join('\n\n');
  }

  function refreshDirectDownloadOutputFromSummary(options = {}) {
    const summary = STATE.lastDirectDownloadSummary || null;
    if (!summary) {
      return '';
    }
    const format = getDirectDownloadExportFormat();
    const text = formatDirectDownloadEntries(summary.entries || [], format);
    summary.exportFormat = format;
    summary.text = text;
    summary.exportFilename = getDirectDownloadExportFilename(format);
    if (!options.skipRender) {
      renderDirectDownloadList();
    }
    return text;
  }

  function getPreparedDirectDownloadFiles() {
    return (STATE.directDownloadPreviewItems || []).filter((item) => item && !item.isDir && item.fileId).map((item) => ({
      fileId: String(item.fileId || ''),
      name: String(item.name || ''),
      path: String(item.path || item.name || ''),
      size: Number(item.size || 0),
      sizeRaw: String(item.sizeRaw || ''),
      sizeText: String(item.sizeText || ''),
      md5: String(item.md5 || ''),
      hashSource: String(item.hashSource || ''),
      hashKind: String(item.hashKind || ''),
      parentId: String(item.parentId || ''),
    }));
  }

  function getDirectDownloadItemSize(item) {
    const size = normalizeMiaochuanInteger(
      item?.size
      ?? item?.fileSize
      ?? item?.raw?.fileSize
      ?? item?.raw?.size
      ?? item?.raw?.bytes
      ?? item?.raw?.resourceSize
    );
    return size == null ? 0 : size;
  }

  function getDirectoryTraversalIdCandidates(item) {
    const raw = item && item.raw && typeof item.raw === 'object' ? item.raw : {};
    const parentId = String(item?.parentId || raw.parentId || raw.parent_id || '').trim();
    const preferred = normalizeIdCandidates([
      raw.fileId,
      raw.id,
      raw.resourceId,
      raw.resId,
      raw.bizId,
      raw.objId,
      raw.shareFileId,
      raw.share_file_id,
      raw.folderId,
      raw.folder_id,
      item?.fileId,
      raw.dirId,
      raw.dir_id,
      item?.dirId,
      ...(Array.isArray(item?.dirIdCandidates) ? item.dirIdCandidates : []),
    ]);

    if (!parentId || preferred.length <= 1) {
      return preferred;
    }

    const withoutParent = preferred.filter((id) => String(id || '').trim() !== parentId);
    return withoutParent.length ? [...withoutParent, parentId] : preferred;
  }

  function buildDirectoryRowEntryFromItem(item) {
    const row = item?.row;
    if (!row || !isUsableListRow(row)) {
      return null;
    }
    return {
      row,
      name: String(item?.name || ''),
      normalizedName: normalizeDomName(item?.name || ''),
      checkbox: getCheckboxInRow(row),
      isDir: true,
    };
  }

  async function collectDirectDownloadFilesByDirectoryNavigation(item, options = {}) {
    const taskControl = options.taskControl || null;
    const onProgress = typeof options.onProgress === 'function' ? options.onProgress : null;
    const expectedName = String(item?.name || '').trim();
    const previousSnapshot = getDirectoryContextSnapshot();
    const rowEntry = options.rowEntry || buildDirectoryRowEntryFromItem(item);

    if (!expectedName) {
      return {
        files: [],
        warning: '',
      };
    }

    if (onProgress) {
      onProgress({
        visible: true,
        percent: 0,
        indeterminate: true,
        text: `正在通过页面打开文件夹:${shortDisplayName(expectedName, 42)}`,
      });
    }

    const openedSnapshot = await openDirectoryByName(expectedName, {
      previousSnapshot,
      childItem: item,
      rowEntry,
      timeoutMs: 4800,
      maxRounds: 28,
    });
    if (!openedSnapshot) {
      return {
        files: [],
        warning: '',
      };
    }

    await sleep(520);

    let childItems = [];
    let requestError = null;
    const openedParentId = String(openedSnapshot.parentId || getCurrentListContext().parentId || '').trim();
    const visibleRows = collectVisibleListRowEntries();
    const visibleRowItems = buildNormalizedItemsFromVisibleRows(visibleRows);

    if (openedParentId) {
      try {
        childItems = await fetchCurrentList({
          parentId: openedParentId,
          pageSize: Math.max(100, Number(UI.fields.pageSize?.value || CONFIG.request.manualListBody.pageSize || 100)),
        });
      } catch (err) {
        requestError = err;
      }
    }

    const snapshotItems = openedParentId ? buildCurrentDirectoryItemsSnapshot(openedParentId) : [];
    childItems = dedupeItems([
      ...(Array.isArray(childItems) ? childItems : []),
      ...snapshotItems,
      ...visibleRowItems,
    ]);

    let nested = {
      files: [],
      warning: '',
    };
    if (childItems.length) {
      nested = await collectDirectDownloadFilesFromItems(childItems, {
        ...options,
        basePath: String(options.basePath || ''),
      });
    }

    const returned = await returnToDirectorySnapshot(previousSnapshot, {
      timeoutMs: 5000,
      historyBackTries: 2,
    });

    const warnings = [];
    if (requestError) {
      warnings.push(`目录“${expectedName}”页面展开后读取列表失败:${getErrorText(requestError)}`);
    }
    if (!returned) {
      warnings.push(`目录“${expectedName}”读取完成后未能自动回到原目录,请手动返回后再继续。`);
    }
    if (!childItems.length && !requestError) {
      warnings.push(`目录“${expectedName}”页面展开后仍未读取到子项。`);
    }
    if (nested.warning) {
      warnings.push(nested.warning);
    }

    return {
      files: nested.files || [],
      warning: warnings.join(';'),
    };
  }

  function buildDirectDownloadEntryFromResolvedUrl(file, direct = {}) {
    return {
      ...file,
      url: String(direct.url || '').trim(),
      requestId: String(direct.requestId || '').trim(),
      outPath: String(file.path || file.name || '').replace(/^\/+/, ''),
    };
  }

  function isRetryableDirectDownloadLinkError(err) {
    const text = getErrorText(err);
    if (!text) {
      return false;
    }
    return /源已修改|源文件已修改|文件已修改|链接已失效|签名|过期|expired|timeout|timed out|network|fetch failed|http 5\d{2}|网关|稍后重试/iu.test(text);
  }

  function formatDirectDownloadFailure(file, err) {
    return {
      fileId: String(file?.fileId || ''),
      name: String(file?.path || file?.name || file?.fileId || '').trim(),
      message: getErrorText(err) || '未知错误',
    };
  }

  function getDirectDownloadFailureSample(failedItems = [], max = 3) {
    return (failedItems || [])
      .filter(Boolean)
      .slice(0, max)
      .map((item) => `${shortDisplayName(item.name || item.fileId || '未命名文件', 28)}:${item.message || '未知错误'}`)
      .join(';');
  }

  async function resolveDirectDownloadBatch(batch = [], options = {}) {
    const taskControl = options.taskControl || null;
    const allowRetry = options.allowRetry !== false;
    const retryDelayMs = Math.max(200, Number(options.retryDelayMs ?? 450));
    const orderedEntries = new Array(batch.length).fill(null);
    const retryQueue = [];
    const failed = [];

    const firstPass = await Promise.all((batch || []).map(async (file, index) => {
      try {
        const direct = await getDirectDownloadLinkByFileId(file.fileId);
        return {
          index,
          ok: true,
          entry: buildDirectDownloadEntryFromResolvedUrl(file, direct),
        };
      } catch (err) {
        return {
          index,
          ok: false,
          file,
          error: err,
        };
      }
    }));

    for (const item of firstPass) {
      if (item?.ok && item.entry) {
        orderedEntries[item.index] = item.entry;
        continue;
      }
      if (allowRetry && isRetryableDirectDownloadLinkError(item?.error)) {
        retryQueue.push(item);
      } else {
        failed.push(formatDirectDownloadFailure(item?.file, item?.error));
      }
    }

    for (const item of retryQueue) {
      await waitForTaskControl(taskControl);
      await controlledDelay(retryDelayMs, taskControl);
      try {
        const direct = await getDirectDownloadLinkByFileId(item.file.fileId);
        orderedEntries[item.index] = buildDirectDownloadEntryFromResolvedUrl(item.file, direct);
      } catch (err) {
        failed.push(formatDirectDownloadFailure(item.file, err));
      }
    }

    return {
      entries: orderedEntries.filter(Boolean),
      failed,
    };
  }

  async function getDirectDownloadLinkByFileId(fileId) {
    const normalizedFileId = String(fileId || '').trim();
    if (!normalizedFileId) {
      throw new Error('缺少 fileId,无法获取下载直链。');
    }
    const response = await postJson(getDownloadUrl(), { fileId: normalizedFileId }, getRequestHeaders());
    if (!response.ok || !isProbablySuccess(response.payload, response)) {
      throw new Error(`获取下载直链失败:${getErrorText(response.payload || response.text || `HTTP ${response.status}`)}`);
    }
    const payload = response.payload || {};
    const downloadUrl = String(
      findFirstValueByKeys(payload, ['signedURL', 'signedUrl', 'downloadUrl', 'download_url', 'url'])
      || ''
    ).trim();
    if (!downloadUrl) {
      throw new Error('接口返回成功,但没有拿到下载直链 URL。');
    }
    return {
      url: downloadUrl,
      requestId: String(findFirstValueByKeys(payload, ['requestId', 'request_id']) || '').trim(),
      payload,
    };
  }

  async function collectDirectDownloadFilesFromItems(items, options = {}) {
    const taskControl = options.taskControl || null;
    const onProgress = typeof options.onProgress === 'function' ? options.onProgress : null;
    const queue = (items || []).filter(Boolean).map((item) => ({
      item,
      parentPath: String(options.basePath || ''),
    }));
    const files = [];
    const seenFileIds = new Set();
    const visitedDirs = new Set();
    const warnings = [];
    let processedCount = 0;

    while (queue.length) {
      await waitForTaskControl(taskControl);
      if (processedCount > 0 && processedCount % 12 === 0) {
        await waitForUiPaint(1);
      }
      const current = queue.shift();
      const item = current.item || {};
      const itemName = sanitizeCloudDirName(item.name, item.isDir ? '未命名目录' : '未命名文件');
      const currentPath = `${current.parentPath}/${itemName}`.replace(/\/{2,}/g, '/');
      const isDir = item.isDir === true || shouldTreatItemAsDirectory(item);
      processedCount += 1;

      if (onProgress) {
        onProgress({
          visible: true,
          percent: 0,
          indeterminate: true,
          text: isDir
            ? `正在展开文件夹:${currentPath || itemName} | 已展开文件 ${files.length} 项`
            : `正在整理文件:${currentPath || itemName} | 已展开文件 ${files.length} 项`,
        });
      }

      if (!isDir) {
        const fileId = String(item.fileId || '').trim();
        if (!fileId || seenFileIds.has(fileId)) {
          continue;
        }
        const size = getDirectDownloadItemSize(item);
        const hash = getGuangyaDirectFileHashInfo(item, {
          deepScan: false,
        });
        seenFileIds.add(fileId);
        files.push({
          fileId,
          name: itemName,
          path: currentPath,
          size,
          sizeRaw: getGuangyaDirectFileSizeText(item, {
            deepScan: false,
          }) || String(size || 0),
          md5: hash.value,
          hashSource: hash.source,
          hashKind: hash.kind,
          parentId: String(item.parentId || ''),
        });
        if (files.length > DIRECT_DOWNLOAD_MAX_FILES) {
          throw new Error(`待下载文件过多,已超过安全上限 ${DIRECT_DOWNLOAD_MAX_FILES} 个。请缩小勾选范围后再试。`);
        }
        continue;
      }

      const dirIdCandidates = getDirectoryTraversalIdCandidates(item);
      const dirKey = dirIdCandidates[0] || String(item.fileId || '').trim();
      if (!dirKey || visitedDirs.has(dirKey)) {
        continue;
      }
      visitedDirs.add(dirKey);
      if (visitedDirs.size > DIRECT_DOWNLOAD_MAX_DIRS) {
        throw new Error(`目录展开过多,已超过安全上限 ${DIRECT_DOWNLOAD_MAX_DIRS} 个目录。请缩小勾选范围后再试。`);
      }

      const listing = await fetchDirectoryItems(dirKey, {
        idCandidates: dirIdCandidates,
        pageSize: Math.max(100, Number(UI.fields.pageSize?.value || CONFIG.request.manualListBody.pageSize || 100)),
        maxPages: DIRECT_DOWNLOAD_MAX_PAGES_PER_DIR,
        delayMs: 0,
        taskControl,
      });
      if (listing.truncated) {
        warnings.push(`目录“${itemName}”分页过多,可能未完全展开。`);
      }
      if (Number(listing.cachedCount || 0) > Number(listing.fetchedCount || 0) && Number(listing.cachedCount || 0) === Number(listing.items?.length || 0)) {
        warnings.push(`目录“${itemName}”已合并缓存列表 ${listing.cachedCount} 项。`);
      }
      if (!listing.items.length) {
        const fallback = await collectDirectDownloadFilesByDirectoryNavigation(item, {
          ...options,
          basePath: currentPath,
          taskControl,
          onProgress,
          rowEntry: buildDirectoryRowEntryFromItem(item),
        });
        if (fallback.warning) {
          warnings.push(fallback.warning);
        }
        for (const file of fallback.files || []) {
          const fileId = String(file?.fileId || '').trim();
          if (!fileId || seenFileIds.has(fileId)) {
            continue;
          }
          seenFileIds.add(fileId);
          files.push(file);
          if (files.length > DIRECT_DOWNLOAD_MAX_FILES) {
            throw new Error(`待下载文件过多,已超过安全上限 ${DIRECT_DOWNLOAD_MAX_FILES} 个。请缩小勾选范围后再试。`);
          }
        }
        continue;
      }
      for (const child of listing.items || []) {
        queue.push({
          item: child,
          parentPath: currentPath,
        });
      }
    }

    return {
      files,
      warning: warnings.join(';'),
    };
  }

  async function buildDirectDownloadEntries(files, options = {}) {
    const onProgress = typeof options.onProgress === 'function' ? options.onProgress : null;
    const taskControl = options.taskControl || null;
    const delayMs = Math.max(0, Number(options.delayMs != null ? options.delayMs : CONFIG.batch.delayMs || 0));
    const batchSize = Math.max(1, Number(options.batchSize || CONFIG.download.directBatchSize || 3));
    const exportFormat = normalizeDirectDownloadExportFormat(options.exportFormat || CONFIG.download.exportFormat);
    const out = [];
    const failed = [];
    let totalSize = 0;

    for (let offset = 0; offset < files.length; offset += batchSize) {
      await waitForTaskControl(taskControl);
      const batch = files.slice(offset, offset + batchSize);
      if (onProgress) {
        onProgress({
          visible: true,
          percent: Math.min(95, Math.round((offset / Math.max(1, files.length)) * 100)),
          indeterminate: false,
          text: `正在获取下载直链:${Math.min(offset + batch.length, files.length)}/${files.length} | 本批 ${batch.length} 个`,
        });
      }
      const batchResult = await resolveDirectDownloadBatch(batch, {
        taskControl,
        allowRetry: true,
        retryDelayMs: Math.max(350, delayMs || 300),
      });
      out.push(...batchResult.entries);
      failed.push(...batchResult.failed);
      totalSize += batchResult.entries.reduce((sum, item) => sum + Number(item.size || 0), 0);
      if (offset + batch.length < files.length) {
        const cooldownMs = batchResult.failed.length
          ? Math.max(delayMs, 700)
          : delayMs;
        if (cooldownMs > 0) {
          await controlledDelay(cooldownMs, taskControl);
        }
      }
    }

    if (!out.length && failed.length) {
      const sample = getDirectDownloadFailureSample(failed);
      throw new Error(`当前 ${failed.length} 项都没拿到可用直链。${sample ? `示例:${sample}` : ''}`);
    }

    return {
      entries: out,
      failed,
      failedCount: failed.length,
      totalSize,
      formattedTotalSize: formatMiaochuanBytes(totalSize),
      text: formatDirectDownloadEntries(out, exportFormat),
    };
  }

  async function previewDirectDownloadSelection(options = {}) {
    await waitForUiPaint(1);
    const selection = await collectResolvedCheckedMoveItems({
      onProgress: options.onProgress,
      taskControl: options.taskControl || null,
      allowPartialVisible: true,
      disableFullSelectionFallback: true,
      avoidSlowScrollScan: true,
      allowExactCapturedSelectionFallback: true,
      ignorePageSelectedCount: false,
      updateMovePreview: false,
      includeMeta: true,
      partialUsageLabel: '批量直链下载',
    });
    const checkedItems = selection.items || [];
    const expanded = await collectDirectDownloadFilesFromItems(checkedItems, {
      onProgress: options.onProgress,
      taskControl: options.taskControl || null,
    });
    if (!expanded.files.length) {
      const checkedDirCount = checkedItems.filter((item) => item && (item.isDir === true || shouldTreatItemAsDirectory(item))).length;
      if (checkedItems.length && checkedDirCount === checkedItems.length) {
        throw new Error('当前勾选的是文件夹。请先打开文件夹后勾选里面的文件,再用批量直链下载。');
      }
      throw new Error('当前勾选里没有可直链下载的文件。请先打开文件夹后勾选里面的文件再试。');
    }
    const warning = [selection.meta?.warning, expanded.warning].filter(Boolean).join(';');
    setDirectDownloadPreview(
      expanded.files.map((file) => ({
        ...file,
        isDir: false,
      })),
      {
        expectedCount: expanded.files.length,
        warning,
      }
    );
    updatePanelStatus(`批量直链下载已展开 ${expanded.files.length} 个文件${warning ? `;${warning}` : ''}`);
    return expanded.files;
  }

  async function generateDirectDownloadLinksFromCheckedItems(options = {}) {
    let files = options.reusePreview !== false ? getPreparedDirectDownloadFiles() : [];
    if (!files.length) {
      await waitForUiPaint(1);
      files = await previewDirectDownloadSelection({
        onProgress: options.onProgress,
        taskControl: options.taskControl || null,
      });
    } else {
      setDirectDownloadPreview(
        files.map((file) => ({
          ...file,
          isDir: false,
        })),
        {
          expectedCount: files.length,
          warning: STATE.directDownloadWarning || '',
          preserveSummary: false,
        }
      );
    }
    const built = await buildDirectDownloadEntries(files, {
      onProgress: options.onProgress,
      taskControl: options.taskControl || null,
      exportFormat: options.exportFormat || getDirectDownloadExportFormat(),
    });
    STATE.lastDirectDownloadSummary = {
      files,
      entries: built.entries,
      text: built.text,
      totalSize: built.totalSize,
      formattedTotalSize: built.formattedTotalSize,
      linkCount: built.entries.length,
      failedCount: built.failedCount || 0,
      failedItems: built.failed || [],
      mode: 'generated',
      generatedAt: Date.now(),
    };
    refreshDirectDownloadOutputFromSummary();
    const failureText = built.failedCount
      ? `;失败 ${built.failedCount} 项${getDirectDownloadFailureSample(built.failed) ? `,示例:${getDirectDownloadFailureSample(built.failed)}` : ''}`
      : '';
    updatePanelStatus(`已生成批量直链 ${built.entries.length} 条,总大小 ${built.formattedTotalSize},导出格式 ${getDirectDownloadExportFormatLabel(getDirectDownloadExportFormat())}${failureText}`);
    return STATE.lastDirectDownloadSummary;
  }

  async function downloadDirectDownloadSelection(options = {}) {
    const format = normalizeDirectDownloadExportFormat(options.exportFormat || getDirectDownloadExportFormat());
    const summary = await generateDirectDownloadLinksFromCheckedItems({
      ...options,
      exportFormat: format,
      reusePreview: options.reusePreview !== false,
    });
    const text = summary?.entries?.length
      ? refreshDirectDownloadOutputFromSummary({ skipRender: true })
      : '';
    if (!text) {
      throw new Error('当前没有可导出的直链结果。');
    }
    const filename = getDirectDownloadExportFilename(format);
    downloadMiaochuanText(filename, text, getDirectDownloadExportMimeType(format));
    updatePanelStatus(`已下载 ${summary.linkCount || 0} 条直链(${getDirectDownloadExportFormatLabel(format)})${summary.failedCount ? `;失败 ${summary.failedCount} 项` : ''}`);
    return {
      count: Number(summary.linkCount || 0),
      filename,
      format,
    };
  }

  async function downloadDirectDownloadMd5SizeSelection(options = {}) {
    let files = options.reusePreview !== false ? getPreparedDirectDownloadFiles() : [];
    if (!files.length) {
      await waitForUiPaint(1);
      files = await previewDirectDownloadSelection({
        onProgress: options.onProgress,
        taskControl: options.taskControl || null,
      });
    } else {
      setDirectDownloadPreview(
        files.map((file) => ({
          ...file,
          isDir: false,
        })),
        {
          expectedCount: files.length,
          warning: STATE.directDownloadWarning || '',
          preserveSummary: false,
        }
      );
    }

    const payload = await enrichDirectDownloadMd5SizePayload(
      buildDirectDownloadMd5SizePayload(files),
      {
        onProgress: options.onProgress,
        taskControl: options.taskControl || null,
      }
    );
    if (!payload.files.length) {
      throw new Error('当前没有可导出的光鸭 MD5/Size 文件信息。');
    }
    const filename = getDirectDownloadMd5SizeFilename();
    downloadMiaochuanText(filename, JSON.stringify(payload, null, 2), 'application/json;charset=utf-8');
    const missingText = [
      payload.missingMd5 ? `${payload.missingMd5} 项缺少 MD5` : '',
      payload.missingSize ? `${payload.missingSize} 项缺少 size` : '',
    ].filter(Boolean).join(',');
    updatePanelStatus(`已导出 ${payload.count} 项光鸭 MD5/Size 到 ${filename}${missingText ? `;${missingText}` : ''}`);
    return {
      count: payload.count,
      filename,
      missingMd5: payload.missingMd5,
      missingSize: payload.missingSize,
      payload,
    };
  }

  function triggerDirectDownloadUrl(url, filename = '') {
    const anchor = document.createElement('a');
    anchor.href = String(url || '');
    if (filename) {
      anchor.download = String(filename || '').split('/').pop() || '';
    }
    anchor.rel = 'noopener noreferrer';
    anchor.referrerPolicy = 'no-referrer';
    anchor.target = '_blank';
    document.body.appendChild(anchor);
    anchor.click();
    anchor.remove();
  }

  async function triggerDirectDownloadsFromCheckedItems(options = {}) {
    const cachedFiles = options.reusePreview !== false ? getPreparedDirectDownloadFiles() : [];
    const files = cachedFiles.length
      ? cachedFiles
      : await previewDirectDownloadSelection({
        onProgress: options.onProgress,
        taskControl: options.taskControl || null,
      });
    if (!files.length) {
      throw new Error('当前没有可触发下载的文件。');
    }

    setDirectDownloadPreview(
      files.map((file) => ({
        ...file,
        isDir: false,
      })),
      {
        expectedCount: files.length,
        warning: STATE.directDownloadWarning || '',
        preserveSummary: false,
      }
    );

    const taskControl = options.taskControl || null;
    const onProgress = typeof options.onProgress === 'function' ? options.onProgress : null;
    const delayMs = Math.max(200, Number(CONFIG.batch.delayMs || 300));
    const batchSize = Math.max(1, Number(options.batchSize || CONFIG.download.directBatchSize || 3));
    const entries = [];
    const failed = [];
    let totalSize = 0;

    for (let offset = 0; offset < files.length; offset += batchSize) {
      await waitForTaskControl(taskControl);
      const batch = files.slice(offset, offset + batchSize);
      if (onProgress) {
        onProgress({
          visible: true,
          percent: Math.min(95, Math.round((offset / Math.max(1, files.length)) * 100)),
          indeterminate: false,
          text: `正在并发获取直链并交给下载器:${Math.min(offset + batch.length, files.length)}/${files.length} | 本批 ${batch.length} 个`,
        });
      }

      const batchResult = await resolveDirectDownloadBatch(batch, {
        taskControl,
        allowRetry: true,
        retryDelayMs: Math.max(450, delayMs),
      });
      const batchEntries = batchResult.entries;
      entries.push(...batchEntries);
      failed.push(...batchResult.failed);
      totalSize += batchEntries.reduce((sum, item) => sum + Number(item.size || 0), 0);

      for (const entry of batchEntries) {
        await waitForTaskControl(taskControl);
        triggerDirectDownloadUrl(entry.url, entry.outPath || entry.name || '');
        await controlledDelay(120, taskControl);
      }

      if (offset + batch.length < files.length) {
        const cooldownMs = batchResult.failed.length
          ? Math.max(delayMs, 900)
          : delayMs;
        await controlledDelay(cooldownMs, taskControl);
      }
    }

    if (!entries.length && failed.length) {
      const sample = getDirectDownloadFailureSample(failed);
      throw new Error(`当前 ${failed.length} 项都没拿到可用直链。${sample ? `示例:${sample}` : ''}`);
    }

    STATE.lastDirectDownloadSummary = {
      files,
      entries,
      text: formatDirectDownloadEntries(entries, getDirectDownloadExportFormat()),
      totalSize,
      formattedTotalSize: formatMiaochuanBytes(totalSize),
      linkCount: entries.length,
      failedCount: failed.length,
      failedItems: failed,
      mode: 'triggered',
      generatedAt: Date.now(),
    };
    renderDirectDownloadList();

    if (onProgress) {
      onProgress({
        visible: true,
        percent: 100,
        indeterminate: false,
        text: `已触发 ${entries.length} 个浏览器/下载器任务${failed.length ? `,失败 ${failed.length} 个` : ''}`,
      });
    }

    const failureText = failed.length
      ? `;仍有 ${failed.length} 项取链失败${getDirectDownloadFailureSample(failed) ? `,示例:${getDirectDownloadFailureSample(failed)}` : ''}`
      : '';
    updatePanelStatus(`批量直链下载已按每批 ${batchSize} 个并发取链,并触发 ${entries.length} 个浏览器/下载器任务${failureText};下载速度和并发由用户自己的下载器控制`);
    return {
      count: entries.length,
      entries,
      failedCount: failed.length,
      failedItems: failed,
    };
  }

  function resolveCheckedMoveItemsByName(previewItems, sourceItems) {
    const exactMap = new Map();
    const looseMap = new Map();
    const normalizedSourceItems = (sourceItems || []).filter((item) => item && item.fileId && !String(item.fileId).startsWith('dom:'));
    const normalizeLooseName = (name) => normalizeDomName(name)
      .replace(/[\u200B-\u200D\uFEFF]/gu, '')
      .replace(/\s+/gu, '')
      .replace(/(?:文件夹|folder|directory)$/iu, '');

    for (const item of normalizedSourceItems) {
      const key = normalizeDomName(item.name);
      if (!key) {
        continue;
      }
      if (!exactMap.has(key)) {
        exactMap.set(key, []);
      }
      exactMap.get(key).push(item);

      const looseKey = normalizeLooseName(item.name);
      if (looseKey) {
        if (!looseMap.has(looseKey)) {
          looseMap.set(looseKey, []);
        }
        looseMap.get(looseKey).push(item);
      }
    }

    const chooseCandidate = (previewItem, exactMatches = [], rowCandidates = []) => {
      const previewIsDir = previewItem.isDir === true;
      const filterType = (list = []) => list.filter((candidate) => {
        const candidateIsDir = candidate.isDir === true || shouldTreatItemAsDirectory(candidate);
        return candidateIsDir === previewIsDir;
      });
      const exactTypeMatched = filterType(exactMatches);
      if (exactTypeMatched.length === 1) {
        return exactTypeMatched[0];
      }
      if (exactMatches.length === 1) {
        return exactMatches[0];
      }

      const previewName = String(previewItem.name || '');
      const previewLoose = normalizeLooseName(previewName);
      const rowNameMatched = (rowCandidates || []).filter((candidate) => {
        const candidateName = String(candidate?.name || '').trim();
        return !candidateName || !previewName || textLooksLikeExpected(candidateName, previewName) || textLooksLikeExpected(previewName, candidateName);
      });
      const rowTypeMatched = filterType(rowNameMatched);
      if (rowTypeMatched.length === 1) {
        return rowTypeMatched[0];
      }
      if (rowNameMatched.length === 1) {
        return rowNameMatched[0];
      }

      const looseMatches = previewLoose ? (looseMap.get(previewLoose) || []) : [];
      const preferredLooseCandidates = dedupeItems([...(rowCandidates || []), ...looseMatches]);
      const looseTypeMatched = filterType(preferredLooseCandidates);
      if (looseTypeMatched.length === 1) {
        return looseTypeMatched[0];
      }
      if (preferredLooseCandidates.length === 1) {
        return preferredLooseCandidates[0];
      }

      const fuzzyMatches = dedupeItems([...(rowCandidates || []), ...normalizedSourceItems]).filter((candidate) => {
        const candidateName = String(candidate?.name || '');
        if (!candidateName) {
          return false;
        }
        const candidateLoose = normalizeLooseName(candidateName);
        return (
          textLooksLikeExpected(candidateName, previewName)
          || textLooksLikeExpected(previewName, candidateName)
          || (previewLoose && candidateLoose && (
            candidateLoose === previewLoose
            || candidateLoose.includes(previewLoose)
            || previewLoose.includes(candidateLoose)
          ))
        );
      });

      const fuzzyTypeMatched = filterType(fuzzyMatches);
      const fuzzyLooseTypeMatched = fuzzyTypeMatched.filter((candidate) => normalizeLooseName(candidate?.name || '') === previewLoose);
      if (fuzzyLooseTypeMatched.length === 1) {
        return fuzzyLooseTypeMatched[0];
      }
      if (fuzzyTypeMatched.length === 1) {
        return fuzzyTypeMatched[0];
      }

      const fuzzyLooseMatched = fuzzyMatches.filter((candidate) => normalizeLooseName(candidate?.name || '') === previewLoose);
      if (fuzzyLooseMatched.length === 1) {
        return fuzzyLooseMatched[0];
      }
      if (fuzzyMatches.length === 1) {
        return fuzzyMatches[0];
      }

      return null;
    };

    const merged = [];
    const resolved = [];
    const unresolved = [];

    for (const item of previewItems || []) {
      if (!item) {
        continue;
      }

      if (item.fileId && !String(item.fileId).startsWith('dom:')) {
        const normalized = {
          fileId: String(item.fileId),
          dirId: String(item.dirId || item.fileId),
          dirIdCandidates: normalizeIdCandidates(item.dirIdCandidates || [item.dirId, item.fileId]),
          name: String(item.name || ''),
          parentId: String(item.parentId || ''),
          isDir: item.isDir === true,
          raw: item.raw,
          resolved: true,
        };
        merged.push(normalized);
        resolved.push(normalized);
        continue;
      }

      const key = normalizeDomName(item.name);
      const matches = key ? (exactMap.get(key) || []) : [];
      const rowCandidates = extractNormalizedItemsFromRow(item.row, item.name, item.isDir === true);
      const chosen = chooseCandidate(item, matches, rowCandidates);

      if (chosen) {
        const normalized = {
          fileId: String(chosen.fileId),
          dirId: String(chosen.dirId || chosen.fileId),
          dirIdCandidates: normalizeIdCandidates(chosen.dirIdCandidates || [chosen.dirId, chosen.fileId]),
          name: String(chosen.name || item.name || ''),
          parentId: String(chosen.parentId || ''),
          isDir: chosen.isDir === true || shouldTreatItemAsDirectory(chosen),
          row: item.row || null,
          raw: chosen.raw,
          resolved: true,
        };
        merged.push(normalized);
        resolved.push(normalized);
      } else {
        const fallback = {
          fileId: String(item.fileId || ''),
          dirId: String(item.dirId || item.fileId || ''),
          dirIdCandidates: normalizeIdCandidates(item.dirIdCandidates || [item.dirId, item.fileId]),
          name: String(item.name || ''),
          parentId: String(item.parentId || ''),
          isDir: item.isDir === true,
          row: item.row || null,
          raw: item.raw,
          resolved: false,
        };
        merged.push(fallback);
        unresolved.push(fallback);
      }
    }

    return {
      merged,
      resolved,
      unresolved,
    };
  }

  async function ensureCheckedMoveItemsHaveRealIds(previewItems, options = {}) {
    const domItems = (previewItems || []).filter((item) => String(item?.fileId || '').startsWith('dom:'));
    if (!domItems.length) {
      return {
        mergedItems: (previewItems || []).map((item) => ({
          ...item,
          fileId: String(item.fileId || ''),
          dirId: String(item.dirId || item.fileId || ''),
          dirIdCandidates: normalizeIdCandidates(item.dirIdCandidates || [item.dirId, item.fileId]),
          name: String(item.name || ''),
          parentId: String(item.parentId || ''),
          isDir: item.isDir === true,
          row: item.row || null,
          resolved: true,
        })),
        resolved: (previewItems || []).map((item) => ({
          ...item,
          fileId: String(item.fileId || ''),
          dirId: String(item.dirId || item.fileId || ''),
          dirIdCandidates: normalizeIdCandidates(item.dirIdCandidates || [item.dirId, item.fileId]),
          name: String(item.name || ''),
          parentId: String(item.parentId || ''),
          isDir: item.isDir === true,
          row: item.row || null,
          resolved: true,
        })),
        unresolved: [],
        source: 'existing',
      };
    }

    const onProgress = typeof options.onProgress === 'function' ? options.onProgress : null;
    const context = getCurrentListContext();
    const candidateSources = [];
    const currentParentId = String(context.parentId || CONFIG.request.manualListBody.parentId || '').trim();
    const verifiedPageSize = Math.max(
      Number(UI.fields.pageSize?.value || 0),
      Number(CONFIG.request.manualListBody.pageSize || 0),
      Number(getCapturedItems().length || 0),
      Number((previewItems || []).length || 0) + 50,
      200
    );

    if (onProgress) {
      onProgress({
        visible: true,
        percent: 8,
        indeterminate: true,
        text: '正在补齐当前勾选项的真实 fileId...',
      });
    }

    try {
      const fetched = currentParentId
        ? (await fetchDirectoryItemsByParentId(currentParentId, {
            pageSize: verifiedPageSize,
            maxPages: Math.max(3, Math.ceil((previewItems.length + 60) / Math.max(1, verifiedPageSize)) + 4),
            delayMs: 0,
          })).items
        : await fetchCurrentList({ pageSize: verifiedPageSize });
      if (fetched.length) {
        candidateSources.push({
          source: currentParentId ? 'api-paged' : 'api',
          items: fetched,
        });
      }
    } catch (err) {
      warn('移动前补齐真实 fileId 时,接口拉取列表失败:', err);
    }

    const captured = getCapturedItems();
    if (captured.length) {
      candidateSources.push({
        source: 'captured',
        items: captured,
      });
    }

    const snapshotItems = buildCurrentDirectoryItemsSnapshot(context.parentId);
    if (snapshotItems.length) {
      candidateSources.push({
        source: 'snapshot',
        items: snapshotItems,
      });
    }

    for (const entry of candidateSources) {
      const mapping = resolveCheckedMoveItemsByName(previewItems, entry.items);
      if (!mapping.unresolved.length) {
        return {
          mergedItems: mapping.merged,
          resolved: mapping.resolved,
          unresolved: [],
          source: entry.source,
        };
      }
    }

    const best = candidateSources.length
      ? resolveCheckedMoveItemsByName(previewItems, candidateSources[0].items)
      : {
          merged: (previewItems || []).map((item) => ({
            ...item,
            fileId: String(item.fileId || ''),
            dirId: String(item.dirId || item.fileId || ''),
            dirIdCandidates: normalizeIdCandidates(item.dirIdCandidates || [item.dirId, item.fileId]),
            name: String(item.name || ''),
            parentId: String(item.parentId || ''),
            isDir: item.isDir === true,
            row: item.row || null,
            resolved: false,
          })),
          resolved: [],
          unresolved: domItems.map((item) => ({
            ...item,
            fileId: String(item.fileId || ''),
            dirId: String(item.dirId || item.fileId || ''),
            dirIdCandidates: normalizeIdCandidates(item.dirIdCandidates || [item.dirId, item.fileId]),
            name: String(item.name || ''),
            parentId: String(item.parentId || ''),
            isDir: item.isDir === true,
            row: item.row || null,
            resolved: false,
          })),
        };

    return {
      mergedItems: best.merged,
      resolved: best.resolved,
      unresolved: best.unresolved,
      source: candidateSources[0]?.source || 'none',
    };
  }

  async function collectResolvedCheckedMoveItems(options = {}) {
    const selection = await collectCheckedPageSelectionPreviewItems({
      onlyDirectories: Boolean(options.onlyDirectories),
      taskControl: options.taskControl || null,
      disableFullSelectionFallback: Boolean(options.disableFullSelectionFallback),
      avoidSlowScrollScan: Boolean(options.avoidSlowScrollScan),
      allowExactCapturedSelectionFallback: Boolean(options.allowExactCapturedSelectionFallback),
      ignorePageSelectedCount: Boolean(options.ignorePageSelectedCount),
    });
    const previewItems = selection.items || [];
    const updateMovePreview = options.updateMovePreview !== false;
    if (updateMovePreview) {
      setMoveSelectionPreview(previewItems, selection.meta);
    }

    if (!previewItems.length) {
      throw new Error(options.onlyDirectories ? '当前页面没有勾选任何文件夹。' : '当前页面没有勾选任何文件或文件夹。');
    }

    if (selection.meta?.partial) {
      if (!options.allowPartialVisible) {
        throw new Error(
          selection.meta.warning
          || `页面显示已选 ${selection.meta?.expectedCount || 0} 项,但当前只能识别到 ${selection.meta?.visibleCount || previewItems.length} 项。`
        );
      }
      selection.meta = {
        ...selection.meta,
        warning: `页面显示已选 ${selection.meta?.expectedCount || 0} 项,但当前只确认识别到 ${selection.meta?.visibleCount || previewItems.length} 项;本次${options.partialUsageLabel || '操作'}只处理这些已确认勾选项,不会按全目录补齐。`,
      };
    }

    const ensured = await ensureCheckedMoveItemsHaveRealIds(previewItems, {
      onProgress: options.onProgress,
    });
    if (updateMovePreview) {
      setMoveSelectionPreview(ensured.mergedItems, selection.meta);
    }

    if (ensured.unresolved.length) {
      const sample = ensured.unresolved.slice(0, 6).map((item) => item.name).filter(Boolean).join('、');
      throw new Error(
        `当前有 ${ensured.unresolved.length} 个勾选项没拿到真实 fileId。请先等待当前目录列表加载完整,或刷新页面后再试。${sample ? ` 未识别示例:${sample}` : ''}`
      );
    }

    if (options.includeMeta) {
      return {
        items: ensured.resolved,
        meta: {
          ...(selection.meta || {}),
          source: ensured.source || selection.meta?.source || 'visible',
          resolvedCount: ensured.resolved.length,
          unresolvedCount: ensured.unresolved.length,
        },
      };
    }

    return ensured.resolved;
  }

  async function moveFilesInBatches(fileIds, parentId, options = {}) {
    const onProgress = typeof options.onProgress === 'function' ? options.onProgress : null;
    const taskControl = options.taskControl || null;
    const batchSize = Math.max(1, Number(options.batchSize || CONFIG.move.batchSize || 100));
    const label = String(options.label || '移动项目');
    const allowSplitRetry = options.allowSplitRetry !== false;
    const verifySourceItems = Array.isArray(options.verifySourceItems) ? options.verifySourceItems.filter(Boolean) : [];
    const uniqueIds = Array.from(new Set((fileIds || []).map((id) => String(id || '').trim()).filter(Boolean)));
    const batches = chunkArray(uniqueIds, batchSize);
    const summary = {
      ok: 0,
      fail: 0,
      submittedBatches: 0,
      taskIds: [],
      movedFileIds: [],
      firstError: '',
    };

    for (let index = 0; index < batches.length; index += 1) {
      await waitForTaskControl(taskControl);
      const batch = batches[index];
      const batchSourceItems = verifySourceItems.filter((item) => batch.includes(String(item?.fileId || '')));
      if (onProgress) {
        onProgress({
          visible: true,
          percent: Math.round((index / Math.max(1, batches.length)) * 100),
          indeterminate: true,
          text: `${label}:正在提交第 ${index + 1}/${batches.length} 批,共 ${batch.length} 项`,
        });
      }

      try {
        const moveRes = await moveFiles(batch, parentId);
        if (!moveRes.ok || !isProbablySuccess(moveRes.payload, moveRes)) {
          throw new Error(getErrorText(moveRes.payload || moveRes.text || `HTTP ${moveRes.status}`));
        }

        summary.submittedBatches += 1;
        const taskId = extractTaskId(moveRes.payload);
        if (!taskId) {
          summary.ok += batch.length;
          summary.movedFileIds.push(...batch);
          continue;
        }

        summary.taskIds.push(taskId);
        if (onProgress) {
          onProgress({
            visible: true,
            percent: Math.max(10, Math.round((index / Math.max(1, batches.length)) * 100)),
            indeterminate: true,
            text: `${label}:第 ${index + 1}/${batches.length} 批已提交,taskId: ${taskId}`,
          });
        }

        const task = await waitTaskUntilDone(taskId, {
          onProgress,
          taskControl,
          expectedTotal: batch.length,
          maxTries: batchSourceItems.length ? Math.min(Math.max(CONFIG.batch.taskPollMaxTries || 12, 12), 24) : Math.max(CONFIG.batch.taskPollMaxTries || 180, 180),
          intervalMs: Math.max(CONFIG.batch.taskPollMs || 1500, 1500),
        });
        if (batchSourceItems.length && (!task.ok || !hasUsefulTaskState(task.result?.payload, batch.length))) {
          const verification = await verifyMovedItemsByList(batchSourceItems, {
            onProgress,
            taskControl,
            maxRounds: 6,
            intervalMs: Math.max(CONFIG.batch.taskPollMs || 1500, 1500),
          });
          if (verification.movedItems.length) {
            summary.ok += verification.movedItems.length;
            summary.fail += verification.remaining.length;
            summary.movedFileIds.push(...verification.movedItems.map((item) => String(item.fileId || '')).filter(Boolean));
            if (verification.ok) {
              continue;
            }
          }
          if (!verification.movedItems.length && allowSplitRetry && batch.length > 1) {
            const nextBatchSize = batch.length > 20 ? 20 : 1;
            if (nextBatchSize < batch.length) {
              if (onProgress) {
                onProgress({
                  visible: true,
                  percent: Math.max(15, Math.round((index / Math.max(1, batches.length)) * 100)),
                  indeterminate: true,
                  text: `${label}:当前整批未实际移走,正在按更小批次重试(${batch.length} -> ${nextBatchSize})`,
                });
              }
              const retried = await moveFilesInBatches(batch, parentId, {
                onProgress,
                taskControl,
                label: `${label}小批重试`,
                batchSize: nextBatchSize,
                verifySourceItems: batchSourceItems,
                allowSplitRetry: nextBatchSize > 1,
              });
              summary.ok += retried.ok;
              summary.fail += retried.fail;
              summary.submittedBatches += retried.submittedBatches;
              summary.taskIds.push(...(retried.taskIds || []));
              summary.movedFileIds.push(...(retried.movedFileIds || []));
              if (!summary.firstError && retried.firstError) {
                summary.firstError = retried.firstError;
              }
              continue;
            }
          }
        }

        if (!task.ok) {
          const payload = task.result?.payload || task.result?.text || {};
          throw new Error(
            task.timeout
              ? `${label}任务超时,taskId: ${taskId}`
              : `${label}任务失败,taskId: ${taskId},${getErrorText(payload) || '未返回更多信息'}`
          );
        }

        const taskCounts = extractTaskCounts(task.result?.payload, batch.length);
        const okCount = taskCounts.hasSuccessCount ? taskCounts.success : batch.length;
        const failCount = taskCounts.hasFailedCount ? taskCounts.failed : 0;
        summary.ok += okCount;
        summary.fail += failCount;
        if (failCount === 0) {
          summary.movedFileIds.push(...batch);
        }
      } catch (err) {
        summary.fail += batch.length;
        summary.firstError = summary.firstError || getErrorText(err);
        warn(`${label}失败:`, err);
        if (CONFIG.batch.stopOnError) {
          break;
        }
      }
    }

    if (onProgress) {
      onProgress({
        visible: true,
        percent: 100,
        indeterminate: false,
        text: `${label}完成:成功 ${summary.ok} 项,失败 ${summary.fail} 项`,
      });
    }

    return summary;
  }

  async function moveCheckedFolderContentsToCurrentDirectory(options = {}) {
    const onProgress = typeof options.onProgress === 'function' ? options.onProgress : null;
    const taskControl = options.taskControl || null;
    const currentParentId = String(getCurrentListContext().parentId || CONFIG.request.manualListBody.parentId || '').trim();
    if (!currentParentId) {
      throw new Error('没有拿到当前目录 parentId。请先打开目标上层目录,或在高级兜底里手填 parentId。');
    }

    const folders = await collectResolvedCheckedMoveItems({
      onlyDirectories: true,
      onProgress,
    });
    const sourceFolders = folders.filter((item) => item.isDir);
    if (!sourceFolders.length) {
      throw new Error('当前页面没有勾选任何可识别的文件夹。');
    }

    const childItems = [];
    const childSeen = new Set();
    let truncatedFolderCount = 0;
    for (let index = 0; index < sourceFolders.length; index += 1) {
      const folder = sourceFolders[index];
      await waitForTaskControl(taskControl);
      if (onProgress) {
        onProgress({
          visible: true,
          percent: Math.min(45, Math.round(((index + 1) / Math.max(1, sourceFolders.length)) * 45)),
          indeterminate: true,
          text: `正在读取文件夹内容 ${index + 1}/${sourceFolders.length}:${shortDisplayName(folder.name, 36)}`,
        });
      }

      const listing = await fetchDirectoryItems(folder.fileId, {
        idCandidates: normalizeIdCandidates([folder.fileId, folder.dirId, ...(folder.dirIdCandidates || [])]),
        taskControl,
      });
      if (listing.truncated) {
        truncatedFolderCount += 1;
      }

      for (const child of listing.items || []) {
        const key = String(child?.fileId || '').trim();
        if (!key || childSeen.has(key)) {
          continue;
        }
        childSeen.add(key);
        childItems.push({
          ...child,
          parentId: currentParentId,
        });
      }
    }

    if (!childItems.length) {
      throw new Error('勾选的文件夹里没有可移动的内容。空文件夹不会自动删除。');
    }

    if (CONFIG.batch.confirmBeforeRun && !window.confirm(`准备拆开 ${sourceFolders.length} 个已勾选文件夹,把里面的 ${childItems.length} 项直接内容移动到当前目录。这个操作不会保留外层文件夹,是否继续?`)) {
      return { ok: 0, fail: 0, movedItems: [], folders: sourceFolders, truncatedFolderCount };
    }

    const result = await moveFilesInBatches(
      childItems.map((item) => item.fileId),
      currentParentId,
      {
        onProgress,
        taskControl,
        label: '文件夹内容提到上一层',
      }
    );

    if (result.fail === 0 && childItems.length) {
      mergeCapturedItems(currentParentId, childItems, { countAsBatch: false });
    }

    return {
      ...result,
      movedItems: childItems,
      folders: sourceFolders,
      truncatedFolderCount,
    };
  }

  async function moveCheckedItemsUpOneLevel(options = {}) {
    const targetParentId = String(options.parentId || getCurrentDirectoryParentIdFromHash(location.hash)).trim();
    if (!targetParentId) {
      throw new Error('没识别出“当前目录的上一层 parentId”。请改用“勾选项移到目标目录”,手动填目标目录 parentId。');
    }

    return moveCheckedItemsToTargetDirectory({
      ...options,
      parentId: targetParentId,
    });
  }

  async function moveCheckedItemsToTargetDirectory(options = {}) {
    const onProgress = typeof options.onProgress === 'function' ? options.onProgress : null;
    const taskControl = options.taskControl || null;
    const currentParentId = String(getCurrentListContext().parentId || CONFIG.request.manualListBody.parentId || '').trim();
    const targetParentId = String(options.parentId != null ? options.parentId : (UI.fields.moveTargetParentId?.value || CONFIG.move.targetParentId || '')).trim();

    if (!targetParentId) {
      throw new Error('请先填写目标目录 parentId。');
    }
    if (currentParentId && targetParentId === currentParentId) {
      throw new Error('目标目录就是当前目录,当前勾选项已经在这里了。');
    }

    const checkedItems = await collectResolvedCheckedMoveItems({
      onlyDirectories: false,
      onProgress,
    });
    if (!checkedItems.length) {
      throw new Error('当前页面没有勾选任何可移动的文件或文件夹。');
    }

    if (CONFIG.batch.confirmBeforeRun && !window.confirm(`准备把当前页面已勾选的 ${checkedItems.length} 项移动到目录 ${targetParentId},是否继续?`)) {
      return { ok: 0, fail: 0, movedItems: checkedItems, targetParentId };
    }

    const result = await moveFilesInBatches(
      checkedItems.map((item) => item.fileId),
      targetParentId,
      {
        onProgress,
        taskControl,
        label: '移动勾选项到目标目录',
        verifySourceItems: checkedItems,
      }
    );

    if ((result.movedFileIds || []).length) {
      removeCapturedItemsByIds(result.movedFileIds);
    } else if (result.fail === 0 && checkedItems.length) {
      removeCapturedItemsByIds(checkedItems.map((item) => item.fileId));
    }

    return {
      ...result,
      movedItems: checkedItems,
      targetParentId,
    };
  }

  const MEDIA_TMDB_HOST = 'api.themoviedb.org';
  const MEDIA_GENRE = Object.freeze({
    animation: 16,
    documentary: 99,
    reality: 10764,
    talk: 10767,
    news: 10763,
  });
  const MEDIA_REGION_GROUPS = Object.freeze({
    cn: new Set(['CN', 'HK', 'TW', 'MO']),
    jpkr: new Set(['JP', 'KR', 'KP']),
    western: new Set(['US', 'GB', 'UK', 'FR', 'DE', 'ES', 'IT', 'NL', 'PT', 'RU', 'CA', 'AU', 'IE', 'SE', 'NO', 'DK', 'FI']),
  });
  const MEDIA_REGION_LABELS = Object.freeze({
    CN: '中国大陆',
    HK: '香港',
    TW: '台湾',
    MO: '澳门',
    JP: '日本',
    KR: '韩国',
    KP: '韩国',
    US: '美国',
    GB: '英国',
    UK: '英国',
    FR: '法国',
    DE: '德国',
    ES: '西班牙',
    IT: '意大利',
    NL: '荷兰',
    PT: '葡萄牙',
    RU: '俄罗斯',
    CA: '加拿大',
    AU: '澳大利亚',
    IE: '爱尔兰',
    SE: '瑞典',
    NO: '挪威',
    DK: '丹麦',
    FI: '芬兰',
    IN: '印度',
    TH: '泰国',
    SG: '新加坡',
    MY: '马来西亚',
    PH: '菲律宾',
    ID: '印度尼西亚',
    VN: '越南',
    MX: '墨西哥',
    BR: '巴西',
  });
  const MEDIA_LANGUAGE_REGION_LABELS = Object.freeze({
    zh: '华语地区',
    ja: '日本',
    ko: '韩国',
    en: '欧美地区',
    fr: '法国',
    de: '德国',
    es: '西班牙',
    it: '意大利',
    pt: '葡萄牙',
    ru: '俄罗斯',
    hi: '印度',
    th: '泰国',
    vi: '越南',
    id: '印度尼西亚',
    ms: '马来西亚',
  });
  const MEDIA_REGION_CODE_PRIORITY = Object.freeze([
    'CN',
    'HK',
    'TW',
    'MO',
    'JP',
    'KR',
    'KP',
    'US',
    'GB',
    'UK',
    'FR',
    'DE',
    'ES',
    'IT',
    'RU',
    'CA',
    'AU',
    'IN',
    'TH',
    'SG',
    'MY',
    'PH',
    'ID',
    'VN',
    'MX',
    'BR',
  ]);

  function normalizeMediaText(text) {
    return String(text || '')
      .replace(/\u3000/g, ' ')
      .replace(/[._]+/g, ' ')
      .replace(/\s+/g, ' ')
      .trim();
  }

  function parseChineseNumber(value) {
    const text = String(value || '').trim();
    if (!text) {
      return null;
    }
    if (/^\d+$/.test(text)) {
      return Number(text);
    }
    const map = {
      零: 0,
      一: 1,
      二: 2,
      两: 2,
      三: 3,
      四: 4,
      五: 5,
      六: 6,
      七: 7,
      八: 8,
      九: 9,
      十: 10,
    };
    if (!/[一二两三四五六七八九十]/u.test(text)) {
      return null;
    }
    if (text === '十') {
      return 10;
    }
    const parts = text.split('十');
    if (parts.length === 2) {
      const tens = parts[0] ? map[parts[0]] || 0 : 1;
      const ones = parts[1] ? map[parts[1]] || 0 : 0;
      return tens * 10 + ones;
    }
    return map[text] ?? null;
  }

  function normalizeMediaSeasonNumber(value) {
    if (value == null || value === '') {
      return null;
    }
    const parsed = /^\d+$/u.test(String(value)) ? Number(value) : parseChineseNumber(value);
    return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
  }

  function parseMediaSeasonRange(text) {
    const value = normalizeMediaText(text);
    const patterns = [
      /\bS(\d{1,2})\s*(?:-|–|—|~|至|到)\s*S?(\d{1,2})\b/iu,
      /\bSeason\s*(\d{1,2})\s*(?:-|–|—|~|至|到)\s*(?:Season\s*)?(\d{1,2})\b/iu,
      /第\s*([一二两三四五六七八九十\d]+)\s*(?:-|–|—|~|至|到)\s*([一二两三四五六七八九十\d]+)\s*季/iu,
    ];
    for (const pattern of patterns) {
      const matched = value.match(pattern);
      if (!matched) {
        continue;
      }
      const start = normalizeMediaSeasonNumber(matched[1]);
      const end = normalizeMediaSeasonNumber(matched[2]);
      if (!start || !end) {
        continue;
      }
      const min = Math.min(start, end);
      const max = Math.max(start, end);
      if (max > min) {
        return { start: min, end: max };
      }
    }
    return null;
  }

  function stripMediaNoiseFromTitle(name) {
    let work = normalizeMediaText(name);
    work = work.replace(/\{tmdbid-\d+\}/i, ' ');
    work = work.replace(/^\s*\d{1,4}\s*[.\-、_]\s*(?=[\u4e00-\u9fa5A-Za-z])/u, '');
    work = work.replace(/[【\[][^【\]\[]*(?:字幕组|公众号|更多|资源|发布|www\.|com|\.cn|网盘|公众号|微信|微博)[^【\]\[]*[】\]]/giu, ' ');
    work = work.replace(/[【\[]([^【\]\[]+)[】\]]/gu, ' $1 ');
    work = work.replace(/\b(?:S\d{1,2}E\d{1,3}|S\d{1,2}|E\d{1,3}|EP\d{1,3}|Episode\s*\d{1,3}|Season\s*\d{1,2})\b.*$/iu, ' ');
    work = work.replace(/(?:第\s*[一二两三四五六七八九十\d]+\s*[季集期话]|全\s*\d+\s*集).*$/u, ' ');
    work = work.replace(/\b(?:2160p|1080p|1080i|720p|4k|8k|uhd|blu-?ray|bdrip|web-?dl|webrip|web|hdtv|remux|x264|x265|h264|h265|hevc|avc|aac|ac3|dts|truehd|atmos|hdr10?|dv|dovi|10bit|nf|netflix|amzn|amazon|disney\+|hulu|apple ?tv)\b.*$/iu, ' ');
    work = work.replace(/(?:国语|粤语|中字|简繁|内封|外挂|双语|多音轨|高码|无删减|未删减|导演剪辑版|完整版|合集|剧集|电影|番剧|动漫).*$/u, ' ');
    work = work.replace(/[《》"'“”‘’()[\]{}()]+/g, ' ');
    work = work.replace(/[-+~]+$/g, ' ');
    return normalizeMediaText(work);
  }

  function extractMediaNameInfo(rawName) {
    const original = String(rawName || '').trim();
    const base = getBaseName(original) || original;
    const clean = normalizeMediaText(base);
    const seasonRange = parseMediaSeasonRange(clean);
    const seasonMatch = clean.match(/(?:\bS(\d{1,2})(?:E\d{1,3})?\b|第\s*([一二两三四五六七八九十\d]+)\s*季|Season\s*(\d{1,2}))/iu);
    const episodeMatch = clean.match(/(?:\bS\d{1,2}E(\d{1,3})\b|\bE(?:P)?\s*(\d{1,3})\b|第\s*([一二两三四五六七八九十\d]+)\s*[集期话])/iu);
    const yearMatches = Array.from(clean.matchAll(/(?:^|[^\d])(19\d{2}|20[0-3]\d)(?=$|[^\d])/g)).map((m) => m[1]);
    const year = yearMatches.length ? yearMatches[yearMatches.length - 1] : '';
    const season = seasonRange
      ? null
      : seasonMatch
      ? Number(seasonMatch[1] || seasonMatch[3] || parseChineseNumber(seasonMatch[2]) || 0) || null
      : null;
    const episode = episodeMatch
      ? Number(episodeMatch[1] || episodeMatch[2] || parseChineseNumber(episodeMatch[3]) || 0) || null
      : null;
    const explicitTv = Boolean(seasonRange || season || episode || /(?:全\s*\d+\s*集|剧集|电视剧|番剧|动漫|综艺|第\s*[一二两三四五六七八九十\d]+\s*[季集期])/iu.test(clean));
    const explicitMovie = /(?:电影|movie|film)/iu.test(clean);
    let title = stripMediaNoiseFromTitle(clean);
    if (year) {
      title = normalizeMediaText(title.replace(new RegExp(`(^|\\D)${year}(?=\\D|$)`, 'g'), '$1'));
    }
    if (!title && clean) {
      title = stripMediaNoiseFromTitle(clean.replace(/\.[a-z0-9]{1,12}$/i, ''));
    }
    return {
      rawName: original,
      title,
      year,
      season,
      seasonRange,
      episode,
      explicitType: explicitTv ? 'tv' : (explicitMovie ? 'movie' : ''),
      isWeakFileName: isWeakMediaFileName(original),
    };
  }

  function isWeakMediaFileName(name) {
    const base = normalizeMediaText(getBaseName(name) || name);
    if (!base) {
      return true;
    }
    if (/^\d{1,4}$/u.test(base)) {
      return true;
    }
    if (/^(?:s\d{1,2}e\d{1,3}|s\d{1,2}|e\d{1,3}|ep\d{1,3})$/iu.test(base)) {
      return true;
    }
    if (/^(?:e|ep|episode|第)?\s*\d{1,4}\s*(?:集|期|话)?$/iu.test(base)) {
      return true;
    }
    if (/^(?:正片|影片|视频|movie|video|main|feature)$/iu.test(base)) {
      return true;
    }
    return false;
  }

  function isSeasonOnlyMediaName(name) {
    const base = normalizeMediaText(getBaseName(name) || name);
    return /^(?:s\d{1,2}|season\s*\d{1,2}|第\s*[一二两三四五六七八九十\d]+\s*季)$/iu.test(base);
  }

  function isSeasonFolderItem(item) {
    return Boolean(item && shouldTreatItemAsDirectory(item) && isSeasonOnlyMediaName(item.name || ''));
  }

  function isSeasonOnlyMediaInfo(info) {
    return Boolean(info?.season && !String(info?.title || '').trim() && (isWeakMediaFileName(info.rawName || '') || isSeasonOnlyMediaName(info.rawName || '')));
  }

  function getCurrentMediaContextInfo() {
    const currentDirName = getCurrentDirectoryDisplayName();
    const currentDirInfo = extractMediaNameInfo(currentDirName);
    if (currentDirInfo.title && !/^(?:当前目录|\(当前目录\)|首页|全部文件|文件)$/u.test(currentDirName)) {
      return currentDirInfo;
    }

    const selectors = [
      '[aria-label*="breadcrumb" i] a',
      '[aria-label*="breadcrumb" i] button',
      '[aria-label*="breadcrumb" i] [aria-current="page"]',
      '[class*="breadcrumb"] a',
      '[class*="breadcrumb"] button',
      '[class*="breadcrumb"] [aria-current="page"]',
      '[class*="crumb"] a',
      '[class*="crumb"] button',
      '[class*="crumb"] [aria-current="page"]',
      '[class*="path"] a',
      '[class*="path"] button',
      '[class*="path"] [aria-current="page"]',
      'nav a',
      'nav button',
      'nav [aria-current="page"]',
    ];
    const names = Array.from(document.querySelectorAll(selectors.join(',')))
      .filter((node) => !isHelperPanelNode(node) && isVisibleElement(node))
      .map((node) => cleanDirectoryTitleCandidate(getVisibleNodeText(node)))
      .filter((name, index, list) => {
        if (!isProbablyUsefulName(name) || /^(?:文件|首页|全部文件|\.{3}|…|\(当前目录\))$/u.test(name)) {
          return false;
        }
        return list.indexOf(name) === index;
      });
    for (let index = names.length - 1; index >= 0; index -= 1) {
      const info = extractMediaNameInfo(names[index]);
      if (info.title) {
        return info;
      }
    }
    return currentDirInfo;
  }

  function mergeSeasonFolderInfoWithContext(seasonInfo, contextInfo) {
    if (!isSeasonOnlyMediaInfo(seasonInfo) || !contextInfo?.title) {
      return seasonInfo;
    }
    return {
      ...contextInfo,
      rawName: seasonInfo.rawName,
      season: seasonInfo.season,
      episode: seasonInfo.episode || null,
      explicitType: seasonInfo.explicitType || contextInfo.explicitType || 'tv',
      isWeakFileName: seasonInfo.isWeakFileName,
    };
  }

  function mergeSeasonNumberIntoMediaInfo(baseInfo, seasonInfo) {
    if (!seasonInfo?.season || !baseInfo?.title) {
      return baseInfo || seasonInfo;
    }
    return {
      ...baseInfo,
      rawName: seasonInfo.rawName,
      season: seasonInfo.season,
      seasonRange: null,
      episode: seasonInfo.episode || null,
      explicitType: baseInfo.explicitType || seasonInfo.explicitType || 'tv',
      isWeakFileName: seasonInfo.isWeakFileName,
    };
  }

  function getMediaTitleComparableName(name) {
    return normalizeMediaText(String(name || '')
      .replace(/\s*\((?:19\d{2}|20[0-3]\d)\)\s*$/u, ' ')
      .replace(/(?:19\d{2}|20[0-3]\d)/gu, ' ')
      .replace(/[《》"'“”‘’()[\]{}()]+/g, ' '))
      .toLowerCase()
      .replace(/[^a-z0-9\u4e00-\u9fa5]+/giu, '');
  }

  function isLikelySameMediaTitleName(left, right) {
    const a = getMediaTitleComparableName(left);
    const b = getMediaTitleComparableName(right);
    if (!a || !b) {
      return false;
    }
    return a === b || a.includes(b) || b.includes(a);
  }

  function getMediaItemIdentity(item) {
    if (!item) {
      return '';
    }
    return String(item.fileId || normalizeDomName(item.name || '') || '').trim();
  }

  function buildMediaSourceGroups(items) {
    const groups = [];
    const byFolderId = new Map();
    const currentParentId = String(getCurrentListContext().parentId || CONFIG.request.manualListBody.parentId || '').trim();
    const folderFirst = CONFIG.mediaOrganize.useFolderNameFirst !== false;
    const currentDirName = getCurrentDirectoryDisplayName();
    const currentDirInfo = extractMediaNameInfo(currentDirName);
    const mediaContextInfo = getCurrentMediaContextInfo();
    const weakOnlyItems = [];

    for (const rawItem of Array.isArray(items) ? items : []) {
      const item = {
        ...rawItem,
        isDir: rawItem.isDir === true || shouldTreatItemAsDirectory(rawItem),
      };
      const id = getMediaItemIdentity(item);
      if (!id) {
        continue;
      }
      if (folderFirst && item.isDir) {
        const info = mergeSeasonFolderInfoWithContext(extractMediaNameInfo(item.name), mediaContextInfo);
        const moveAsFolder = CONFIG.mediaOrganize.moveBySourceFolder === true;
        const key = `folder:${item.fileId || item.name}`;
        byFolderId.set(String(item.fileId || ''), key);
        groups.push({
          key,
          sourceName: item.name,
          sourceInfo: info,
          sourceItem: item,
          items: moveAsFolder ? [item] : [],
          itemIds: moveAsFolder ? [id] : [],
          moveAsFolder,
          parentId: String(item.parentId || currentParentId || ''),
          needsChildListing: !moveAsFolder,
        });
        continue;
      }

      const parentKey = item.parentId && byFolderId.get(String(item.parentId || ''));
      if (folderFirst && parentKey) {
        const group = groups.find((entry) => entry.key === parentKey);
        if (group) {
          group.items.push(item);
          group.itemIds.push(id);
          continue;
        }
      }

      const info = extractMediaNameInfo(item.name);
      if (folderFirst && !item.isDir && info.isWeakFileName && currentDirInfo.title && !/^(?:当前目录|\(当前目录\)|首页|全部文件)$/u.test(currentDirName)) {
        weakOnlyItems.push(item);
        continue;
      }
      groups.push({
        key: `item:${id}`,
        sourceName: item.name,
        sourceInfo: info,
        sourceItem: item,
        items: [item],
        itemIds: [id],
        moveAsFolder: false,
        parentId: String(item.parentId || currentParentId || ''),
      });
    }

    if (weakOnlyItems.length) {
      groups.unshift({
        key: `current-folder:${currentParentId || currentDirName}`,
        sourceName: currentDirName,
        sourceInfo: currentDirInfo,
        sourceItem: null,
        items: weakOnlyItems,
        itemIds: weakOnlyItems.map((item) => getMediaItemIdentity(item)).filter(Boolean),
        moveAsFolder: false,
        parentId: currentParentId,
      });
    }

    return groups;
  }

  function tmdbGet(url) {
    return new Promise((resolve, reject) => {
      if (typeof GM_xmlhttpRequest !== 'function') {
        reject(new Error('未检测到 GM_xmlhttpRequest 权限,无法直连 TMDB。'));
        return;
      }
      GM_xmlhttpRequest({
        method: 'GET',
        url,
        timeout: 30000,
        onload: (res) => {
          if (res.status < 200 || res.status >= 300) {
            reject(new Error(`TMDB HTTP ${res.status}`));
            return;
          }
          try {
            resolve(JSON.parse(res.responseText || '{}'));
          } catch (err) {
            reject(new Error(`TMDB 返回不是 JSON:${getErrorText(err)}`));
          }
        },
        onerror: (err) => reject(new Error(`TMDB 网络错误:${getErrorText(err) || '未知错误'}`)),
        ontimeout: () => reject(new Error('TMDB 请求超时')),
      });
    });
  }

  function getTmdbCacheKey(kind, params) {
    return `${kind}:${JSON.stringify(params || {})}`;
  }

  async function fetchTmdbJsonCached(kind, url, params) {
    const key = getTmdbCacheKey(kind, params);
    if (STATE.tmdbCache[key]) {
      return STATE.tmdbCache[key];
    }
    const data = await tmdbGet(url);
    STATE.tmdbCache[key] = data;
    return data;
  }

  function getMediaTmdbKey() {
    return String(CONFIG.mediaOrganize.tmdbApiKey || UI.fields.mediaTmdbApiKey?.value || '').trim();
  }

  async function searchTmdbMedia(info, options = {}) {
    const apiKey = getMediaTmdbKey();
    if (!apiKey) {
      return null;
    }
    const title = String(info?.title || '').trim();
    if (!title) {
      return null;
    }

    const language = String(CONFIG.mediaOrganize.tmdbLanguage || 'zh-CN').trim() || 'zh-CN';
    const expectedType = options.expectedType || info.explicitType || '';
    const endpoints = expectedType === 'movie'
      ? ['movie', 'tv', 'multi']
      : expectedType === 'tv'
        ? ['tv', 'movie', 'multi']
        : ['multi', 'tv', 'movie'];
    const seen = new Set();
    let best = null;

    for (const endpoint of endpoints) {
      if (seen.has(endpoint)) {
        continue;
      }
      seen.add(endpoint);
      const url = `https://${MEDIA_TMDB_HOST}/3/search/${endpoint}?api_key=${encodeURIComponent(apiKey)}&language=${encodeURIComponent(language)}&query=${encodeURIComponent(title)}&page=1${info.year ? `&year=${encodeURIComponent(info.year)}&first_air_date_year=${encodeURIComponent(info.year)}` : ''}`;
      const data = await fetchTmdbJsonCached('search', url, { endpoint, title, year: info.year || '', language });
      const candidates = (data?.results || [])
        .filter((item) => item && !['person'].includes(item.media_type))
        .map((item) => normalizeTmdbSearchResult(item, endpoint, info, expectedType))
        .filter(Boolean)
        .sort((a, b) => b.score - a.score);
      if (candidates.length && (!best || candidates[0].score > best.score)) {
        best = candidates[0];
      }
      if (best && best.score >= 175) {
        break;
      }
    }

    if (!best) {
      return null;
    }
    const details = await fetchTmdbDetails(best, apiKey, language);
    return {
      ...best,
      details,
    };
  }

  function normalizeTmdbSearchResult(item, endpoint, info, expectedType) {
    const mediaType = item.media_type || (endpoint === 'movie' || endpoint === 'tv' ? endpoint : '');
    if (!mediaType || !['movie', 'tv'].includes(mediaType)) {
      return null;
    }
    const title = item.title || item.name || item.original_title || item.original_name || '';
    const originalTitle = item.original_title || item.original_name || '';
    const date = item.release_date || item.first_air_date || '';
    const year = date ? String(date).slice(0, 4) : '';
    const normalizedQuery = normalizeMediaText(info.title).toLowerCase();
    const normalizedTitle = normalizeMediaText(title).toLowerCase();
    const normalizedOriginal = normalizeMediaText(originalTitle).toLowerCase();
    let score = 0;
    if (expectedType && mediaType === expectedType) {
      score += 90;
    }
    if (info.year && year === String(info.year)) {
      score += 80;
    } else if (info.year && year && Math.abs(Number(year) - Number(info.year)) <= 1) {
      score += 28;
    }
    if (normalizedTitle === normalizedQuery || normalizedOriginal === normalizedQuery) {
      score += 80;
    } else if (normalizedTitle.includes(normalizedQuery) || normalizedOriginal.includes(normalizedQuery)) {
      score += 36;
    }
    score += Math.min(40, Number(item.popularity || 0));
    score += Math.round(Number(item.vote_count || 0) > 0 ? Math.min(25, Math.log(Number(item.vote_count || 0) + 1) * 4) : 0);
    return {
      id: item.id,
      mediaType,
      title,
      originalTitle,
      year,
      poster: item.poster_path ? `https://image.tmdb.org/t/p/w92${item.poster_path}` : '',
      score,
      raw: item,
    };
  }

  async function fetchTmdbDetails(result, apiKey, language) {
    if (!result?.id || !result?.mediaType) {
      return null;
    }
    const url = `https://${MEDIA_TMDB_HOST}/3/${result.mediaType}/${result.id}?api_key=${encodeURIComponent(apiKey)}&language=${encodeURIComponent(language)}`;
    return fetchTmdbJsonCached('details', url, { type: result.mediaType, id: result.id, language });
  }

  function getTmdbGenreIds(tmdb) {
    const source = tmdb?.details || tmdb?.raw || {};
    const genreIds = [];
    if (Array.isArray(source.genres)) {
      genreIds.push(...source.genres.map((item) => Number(item.id)).filter(Number.isFinite));
    }
    if (Array.isArray(source.genre_ids)) {
      genreIds.push(...source.genre_ids.map(Number).filter(Number.isFinite));
    }
    return Array.from(new Set(genreIds));
  }

  function getTmdbRegions(tmdb) {
    const source = tmdb?.details || tmdb?.raw || {};
    const values = [
      ...(Array.isArray(source.origin_country) ? source.origin_country : []),
      source.production_countries?.[0]?.iso_3166_1,
      ...(Array.isArray(source.production_countries) ? source.production_countries.map((item) => item.iso_3166_1) : []),
    ];
    return Array.from(new Set(values.map((item) => String(item || '').trim().toUpperCase()).filter(Boolean)));
  }

  function getTmdbLanguage(tmdb) {
    const source = tmdb?.details || tmdb?.raw || {};
    return String(source.original_language || '').trim().toLowerCase();
  }

  function classifyMediaByTmdb(tmdb, info) {
    const genreIds = getTmdbGenreIds(tmdb);
    const regions = getTmdbRegions(tmdb);
    const language = getTmdbLanguage(tmdb);
    const mediaType = tmdb?.mediaType || tmdb?.media_type || info?.explicitType || '';
    const regionGroup = getMediaRegionGroup(regions, language);
    const regionLabel = getMediaRegionLabel(regions, language);
    const nameText = `${info?.rawName || ''} ${info?.title || ''}`.toLowerCase();
    let category = '其他';
    let subCategory = '未识别';

    if (mediaType === 'movie') {
      category = '电影';
      if (genreIds.includes(MEDIA_GENRE.animation)) {
        subCategory = '动画电影';
      } else if (regionGroup === 'cn') {
        subCategory = '华语电影';
      } else if (regionGroup === 'jpkr') {
        subCategory = '日韩电影';
      } else if (regionGroup === 'western') {
        subCategory = '欧美电影';
      } else {
        subCategory = '其他电影';
      }
    } else if (mediaType === 'tv') {
      const looksVariety = genreIds.includes(MEDIA_GENRE.reality) || genreIds.includes(MEDIA_GENRE.talk) || /综艺|真人秀|脱口秀|晚会|演唱会|episode|ep\b|第.+期/iu.test(nameText);
      const looksAnime = genreIds.includes(MEDIA_GENRE.animation) || /动漫|动画|番剧|新番|日番|国漫|baha|crunchyroll|\bcr\b/iu.test(nameText);
      if (looksVariety) {
        category = '综艺';
        subCategory = regionGroup === 'cn' ? '内地综艺' : (regionGroup === 'jpkr' ? '日韩综艺' : (regionGroup === 'western' ? '欧美综艺' : '其他综艺'));
      } else if (looksAnime) {
        category = '动漫';
        subCategory = regionGroup === 'cn' ? '国漫' : (regionGroup === 'jpkr' ? '日番' : (regionGroup === 'western' ? '欧美动画' : '其他动漫'));
      } else {
        category = '电视剧';
        subCategory = regionGroup === 'cn' ? '国产剧' : (regionGroup === 'jpkr' ? '日韩剧' : (regionGroup === 'western' ? '欧美剧' : '其他剧'));
      }
    }

    return {
      category,
      subCategory,
      regionGroup,
      regionLabel,
      genreIds,
      regions,
      language,
    };
  }

  function getMediaRegionGroup(regions, language) {
    const set = new Set((regions || []).map((item) => String(item || '').toUpperCase()));
    if ([...set].some((item) => MEDIA_REGION_GROUPS.cn.has(item)) || ['zh', 'cn', 'bo', 'za'].includes(language)) {
      return 'cn';
    }
    if ([...set].some((item) => MEDIA_REGION_GROUPS.jpkr.has(item)) || ['ja', 'ko'].includes(language)) {
      return 'jpkr';
    }
    if ([...set].some((item) => MEDIA_REGION_GROUPS.western.has(item)) || ['en', 'fr', 'de', 'es', 'it', 'nl', 'pt', 'ru'].includes(language)) {
      return 'western';
    }
    return 'other';
  }

  function getMediaLanguageRegionPriority(language) {
    const lang = String(language || '').trim().toLowerCase();
    if (lang === 'zh') return ['CN', 'HK', 'TW', 'MO'];
    if (lang === 'ja') return ['JP'];
    if (lang === 'ko') return ['KR', 'KP'];
    if (lang === 'en') return ['US', 'GB', 'UK', 'CA', 'AU', 'IE'];
    if (lang === 'fr') return ['FR', 'CA'];
    if (lang === 'de') return ['DE'];
    if (lang === 'es') return ['ES', 'MX'];
    if (lang === 'pt') return ['PT', 'BR'];
    if (lang === 'ru') return ['RU'];
    if (lang === 'hi') return ['IN'];
    if (lang === 'th') return ['TH'];
    if (lang === 'vi') return ['VN'];
    if (lang === 'id') return ['ID'];
    if (lang === 'ms') return ['MY', 'SG'];
    return [];
  }

  function getMediaRegionLabel(regions, language) {
    const codes = Array.from(new Set((regions || [])
      .map((item) => String(item || '').trim().toUpperCase())
      .filter(Boolean)));
    if (codes.length) {
      const langPriority = getMediaLanguageRegionPriority(language);
      const preferredCode = [...langPriority, ...MEDIA_REGION_CODE_PRIORITY, ...codes].find((code) => codes.includes(code) && MEDIA_REGION_LABELS[code]);
      if (preferredCode) {
        return MEDIA_REGION_LABELS[preferredCode];
      }
    }
    const lang = String(language || '').trim().toLowerCase();
    return MEDIA_LANGUAGE_REGION_LABELS[lang] || '其他地区';
  }

  function buildLocalMediaText(info) {
    return normalizeMediaText([
      info?.rawName || '',
      info?.title || '',
    ].join(' ')).toLowerCase();
  }

  function localTextHas(text, patterns) {
    return patterns.some((pattern) => pattern.test(text));
  }

  function detectLocalRegionGroup(text) {
    const cnStrong = [
      /(^|[^a-z])(?:cn|chn|china)([^a-z]|$)/iu,
      /华语|国产|国剧|陆剧|大陆|内地|中国|香港|港剧|台湾|台剧|港片|台片|国漫/iu,
    ];
    const jpkrStrong = [
      /(^|[^a-z])(?:jp|jpn|japanese|kr|kor|korean)([^a-z]|$)/iu,
      /日韩|日本|日剧|日影|日番|日漫|韩剧|韩综|韩国|韩影|韩语|日语|baha|bahamut|ani-one|crunchyroll|b-global/iu,
    ];
    const westernStrong = [
      /(^|[^a-z])(?:us|usa|uk|gb|eng|english|fr|fra|deu|ger|esp|ita|can|aus)([^a-z]|$)/iu,
      /欧美|美国|美剧|英剧|英国|法国|法剧|德国|德剧|西班牙|意大利|加拿大|澳洲|澳大利亚|俄罗斯|俄剧/iu,
    ];

    if (localTextHas(text, cnStrong)) {
      return 'cn';
    }
    if (localTextHas(text, jpkrStrong)) {
      return 'jpkr';
    }
    if (localTextHas(text, westernStrong)) {
      return 'western';
    }
    return 'other';
  }

  function detectLocalRegionLabel(text, regionGroup = 'other') {
    const rules = [
      {
        label: '香港',
        patterns: [
          /香港|港剧|港片|港综|粤语|粤配|粵語|粵配/iu,
          /(^|[^a-z])(?:hk|hong\s*kong)([^a-z]|$)/iu,
        ],
      },
      {
        label: '台湾',
        patterns: [
          /台湾|臺灣|台剧|台劇|台片|台综|台綜/iu,
          /(^|[^a-z])(?:tw|taiwan)([^a-z]|$)/iu,
        ],
      },
      {
        label: '澳门',
        patterns: [/澳门|澳門|(^|[^a-z])mo([^a-z]|$)/iu],
      },
      {
        label: '中国大陆',
        patterns: [
          /中国大陆|大陆|内地|內地|国产|国剧|國劇|陆剧|陸劇|国漫|國漫|内地综艺|內地綜藝/iu,
          /(^|[^a-z])(?:cn|chn|china|mainland)([^a-z]|$)/iu,
        ],
      },
      {
        label: '日本',
        patterns: [
          /日本|日剧|日劇|日影|日番|日漫|日综|日綜|日语|日語/iu,
          /(^|[^a-z])(?:jp|jpn|japanese|japan)([^a-z]|$)/iu,
        ],
      },
      {
        label: '韩国',
        patterns: [
          /韩国|韓國|韩剧|韓劇|韩影|韓影|韩综|韓綜|韩语|韓語/iu,
          /(^|[^a-z])(?:kr|kor|korean|korea)([^a-z]|$)/iu,
        ],
      },
      {
        label: '美国',
        patterns: [
          /美国|美國|美剧|美劇|美影|好莱坞|好萊塢/iu,
          /(^|[^a-z])(?:us|usa|american|america)([^a-z]|$)/iu,
        ],
      },
      {
        label: '英国',
        patterns: [
          /英国|英國|英剧|英劇|英影|英综|英綜/iu,
          /(^|[^a-z])(?:uk|gb|british|england)([^a-z]|$)/iu,
        ],
      },
      {
        label: '法国',
        patterns: [/法国|法國|法剧|法劇|法影|(^|[^a-z])(?:fr|fra|french|france)([^a-z]|$)/iu],
      },
      {
        label: '德国',
        patterns: [/德国|德國|德剧|德劇|德影|(^|[^a-z])(?:de|deu|ger|german|germany)([^a-z]|$)/iu],
      },
      {
        label: '西班牙',
        patterns: [/西班牙|西剧|西劇|西影|(^|[^a-z])(?:es|esp|spanish|spain)([^a-z]|$)/iu],
      },
      {
        label: '意大利',
        patterns: [/意大利|意剧|意劇|意影|(^|[^a-z])(?:it|ita|italian|italy)([^a-z]|$)/iu],
      },
      {
        label: '俄罗斯',
        patterns: [/俄罗斯|俄羅斯|俄剧|俄劇|俄影|(^|[^a-z])(?:ru|rus|russian|russia)([^a-z]|$)/iu],
      },
      {
        label: '加拿大',
        patterns: [/加拿大|加剧|加劇|(^|[^a-z])(?:ca|can|canadian|canada)([^a-z]|$)/iu],
      },
      {
        label: '澳大利亚',
        patterns: [/澳大利亚|澳大利亞|澳洲|澳剧|澳劇|(^|[^a-z])(?:au|aus|australian|australia)([^a-z]|$)/iu],
      },
      {
        label: '印度',
        patterns: [/印度|宝莱坞|寶萊塢|(^|[^a-z])(?:in|ind|hindi|india|indian)([^a-z]|$)/iu],
      },
      {
        label: '泰国',
        patterns: [/泰国|泰國|泰剧|泰劇|泰影|泰语|泰語|(^|[^a-z])(?:th|tha|thai|thailand)([^a-z]|$)/iu],
      },
      {
        label: '新加坡',
        patterns: [/新加坡|星剧|星劇|(^|[^a-z])(?:sg|singapore)([^a-z]|$)/iu],
      },
      {
        label: '马来西亚',
        patterns: [/马来西亚|馬來西亞|大马|大馬|(^|[^a-z])(?:my|malaysia|malay)([^a-z]|$)/iu],
      },
      {
        label: '越南',
        patterns: [/越南|(^|[^a-z])(?:vn|vietnam|vietnamese)([^a-z]|$)/iu],
      },
      {
        label: '菲律宾',
        patterns: [/菲律宾|菲律賓|(^|[^a-z])(?:ph|philippines|filipino)([^a-z]|$)/iu],
      },
      {
        label: '印度尼西亚',
        patterns: [/印度尼西亚|印尼|印尼剧|印尼劇|(^|[^a-z])(?:id|indonesia|indonesian)([^a-z]|$)/iu],
      },
      {
        label: '墨西哥',
        patterns: [/墨西哥|墨剧|墨劇|(^|[^a-z])(?:mx|mexico|mexican)([^a-z]|$)/iu],
      },
      {
        label: '巴西',
        patterns: [/巴西|巴剧|巴劇|(^|[^a-z])(?:br|brazil|brazilian)([^a-z]|$)/iu],
      },
      {
        label: '华语地区',
        patterns: [/华语|華語/iu],
      },
      {
        label: '日韩地区',
        patterns: [/日韩|日韓|东亚|東亞/iu],
      },
      {
        label: '欧美地区',
        patterns: [/欧美|歐美|西方|western/iu],
      },
    ];

    const matched = rules.find((rule) => localTextHas(text, rule.patterns));
    if (matched) {
      return matched.label;
    }
    if (regionGroup === 'cn') return '华语地区';
    if (regionGroup === 'jpkr') return '日韩地区';
    if (regionGroup === 'western') return '欧美地区';
    return '其他地区';
  }

  function getLocalSubCategory(category, regionGroup, text, flags = {}) {
    if (category === '电影') {
      if (flags.anime || /动画电影|动画片|剧场版|animation|anime movie/iu.test(text)) {
        return '动画电影';
      }
      if (regionGroup === 'cn') return '华语电影';
      if (regionGroup === 'jpkr') return '日韩电影';
      if (regionGroup === 'western') return '欧美电影';
      return '其他电影';
    }
    if (category === '电视剧') {
      if (regionGroup === 'cn') return '国产剧';
      if (regionGroup === 'jpkr') return '日韩剧';
      if (regionGroup === 'western') return '欧美剧';
      return '其他剧';
    }
    if (category === '综艺') {
      if (regionGroup === 'cn') return '内地综艺';
      if (regionGroup === 'jpkr') return '日韩综艺';
      if (regionGroup === 'western') return '欧美综艺';
      return '其他综艺';
    }
    if (category === '动漫') {
      if (regionGroup === 'cn') return '国漫';
      if (regionGroup === 'western') return '欧美动画';
      if (regionGroup === 'jpkr') return '日番';
      return '其他动漫';
    }
    return '未识别';
  }

  function classifyMediaByLocalRules(info) {
    const text = buildLocalMediaText(info);
    const regionGroup = detectLocalRegionGroup(text);
    const regionLabel = detectLocalRegionLabel(text, regionGroup);
    const hasSeasonEpisode = Boolean(info?.season || info?.episode || /(?:\bs\d{1,2}e\d{1,3}\b|\bs\d{1,2}\b|\be(?:p)?\d{1,3}\b|第\s*[一二两三四五六七八九十\d]+\s*[集季话]|全\s*\d+\s*集)/iu.test(text));
    const looksVariety = /综艺|真人秀|脱口秀|访谈|访谈节目|选秀|竞演|喜剧大赛|歌手|哥哥|姐姐|晚会|春晚|演唱会|音乐会|第\s*[一二两三四五六七八九十\d]+\s*期|\b\d{8}\b|\b20\d{2}[.\-_]\d{1,2}[.\-_]\d{1,2}\b/iu.test(text);
    const looksAnime = /动漫|动画|番剧|新番|日番|国漫|日漫|美漫|ova|oad|baha|bahamut|ani-one|crunchyroll|b-global|\bani\b/iu.test(text);
    const looksMovie = /电影|影片|movie|film|剧场版|theatrical|bluray|bdrip|remux/iu.test(text);
    const looksTv = hasSeasonEpisode || /电视剧|剧集|连续剧|迷你剧|短剧|season|episode|series|drama|web-?dl/iu.test(text);
    const looksDocumentary = /纪录片|documentary|docu|bbc|national geographic|ngc|discovery/iu.test(text);

    let category = '其他';
    let confidence = 18;
    if (looksVariety) {
      category = '综艺';
      confidence = 64;
    } else if (looksAnime && !looksMovie) {
      category = '动漫';
      confidence = 66;
    } else if (looksDocumentary && hasSeasonEpisode) {
      category = '电视剧';
      confidence = 58;
    } else if (looksDocumentary) {
      category = '电影';
      confidence = 50;
    } else if (looksTv || info?.explicitType === 'tv') {
      category = '电视剧';
      confidence = hasSeasonEpisode ? 68 : 52;
    } else if (looksMovie || info?.explicitType === 'movie' || info?.year) {
      category = '电影';
      confidence = info?.year ? 56 : 46;
    } else if (looksAnime) {
      category = '动漫';
      confidence = 52;
    }

    const subCategory = getLocalSubCategory(category, regionGroup, text, { anime: looksAnime });
    if (regionGroup !== 'other') {
      confidence += 10;
    }
    if (info?.title) {
      confidence += 6;
    }
    if (info?.year) {
      confidence += 6;
    }
    return {
      category,
      subCategory,
      regionGroup,
      regionLabel,
      genreIds: [],
      regions: [],
      language: '',
      source: 'local',
      confidence: Math.max(10, Math.min(86, confidence)),
    };
  }

  function shouldIncludeMediaRegionFolder(classification = {}) {
    if (CONFIG.mediaOrganize.includeRegionFolder === false) {
      return false;
    }
    const regionLabel = String(classification.regionLabel || '').trim();
    if (!regionLabel || regionLabel === '其他地区') {
      return false;
    }
    const subCategory = String(classification.subCategory || '').trim();
    const groupedSubCategories = new Set([
      '华语电影',
      '日韩电影',
      '欧美电影',
      '国产剧',
      '日韩剧',
      '欧美剧',
      '内地综艺',
      '日韩综艺',
      '欧美综艺',
      '国漫',
      '日番',
      '欧美动画',
    ]);
    return !groupedSubCategories.has(subCategory);
  }

  function getMediaClassificationDisplay(item = {}) {
    const classification = item.classification || {};
    const parts = [
      classification.category || '其他',
      classification.subCategory || '未识别',
    ];
    if (shouldIncludeMediaRegionFolder(classification)) {
      parts.push(classification.regionLabel || '其他地区');
    }
    return parts.join(' / ');
  }

  function buildMediaTargetPath(classification, tmdb, info) {
    const segments = [
      classification.category || '其他',
      classification.subCategory || '未识别',
    ];
    if (shouldIncludeMediaRegionFolder(classification)) {
      segments.push(classification.regionLabel || '其他地区');
    }
    if (CONFIG.mediaOrganize.includeTitleFolder !== false && tmdb?.title) {
      const yearPart = tmdb.year && tmdb.year !== 'N/A' ? ` (${tmdb.year})` : (info?.year ? ` (${info.year})` : '');
      segments.push(`${tmdb.title}${yearPart}`);
    } else if (CONFIG.mediaOrganize.includeTitleFolder !== false && info?.title) {
      segments.push(`${info.title}${info.year ? ` (${info.year})` : ''}`);
    }
    const season = info?.season || null;
    if (CONFIG.mediaOrganize.includeSeasonFolder !== false && season && ['电视剧', '动漫', '综艺'].includes(classification.category)) {
      segments.push(`Season ${String(season).padStart(2, '0')}`);
    }
    return segments.map((segment) => sanitizeCloudDirName(segment, '未识别')).filter(Boolean);
  }

  async function resolveMediaGroupMoveItems(group, options = {}) {
    if (!group?.needsChildListing || group.moveAsFolder) {
      return group;
    }
    const taskControl = options.taskControl || null;
    const includeSeasonFolder = CONFIG.mediaOrganize.includeSeasonFolder !== false;
    const folderId = String(group.sourceItem?.fileId || group.sourceItem?.dirId || '').trim();
    if (!folderId) {
      return {
        ...group,
        moveAsFolder: true,
        items: group.sourceItem ? [group.sourceItem] : group.items,
        itemIds: group.sourceItem?.fileId ? [String(group.sourceItem.fileId)] : group.itemIds,
        needsChildListing: false,
      };
    }
    const listing = await fetchDirectoryItems(folderId, {
      idCandidates: normalizeIdCandidates([folderId, group.sourceItem?.dirId, ...(group.sourceItem?.dirIdCandidates || [])]),
      taskControl,
      pageSize: Math.max(100, Number(CONFIG.request.manualListBody.pageSize || 100)),
      maxPages: 50,
    });
    const childItems = (listing.items || []).filter((item) => item && getMediaItemIdentity(item));
    const childDirs = childItems.filter((item) => shouldTreatItemAsDirectory(item));
    const childFiles = childItems.filter((item) => !shouldTreatItemAsDirectory(item));
    const singleWrapperDir = childDirs.length === 1 && childFiles.length === 0 ? childDirs[0] : null;
    if (
      singleWrapperDir
      && !isSeasonFolderItem(singleWrapperDir)
      && group.sourceInfo?.title
      && isLikelySameMediaTitleName(singleWrapperDir.name || '', group.sourceInfo.title)
    ) {
      const wrapperId = String(singleWrapperDir.fileId || singleWrapperDir.dirId || '').trim();
      if (wrapperId) {
        const wrapperListing = await fetchDirectoryItems(wrapperId, {
          idCandidates: normalizeIdCandidates([wrapperId, singleWrapperDir.dirId, ...(singleWrapperDir.dirIdCandidates || [])]),
          taskControl,
          pageSize: Math.max(100, Number(CONFIG.request.manualListBody.pageSize || 100)),
          maxPages: 50,
        });
        const wrapperItems = (wrapperListing.items || []).filter((item) => item && getMediaItemIdentity(item));
        if (wrapperItems.length) {
          return {
            ...group,
            items: wrapperItems,
            itemIds: wrapperItems.map((item) => getMediaItemIdentity(item)).filter(Boolean),
            moveAsFolder: false,
            needsChildListing: false,
            childListingTruncated: Boolean(listing.truncated || wrapperListing.truncated),
            unwrappedSourceFolders: [singleWrapperDir],
          };
        }
      }
    }
    const seasonFolderItems = includeSeasonFolder
      ? childItems
        .filter((item) => {
          if (!isSeasonFolderItem(item)) {
            return false;
          }
          const info = extractMediaNameInfo(item.name || '');
          return Boolean(info.season);
        })
        .sort((left, right) => (extractMediaNameInfo(left.name || '').season || 0) - (extractMediaNameInfo(right.name || '').season || 0))
      : [];
    if (seasonFolderItems.length > 1) {
      return {
        ...group,
        items: seasonFolderItems,
        itemIds: seasonFolderItems.map((item) => getMediaItemIdentity(item)).filter(Boolean),
        moveAsFolder: true,
        needsChildListing: false,
        seasonFolderGroups: seasonFolderItems.map((item) => {
          const seasonInfo = extractMediaNameInfo(item.name || '');
          return {
            ...group,
            key: `${group.key || 'season-folder'}:${getMediaItemIdentity(item)}`,
            sourceName: `${group.sourceName || ''}/${item.name || ''}`.replace(/^\/+/, ''),
            sourceInfo: mergeSeasonNumberIntoMediaInfo(group.sourceInfo, seasonInfo),
            sourceItem: item,
            items: [],
            itemIds: [],
            moveAsFolder: false,
            parentId: String(item.parentId || folderId || group.parentId || ''),
            needsChildListing: true,
            parentGroupName: group.sourceName || '',
            parentSourceItem: group.sourceItem || null,
          };
        }),
      };
    }
    if (!childItems.length) {
      return {
        ...group,
        moveAsFolder: true,
        items: group.sourceItem ? [group.sourceItem] : group.items,
        itemIds: group.sourceItem?.fileId ? [String(group.sourceItem.fileId)] : group.itemIds,
        needsChildListing: false,
        childListingWarning: '未读取到子项目,已退回为移动整个文件夹',
      };
    }
    return {
      ...group,
      items: childItems,
      itemIds: childItems.map((item) => getMediaItemIdentity(item)).filter(Boolean),
      moveAsFolder: false,
      needsChildListing: false,
      childListingTruncated: Boolean(listing.truncated),
    };
  }

  async function analyzeMediaSourceGroup(group, options = {}) {
    const onProgress = typeof options.onProgress === 'function' ? options.onProgress : null;
    const infoCandidates = [];
    const sourceInfoHasSeasonRange = Boolean(group.sourceInfo?.seasonRange && !group.sourceInfo?.season);
    if (CONFIG.mediaOrganize.useFolderNameFirst !== false && group.sourceInfo?.title) {
      infoCandidates.push(group.sourceInfo);
    }
    for (const item of group.items || []) {
      const info = extractMediaNameInfo(item.name || '');
      if (sourceInfoHasSeasonRange && isSeasonOnlyMediaInfo(info)) {
        continue;
      }
      if (!info.isWeakFileName || !infoCandidates.length) {
        infoCandidates.push(info);
      }
    }
    if (!infoCandidates.length && group.sourceInfo) {
      infoCandidates.push(group.sourceInfo);
    }
    let info = infoCandidates.find((item) => item.title && item.year) || infoCandidates.find((item) => item.title) || group.sourceInfo || extractMediaNameInfo(group.sourceName);
    if (group.sourceInfo?.season && group.sourceInfo.season !== info?.season && (isWeakMediaFileName(group.sourceName || group.sourceInfo.rawName || '') || isSeasonOnlyMediaName(group.sourceName || group.sourceInfo.rawName || ''))) {
      info = {
        ...info,
        season: group.sourceInfo.season,
        seasonRange: null,
        episode: group.sourceInfo.episode || info?.episode || null,
        explicitType: info?.explicitType || group.sourceInfo.explicitType || 'tv',
      };
    }

    let tmdb = null;
    let error = '';
    try {
      if (getMediaTmdbKey() && info?.title) {
        if (onProgress) {
          onProgress({
            visible: true,
            percent: 0,
            indeterminate: true,
            text: `正在识别媒体:${shortDisplayName(info.title || group.sourceName, 34)}${info.year ? ` (${info.year})` : ''}`,
          });
        }
        tmdb = await searchTmdbMedia(info);
      }
    } catch (err) {
      error = getErrorText(err);
      warn('TMDB 识别失败:', err);
    }

    const classification = tmdb ? classifyMediaByTmdb(tmdb, info) : classifyMediaByLocalRules(info);
    const confidence = tmdb
      ? Math.max(55, Math.min(99, Math.round((tmdb.score || 0) / 2)))
      : (classification.confidence || (info.title ? 38 : 12));
    const targetSegments = buildMediaTargetPath(classification, tmdb, info);
    return {
      ...group,
      info,
      tmdb,
      classification,
      confidence,
      targetSegments,
      targetPath: targetSegments.join('/'),
      error,
    };
  }

  async function ensureCloudDirectoryPath(segments, rootParentId, options = {}) {
    let parentId = String(rootParentId || '');
    const cache = options.cache || new Map();
    for (const segment of segments || []) {
      const dirName = sanitizeCloudDirName(segment, '未识别');
      const cacheKey = `${parentId}::${dirName}`;
      if (cache.has(cacheKey) && isLikelyDirectoryId(cache.get(cacheKey))) {
        parentId = String(cache.get(cacheKey) || '');
        continue;
      }
      const existingDirId = await findChildDirectoryIdByName(parentId, dirName, options);
      if (existingDirId) {
        cache.set(cacheKey, existingDirId);
        parentId = existingDirId;
        continue;
      }
      const response = await postJson(
        getCreateDirUrl(),
        {
          dirName,
          parentId,
          failIfNameExist: true,
        },
        getRequestHeaders()
      );
      if (!response.ok || !isProbablySuccess(response.payload, response)) {
        const existingFromPayload = extractCreatedDirId(response.payload);
        if (isLikelyDirectoryId(existingFromPayload)) {
          cache.set(cacheKey, existingFromPayload);
          parentId = existingFromPayload;
          continue;
        }
        const existingAfterCreateError = await findChildDirectoryIdByName(parentId, dirName, options);
        if (existingAfterCreateError) {
          cache.set(cacheKey, existingAfterCreateError);
          parentId = existingAfterCreateError;
          continue;
        }
        if (isDirectoryExistsResponse(response.payload, response)) {
          throw new Error(`目录已存在但未能定位目录 ID:${dirName}。请进入或刷新整理根目录,让脚本捕获目录列表后再试。`);
        }
        throw new Error(`创建目录失败:${dirName} | ${getErrorText(response.payload || response.text || `HTTP ${response.status}`)}`);
      }
      let dirId = extractCreatedDirId(response.payload);
      if (!isLikelyDirectoryId(dirId)) {
        dirId = '';
      }
      for (let retry = 0; !dirId && retry < 4; retry += 1) {
        if (retry > 0) {
          await controlledDelay(350 * retry, options.taskControl || null);
        }
        const confirmedDirId = await findChildDirectoryIdByName(parentId, dirName, options);
        if (isLikelyDirectoryId(confirmedDirId)) {
          dirId = confirmedDirId;
        }
      }
      if (!dirId) {
        throw new Error(`创建目录成功但未确认目录 ID:${dirName}`);
      }
      cache.set(cacheKey, dirId);
      parentId = dirId;
    }
    return parentId;
  }

  function findChildDirectoryIdByNameFromCached(parentId, dirName) {
    const normalizedName = normalizeDomName(dirName);
    if (!normalizedName) {
      return '';
    }
    const candidates = [
      ...getCapturedItemsByParentId(parentId),
      ...(String(parentId || '') === String(getCurrentListContext().parentId || CONFIG.request.manualListBody.parentId || '').trim() ? getCapturedItems() : []),
    ];
    const matched = dedupeItems(candidates).find((item) => {
      if (!item || !shouldTreatItemAsDirectory(item)) {
        return false;
      }
      return normalizeDomName(item.name || '') === normalizedName;
    });
    const cachedId = String(matched?.fileId || matched?.dirId || '').trim();
    return isLikelyDirectoryId(cachedId) ? cachedId : '';
  }

  async function findChildDirectoryIdByName(parentId, dirName, options = {}) {
    const normalizedName = normalizeDomName(dirName);
    if (!normalizedName) {
      return '';
    }
    const cached = findChildDirectoryIdByNameFromCached(parentId, dirName);
    if (cached) {
      return cached;
    }
    try {
      const listing = await fetchDirectoryItemsByParentId(parentId, {
        pageSize: Math.max(100, Number(CONFIG.request.manualListBody.pageSize || 100)),
        maxPages: 10,
        delayMs: 0,
        taskControl: options.taskControl || null,
      });
      const matched = (listing.items || []).find((item) => {
        if (!item || !shouldTreatItemAsDirectory(item)) {
          return false;
        }
        return normalizeDomName(item.name || '') === normalizedName;
      });
      const matchedId = String(matched?.fileId || matched?.dirId || '').trim();
      return isLikelyDirectoryId(matchedId) ? matchedId : '';
    } catch (err) {
      warn('查找已存在目录失败:', err);
      return '';
    }
  }

  async function listMediaDirectoryChildren(parentId, options = {}) {
    if (!parentId) {
      return [];
    }
    const listing = await fetchDirectoryItemsByParentId(parentId, {
      pageSize: Math.max(100, Number(CONFIG.request.manualListBody.pageSize || 100)),
      maxPages: 20,
      delayMs: 0,
      taskControl: options.taskControl || null,
    });
    return listing.items || [];
  }

  async function filterMediaItemsAlreadyInTarget(items, targetParentId, options = {}) {
    const source = (Array.isArray(items) ? items : []).filter((item) => item && getMediaItemIdentity(item));
    if (!source.length || CONFIG.mediaOrganize.skipDuplicateTargets === false) {
      return { moveItems: source, duplicateItems: [] };
    }
    const targetItems = await listMediaDirectoryChildren(targetParentId, options);
    const targetNames = new Set(targetItems.map((item) => normalizeDomName(item?.name || '')).filter(Boolean));
    const duplicateItems = [];
    const moveItems = [];
    for (const item of source) {
      const name = normalizeDomName(item.name || '');
      if (name && targetNames.has(name)) {
        duplicateItems.push(item);
      } else {
        moveItems.push(item);
      }
    }
    return { moveItems, duplicateItems };
  }

  async function deleteMediaSourceFoldersIfEmpty(groups, options = {}) {
    if (CONFIG.mediaOrganize.cleanupEmptySourceFolders === false) {
      return { deleted: 0, skipped: 0, failed: 0, failures: [] };
    }
    const taskControl = options.taskControl || null;
    const onProgress = typeof options.onProgress === 'function' ? options.onProgress : null;
    const candidates = [];
    const seen = new Set();
    for (const group of Array.isArray(groups) ? groups : []) {
      const sourceItem = group?.sourceItem || null;
      const id = String(sourceItem?.fileId || sourceItem?.dirId || '').trim();
      if (!id || seen.has(id) || !shouldTreatItemAsDirectory(sourceItem)) {
        continue;
      }
      seen.add(id);
      candidates.push(sourceItem);
    }
    const result = { deleted: 0, skipped: 0, failed: 0, failures: [] };
    for (let index = 0; index < candidates.length; index += 1) {
      await waitForTaskControl(taskControl);
      const folder = candidates[index];
      const folderId = String(folder.fileId || folder.dirId || '').trim();
      try {
        const children = await listMediaDirectoryChildren(folderId, { taskControl });
        if (children.length) {
          result.skipped += 1;
          continue;
        }
        if (onProgress) {
          onProgress({
            visible: true,
            percent: 100,
            indeterminate: true,
            text: `正在清理空源文件夹 ${index + 1}/${candidates.length}:${shortDisplayName(folder.name, 34)}`,
          });
        }
        const deleteRes = await deleteFiles([folderId]);
        if (!deleteRes.ok || !isProbablySuccess(deleteRes.payload, deleteRes)) {
          throw new Error(getErrorText(deleteRes.payload || deleteRes.text || `HTTP ${deleteRes.status}`));
        }
        const taskId = extractTaskId(deleteRes.payload);
        if (taskId) {
          const task = await waitTaskUntilDone(taskId, {
            onProgress,
            taskControl,
            expectedTotal: 1,
            maxTries: Math.min(Math.max(CONFIG.batch.taskPollMaxTries || 12, 12), 24),
            intervalMs: Math.max(CONFIG.batch.taskPollMs || 1500, 1500),
          });
          if (!task.ok) {
            throw new Error(`删除空源文件夹任务未确认完成,taskId: ${taskId}`);
          }
        }
        result.deleted += 1;
        removeCapturedItemsByIds([folderId]);
      } catch (err) {
        result.failed += 1;
        result.failures.push(`${folder.name || folderId}:${getErrorText(err) || '未知错误'}`);
        warn('清理空源文件夹失败:', err);
      }
    }
    return result;
  }

  function renderMediaOrganizeList() {
    if (!UI.mediaOrganizeList || !UI.mediaOrganizeCount) {
      return;
    }
    const items = Array.isArray(STATE.mediaOrganizePreviewItems) ? STATE.mediaOrganizePreviewItems : [];
    const warning = String(STATE.mediaOrganizeWarning || '').trim();
    UI.mediaOrganizeCount.textContent = items.length ? `待整理 ${items.length} 组` : '待整理 0 组';
    if (!items.length) {
      UI.mediaOrganizeList.innerHTML = `
        ${warning ? `<div class="gyp-import-empty">${escapeHtml(warning)}</div>` : ''}
        <div class="gyp-import-empty">勾选文件夹或文件后点“识别并预览”,这里会显示分类和目标目录。</div>
      `;
      return;
    }
    UI.mediaOrganizeList.innerHTML = `
      ${warning ? `<div class="gyp-import-empty">${escapeHtml(warning)}</div>` : ''}
      ${items.map((item) => `
        <div class="gyp-import-row">
          <div class="gyp-import-name" title="${escapeHtml(item.sourceName || '')}">${escapeHtml(item.sourceName || '')}</div>
          <div class="gyp-import-meta">
            ${escapeHtml(item.tmdb?.title || item.info?.title || '未识别标题')}${item.tmdb?.year || item.info?.year ? ` (${escapeHtml(item.tmdb?.year || item.info?.year)})` : ''}
            | ${escapeHtml(getMediaClassificationDisplay(item))}
            | 置信度 ${escapeHtml(String(item.confidence || 0))}%
          </div>
          <div class="gyp-import-meta"><span class="gyp-import-target">目标:${escapeHtml(item.targetPath || '其他/未识别')}</span> | ${item.moveAsFolder ? '移动整个文件夹' : `移动 ${item.itemIds?.length || 0} 项`}${item.error ? ` | TMDB: ${escapeHtml(item.error)}` : ''}</div>
        </div>
      `).join('')}
    `;
  }

  async function previewMediaOrganizeSelection(options = {}) {
    const onProgress = typeof options.onProgress === 'function' ? options.onProgress : null;
    const taskControl = options.taskControl || null;
    const selection = await collectResolvedCheckedMoveItems({
      onlyDirectories: false,
      onProgress,
      taskControl,
      includeMeta: true,
      allowPartialVisible: true,
      partialUsageLabel: '智能整理预览',
    });
    const checkedItems = selection.items || [];
    if (!checkedItems.length) {
      throw new Error('当前页面没有勾选任何可整理的文件或文件夹。');
    }
    const groups = buildMediaSourceGroups(checkedItems);
    const analyzed = [];
    for (let index = 0; index < groups.length; index += 1) {
      await waitForTaskControl(taskControl);
      if (onProgress) {
        onProgress({
          visible: true,
          percent: Math.round((index / Math.max(1, groups.length)) * 70),
          indeterminate: true,
          text: `正在识别媒体 ${index + 1}/${groups.length}:${shortDisplayName(groups[index].sourceName, 34)}`,
        });
      }
      const resolvedGroup = await resolveMediaGroupMoveItems(groups[index], { taskControl });
      const analyzeTargets = Array.isArray(resolvedGroup.seasonFolderGroups) && resolvedGroup.seasonFolderGroups.length
        ? resolvedGroup.seasonFolderGroups
        : [resolvedGroup];
      for (const targetGroup of analyzeTargets) {
        const finalGroup = targetGroup.needsChildListing
          ? await resolveMediaGroupMoveItems(targetGroup, { taskControl })
          : targetGroup;
        analyzed.push(await analyzeMediaSourceGroup(finalGroup, { onProgress }));
      }
    }
    STATE.mediaOrganizePreviewItems = analyzed;
    STATE.mediaOrganizeWarning = selection.meta?.warning || '';
    STATE.mediaOrganizePlan = {
      createdAt: Date.now(),
      rootParentId: getMediaOrganizeRootParentId(),
      groups: analyzed,
    };
    renderMediaOrganizeList();
    if (UI.mediaOrganizeDetails) {
      UI.mediaOrganizeDetails.open = true;
    }
    return analyzed;
  }

  function getMediaOrganizeRootParentId() {
    return String(CONFIG.mediaOrganize.rootParentId || UI.fields.mediaRootParentId?.value || getCurrentListContext().parentId || CONFIG.request.manualListBody.parentId || '').trim();
  }

  async function executeMediaOrganizePlan(options = {}) {
    const onProgress = typeof options.onProgress === 'function' ? options.onProgress : null;
    const taskControl = options.taskControl || null;
    const groups = Array.isArray(STATE.mediaOrganizePreviewItems) && STATE.mediaOrganizePreviewItems.length
      ? STATE.mediaOrganizePreviewItems
      : await previewMediaOrganizeSelection(options);
    if (!groups.length) {
      throw new Error('当前没有可执行的智能整理预览。');
    }
    const rootParentId = getMediaOrganizeRootParentId();
    const summaryText = groups.slice(0, 8).map((item) => `${item.sourceName} -> ${item.targetPath || '其他/未识别'}`).join('\n');
    if (CONFIG.batch.confirmBeforeRun && !window.confirm(`准备按智能识别整理 ${groups.length} 组内容:\n${summaryText}${groups.length > 8 ? `\n...另有 ${groups.length - 8} 组` : ''}\n\n整理根目录:${rootParentId || '光鸭根目录'}\n是否继续?`)) {
      return { ok: 0, fail: 0, groups };
    }

    const dirCache = new Map();
    const result = {
      ok: 0,
      fail: 0,
      groups,
      movedFileIds: [],
      failures: [],
      duplicateSkipped: 0,
      cleanup: null,
    };
    for (let index = 0; index < groups.length; index += 1) {
      await waitForTaskControl(taskControl);
      const group = groups[index];
      if (onProgress) {
        onProgress({
          visible: true,
          percent: Math.round((index / Math.max(1, groups.length)) * 100),
          indeterminate: true,
          text: `正在整理 ${index + 1}/${groups.length}:${shortDisplayName(group.sourceName, 34)} -> ${group.targetPath}`,
        });
      }
      try {
        const targetParentId = await ensureCloudDirectoryPath(group.targetSegments || ['其他', '未识别'], rootParentId, { cache: dirCache, taskControl });
        const sourceItems = group.moveAsFolder && group.sourceItem?.fileId
          ? [group.sourceItem]
          : (Array.isArray(group.items) && group.items.length
            ? group.items
            : (group.itemIds || []).map((id) => ({ fileId: String(id || '') })));
        const duplicateCheck = await filterMediaItemsAlreadyInTarget(sourceItems, targetParentId, { taskControl });
        if (duplicateCheck.duplicateItems.length) {
          result.duplicateSkipped += duplicateCheck.duplicateItems.length;
          result.fail += duplicateCheck.duplicateItems.length;
          result.failures.push(`${group.sourceName}:目标目录已有同名项目,已跳过 ${duplicateCheck.duplicateItems.length} 项`);
        }
        const ids = Array.from(new Set(duplicateCheck.moveItems.map((item) => getMediaItemIdentity(item)).filter(Boolean)));
        if (!ids.length) {
          continue;
        }
        const moved = await moveFilesInBatches(ids, targetParentId, {
          onProgress,
          taskControl,
          label: `智能整理:${shortDisplayName(group.sourceName, 18)}`,
          batchSize: Math.max(1, Number(CONFIG.mediaOrganize.batchSize || CONFIG.move.batchSize || 10)),
          verifySourceItems: duplicateCheck.moveItems || [],
        });
        result.ok += moved.ok;
        result.fail += moved.fail;
        result.movedFileIds.push(...(moved.movedFileIds || []));
        if (moved.firstError) {
          result.failures.push(`${group.sourceName}:${moved.firstError}`);
        }
      } catch (err) {
        result.fail += Math.max(1, group.itemIds?.length || 1);
        result.failures.push(`${group.sourceName}:${getErrorText(err) || '未知错误'}`);
        warn('智能整理失败:', err);
        if (CONFIG.batch.stopOnError) {
          break;
        }
      }
    }
    if (result.movedFileIds.length) {
      removeCapturedItemsByIds(result.movedFileIds);
    }
    const cleanupGroups = groups.filter((group) => {
      const sourceId = String(group.sourceItem?.fileId || group.sourceItem?.dirId || '').trim();
      if (!sourceId || !shouldTreatItemAsDirectory(group.sourceItem)) {
        return false;
      }
      const groupIds = Array.from(new Set((group.itemIds || []).map((id) => String(id || '').trim()).filter(Boolean)));
      return groupIds.length > 0 && groupIds.every((id) => result.movedFileIds.includes(id));
    });
    for (const group of groups) {
      const unwrapped = Array.isArray(group.unwrappedSourceFolders) ? group.unwrappedSourceFolders : [];
      for (const folder of unwrapped) {
        const folderId = String(folder?.fileId || folder?.dirId || '').trim();
        if (!folderId || !shouldTreatItemAsDirectory(folder)) {
          continue;
        }
        cleanupGroups.push({
          sourceItem: folder,
          itemIds: [folderId],
          sourceName: folder.name || folderId,
          parentSourceItem: group.sourceItem || null,
          parentGroupName: group.sourceName || '',
        });
      }
    }
    const cleanupParentItems = [];
    const cleanupParentSeen = new Set();
    for (const group of cleanupGroups) {
      const parentItem = group.parentSourceItem || null;
      const parentId = String(parentItem?.fileId || parentItem?.dirId || '').trim();
      if (!parentId || cleanupParentSeen.has(parentId) || !shouldTreatItemAsDirectory(parentItem)) {
        continue;
      }
      cleanupParentSeen.add(parentId);
      cleanupParentItems.push({
        sourceItem: parentItem,
        itemIds: [parentId],
        sourceName: parentItem.name || group.parentGroupName || parentId,
      });
    }
    result.cleanup = await deleteMediaSourceFoldersIfEmpty(cleanupGroups, { onProgress, taskControl });
    if (cleanupParentItems.length) {
      const parentCleanup = await deleteMediaSourceFoldersIfEmpty(cleanupParentItems, { onProgress, taskControl });
      result.cleanup = {
        deleted: (result.cleanup?.deleted || 0) + (parentCleanup.deleted || 0),
        skipped: (result.cleanup?.skipped || 0) + (parentCleanup.skipped || 0),
        failed: (result.cleanup?.failed || 0) + (parentCleanup.failed || 0),
        failures: [...(result.cleanup?.failures || []), ...(parentCleanup.failures || [])],
      };
    }
    if (onProgress) {
      const cleanupText = result.cleanup?.deleted ? `,清理空文件夹 ${result.cleanup.deleted} 个` : '';
      const duplicateText = result.duplicateSkipped ? `,跳过重复 ${result.duplicateSkipped} 项` : '';
      onProgress({
        visible: true,
        percent: 100,
        indeterminate: false,
        text: `智能整理完成:成功 ${result.ok} 项,失败 ${result.fail} 项${duplicateText}${cleanupText}`,
      });
    }
    return result;
  }

  async function run(options = {}) {
    const targets = await preview(options);
    const onProgress = typeof options.onProgress === 'function' ? options.onProgress : null;
    const taskControl = options.taskControl || null;

    if (!targets.length) {
      warn('没有需要改名的项目。');
      return { ok: 0, fail: 0, targets };
    }

    if (targets.some((item) => String(item.fileId || '').startsWith('dom:'))) {
      throw new Error('当前只拿到了页面名称,请刷新页面等待脚本捕获真实列表后再试。');
    }

    if (CONFIG.batch.confirmBeforeRun && !window.confirm(`准备重命名 ${targets.length} 个项目,是否继续?`)) {
      return { ok: 0, fail: 0, targets };
    }

    let ok = 0;
    let failed = 0;
    let index = 0;
    let firstError = '';

    for (const target of targets) {
      await waitForTaskControl(taskControl);
      index += 1;
      let currentTarget = { ...target };
      let success = false;
      let attempt = 0;
      let lastRes = null;

      // 自动重试逻辑:如果重名,尝试 (1), (2), (3)
      while (attempt <= 3 && !success) {
        await waitForTaskControl(taskControl);
        if (onProgress) {
          onProgress({
            visible: true,
            percent: ((index - 1) / targets.length) * 100,
            text: `进度 ${index}/${targets.length} | 成功 ${ok} | 失败 ${failed}\n当前:${shortDisplayName(currentTarget.oldName)} -> ${shortDisplayName(currentTarget.newName)}`,
          });
        }

        try {
          lastRes = await renameOne(currentTarget);
          if (lastRes.ok) {
            success = true;
            ok += 1;
            renameCapturedItem(target.fileId, currentTarget.newName);
            console.log(LOG_PREFIX, `成功:${currentTarget.oldName} -> ${currentTarget.newName}`);
          } else if (lastRes.payload && lastRes.payload.code === 160) {
            // 【核心逻辑】如果服务器报 160 (已存在),自动加后缀重试
            attempt++;
            const base = getBaseName(target.newName);
            const ext = getExt(target.newName);
            currentTarget.newName = `${base}(${attempt})${ext}`;
            console.warn(LOG_PREFIX, `重名冲突,自动尝试第 ${attempt} 次重试: ${currentTarget.newName}`);
          } else {
            break; // 其他错误(如 token 失效),跳出重试
          }
        } catch (err) {
          break;
        }
      }

      if (!success) {
        failed += 1;
        const errMsg = lastRes?.payload?.msg || lastRes?.text || '未知错误';
        firstError = firstError || errMsg;
        fail(`最终失败 [${target.oldName}] -> 尝试改名为 [${currentTarget.newName}]`, errMsg);
        if (CONFIG.batch.stopOnError) break;
      }

      if (CONFIG.batch.delayMs > 0) await controlledDelay(CONFIG.batch.delayMs, taskControl);
    }

    if (onProgress) {
      onProgress({ visible: true, percent: 100, text: `执行完成!成功 ${ok},失败 ${failed}` });
    }
    return { ok, fail: failed, firstError };
  }

  function exportState() {
    const visibleHeaders = { ...getMergedHeaders() };
    if (visibleHeaders.authorization) {
      visibleHeaders.authorization = 'Bearer ***';
    }

    return {
      installedAt: STATE.installedAt,
      headers: visibleHeaders,
      lastListUrl: STATE.lastListUrl,
      lastListBody: STATE.lastListBody,
      lastItemsSource: STATE.lastItemsSource,
      lastRenameUrl: STATE.lastRenameRequest?.url || '',
      lastRenameBody: STATE.lastRenameRequest?.requestBody || null,
      capturedItemCount: getCapturedItems().length,
      magnetImportFiles: (STATE.magnetImportFiles || []).map((item) => ({
        name: item.name,
        magnetCount: item.magnetCount || item.magnets?.length || 0,
      })),
      lastCloudImportSummary: STATE.lastCloudImportSummary,
      lastCloudTaskCount: extractCloudTaskRows(STATE.lastCloudTaskList).length,
      lastMiaochuanJson: STATE.lastMiaochuanJsonResult
        ? {
            outputCount: STATE.lastMiaochuanJsonResult.normalized?.files?.length || 0,
            source: STATE.lastMiaochuanJsonResult.sourceInfo?.summary || '',
            errors: STATE.lastMiaochuanJsonResult.errors?.length || 0,
            warnings: STATE.lastMiaochuanJsonResult.warnings?.length || 0,
          }
        : null,
      miaochuanCapturedCount: Array.isArray(STATE.miaochuanCapturedRows) ? STATE.miaochuanCapturedRows.length : 0,
      lastMiaochuanCaptureUrl: STATE.lastMiaochuanCaptureUrl || '',
      moveSelectionPreviewCount: Array.isArray(STATE.moveSelectionPreviewItems) ? STATE.moveSelectionPreviewItems.length : 0,
      moveSelectionExpectedCount: Number(STATE.moveSelectionExpectedCount || 0),
      moveSelectionSource: STATE.moveSelectionSource || 'visible',
      moveSelectionWarning: STATE.moveSelectionWarning || '',
      lastEmptyDirScan: STATE.lastEmptyDirScan
        ? {
            rootParentId: STATE.lastEmptyDirScan.rootParentId,
            scannedDirs: STATE.lastEmptyDirScan.scannedDirs,
            scannedItems: STATE.lastEmptyDirScan.scannedItems,
            emptyDirCount: STATE.lastEmptyDirScan.emptyDirs?.length || 0,
            truncated: Boolean(STATE.lastEmptyDirScan.truncated),
          }
        : null,
      sampleItems: getCapturedItems().slice(0, 5),
    };
  }

  function isElementChecked(node) {
    if (!node) {
      return false;
    }

    if (node instanceof HTMLInputElement && node.type === 'checkbox') {
      return Boolean(node.checked);
    }

    const ariaChecked = node.getAttribute && node.getAttribute('aria-checked');
    if (ariaChecked === 'true') {
      return true;
    }
    if (ariaChecked === 'false') {
      return false;
    }

    const stateAttrs = [
      node.getAttribute?.('checked'),
      node.getAttribute?.('data-state'),
      node.getAttribute?.('data-selected'),
      node.getAttribute?.('data-checked'),
      node.getAttribute?.('aria-selected'),
    ].map((value) => String(value || '').toLowerCase()).filter(Boolean);
    if (stateAttrs.some((value) => ['true', 'checked', 'selected', 'on'].includes(value))) {
      return true;
    }
    if (stateAttrs.some((value) => ['false', 'unchecked', 'unselected', 'off'].includes(value))) {
      return false;
    }

    const className = String(node.className || '').toLowerCase();
    if (/(^|[\s:_-])(un|not)-?(checked|selected)([\s:_-]|$)/u.test(className)) {
      return false;
    }
    return /(^|[\s:_-])(?:is-)?checked([\s:_-]|$)/u.test(className)
      || /(^|[\s:_-])(?:is-)?selected([\s:_-]|$)/u.test(className)
      || /(^|[\s:_-])\w+-(?:checked|selected)([\s:_-]|$)/u.test(className);
  }

  function isVisibleElement(node) {
    return Boolean(node && typeof node.getClientRects === 'function' && node.getClientRects().length > 0);
  }

  function dedupeElements(nodes) {
    const out = [];
    const seen = new Set();
    for (const node of nodes || []) {
      if (!node || seen.has(node)) {
        continue;
      }
      seen.add(node);
      out.push(node);
    }
    return out;
  }

  function getRowSelector() {
    return [
      '[role="row"]',
      'tr',
      'li',
      '[class*="row"]',
      '[class*="item"]',
      '[class*="file"]',
      '[class*="entry"]',
      '[class*="list-item"]',
      '[data-row-key]',
      '[data-id]',
    ].join(', ');
  }

  function getClosestRow(node) {
    if (!node || typeof node.closest !== 'function') {
      return null;
    }
    return node.closest(getRowSelector());
  }

  function buildExpectedNameTokens(name) {
    const normalized = normalizeDomName(name);
    if (!normalized) {
      return [];
    }

    const stem = normalized.replace(/\.[a-z0-9]{1,12}$/i, '');
    const tokens = [
      normalized,
      stem,
      stem.slice(0, 18),
      stem.slice(0, 28),
    ].map((x) => normalizeDomName(x)).filter((x) => x && x.length >= 8);

    return Array.from(new Set(tokens)).sort((a, b) => b.length - a.length);
  }

  function textLooksLikeExpected(text, expectedName) {
    const normalizedText = normalizeDomName(text);
    const normalizedExpected = normalizeDomName(expectedName);
    if (!normalizedText || !normalizedExpected) {
      return false;
    }
    if (normalizedText === normalizedExpected) {
      return true;
    }
    if (normalizedText.includes(normalizedExpected) || normalizedExpected.includes(normalizedText)) {
      return true;
    }

    const tokens = buildExpectedNameTokens(normalizedExpected);
    return tokens.some((token) => normalizedText.includes(token));
  }

  function getCheckboxInRow(row) {
    if (!row) {
      return null;
    }

    const selectors = [
      'label[role="checkbox"]',
      '[role="checkbox"]',
      '[aria-label*="选择"]',
      'button[aria-label*="选择"]',
      '[data-testid*="checkbox"]',
      '[class*="checkbox"]',
      '[class*="check"]',
      'input[type="checkbox"]',
    ];

    const searchRoots = dedupeElements([
      row,
      row.parentElement,
      row.previousElementSibling,
      row.nextElementSibling,
      row.firstElementChild,
      row.lastElementChild,
    ]);

    for (const root of searchRoots) {
      if (!root) {
        continue;
      }
      for (const selector of selectors) {
        const nodes = [];
        if (root.matches && root.matches(selector)) {
          nodes.push(root);
        }
        if (root.querySelectorAll) {
          nodes.push(...root.querySelectorAll(selector));
        }

        for (const node of nodes) {
          let current = node;
          while (current && current !== document.body) {
            if (isVisibleElement(current)) {
              return current;
            }
            if (current === row || current === root) {
              break;
            }
            current = current.parentElement;
          }
        }
      }
    }

    return null;
  }

  function getListRows() {
    return dedupeElements(Array.from(document.querySelectorAll(getRowSelector())).filter((node) => isUsableListRow(node)));
  }

  function findScrollableListContainer() {
    const rows = getListRows().filter(isVisibleElement).slice(0, 12);
    const scored = [];

    for (const row of rows) {
      let current = row.parentElement;
      while (current && current !== document.body) {
        const style = window.getComputedStyle(current);
        const overflowY = style ? style.overflowY : '';
        const canScroll =
          current.scrollHeight > current.clientHeight + 40 &&
          /(auto|scroll|overlay)/i.test(String(overflowY || ''));
        if (canScroll) {
          scored.push({
            node: current,
            score: current.scrollHeight - current.clientHeight,
          });
        }
        current = current.parentElement;
      }
    }

    scored.sort((a, b) => b.score - a.score);
    return scored[0]?.node || document.scrollingElement || document.documentElement;
  }

  async function scrollListContainer(container, deltaY) {
    if (!container) {
      return false;
    }

    const isDocumentScroller =
      container === document.scrollingElement ||
      container === document.documentElement ||
      container === document.body;

    const before = isDocumentScroller ? (window.scrollY || window.pageYOffset || 0) : container.scrollTop;
    if (isDocumentScroller) {
      window.scrollTo({ top: Math.max(0, before + deltaY), behavior: 'auto' });
    } else {
      container.scrollTop = Math.max(0, before + deltaY);
    }
    await sleep(220);
    const after = isDocumentScroller ? (window.scrollY || window.pageYOffset || 0) : container.scrollTop;
    return after !== before;
  }

  function isCheckboxLikeNode(node) {
    if (!node || typeof node.closest !== 'function') {
      return false;
    }
    return Boolean(node.closest(
      'label[role="checkbox"], [role="checkbox"], [aria-label*="选择"], button[aria-label*="选择"], [data-testid*="checkbox"], [class*="checkbox"], [class*="check"], input[type="checkbox"]'
    ));
  }

  function collectVisibleListRowEntries(expectedNames = []) {
    const expectedSet = new Set((expectedNames || []).map((name) => normalizeDomName(name)).filter(Boolean));
    const seen = new Set();

    return getListRows()
      .filter((row) => isUsableListRow(row))
      .map((row, index) => {
        const matchedName = expectedSet.size ? findExpectedNameInRow(row, expectedSet) : '';
        const name = matchedName || extractNameFromRow(row);
        const normalizedName = normalizeDomName(name);
        if (!isProbablyUsefulName(name) || !normalizedName || seen.has(normalizedName)) {
          return null;
        }
        seen.add(normalizedName);
        return {
          index,
          row,
          name,
          normalizedName,
          checkbox: getCheckboxInRow(row),
          isDir: guessDomRowIsDirectory(row, name),
        };
      })
      .filter(Boolean);
  }

  function collectVisibleDirectoryRows(expectedNames = []) {
    return collectVisibleListRowEntries(expectedNames).filter((item) => item.isDir);
  }

  function buildEmptyScanChildDirs(items = [], visibleRows = []) {
    const normalizedItems = Array.isArray(items) ? items.filter(Boolean) : [];
    const visibleDirs = (Array.isArray(visibleRows) ? visibleRows : [])
      .filter((item) => item && item.isDir && String(item.name || '').trim());
    const visibleByName = new Map();

    for (const item of visibleDirs) {
      const key = normalizeDomName(item.name);
      if (key && !visibleByName.has(key)) {
        visibleByName.set(key, item);
      }
    }

    const merged = [];
    const seenKeys = new Set();
    const pushMerged = (item) => {
      if (!item) {
        return;
      }
      const key = String(item.fileId || normalizeDomName(item.name) || '').trim();
      if (!key || seenKeys.has(key)) {
        return;
      }
      seenKeys.add(key);
      merged.push(item);
    };

    for (const item of normalizedItems) {
      const nameKey = normalizeDomName(item?.name || '');
      const visibleMatch = nameKey ? visibleByName.get(nameKey) : null;
      if (!visibleMatch && !shouldTreatItemAsDirectory(item)) {
        continue;
      }

      pushMerged({
        ...item,
        isDir: true,
        dirIdCandidates: normalizeIdCandidates(item?.dirIdCandidates || [item?.dirId, item?.fileId]),
        raw: {
          ...(item?.raw || {}),
          domIsDir: Boolean(visibleMatch || item?.raw?.domIsDir),
        },
      });

      if (visibleMatch && nameKey) {
        visibleByName.delete(nameKey);
      }
    }

    for (const visible of visibleByName.values()) {
      const syntheticId = `domdir:${normalizeDomName(visible.name)}`;
      pushMerged({
        fileId: syntheticId,
        dirId: syntheticId,
        dirIdCandidates: [syntheticId],
        name: String(visible.name || ''),
        isDir: true,
        raw: {
          fromDom: true,
          domIsDir: true,
        },
      });
    }

    return merged;
  }

  function scoreDirectoryOpenTarget(node, row, expectedName = '') {
    if (!node || !isVisibleElement(node) || isHelperPanelNode(node) || isCheckboxLikeNode(node)) {
      return Number.NEGATIVE_INFINITY;
    }

    let score = node === row ? 5 : 0;
    if (node.matches && node.matches('a, [role="link"]')) {
      score += 80;
    }
    if (node.matches && node.matches('button')) {
      score += 30;
    }
    if (node.matches && node.matches('[data-name], [data-filename], [title], [aria-label], [class*="name"], [class*="title"]')) {
      score += 45;
    }

    const expected = normalizeDomName(expectedName);
    if (expected) {
      const candidates = collectTextCandidates(node);
      const matched = candidates.find((text) => textLooksLikeExpected(text, expected));
      if (matched) {
        score += 140 - Math.min(40, Math.abs(normalizeDomName(matched).length - expected.length));
      }
    }

    return score;
  }

  function getDirectoryOpenTarget(row, expectedName = '') {
    if (!row) {
      return null;
    }

    const candidates = dedupeElements([
      row,
      ...Array.from(row.querySelectorAll('a, button, [role="link"], [data-name], [data-filename], [title], [aria-label], [class*="name"], [class*="title"], span, div, p, strong, td')),
    ]).filter((node) => !isHelperPanelNode(node) && !isBreadcrumbContainerNode(node));

    candidates.sort((left, right) => scoreDirectoryOpenTarget(right, row, expectedName) - scoreDirectoryOpenTarget(left, row, expectedName));
    return candidates[0] || row;
  }

  async function locateDirectoryRowByName(expectedName, options = {}) {
    const expected = normalizeDomName(expectedName);
    if (!expected) {
      return null;
    }

    const container = options.container || findScrollableListContainer();
    const maxRounds = Math.max(1, Number(options.maxRounds || 20));
    const deltaY = Math.max(280, Math.floor((container?.clientHeight || window.innerHeight || 640) * 0.72));

    if (container && options.resetScroll !== false) {
      const isDocumentScroller =
        container === document.scrollingElement ||
        container === document.documentElement ||
        container === document.body;
      if (isDocumentScroller) {
        window.scrollTo({ top: 0, behavior: 'auto' });
      } else {
        container.scrollTop = 0;
      }
      await sleep(180);
    }

    for (let round = 0; round < maxRounds; round += 1) {
      const visibleRows = collectVisibleDirectoryRows([expected]);
      const exact = visibleRows.find((item) => item.normalizedName === expected);
      if (exact) {
        return exact;
      }
      if (visibleRows.length) {
        return visibleRows[0];
      }

      const searchedRows = collectRowsByDocumentSearch([expected])
        .filter((item) => item.row && isUsableListRow(item.row) && guessDomRowIsDirectory(item.row, item.name || expected));
      if (searchedRows.length) {
        return searchedRows[0];
      }

      if (round >= maxRounds - 1) {
        break;
      }

      const moved = await scrollListContainer(container, deltaY);
      if (!moved) {
        break;
      }
    }

    return null;
  }

  function triggerSyntheticDblClick(target) {
    if (!target) {
      return;
    }

    try {
      if (target.scrollIntoView) {
        target.scrollIntoView({ block: 'center', inline: 'nearest' });
      }
    } catch {}

    const mouseInit = {
      bubbles: true,
      cancelable: true,
      composed: true,
      view: window,
    };

    try {
      target.dispatchEvent(new MouseEvent('dblclick', mouseInit));
    } catch {}
  }

  async function openDirectoryByName(expectedName, options = {}) {
    const previousSnapshot = options.previousSnapshot || getDirectoryContextSnapshot();
    const childItem = options.childItem || null;

    if (childItem && !isSyntheticDomId(childItem.fileId || childItem.dirId || '')) {
      const directHash = buildChildDirectoryHash(previousSnapshot, childItem);
      const directSnapshot = await navigateToDirectoryHash(directHash, previousSnapshot, {
        expectedName,
        timeoutMs: 4600,
        intervalMs: 180,
        stableMs: 420,
      });
      if (directSnapshot) {
        return directSnapshot;
      }
    }

    const rowEntry = options.rowEntry || await locateDirectoryRowByName(expectedName, options);
    if (!rowEntry?.row) {
      return null;
    }

    const targets = dedupeElements([
      getDirectoryOpenTarget(rowEntry.row, expectedName),
      rowEntry.row,
    ]);

    for (const target of targets) {
      triggerSyntheticClick(target);
      let changed = await waitForDirectoryChange(previousSnapshot, {
        timeoutMs: Number(options.timeoutMs || 4200),
        intervalMs: 120,
      });
      if (changed) {
        await sleep(520);
        const fresh = await waitForFreshDirectoryContext(previousSnapshot, {
          expectedName,
          timeoutMs: 2400,
          intervalMs: 180,
          stableMs: 360,
        });
        if (fresh) {
          return fresh;
        }
      }

      triggerSyntheticDblClick(target);
      changed = await waitForDirectoryChange(previousSnapshot, {
        timeoutMs: Number(options.timeoutMs || 4200),
        intervalMs: 120,
      });
      if (changed) {
        await sleep(520);
        const fresh = await waitForFreshDirectoryContext(previousSnapshot, {
          expectedName,
          timeoutMs: 2400,
          intervalMs: 180,
          stableMs: 360,
        });
        if (fresh) {
          return fresh;
        }
      }
    }

    return null;
  }

  function collectBreadcrumbTargets(expectedName = '') {
    const expected = normalizeDomName(expectedName);
    const selectors = [
      '[aria-label*="breadcrumb" i] a',
      '[aria-label*="breadcrumb" i] button',
      '[aria-label*="breadcrumb" i] [role="link"]',
      '[aria-label*="breadcrumb" i] [role="button"]',
      '[class*="breadcrumb"] a',
      '[class*="breadcrumb"] button',
      '[class*="breadcrumb"] [role="link"]',
      '[class*="breadcrumb"] [role="button"]',
      '[class*="crumb"] a',
      '[class*="crumb"] button',
      '[class*="crumb"] [role="link"]',
      '[class*="crumb"] [role="button"]',
      '[class*="path"] a',
      '[class*="path"] button',
      '[class*="path"] [role="link"]',
      '[class*="path"] [role="button"]',
      'nav a',
      'nav button',
      'nav [role="link"]',
      'nav [role="button"]',
    ];

    const nodes = dedupeElements(Array.from(document.querySelectorAll(selectors.join(', '))))
      .filter((node) => isVisibleElement(node) && !isHelperPanelNode(node) && !node.querySelector('a, button, [role="link"], [role="button"]'));

    return nodes
      .map((node) => {
        const text = cleanDirectoryTitleCandidate(getVisibleNodeText(node));
        const normalizedText = normalizeDomName(text);
        if (!normalizedText || node.getAttribute?.('aria-current') === 'page') {
          return null;
        }
        if (ROOT_DIRECTORY_NAMES.has(normalizedText) && normalizedText !== expected) {
          return null;
        }
        let score = 0;
        if (expected) {
          if (!textLooksLikeExpected(normalizedText, expected)) {
            return null;
          }
          score += normalizedText === expected ? 200 : 120;
        }
        if (node.matches && node.matches('a, button, [role="link"], [role="button"]')) {
          score += 40;
        }
        return { node, text: normalizedText, score };
      })
      .filter(Boolean)
      .sort((left, right) => right.score - left.score);
  }

  async function returnToDirectorySnapshot(targetSnapshot, options = {}) {
    if (!targetSnapshot) {
      return false;
    }

    const alreadyThere = await waitForDirectoryMatch(targetSnapshot, {
      timeoutMs: 120,
      intervalMs: 60,
    });
    if (alreadyThere) {
      return true;
    }

    const hashMatched = await navigateToDirectoryHash(targetSnapshot.hash, getDirectoryContextSnapshot(), {
      expectedName: targetSnapshot.name,
      timeoutMs: Number(options.timeoutMs || 4200),
      intervalMs: 180,
      stableMs: 420,
    });
    if (hashMatched && isSameDirectorySnapshot(targetSnapshot, hashMatched)) {
      await sleep(260);
      return true;
    }

    const breadcrumbTargets = collectBreadcrumbTargets(targetSnapshot.name);
    const bestTarget = breadcrumbTargets[0] || null;
    if (bestTarget) {
      triggerSyntheticClick(bestTarget.node);
      const matched = await waitForDirectoryMatch(targetSnapshot, {
        timeoutMs: Number(options.timeoutMs || 4200),
        intervalMs: 120,
      });
      if (matched) {
        await sleep(420);
        return true;
      }
    }

    const historyBackTries = Math.max(1, Number(options.historyBackTries || 1));
    for (let index = 0; index < historyBackTries; index += 1) {
      try {
        window.history.back();
      } catch {}

      const matched = await waitForDirectoryMatch(targetSnapshot, {
        timeoutMs: Number(options.timeoutMs || 4200),
        intervalMs: 120,
      });
      if (matched) {
        await sleep(420);
        return true;
      }
    }

    return false;
  }

  async function inspectCurrentDirectoryForEmptyScan(options = {}) {
    const currentSnapshot = getDirectoryContextSnapshot();
    const parentId = String(options.parentId || currentSnapshot.parentId || '').trim();
    if (!parentId || isSyntheticDomId(parentId)) {
      return {
        items: [],
        childDirs: [],
        visibleRows: [],
        emptyStateInfo: { visible: false, text: '', via: '' },
        isEmpty: false,
        uncertain: true,
        requestError: new Error(!parentId ? '当前目录 parentId 为空' : `当前目录仍是伪 ID:${parentId}`),
      };
    }

    let items = [];
    let visibleRows = [];
    let requestError = null;
    let emptyStateInfo = {
      visible: false,
      text: '',
      via: '',
    };
    const maxAttempts = Math.max(2, Number(options.maxAttempts || 3));
    const settleDelayMs = Math.max(240, Number(options.settleDelayMs || 800));

    for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
      requestError = null;
      let apiItems = [];
      try {
        apiItems = await fetchCurrentList({ parentId });
      } catch (err) {
        requestError = err;
      }

      const snapshotItems = buildCurrentDirectoryItemsSnapshot(parentId);
      items = dedupeItems([...(Array.isArray(apiItems) ? apiItems : []), ...snapshotItems]);
      visibleRows = collectVisibleListRowEntries();
      emptyStateInfo = findVisibleEmptyStateInfo();
      const childDirs = buildEmptyScanChildDirs(items, visibleRows);
      const hasRenderableContent = Boolean(items.length || visibleRows.length || childDirs.length);
      const stillOnExpectedDirectory = !getDirectoryContextSnapshot().parentId || getDirectoryContextSnapshot().parentId === parentId;

      if (hasRenderableContent) {
        break;
      }

      if (emptyStateInfo.visible && stillOnExpectedDirectory && !requestError) {
        break;
      }

      if (attempt >= maxAttempts - 1) {
        break;
      }

      await sleep(settleDelayMs);
    }

    const childDirs = buildEmptyScanChildDirs(items, visibleRows);
    const hasRenderableContent = Boolean(items.length || visibleRows.length || childDirs.length);
    const isEmptyConfirmed = !hasRenderableContent && emptyStateInfo.visible && !requestError;
    const isUncertain = !hasRenderableContent && !isEmptyConfirmed;
    return {
      items,
      childDirs,
      visibleRows,
      emptyStateInfo,
      isEmpty: isEmptyConfirmed,
      uncertain: isUncertain,
      requestError,
    };
  }

  function collectVisibleDuplicateRows(expectedNames = null) {
    const duplicateRe = getDuplicateRegex();
    const expectedSet =
      expectedNames && expectedNames.length
        ? new Set(expectedNames.map((name) => normalizeDomName(name)))
        : null;
    const rows = getListRows();

    return rows
      .map((row, index) => {
        const expectedName = findExpectedNameInRow(row, expectedSet);
        const name = expectedName || extractNameFromRow(row);
        const checkbox = getCheckboxInRow(row);
        return {
          index,
          row,
          name,
          normalizedName: normalizeDomName(name),
          checkbox,
        };
      })
      .filter((item) => {
        if (!item.checkbox || !isProbablyUsefulName(item.name)) {
          return false;
        }
        if (expectedSet && expectedSet.size) {
          return expectedSet.has(item.normalizedName);
        }
        return duplicateRe.test(item.name);
      });
  }

  function collectRowsByDocumentSearch(expectedNames = []) {
    const expectedList = Array.from(new Set((expectedNames || []).map((name) => normalizeDomName(name)).filter(Boolean)));
    if (!expectedList.length) {
      return [];
    }

    const nodes = Array.from(
      document.querySelectorAll('[title], [aria-label], [data-name], [data-filename], span, div, p, a, strong, td')
    );
    const rows = [];

    for (const node of nodes) {
      if (!isVisibleElement(node) || isHelperPanelNode(node)) {
        continue;
      }

      const texts = dedupeElements([
        node.getAttribute && node.getAttribute('title'),
        node.getAttribute && node.getAttribute('aria-label'),
        node.getAttribute && node.getAttribute('data-name'),
        node.getAttribute && node.getAttribute('data-filename'),
        node.textContent,
      ]).map((x) => String(x || ''));

      const matchedExpected = expectedList.find((expected) => texts.some((text) => textLooksLikeExpected(text, expected)));
      if (!matchedExpected) {
        continue;
      }

      const row = getClosestRow(node);
      if (isHelperPanelNode(row) || !isUsableListRow(row)) {
        continue;
      }
      const checkbox = getCheckboxInRow(row);
      if (!row || !checkbox) {
        continue;
      }

      rows.push({
        row,
        checkbox,
        name: matchedExpected,
        normalizedName: matchedExpected,
      });
    }

    return dedupeElements(rows.map((item) => item.row)).map((row, index) => ({
      index,
      row,
      checkbox: getCheckboxInRow(row),
      name: expectedList.find((expected) => textLooksLikeExpected(row.innerText || row.textContent || '', expected)) || extractNameFromRow(row),
      normalizedName: normalizeDomName(
        expectedList.find((expected) => textLooksLikeExpected(row.innerText || row.textContent || '', expected)) || extractNameFromRow(row)
      ),
    })).filter((item) => item.checkbox);
  }

  function triggerSyntheticClick(target) {
    if (!target) {
      return;
    }

    try {
      if (target.scrollIntoView) {
        target.scrollIntoView({ block: 'center', inline: 'nearest' });
      }
    } catch {}

    const mouseInit = {
      bubbles: true,
      cancelable: true,
      composed: true,
      view: window,
    };

    for (const type of ['pointerdown', 'mousedown', 'pointerup', 'mouseup', 'click']) {
      try {
        const EventCtor = type.startsWith('pointer') && typeof PointerEvent === 'function' ? PointerEvent : MouseEvent;
        target.dispatchEvent(new EventCtor(type, mouseInit));
      } catch {}
    }

    try {
      if (typeof target.click === 'function') {
        target.click();
      }
    } catch {}
  }

  async function toggleCheckboxRobustly(item) {
    const targets = dedupeElements([
      item.checkbox,
      item.checkbox?.closest?.('label, button, [role="checkbox"]'),
      item.row,
      item.row?.querySelector?.('label[role="checkbox"], [role="checkbox"], input[type="checkbox"], [class*="checkbox"], [class*="check"]'),
    ]);

    for (const target of targets) {
      triggerSyntheticClick(target);
      await sleep(120);
      if (isElementChecked(item.checkbox)) {
        return true;
      }
    }

    return isElementChecked(item.checkbox);
  }

  async function selectDuplicateRows(options = {}) {
    const onProgress = typeof options.onProgress === 'function' ? options.onProgress : null;
    const taskControl = options.taskControl || null;
    let duplicates = getSelectedDuplicatePreviewItems();
    try {
      if (!duplicates.length) {
        const previewItems = await previewDuplicates({
          refresh: Boolean(options.refresh),
          listBody: options.listBody || {},
        });
        setDuplicatePreview(previewItems, { preserveSelection: true });
        duplicates = getSelectedDuplicatePreviewItems();
      }
    } catch (err) {
      warn('重复项预览失败,改为直接扫描当前可见行:', err);
    }

    const expectedNames = duplicates.map((item) => item.name);
    const pendingNames = new Set(expectedNames.map((name) => normalizeDomName(name)).filter(Boolean));
    const matchedNames = new Set();
    const visibleOnlyMode = !pendingNames.size;

    if (visibleOnlyMode) {
      warn('接口没有识别到重复项,勾选将退回为当前页面可见项扫描模式。');
    }

    let clicked = 0;
    let skipped = 0;
    const container = findScrollableListContainer();
    const maxRounds = visibleOnlyMode ? 1 : 24;

    if (container && !visibleOnlyMode) {
      const isDocumentScroller =
        container === document.scrollingElement ||
        container === document.documentElement ||
        container === document.body;
      if (isDocumentScroller) {
        window.scrollTo({ top: 0, behavior: 'auto' });
      } else {
        container.scrollTop = 0;
      }
      await controlledDelay(160, taskControl);
    }

    for (let round = 0; round < maxRounds; round += 1) {
      await waitForTaskControl(taskControl);
      const targetNames = visibleOnlyMode ? null : Array.from(pendingNames);
      let visibleRows = collectVisibleDuplicateRows(targetNames);
      if (!visibleRows.length && targetNames && targetNames.length) {
        visibleRows = collectRowsByDocumentSearch(targetNames);
      }

      for (let i = 0; i < visibleRows.length; i += 1) {
        await waitForTaskControl(taskControl);
        const item = visibleRows[i];
        if (matchedNames.has(item.normalizedName)) {
          continue;
        }

        matchedNames.add(item.normalizedName);
        pendingNames.delete(item.normalizedName);

        updatePanelStatus(
          visibleOnlyMode
            ? `勾选当前可见重复项 ${matchedNames.size} | 当前:${shortDisplayName(item.name)}`
            : `勾选重复项 ${matchedNames.size}/${duplicates.length} | 当前:${shortDisplayName(item.name)}`
        );
        if (onProgress) {
          onProgress({
            visible: true,
            percent: visibleOnlyMode
              ? Math.min(95, matchedNames.size * 12)
              : Math.min(95, (matchedNames.size / Math.max(1, duplicates.length)) * 100),
            indeterminate: false,
            text: visibleOnlyMode
              ? `勾选当前可见重复项 ${matchedNames.size} | 当前:${shortDisplayName(item.name)}`
              : `勾选重复项 ${matchedNames.size}/${duplicates.length} | 当前:${shortDisplayName(item.name)}`,
          });
        }

        if (!isElementChecked(item.checkbox)) {
          const checked = await toggleCheckboxRobustly(item);
          if (checked) {
            clicked += 1;
          }
        } else {
          skipped += 1;
        }
      }

      if (visibleOnlyMode || !pendingNames.size) {
        break;
      }

      const deltaY = Math.max(280, Math.floor((container?.clientHeight || window.innerHeight || 640) * 0.75));
      await waitForTaskControl(taskControl);
      const moved = await scrollListContainer(container, deltaY);
      if (!moved) {
        break;
      }
    }

    const missing = Array.from(pendingNames);
    if (!matchedNames.size) {
      updatePanelStatus(visibleOnlyMode ? '当前页面没有识别到可勾选的重复项' : `接口识别到 ${duplicates.length} 个重复项,但页面里一个都没定位到`);
      return { matched: 0, clicked: 0, skipped: 0, missing };
    }

    updatePanelStatus(
      missing.length
        ? `接口识别 ${duplicates.length} 个,已定位 ${matchedNames.size} 个,勾选 ${clicked} 个,剩余 ${missing.length} 个未定位`
        : `重复项已定位 ${matchedNames.size} 个,勾选 ${clicked} 个,已跳过 ${skipped} 个`
    );
    if (onProgress) {
      onProgress({
        visible: true,
        percent: 100,
        indeterminate: false,
        text: missing.length
          ? `接口识别 ${duplicates.length} 个,已定位 ${matchedNames.size} 个,勾选 ${clicked} 个,剩余 ${missing.length} 个未定位`
          : `重复项已定位 ${matchedNames.size} 个,勾选 ${clicked} 个,已跳过 ${skipped} 个`,
      });
    }
    console.table(Array.from(matchedNames).map((name) => ({ name })));
    return { matched: matchedNames.size, clicked, skipped, missing };
  }

  async function collectCheckedDuplicateTargets(duplicates, options = {}) {
    const onProgress = typeof options.onProgress === 'function' ? options.onProgress : null;
    const taskControl = options.taskControl || null;
    const expectedNames = duplicates.map((item) => item.name);
    const pendingNames = new Set(expectedNames.map((name) => normalizeDomName(name)).filter(Boolean));
    const checkedNames = new Set();
    const scannedNames = new Set();
    const container = findScrollableListContainer();
    const maxRounds = 24;

    if (container) {
      const isDocumentScroller =
        container === document.scrollingElement ||
        container === document.documentElement ||
        container === document.body;
      if (isDocumentScroller) {
        window.scrollTo({ top: 0, behavior: 'auto' });
      } else {
        container.scrollTop = 0;
      }
      await controlledDelay(180, taskControl);
    }

    for (let round = 0; round < maxRounds; round += 1) {
      await waitForTaskControl(taskControl);
      const targetNames = Array.from(pendingNames);
      let visibleRows = collectVisibleDuplicateRows(targetNames);
      if (!visibleRows.length && targetNames.length) {
        visibleRows = collectRowsByDocumentSearch(targetNames);
      }

      for (const item of visibleRows) {
        scannedNames.add(item.normalizedName);
        pendingNames.delete(item.normalizedName);
        if (isElementChecked(item.checkbox)) {
          checkedNames.add(item.normalizedName);
        } else {
          checkedNames.delete(item.normalizedName);
        }
      }

      if (onProgress) {
        onProgress({
          visible: true,
          percent: Math.min(95, ((round + 1) / maxRounds) * 100),
          indeterminate: false,
          text: `正在读取当前勾选状态 | 已扫描 ${scannedNames.size}/${duplicates.length} | 已勾选 ${checkedNames.size}`,
        });
      }

      if (!pendingNames.size) {
        break;
      }

      const deltaY = Math.max(280, Math.floor((container?.clientHeight || window.innerHeight || 640) * 0.75));
      await waitForTaskControl(taskControl);
      const moved = await scrollListContainer(container, deltaY);
      if (!moved) {
        break;
      }
    }

    const checkedTargets = duplicates.filter((item) => checkedNames.has(normalizeDomName(item.name)));
    return {
      checkedTargets,
      checkedNames,
      scannedNames,
      missing: duplicates.filter((item) => !scannedNames.has(normalizeDomName(item.name))),
    };
  }

  function getTaskProgressState(payload, fallbackPercent = 10) {
    const raw = Number(findFirstValueByKeys(payload, ['progress', 'percent', 'percentage']));
    if (!Number.isFinite(raw)) {
      return {
        percent: fallbackPercent,
        indeterminate: true,
      };
    }
    if (raw <= 1) {
      return {
        percent: Math.max(0, Math.min(100, raw * 100)),
        indeterminate: false,
      };
    }
    return {
      percent: Math.max(0, Math.min(100, raw)),
      indeterminate: false,
    };
  }

  function getTaskProgressText(payload, attempt, maxTries, expectedTotal = 0) {
    const status = extractTaskStatus(payload) || 'UNKNOWN';
    const counts = extractTaskCounts(payload, expectedTotal);
    const parts = [`任务状态: ${getTaskStatusLabel(status)}`, `轮询 ${attempt}/${maxTries}`];

    if (counts.total > 0) {
      parts.push(counts.hasExplicitCounts ? `已处理 ${counts.processed}/${counts.total}` : `目标 ${counts.total} 项`);
    }
    if (counts.hasSuccessCount) {
      parts.push(`成功 ${counts.success}`);
    }
    if (counts.hasFailedCount) {
      parts.push(`失败 ${counts.failed}`);
    }
    return parts.join(' | ');
  }

  async function waitTaskUntilDone(taskId, options = {}) {
    const onProgress = typeof options.onProgress === 'function' ? options.onProgress : null;
    const taskControl = options.taskControl || null;
    const maxTries = Number(options.maxTries || CONFIG.batch.taskPollMaxTries || 30);
    const intervalMs = Number(options.intervalMs || CONFIG.batch.taskPollMs || 1200);
    const expectedTotal = Number(options.expectedTotal || 0);
    let lastResult = null;

    for (let attempt = 1; attempt <= maxTries; attempt += 1) {
      await waitForTaskControl(taskControl);
      const result = await getTaskStatus(taskId);
      lastResult = result;
      const progress = getTaskProgressState(result.payload, 10);

      if (onProgress) {
        onProgress({
          visible: true,
          percent: Math.max(10, progress.percent),
          indeterminate: progress.indeterminate,
          text: getTaskProgressText(result.payload, attempt, maxTries, expectedTotal),
        });
      }

      if (!result.ok) {
        throw new Error(`任务状态查询失败:HTTP ${result.status}`);
      }

      if (isTaskFinished(result.payload, { expectedTotal })) {
        return {
          ok: isTaskSuccessful(result.payload, { expectedTotal }),
          taskId,
          result,
          timeout: false,
        };
      }

      if (attempt < maxTries) {
        await controlledDelay(intervalMs, taskControl);
      }
    }

    return {
      ok: false,
      taskId,
      result: lastResult,
      timeout: true,
    };
  }

  function removeDuplicatePreviewItemsByIds(fileIds, options = {}) {
    const deletedIds = new Set((fileIds || []).map((id) => String(id)));
    if (!deletedIds.size) {
      return;
    }

    removeCapturedItemsByIds(Array.from(deletedIds));
    setDuplicatePreview(
      (STATE.duplicatePreviewItems || []).filter((item) => !deletedIds.has(String(item.fileId))),
      { preserveSelection: options.preserveSelection !== false }
    );
  }

  async function verifyDeletedItemsByList(targets, options = {}) {
    const onProgress = typeof options.onProgress === 'function' ? options.onProgress : null;
    const taskControl = options.taskControl || null;
    const maxRounds = Math.max(1, Number(options.maxRounds || 8));
    const intervalMs = Math.max(300, Number(options.intervalMs || 1800));
    const verifiedPageSize = Math.max(
      Number(UI.fields.pageSize?.value || 0),
      Number(CONFIG.request.manualListBody.pageSize || 0),
      Number(getCapturedItems().length || 0),
      Number((targets || []).length || 0) + 50,
      200
    );
    const sourceTargets = Array.isArray(targets) ? targets.filter(Boolean) : [];
    let remaining = sourceTargets.slice();
    let confirmedDeleted = [];
    let lastError = null;

    for (let round = 1; round <= maxRounds; round += 1) {
      await waitForTaskControl(taskControl);
      try {
        const items = await fetchCurrentList({ pageSize: verifiedPageSize, __returnBatchOnly: true });
        const existingIds = new Set((items || []).map((item) => String(item.fileId)));
        remaining = sourceTargets.filter((item) => existingIds.has(String(item.fileId)));
        confirmedDeleted = sourceTargets.filter((item) => !existingIds.has(String(item.fileId)));

        if (onProgress) {
          onProgress({
            visible: true,
            percent: Math.min(99, 82 + (round / maxRounds) * 16),
            indeterminate: false,
            text: `任务状态未确认,正在刷新目录核对删除结果 | 已确认 ${confirmedDeleted.length}/${sourceTargets.length} | 第 ${round}/${maxRounds} 次`,
          });
        }

        if (!remaining.length) {
          return {
            ok: true,
            deletedItems: confirmedDeleted,
            remaining: [],
            rounds: round,
            pageSize: verifiedPageSize,
          };
        }
      } catch (err) {
        lastError = err;
        warn('删除结果核对时刷新目录失败:', err);
      }

      if (round < maxRounds) {
        await controlledDelay(intervalMs, taskControl);
      }
    }

    return {
      ok: false,
      deletedItems: confirmedDeleted,
      remaining,
      rounds: maxRounds,
      pageSize: verifiedPageSize,
      error: lastError,
    };
  }

  async function verifyMovedItemsByList(targets, options = {}) {
    const onProgress = typeof options.onProgress === 'function' ? options.onProgress : null;
    const taskControl = options.taskControl || null;
    const maxRounds = Math.max(1, Number(options.maxRounds || 6));
    const intervalMs = Math.max(300, Number(options.intervalMs || 1500));
    const verifiedPageSize = Math.max(
      Number(UI.fields.pageSize?.value || 0),
      Number(CONFIG.request.manualListBody.pageSize || 0),
      Number(getCapturedItems().length || 0),
      Number((targets || []).length || 0) + 50,
      200
    );
    const sourceTargets = Array.isArray(targets) ? targets.filter(Boolean) : [];
    let remaining = sourceTargets.slice();
    let movedItems = [];
    let lastError = null;

    for (let round = 1; round <= maxRounds; round += 1) {
      await waitForTaskControl(taskControl);
      try {
        const items = await fetchCurrentList({ pageSize: verifiedPageSize, __returnBatchOnly: true });
        const existingIds = new Set((items || []).map((item) => String(item.fileId)));
        remaining = sourceTargets.filter((item) => existingIds.has(String(item.fileId)));
        movedItems = sourceTargets.filter((item) => !existingIds.has(String(item.fileId)));

        if (onProgress) {
          onProgress({
            visible: true,
            percent: Math.min(99, 82 + (round / maxRounds) * 16),
            indeterminate: false,
            text: `任务状态未确认,正在刷新目录核对移动结果 | 已确认移走 ${movedItems.length}/${sourceTargets.length} | 第 ${round}/${maxRounds} 次`,
          });
        }

        if (!remaining.length) {
          return {
            ok: true,
            movedItems,
            remaining: [],
            rounds: round,
            pageSize: verifiedPageSize,
          };
        }
      } catch (err) {
        lastError = err;
        warn('移动结果核对时刷新目录失败:', err);
      }

      if (round < maxRounds) {
        await controlledDelay(intervalMs, taskControl);
      }
    }

    return {
      ok: false,
      movedItems,
      remaining,
      rounds: maxRounds,
      pageSize: verifiedPageSize,
      error: lastError,
    };
  }

  async function deleteDuplicateItems(options = {}) {
    const onProgress = typeof options.onProgress === 'function' ? options.onProgress : null;
    const taskControl = options.taskControl || null;
    if (!STATE.duplicatePreviewItems.length) {
      await waitForTaskControl(taskControl);
      const previewItems = await previewDuplicates({
        refresh: options.refresh !== false,
        listBody: options.listBody || {},
      });
      setDuplicatePreview(previewItems);
      throw new Error(`已加载 ${previewItems.length} 个重复项。请先在面板里取消不想删的项目,再点“删除重复项”。`);
    }

    let duplicates = getSelectedDuplicatePreviewItems();

    if (!duplicates.length) {
      warn('当前面板里没有勾选任何重复项。');
      return { ok: 0, fail: 0, deleted: 0, taskId: '', duplicates };
    }

    if (duplicates.some((item) => String(item.fileId || '').startsWith('dom:'))) {
      await waitForTaskControl(taskControl);
      const ensured = await ensureDuplicateItemsHaveRealIds(STATE.duplicatePreviewItems, {
        onProgress,
      });

      if (ensured.mergedItems.length) {
        updateDuplicatePreviewResolvedItems(ensured.mergedItems);
      }
      duplicates = getSelectedDuplicatePreviewItems();

      if (duplicates.some((item) => String(item.fileId || '').startsWith('dom:'))) {
        const unresolvedSelected = duplicates.filter((item) => String(item.fileId || '').startsWith('dom:'));
        throw new Error(
          `当前仍有 ${unresolvedSelected.length} 个重复项没拿到真实 fileId。已自动补齐 ${ensured.resolved.length} 项;请稍等页面继续加载这一批项目,或下拉目录后再重试。`
        );
      }

      updatePanelStatus(`已自动补齐真实 fileId,准备删除 ${duplicates.length} 个重复项`);
    }

    if (CONFIG.batch.confirmBeforeRun && !window.confirm(`准备删除面板里已勾选的 ${duplicates.length} 个重复项,是否继续?`)) {
      return { ok: 0, fail: 0, deleted: 0, taskId: '', duplicates };
    }

    if (onProgress) {
      onProgress({
        visible: true,
        percent: 5,
        indeterminate: true,
        text: `正在提交删除任务,共 ${duplicates.length} 项`,
      });
    }

    await waitForTaskControl(taskControl);
    const deleteRes = await deleteFiles(duplicates.map((item) => item.fileId));
    if (!deleteRes.ok || !isProbablySuccess(deleteRes.payload, deleteRes)) {
      throw new Error(getErrorText(deleteRes.payload || deleteRes.text || `HTTP ${deleteRes.status}`));
    }

    const taskId = extractTaskId(deleteRes.payload);
    if (!taskId) {
      removeDuplicatePreviewItemsByIds(duplicates.map((item) => item.fileId));
      if (onProgress) {
        onProgress({
          visible: true,
          percent: 100,
          indeterminate: false,
          text: `删除请求已提交,共 ${duplicates.length} 项;接口未返回 taskId,无法轮询进度`,
        });
      }
      return {
        ok: duplicates.length,
        fail: 0,
        deleted: duplicates.length,
        taskId: '',
        duplicates,
        deleteRes,
      };
    }

    if (onProgress) {
      onProgress({
        visible: true,
        percent: 10,
        indeterminate: true,
        text: `删除任务已提交,taskId: ${taskId}`,
      });
    }

    const task = await waitTaskUntilDone(taskId, {
      onProgress,
      taskControl,
      expectedTotal: duplicates.length,
      maxTries: Math.max(CONFIG.batch.taskPollMaxTries || 180, 180),
      intervalMs: Math.max(CONFIG.batch.taskPollMs || 1500, 1500),
    });
    if (!task.ok) {
      const payload = task.result?.payload || task.result?.text || {};
      warn('删除任务轮询未确认,准备改用目录列表核对结果:', {
        taskId,
        payload,
      });

      const verification = await verifyDeletedItemsByList(duplicates, {
        onProgress,
        taskControl,
        maxRounds: 8,
        intervalMs: 1800,
      });

      if (verification.deletedItems.length) {
        removeDuplicatePreviewItemsByIds(verification.deletedItems.map((item) => item.fileId));
      }

      if (verification.ok) {
        if (onProgress) {
          onProgress({
            visible: true,
            percent: 100,
            indeterminate: false,
            text: `删除已确认完成,共 ${verification.deletedItems.length} 项,taskId: ${taskId}`,
          });
        }

        return {
          ok: verification.deletedItems.length,
          fail: 0,
          deleted: verification.deletedItems.length,
          taskId,
          duplicates,
          task,
          verification,
        };
      }

      const partialText = verification.deletedItems.length
        ? `;已确认删除 ${verification.deletedItems.length} 项,剩余 ${verification.remaining.length} 项未确认`
        : '';
      const verifyErrorText = verification.error ? `;列表核对失败:${getErrorText(verification.error)}` : '';
      throw new Error(
        task.timeout
          ? `删除任务超时,taskId: ${taskId}${partialText}${verifyErrorText}`
          : `删除任务失败,taskId: ${taskId},${getErrorText(payload) || '未返回更多信息'}${partialText}${verifyErrorText}`
      );
    }

    const taskCounts = extractTaskCounts(task.result?.payload, duplicates.length);
    const deletedCount = taskCounts.hasSuccessCount ? taskCounts.success : duplicates.length;
    const failedCount = taskCounts.hasFailedCount ? taskCounts.failed : 0;

    if (onProgress) {
      onProgress({
        visible: true,
        percent: 100,
        indeterminate: false,
        text: `删除完成,共处理 ${deletedCount + failedCount} 项,成功 ${deletedCount} 项,失败 ${failedCount} 项,taskId: ${taskId}`,
      });
    }

    removeDuplicatePreviewItemsByIds(duplicates.map((item) => item.fileId));

    return {
      ok: deletedCount,
      fail: failedCount,
      deleted: deletedCount,
      taskId,
      duplicates,
      task,
    };
  }

  async function deleteEmptyDirItems(options = {}) {
    const onProgress = typeof options.onProgress === 'function' ? options.onProgress : null;
    const taskControl = options.taskControl || null;
    const scan = STATE.lastEmptyDirScan || null;
    if (!scan || !Array.isArray(scan.emptyDirs) || !scan.emptyDirs.length) {
      const result = await scanEmptyLeafDirectories({
        onProgress,
        taskControl,
      });
      if (!result.emptyDirs.length) {
        return { ok: 0, fail: 0, deleted: 0, taskId: '', emptyDirs: [] };
      }
    }

    const targets = getSelectedEmptyDirItems();
    if (!targets.length) {
      warn('当前面板里没有勾选任何空目录。');
      return { ok: 0, fail: 0, deleted: 0, taskId: '', emptyDirs: [] };
    }

    if (CONFIG.batch.confirmBeforeRun && !window.confirm(`准备删除面板里已勾选的 ${targets.length} 个空目录,是否继续?`)) {
      return { ok: 0, fail: 0, deleted: 0, taskId: '', emptyDirs: targets };
    }

    if (onProgress) {
      onProgress({
        visible: true,
        percent: 5,
        indeterminate: true,
        text: `正在提交空目录删除任务,共 ${targets.length} 项`,
      });
    }

    await waitForTaskControl(taskControl);
    const deleteRes = await deleteFiles(targets.map((item) => item.fileId));
    if (!deleteRes.ok || !isProbablySuccess(deleteRes.payload, deleteRes)) {
      throw new Error(getErrorText(deleteRes.payload || deleteRes.text || `HTTP ${deleteRes.status}`));
    }

    const taskId = extractTaskId(deleteRes.payload);
    if (!taskId) {
      removeEmptyDirScanItemsByIds(targets.map((item) => item.fileId));
      if (onProgress) {
        onProgress({
          visible: true,
          percent: 100,
          indeterminate: false,
          text: `空目录删除请求已提交,共 ${targets.length} 项;接口未返回 taskId,先按成功处理`,
        });
      }
      return {
        ok: targets.length,
        fail: 0,
        deleted: targets.length,
        taskId: '',
        emptyDirs: targets,
        deleteRes,
      };
    }

    if (onProgress) {
      onProgress({
        visible: true,
        percent: 10,
        indeterminate: true,
        text: `空目录删除任务已提交,taskId: ${taskId}`,
      });
    }

    const task = await waitTaskUntilDone(taskId, {
      onProgress,
      taskControl,
      expectedTotal: targets.length,
      maxTries: Math.max(CONFIG.batch.taskPollMaxTries || 180, 180),
      intervalMs: Math.max(CONFIG.batch.taskPollMs || 1500, 1500),
    });
    if (!task.ok) {
      const payload = task.result?.payload || task.result?.text || {};
      throw new Error(
        task.timeout
          ? `空目录删除任务超时,taskId: ${taskId}`
          : `空目录删除任务失败,taskId: ${taskId},${getErrorText(payload) || '未返回更多信息'}`
      );
    }

    const taskCounts = extractTaskCounts(task.result?.payload, targets.length);
    const deletedCount = taskCounts.hasSuccessCount ? taskCounts.success : targets.length;
    const failedCount = taskCounts.hasFailedCount ? taskCounts.failed : 0;
    removeEmptyDirScanItemsByIds(targets.map((item) => item.fileId));

    if (onProgress) {
      onProgress({
        visible: true,
        percent: 100,
        indeterminate: false,
        text: `空目录删除完成,共处理 ${deletedCount + failedCount} 项,成功 ${deletedCount} 项,失败 ${failedCount} 项,taskId: ${taskId}`,
      });
    }

    return {
      ok: deletedCount,
      fail: failedCount,
      deleted: deletedCount,
      taskId,
      emptyDirs: targets,
      task,
    };
  }

  function getStatusSummary(extraText = '') {
    const mergedHeaders = getMergedHeaders();
    const context = getCurrentListContext();
    const magnetStats = getSelectedMagnetImportStats();
    const bits = [
      mergedHeaders.authorization ? '授权已就绪' : '授权未就绪',
      context.parentId ? `目录已识别` : '目录未识别',
      `已累计 ${context.capturedCount || getCapturedItems().length} 项`,
      `来源:${getItemsSourceLabel()}`,
      STATE.lastRenameRequest?.url ? '改名请求已学习' : '改名请求未学习',
    ];

    if (magnetStats.fileCount > 0) {
      bits.push(`磁力文本 ${magnetStats.fileCount} 个 / 磁力 ${magnetStats.magnetCount} 条`);
    }

    if (STATE.lastEmptyDirScan) {
      bits.push(`空目录 ${Number(STATE.lastEmptyDirScan.emptyDirs?.length || 0)} 个`);
    }
    if (STATE.lastDirectDownloadSummary?.linkCount) {
      bits.push(`直链 ${Number(STATE.lastDirectDownloadSummary.linkCount || 0)} 条${STATE.lastDirectDownloadSummary.failedCount ? ` / 失败 ${Number(STATE.lastDirectDownloadSummary.failedCount || 0)}` : ''}`);
    }

    if (context.batchCount > 1) {
      bits.push(`已合并 ${context.batchCount} 批`);
    }

    if (extraText) {
      bits.push(extraText);
    }
    return bits.join(' | ');
  }

  function updatePanelStatus(extraText = '') {
    if (UI.status) {
      UI.status.textContent = getStatusSummary(extraText);
    }
    if (UI.summary) {
      const context = getCurrentListContext();
      const duplicateSelected = getSelectedDuplicatePreviewItems().length;
      const duplicateTotal = (STATE.duplicatePreviewItems || []).length;
      const magnetStats = getSelectedMagnetImportStats();
      const cloudSummary = STATE.lastCloudImportSummary || null;
      const emptyDirSummary = STATE.lastEmptyDirScan || null;
      const authStatus = STATE.headers.authorization
        ? '已自动识别最新认证'
        : (CONFIG.request.manualHeaders.authorization ? '使用手填认证兜底' : '未识别');
      UI.summary.textContent = [
        `parentId: ${context.parentId || '(未获取)'}`,
        `当前目录累计: ${context.capturedCount || getCapturedItems().length}`,
        `最近一批: ${context.lastBatchSize || 0}`,
        `已捕获批次: ${context.batchCount || 0}`,
        `listUrl: ${context.listUrl || '(未识别)'}`,
        `认证: ${authStatus}`,
        `预处理: ${getRuleModeLabel()}`,
        `改名方式: ${getRenameOutputModeLabel()}`,
        `重复项编号: ${CONFIG.duplicate.numbers || DEFAULT_DUPLICATE_NUMBERS}`,
        `删除勾选: ${duplicateSelected}/${duplicateTotal}`,
        `磁力文本: ${magnetStats.fileCount} 个`,
        `磁力条数: ${magnetStats.magnetCount} 条`,
        `云添加每批: ${CONFIG.cloud.maxFilesPerTask || 500} 文件`,
        `云添加目录前缀: ${CONFIG.cloud.sourceDirPrefix || '磁力导入'}`,
        STATE.lastDirectDownloadSummary
          ? `最近直链下载: ${STATE.lastDirectDownloadSummary.linkCount || 0} 条 / ${STATE.lastDirectDownloadSummary.formattedTotalSize || '0 B'}${STATE.lastDirectDownloadSummary.failedCount ? ` / 失败 ${STATE.lastDirectDownloadSummary.failedCount}` : ''}`
          : '最近直链下载: 暂无记录',
        emptyDirSummary
          ? `最近空目录扫描: 空目录 ${emptyDirSummary.emptyDirs?.length || 0} / 已扫目录 ${emptyDirSummary.scannedDirs || 0}${emptyDirSummary.truncated ? ' / 可能未扫全' : ''}`
          : '最近空目录扫描: 暂无记录',
        cloudSummary
          ? `最近云添加: 成功磁力 ${cloudSummary.submittedMagnets || 0} / 跳过 ${cloudSummary.skippedMagnets || 0} / 失败 ${cloudSummary.failedMagnets || 0} / 提交批次 ${cloudSummary.submittedTaskBatches || 0}`
          : '最近云添加: 暂无记录',
        '说明: 页面继续下拉时,新一批 get_file_list 会自动累计进当前目录。',
      ].join('\n');
    }
  }


  function createTaskAbortError(message = '已停止当前任务') {
    const error = new Error(String(message || '已停止当前任务'));
    error.name = 'GypTaskAbortError';
    error.isUserAbort = true;
    return error;
  }

  function isTaskAbortError(err) {
    return Boolean(err && (err.isUserAbort || err.name === 'GypTaskAbortError'));
  }

  function getActiveTaskControl() {
    return STATE.activeTaskControl || null;
  }

  function releaseTaskControlWaiters(control) {
    if (!control || !Array.isArray(control.waiters) || !control.waiters.length) {
      return;
    }
    const waiters = control.waiters.splice(0, control.waiters.length);
    for (const resolve of waiters) {
      try {
        resolve();
      } catch {}
    }
  }

  function syncTaskControlUi() {
    if (!UI.pauseTaskButton || !UI.stopTaskButton) {
      return;
    }
    const control = getActiveTaskControl();
    const hasActive = Boolean(control);
    UI.pauseTaskButton.disabled = !hasActive;
    UI.stopTaskButton.disabled = !hasActive;
    UI.pauseTaskButton.textContent = hasActive && control.paused ? '继续' : '暂停';
  }

  function beginTaskControl(label = '') {
    const control = {
      id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
      label: String(label || '当前任务'),
      paused: false,
      stopped: false,
      waiters: [],
    };
    STATE.activeTaskControl = control;
    syncTaskControlUi();
    return control;
  }

  function finishTaskControl(control) {
    if (control && STATE.activeTaskControl === control) {
      STATE.activeTaskControl = null;
    }
    syncTaskControlUi();
  }

  function togglePauseActiveTask() {
    const control = getActiveTaskControl();
    if (!control || control.stopped) {
      return false;
    }

    control.paused = !control.paused;
    if (!control.paused) {
      releaseTaskControlWaiters(control);
    }
    syncTaskControlUi();

    const baseState = STATE.lastProgressState || {
      visible: true,
      percent: 0,
      indeterminate: true,
      text: control.label || '当前任务',
    };
    setProgressBar({
      ...baseState,
      visible: true,
      text: control.paused
        ? `已暂停 | ${baseState.text || control.label || '当前任务'}`
        : String(baseState.text || control.label || '当前任务').replace(/^已暂停\s*\|\s*/u, ''),
    });
    updatePanelStatus(`${control.paused ? '已暂停' : '已继续'}:${control.label || '当前任务'}`);
    return true;
  }

  function stopActiveTask() {
    const control = getActiveTaskControl();
    if (!control || control.stopped) {
      return false;
    }

    control.stopped = true;
    control.paused = false;
    releaseTaskControlWaiters(control);
    syncTaskControlUi();

    const baseState = STATE.lastProgressState || {
      visible: true,
      percent: 100,
      indeterminate: false,
      text: control.label || '当前任务',
    };
    setProgressBar({
      ...baseState,
      visible: true,
      indeterminate: false,
      percent: Math.max(0, Math.min(100, Number(baseState.percent || 100))),
      text: `正在停止 | ${baseState.text || control.label || '当前任务'}`,
    });
    updatePanelStatus(`正在停止:${control.label || '当前任务'}(会在当前步骤结束后停下)`);
    if (typeof control.abortCurrent === 'function') {
      try {
        control.abortCurrent();
      } catch {}
    }
    return true;
  }

  async function waitForTaskControl(taskControl) {
    if (!taskControl) {
      return;
    }
    if (taskControl.stopped) {
      throw createTaskAbortError(`${taskControl.label || '当前任务'}已停止`);
    }

    while (taskControl.paused) {
      await new Promise((resolve) => {
        taskControl.waiters.push(resolve);
      });
      if (taskControl.stopped) {
        throw createTaskAbortError(`${taskControl.label || '当前任务'}已停止`);
      }
    }

    if (taskControl.stopped) {
      throw createTaskAbortError(`${taskControl.label || '当前任务'}已停止`);
    }
  }

  async function controlledDelay(ms, taskControl) {
    let remaining = Math.max(0, Number(ms || 0));
    while (remaining > 0) {
      await waitForTaskControl(taskControl);
      const step = Math.min(150, remaining);
      await sleep(step);
      remaining -= step;
    }
    await waitForTaskControl(taskControl);
  }

  async function runWithTaskControl(label, taskRunner) {
    const control = beginTaskControl(label);
    try {
      return await taskRunner(control);
    } finally {
      finishTaskControl(control);
    }
  }

  function setProgressBar(state = {}) {
    if (!UI.progressWrap || !UI.progressBar || !UI.progressText) {
      return;
    }

    const visible = Boolean(state.visible);
    const percent = Math.max(0, Math.min(100, Number(state.percent || 0)));
    const indeterminate = Boolean(state.indeterminate);
    const control = getActiveTaskControl();
    let text = state.text || '';
    if (visible && control?.paused) {
      text = `已暂停 | ${text || control.label || '当前任务'}`;
    } else if (visible && control?.stopped) {
      text = `正在停止 | ${text || control.label || '当前任务'}`;
    }

    STATE.lastProgressState = {
      visible,
      percent,
      indeterminate,
      text: state.text || '',
    };

    UI.progressWrap.style.display = visible ? 'block' : 'none';
    if (!visible) {
      UI.progressBar.style.width = '0%';
      UI.progressText.textContent = '';
      return;
    }

    UI.progressBar.classList.toggle('gyp-indeterminate', indeterminate);
    UI.progressBar.style.width = indeterminate ? '36%' : `${percent}%`;
    UI.progressText.textContent = text;
  }

  function syncPanelFromConfig(options = {}) {
    if (!UI.root) {
      return;
    }

    const fillEmptyOnly = Boolean(options.fillEmptyOnly);
    const firstRule = CONFIG.rename.rules[0] || {};
    const context = getCurrentListContext();
    const ruleMode = getCurrentRuleMode(firstRule);
    const output = CONFIG.rename.output || {};
    const values = {
      ruleMode,
      outputMode: output.mode || 'keep-clean',
      authorization: STATE.headers.authorization || CONFIG.request.manualHeaders.authorization || '',
      did: STATE.headers.did || CONFIG.request.manualHeaders.did || '',
      dt: STATE.headers.dt || CONFIG.request.manualHeaders.dt || '',
      parentId: context.parentId || CONFIG.request.manualListBody.parentId || '',
      pageSize: String(CONFIG.request.manualListBody.pageSize || context.pageSize || 100),
      template: output.template || CONFIG.rename.template || '{clean}',
      duplicateNumbers: CONFIG.duplicate.numbers || DEFAULT_DUPLICATE_NUMBERS,
      cloudBatchLimit: String(CONFIG.cloud.maxFilesPerTask || 500),
      cloudDirPrefix: CONFIG.cloud.sourceDirPrefix || '磁力导入',
      moveTargetParentId: CONFIG.move.targetParentId || '',
      mediaTmdbApiKey: CONFIG.mediaOrganize.tmdbApiKey || '',
      mediaRootParentId: CONFIG.mediaOrganize.rootParentId || '',
      mediaTmdbLanguage: CONFIG.mediaOrganize.tmdbLanguage || 'zh-CN',
      mediaUseFolderNameFirst: CONFIG.mediaOrganize.useFolderNameFirst !== false,
      mediaIncludeTitleFolder: CONFIG.mediaOrganize.includeTitleFolder !== false,
      mediaIncludeRegionFolder: CONFIG.mediaOrganize.includeRegionFolder !== false,
      mediaIncludeSeasonFolder: CONFIG.mediaOrganize.includeSeasonFolder !== false,
      mediaMoveBySourceFolder: CONFIG.mediaOrganize.moveBySourceFolder !== false,
      mediaCleanupEmptySourceFolders: CONFIG.mediaOrganize.cleanupEmptySourceFolders !== false,
      mediaSkipDuplicateTargets: CONFIG.mediaOrganize.skipDuplicateTargets !== false,
      mediaBatchSize: String(CONFIG.mediaOrganize.batchSize || 10),
      directDownloadBatchSize: String(CONFIG.download.directBatchSize || 3),
      directDownloadExportFormat: normalizeDirectDownloadExportFormat(CONFIG.download.exportFormat),
      ruleSearchText: firstRule.type === 'text' ? (firstRule.search || '') : '',
      ruleReplaceText: firstRule.type === 'text' ? (firstRule.replace || '') : '',
      addText: output.addText || '',
      addPosition: output.addPosition || 'suffix',
      addIgnoreExtension: output.addIgnoreExtension !== false,
      outputFindText: output.findText || '',
      outputReplaceText: output.replaceText || '',
      formatStyle: output.formatStyle || 'text-and-index',
      formatText: output.formatText || '文件',
      formatPosition: output.formatPosition || 'suffix',
      startIndex: String(output.startIndex ?? 0),
      exampleName: getDefaultExampleName(),
      rulePattern: firstRule.pattern || DEFAULT_LEADING_BRACKET_PATTERN,
      ruleFlags: firstRule.flags || '',
      ruleReplace: firstRule.replace || '',
      delayMs: String(CONFIG.batch.delayMs ?? 300),
    };

    for (const [key, value] of Object.entries(values)) {
      const el = UI.fields[key];
      if (!el) {
        continue;
      }
      if (el.type === 'checkbox') {
        el.checked = Boolean(value);
        continue;
      }
      if (!fillEmptyOnly || !el.value) {
        el.value = value;
      }
    }

    updatePanelStatus();
    updateRenameModePreview();
  }

  function applyPanelConfig() {
    if (!UI.root) {
      return;
    }

    const firstRule = CONFIG.rename.rules[0] || { enabled: true, type: 'regex' };
    CONFIG.rename.rules[0] = firstRule;
    const ruleMode = UI.fields.ruleMode?.value || 'remove-leading-bracket';

    CONFIG.request.manualHeaders.authorization = (UI.fields.authorization?.value || '').trim();
    CONFIG.request.manualHeaders.did = (UI.fields.did?.value || '').trim();
    CONFIG.request.manualHeaders.dt = (UI.fields.dt?.value || '').trim();
    CONFIG.request.manualListBody.parentId = (UI.fields.parentId?.value || '').trim();

    const pageSize = Number(UI.fields.pageSize?.value || 100);
    CONFIG.request.manualListBody.pageSize = Number.isFinite(pageSize) && pageSize > 0 ? pageSize : 100;

    CONFIG.rename.template = (UI.fields.template?.value || '{clean}').trim() || '{clean}';
    CONFIG.rename.ruleMode = ruleMode;
    firstRule.enabled = ruleMode !== 'none';
    firstRule.type = 'regex';
    firstRule.search = '';
    if (ruleMode === 'remove-leading-bracket') {
      firstRule.pattern = DEFAULT_LEADING_BRACKET_PATTERN;
      firstRule.flags = 'u';
      firstRule.replace = '';
    } else if (ruleMode === 'replace-text') {
      firstRule.type = 'text';
      firstRule.pattern = '';
      firstRule.flags = '';
      firstRule.search = UI.fields.ruleSearchText?.value || '';
      firstRule.replace = UI.fields.ruleReplaceText?.value || '';
    } else if (ruleMode === 'custom-regex') {
      firstRule.pattern = UI.fields.rulePattern?.value || '';
      firstRule.flags = UI.fields.ruleFlags?.value || '';
      firstRule.replace = UI.fields.ruleReplace?.value || '';
    }

    CONFIG.rename.output.mode = UI.fields.outputMode?.value || 'keep-clean';
    CONFIG.rename.output.addText = UI.fields.addText?.value || '';
    CONFIG.rename.output.addPosition = UI.fields.addPosition?.value || 'suffix';
    CONFIG.rename.output.addIgnoreExtension = UI.fields.addIgnoreExtension?.checked !== false;
    CONFIG.rename.output.findText = UI.fields.outputFindText?.value || '';
    CONFIG.rename.output.replaceText = UI.fields.outputReplaceText?.value || '';
    CONFIG.rename.output.formatStyle = UI.fields.formatStyle?.value || 'text-and-index';
    CONFIG.rename.output.formatText = UI.fields.formatText?.value || '文件';
    CONFIG.rename.output.formatPosition = UI.fields.formatPosition?.value || 'suffix';
    const startIndex = Number(UI.fields.startIndex?.value || 0);
    CONFIG.rename.output.startIndex = Number.isFinite(startIndex) ? startIndex : 0;
    CONFIG.rename.output.template = CONFIG.rename.template;

    CONFIG.duplicate.mode = 'numbers';
    CONFIG.duplicate.numbers = (UI.fields.duplicateNumbers?.value || DEFAULT_DUPLICATE_NUMBERS).trim() || DEFAULT_DUPLICATE_NUMBERS;
    CONFIG.duplicate.pattern = buildDuplicatePatternFromNumbers(CONFIG.duplicate.numbers);
    CONFIG.duplicate.flags = 'u';

    const cloudBatchLimit = Number(UI.fields.cloudBatchLimit?.value || 500);
    CONFIG.cloud.maxFilesPerTask = Number.isFinite(cloudBatchLimit) && cloudBatchLimit > 0 ? Math.max(1, cloudBatchLimit) : 500;
    CONFIG.cloud.sourceDirPrefix = (UI.fields.cloudDirPrefix?.value || '磁力导入').trim() || '磁力导入';
    CONFIG.move.targetParentId = (UI.fields.moveTargetParentId?.value || '').trim();
    CONFIG.mediaOrganize.tmdbApiKey = (UI.fields.mediaTmdbApiKey?.value || '').trim();
    CONFIG.mediaOrganize.rootParentId = (UI.fields.mediaRootParentId?.value || '').trim();
    CONFIG.mediaOrganize.tmdbLanguage = (UI.fields.mediaTmdbLanguage?.value || 'zh-CN').trim() || 'zh-CN';
    CONFIG.mediaOrganize.useFolderNameFirst = UI.fields.mediaUseFolderNameFirst?.checked !== false;
    CONFIG.mediaOrganize.includeTitleFolder = UI.fields.mediaIncludeTitleFolder?.checked !== false;
    CONFIG.mediaOrganize.includeRegionFolder = UI.fields.mediaIncludeRegionFolder?.checked !== false;
    CONFIG.mediaOrganize.includeSeasonFolder = UI.fields.mediaIncludeSeasonFolder?.checked !== false;
    CONFIG.mediaOrganize.moveBySourceFolder = UI.fields.mediaMoveBySourceFolder?.checked !== false;
    CONFIG.mediaOrganize.cleanupEmptySourceFolders = UI.fields.mediaCleanupEmptySourceFolders?.checked !== false;
    CONFIG.mediaOrganize.skipDuplicateTargets = UI.fields.mediaSkipDuplicateTargets?.checked !== false;
    const mediaBatchSize = Number(UI.fields.mediaBatchSize?.value || 10);
    CONFIG.mediaOrganize.batchSize = Number.isFinite(mediaBatchSize) && mediaBatchSize > 0 ? Math.max(1, mediaBatchSize) : 10;
    const directDownloadBatchSize = Number(UI.fields.directDownloadBatchSize?.value || 3);
    CONFIG.download.directBatchSize = Number.isFinite(directDownloadBatchSize) && directDownloadBatchSize > 0 ? Math.max(1, directDownloadBatchSize) : 3;
    CONFIG.download.exportFormat = normalizeDirectDownloadExportFormat(UI.fields.directDownloadExportFormat?.value || CONFIG.download.exportFormat);

    const delayMs = Number(UI.fields.delayMs?.value || 300);
    CONFIG.batch.delayMs = Number.isFinite(delayMs) && delayMs >= 0 ? delayMs : 300;

    savePersistedConfig();
    updatePanelStatus('配置已应用');
    updateRenameModePreview();
  }

  function fillPanelFromCaptured() {
    if (!UI.root) {
      return;
    }

    const context = getCurrentListContext();
    if (UI.fields.did && STATE.headers.did) {
      UI.fields.did.value = STATE.headers.did;
    }
    if (UI.fields.dt && STATE.headers.dt) {
      UI.fields.dt.value = STATE.headers.dt;
    }
    if (UI.fields.parentId && context.parentId) {
      UI.fields.parentId.value = context.parentId;
    }
    if (UI.fields.pageSize && context.pageSize) {
      UI.fields.pageSize.value = String(context.pageSize);
    }

    if (UI.fields.authorization) {
      UI.fields.authorization.value =
        STATE.headers.authorization || CONFIG.request.manualHeaders.authorization || UI.fields.authorization.value || '';
    }

    updatePanelStatus('已把可见上下文填入表单');
    updateRenameModePreview();
  }

  function setPanelBusy(busy) {
    if (!UI.root) {
      return;
    }
    UI.root.querySelectorAll('button, input, textarea, select').forEach((el) => {
      if (el.dataset.keepEnabled === 'true') {
        return;
      }
      el.disabled = busy;
    });
  }

  function reorderPanelSections(root) {
    const container = root?.querySelector?.('.gyp-sections');
    if (!container) {
      return;
    }
    [
      'direct-download-details',
      'share-link-details',
      'miaochuan-details',
      'magnet-details',
      'media-organize-details',
      'move-details',
      'rename-details',
      'duplicate-details',
      'empty-dir-details',
      'advanced-details',
    ].forEach((role) => {
      const section = container.querySelector(`[data-role="${role}"]`);
      if (section) {
        container.appendChild(section);
      }
    });
  }

  function closeFeatureSections(root = UI.root) {
    root?.querySelectorAll?.('.gyp-sections > details.gyp-section').forEach((section) => {
      section.open = false;
    });
  }

  function buildGuideCard(title, steps = [], note = '') {
    const stepHtml = (Array.isArray(steps) ? steps : [])
      .map((step, index) => `
        <div class="gyp-guide-step">
          <span class="gyp-guide-number">${index + 1}</span>
          <span class="gyp-guide-text">${escapeHtml(step)}</span>
        </div>
      `)
      .join('<span class="gyp-guide-arrow" aria-hidden="true">→</span>');
    return `
      <div class="gyp-guide-card">
        <div class="gyp-guide-title">${escapeHtml(title)}</div>
        <div class="gyp-guide-flow">${stepHtml}</div>
        ${note ? `<div class="gyp-guide-note">${escapeHtml(note)}</div>` : ''}
      </div>
    `;
  }

  function createPanel() {
    if (UI.root || !document.body) {
      return;
    }

    const style = document.createElement('style');
    style.textContent = `
      #gyp-batch-rename-root {
        position: fixed;
        right: 16px;
        bottom: 16px;
        z-index: 2147483647;
        font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
        color: #172033;
        width: 64px;
        height: 64px;
      }
      #gyp-batch-rename-root .gyp-fab {
        position: absolute;
        right: 0;
        bottom: 0;
        width: 62px;
        height: 62px;
        padding: 0;
        border: 0;
        border-radius: 999px;
        cursor: pointer;
        display: flex;
        align-items: center;
        justify-content: center;
        background: radial-gradient(circle at 30% 30%, #ffffff 0%, #edf5ff 100%);
        border: 1px solid rgba(15, 23, 42, 0.08);
        box-shadow: 0 16px 34px rgba(15, 98, 254, 0.24);
        overflow: hidden;
        user-select: none;
        transition: transform 0.18s ease, box-shadow 0.18s ease;
      }
      #gyp-batch-rename-root .gyp-fab:hover {
        transform: translateY(-1px) scale(1.02);
        box-shadow: 0 20px 40px rgba(15, 98, 254, 0.3);
      }
      #gyp-batch-rename-root .gyp-fab:active {
        transform: scale(0.98);
      }
      #gyp-batch-rename-root .gyp-fab-icon {
        width: 78%;
        height: 78%;
        display: flex;
        align-items: center;
        justify-content: center;
      }
      #gyp-batch-rename-root .gyp-fab svg {
        display: block;
        width: 100%;
        height: 100%;
      }
      #gyp-batch-rename-root .gyp-panel {
        position: absolute;
        right: 0;
        bottom: 72px;
        width: min(468px, calc(100vw - 24px));
        height: min(760px, calc(100vh - 96px));
        max-height: calc(100vh - 96px);
        overflow-x: hidden;
        overflow-y: auto;
        padding: 14px 14px 24px;
        box-sizing: border-box;
        border: 1px solid rgba(15, 23, 42, 0.14);
        border-radius: 18px;
        background: rgba(255, 255, 255, 0.96);
        box-shadow: 0 18px 48px rgba(15, 23, 42, 0.18);
        backdrop-filter: blur(12px);
        opacity: 0;
        transform: translateY(8px) scale(0.98);
        transform-origin: bottom right;
        pointer-events: none;
        transition: opacity 0.18s ease, transform 0.18s ease;
        overscroll-behavior: contain;
        scrollbar-gutter: stable;
      }
      #gyp-batch-rename-root.gyp-open .gyp-panel {
        opacity: 1;
        transform: translateY(0) scale(1);
        pointer-events: auto;
      }
      #gyp-batch-rename-root .gyp-head {
        display: flex;
        flex-wrap: wrap;
        justify-content: space-between;
        align-items: flex-start;
        margin-bottom: 10px;
        gap: 10px;
      }
      #gyp-batch-rename-root .gyp-head > div:first-child {
        min-width: 0;
        flex: 1 1 260px;
      }
      #gyp-batch-rename-root .gyp-title-row {
        display: flex;
        align-items: center;
        gap: 10px;
        min-width: 0;
      }
      #gyp-batch-rename-root .gyp-title-mark {
        width: 28px;
        height: 28px;
        flex: 0 0 auto;
        object-fit: contain;
        border-radius: 8px;
      }
      #gyp-batch-rename-root .gyp-title-stack {
        min-width: 0;
      }
      #gyp-batch-rename-root .gyp-title {
        font-size: 14px;
        font-weight: 700;
      }
      #gyp-batch-rename-root .gyp-subtitle {
        margin-top: 2px;
        font-size: 11px;
        color: #667085;
      }
      #gyp-batch-rename-root .gyp-version {
        margin-top: 2px;
        font-size: 11px;
        color: #0f62fe;
        font-weight: 600;
      }
      #gyp-batch-rename-root .gyp-head button,
      #gyp-batch-rename-root .gyp-actions button,
      #gyp-batch-rename-root .gyp-section-actions button,
      #gyp-batch-rename-root .gyp-config-actions button {
        border: 0;
        border-radius: 10px;
        background: #0f62fe;
        color: #fff;
        padding: 8px 10px;
        cursor: pointer;
        font-size: 12px;
      }
      #gyp-batch-rename-root .gyp-head button {
        padding: 7px 10px;
        white-space: nowrap;
      }
      #gyp-batch-rename-root .gyp-head button.secondary,
      #gyp-batch-rename-root .gyp-actions button.secondary,
      #gyp-batch-rename-root .gyp-section-actions button.secondary,
      #gyp-batch-rename-root .gyp-config-actions button.secondary {
        background: #eef2ff;
        color: #1f2a44;
      }
      #gyp-batch-rename-root .gyp-head button.danger,
      #gyp-batch-rename-root .gyp-actions button.danger,
      #gyp-batch-rename-root .gyp-section-actions button.danger,
      #gyp-batch-rename-root .gyp-config-actions button.danger {
        background: #d92d20;
        color: #fff;
      }
      #gyp-batch-rename-root .gyp-section-actions button[data-variant="primary-step"] {
        background: linear-gradient(135deg, #155eef, #0f62fe);
        color: #fff;
        font-weight: 800;
        box-shadow: 0 10px 22px rgba(15, 98, 254, 0.22);
      }
      #gyp-batch-rename-root .gyp-section-actions button[data-variant="success-step"] {
        background: linear-gradient(135deg, #119c59, #16a34a);
        color: #fff;
        font-weight: 800;
        box-shadow: 0 10px 22px rgba(22, 163, 74, 0.22);
      }
      #gyp-batch-rename-root .gyp-section-actions button[data-variant="primary-step"]:hover,
      #gyp-batch-rename-root .gyp-section-actions button[data-variant="success-step"]:hover {
        filter: brightness(1.04);
        transform: translateY(-1px);
      }
      #gyp-batch-rename-root .gyp-section-actions button[data-action="generate-miaochuan-from-page"] {
        background: linear-gradient(135deg, #155eef, #0f62fe);
        color: #fff;
        font-weight: 700;
        box-shadow: 0 8px 18px rgba(15, 98, 254, 0.18);
      }
      #gyp-batch-rename-root .gyp-section-actions button[data-action="import-miaochuan-to-guangya"] {
        background: linear-gradient(135deg, #119c59, #16a34a);
        color: #fff;
        font-weight: 700;
        box-shadow: 0 8px 18px rgba(22, 163, 74, 0.18);
      }
      #gyp-batch-rename-root .gyp-status {
        margin-bottom: 10px;
        padding: 10px;
        border-radius: 10px;
        background: #f5f8ff;
        font-size: 12px;
        line-height: 1.5;
        white-space: pre-wrap;
        overflow-wrap: anywhere;
        word-break: break-word;
      }
      #gyp-batch-rename-root .gyp-progress {
        display: none;
        margin-bottom: 10px;
        padding: 10px;
        border-radius: 10px;
        background: #eef4ff;
      }
      #gyp-batch-rename-root .gyp-progress-track {
        height: 10px;
        border-radius: 999px;
        background: rgba(15, 98, 254, 0.12);
        overflow: hidden;
      }
      #gyp-batch-rename-root .gyp-progress-bar {
        width: 0%;
        height: 100%;
        border-radius: 999px;
        background: linear-gradient(90deg, #0f62fe, #45a1ff);
        transition: width 0.2s ease;
      }
      #gyp-batch-rename-root .gyp-progress-bar.gyp-indeterminate {
        background: linear-gradient(90deg, #0f62fe 0%, #69baff 48%, #0f62fe 100%);
        animation: gyp-progress-indeterminate 1.15s ease-in-out infinite;
        will-change: transform;
      }
      #gyp-batch-rename-root .gyp-progress-text {
        margin-top: 8px;
        font-size: 12px;
        line-height: 1.5;
        color: #26437a;
        white-space: pre-wrap;
        overflow-wrap: anywhere;
        word-break: break-word;
      }
      #gyp-batch-rename-root .gyp-progress-tools {
        display: flex;
        gap: 8px;
        margin-top: 8px;
      }
      #gyp-batch-rename-root .gyp-progress-tools button {
        flex: 1 1 0;
        font-weight: 700;
        border: 1px solid transparent;
        border-radius: 14px;
        min-height: 42px;
        padding: 10px 12px;
      }
      #gyp-batch-rename-root .gyp-progress-tools button.secondary {
        background: #facc15;
        color: #422006;
        border-color: #eab308;
      }
      #gyp-batch-rename-root .gyp-progress-tools button.secondary:hover:not(:disabled) {
        background: #eab308;
      }
      #gyp-batch-rename-root .gyp-progress-tools button.danger {
        background: #dc2626;
        color: #fff;
        border-color: #b91c1c;
      }
      #gyp-batch-rename-root .gyp-progress-tools button.danger:hover:not(:disabled) {
        background: #b91c1c;
      }
      #gyp-batch-rename-root .gyp-progress-tools button:disabled {
        opacity: 0.45;
        cursor: not-allowed;
      }
      @keyframes gyp-progress-indeterminate {
        0% {
          transform: translateX(-130%);
        }
        100% {
          transform: translateX(260%);
        }
      }
      #gyp-batch-rename-root .gyp-actions,
      #gyp-batch-rename-root .gyp-config-actions {
        display: flex;
        flex-wrap: wrap;
        gap: 8px;
        margin-bottom: 10px;
      }
      #gyp-batch-rename-root .gyp-sections {
        display: flex;
        flex-direction: column;
        gap: 10px;
        margin-bottom: 10px;
      }
      #gyp-batch-rename-root .gyp-section {
        margin: 0;
      }
      #gyp-batch-rename-root .gyp-section > summary {
        list-style: none;
      }
      #gyp-batch-rename-root .gyp-section > summary::-webkit-details-marker {
        display: none;
      }
      #gyp-batch-rename-root .gyp-section-summary {
        display: flex;
        align-items: center;
        justify-content: space-between;
        gap: 10px;
      }
      #gyp-batch-rename-root .gyp-section-headline {
        display: flex;
        flex-direction: column;
        gap: 2px;
        min-width: 0;
      }
      #gyp-batch-rename-root .gyp-section-title-line {
        display: inline-flex;
        align-items: center;
        gap: 8px;
        min-width: 0;
      }
      #gyp-batch-rename-root .gyp-section-icon {
        flex: 0 0 auto;
        font-size: 16px;
        line-height: 1;
      }
      #gyp-batch-rename-root .gyp-section-title {
        font-size: 13px;
        font-weight: 700;
        color: #22324d;
      }
      #gyp-batch-rename-root .gyp-section-desc {
        font-size: 11px;
        line-height: 1.45;
        color: #667085;
      }
      #gyp-batch-rename-root .gyp-section-badge {
        flex: 0 0 auto;
        padding: 4px 8px;
        border-radius: 999px;
        background: #eef4ff;
        color: #26437a;
        font-size: 11px;
        font-weight: 700;
        white-space: nowrap;
      }
      #gyp-batch-rename-root .gyp-section-body {
        margin-top: 10px;
        display: flex;
        flex-direction: column;
        gap: 10px;
      }
      #gyp-batch-rename-root .gyp-section-actions {
        display: flex;
        flex-wrap: wrap;
        gap: 8px;
      }
      #gyp-batch-rename-root .gyp-section-actions button {
        flex: 1 1 calc(50% - 4px);
      }
      #gyp-batch-rename-root .gyp-section-note {
        font-size: 11px;
        line-height: 1.5;
        color: #667085;
      }
      #gyp-batch-rename-root .gyp-guide-card {
        padding: 10px;
        border-radius: 14px;
        background:
          radial-gradient(circle at 8% 12%, rgba(15, 98, 254, 0.14), transparent 34%),
          linear-gradient(135deg, #f8fbff, #eef6ff);
        border: 1px solid rgba(15, 98, 254, 0.16);
        box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.75);
      }
      #gyp-batch-rename-root .gyp-guide-title {
        font-size: 12px;
        font-weight: 800;
        color: #14315f;
        margin-bottom: 8px;
      }
      #gyp-batch-rename-root .gyp-guide-flow {
        display: flex;
        align-items: stretch;
        gap: 6px;
      }
      #gyp-batch-rename-root .gyp-guide-step {
        flex: 1 1 0;
        min-width: 0;
        display: flex;
        align-items: center;
        gap: 7px;
        padding: 8px 9px;
        border-radius: 12px;
        background: rgba(255, 255, 255, 0.86);
        border: 1px solid rgba(15, 23, 42, 0.08);
      }
      #gyp-batch-rename-root .gyp-guide-number {
        flex: 0 0 20px;
        width: 20px;
        height: 20px;
        display: inline-flex;
        align-items: center;
        justify-content: center;
        border-radius: 999px;
        background: #0f62fe;
        color: #fff;
        font-size: 11px;
        font-weight: 800;
      }
      #gyp-batch-rename-root .gyp-guide-text {
        min-width: 0;
        font-size: 11px;
        line-height: 1.4;
        color: #23314b;
        font-weight: 700;
      }
      #gyp-batch-rename-root .gyp-guide-arrow {
        flex: 0 0 auto;
        display: inline-flex;
        align-items: center;
        color: #0f62fe;
        font-weight: 800;
      }
      #gyp-batch-rename-root .gyp-guide-note {
        margin-top: 8px;
        padding: 7px 9px;
        border-radius: 10px;
        background: rgba(15, 98, 254, 0.08);
        color: #475467;
        font-size: 11px;
        line-height: 1.45;
      }
      #gyp-batch-rename-root .gyp-miaochuan-diagnosis {
        padding: 10px;
        border-radius: 12px;
        background: #fff7ed;
        border: 1px solid #fed7aa;
        color: #7c2d12;
        font-size: 12px;
        line-height: 1.6;
        white-space: pre-wrap;
        overflow-wrap: anywhere;
        word-break: break-word;
      }
      #gyp-batch-rename-root .gyp-miaochuan-input {
        min-height: 160px;
        max-height: min(34vh, 280px);
        overflow: auto;
      }
      #gyp-batch-rename-root .gyp-miaochuan-log {
        min-height: 86px;
        max-height: min(22vh, 180px);
        overflow: auto;
      }
      #gyp-batch-rename-root .gyp-miaochuan-token {
        min-height: 56px;
        max-height: 120px;
        font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
        font-size: 11px;
        overflow: auto;
      }
      #gyp-batch-rename-root .gyp-miaochuan-output,
      #gyp-batch-rename-root .gyp-miaochuan-report {
        height: clamp(140px, 24vh, 220px);
        min-height: 130px;
        max-height: min(34vh, 300px);
        overflow: auto;
        font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
        font-size: 11px;
        line-height: 1.55;
      }
      #gyp-batch-rename-root .gyp-actions button {
        flex: 1 1 calc(50% - 4px);
      }
      #gyp-batch-rename-root details {
        margin: 10px 0;
        border: 1px solid rgba(15, 23, 42, 0.1);
        border-radius: 10px;
        padding: 10px;
        background: #fbfcfe;
      }
      #gyp-batch-rename-root summary {
        cursor: pointer;
        font-size: 13px;
        font-weight: 600;
      }
      #gyp-batch-rename-root .gyp-field {
        display: flex;
        flex-direction: column;
        gap: 6px;
        margin-top: 10px;
      }
      #gyp-batch-rename-root .gyp-field span {
        font-size: 12px;
        color: #344054;
      }
      #gyp-batch-rename-root .gyp-field input,
      #gyp-batch-rename-root .gyp-field select,
      #gyp-batch-rename-root .gyp-field textarea {
        width: 100%;
        box-sizing: border-box;
        border: 1px solid #d0d5dd;
        border-radius: 8px;
        padding: 8px 10px;
        font-size: 12px;
        color: #172033;
        background: #fff;
      }
      #gyp-batch-rename-root .gyp-field textarea {
        min-height: 58px;
        resize: vertical;
      }
      #gyp-batch-rename-root .gyp-inline-help {
        font-size: 11px;
        line-height: 1.5;
        color: #667085;
        overflow-wrap: anywhere;
        word-break: break-word;
      }
      #gyp-batch-rename-root .gyp-example {
        margin-top: 10px;
        padding: 10px;
        border-radius: 12px;
        background: #f7faff;
        border: 1px solid rgba(15, 98, 254, 0.12);
      }
      #gyp-batch-rename-root .gyp-example-title {
        font-size: 12px;
        font-weight: 700;
        color: #22324d;
        margin-bottom: 8px;
      }
      #gyp-batch-rename-root .gyp-example-row {
        margin-top: 8px;
        display: flex;
        flex-direction: column;
        gap: 4px;
      }
      #gyp-batch-rename-root .gyp-example-label {
        font-size: 11px;
        color: #667085;
      }
      #gyp-batch-rename-root .gyp-example-value {
        font-size: 12px;
        line-height: 1.5;
        color: #172033;
        padding: 8px 10px;
        border-radius: 8px;
        background: #fff;
        border: 1px solid rgba(15, 23, 42, 0.08);
        overflow-wrap: anywhere;
        word-break: break-word;
      }
      #gyp-batch-rename-root .gyp-advanced {
        margin-top: 12px;
        border-top: 1px dashed rgba(15, 23, 42, 0.12);
        padding-top: 10px;
      }
      #gyp-batch-rename-root .gyp-advanced-title {
        font-size: 12px;
        font-weight: 600;
        color: #344054;
      }
      #gyp-batch-rename-root .gyp-help,
      #gyp-batch-rename-root .gyp-summary {
        font-size: 12px;
        line-height: 1.5;
        color: #475467;
        white-space: pre-wrap;
        overflow-wrap: anywhere;
        word-break: break-word;
      }
      #gyp-batch-rename-root .gyp-summary {
        padding: 10px;
        border-radius: 10px;
        background: #fff7ed;
        margin-top: 10px;
      }
      #gyp-batch-rename-root .gyp-debug-details {
        margin-top: 8px;
        border: 1px dashed rgba(15, 23, 42, 0.12);
        border-radius: 10px;
        padding: 8px 10px;
        background: #fcfcfd;
      }
      #gyp-batch-rename-root .gyp-debug-details > summary {
        font-size: 12px;
        font-weight: 600;
        color: #667085;
      }
      #gyp-batch-rename-root .gyp-debug-details .gyp-summary {
        margin-top: 8px;
        margin-bottom: 0;
      }
      #gyp-batch-rename-root .gyp-duplicate-panel {
        margin-bottom: 0;
        border: 1px solid rgba(15, 23, 42, 0.1);
        border-radius: 12px;
        background: #f9fbff;
        padding: 10px;
      }
      #gyp-batch-rename-root .gyp-duplicate-head {
        display: flex;
        align-items: center;
        justify-content: space-between;
        gap: 8px;
        margin-bottom: 8px;
      }
      #gyp-batch-rename-root .gyp-duplicate-title {
        font-size: 12px;
        font-weight: 700;
        color: #22324d;
      }
      #gyp-batch-rename-root .gyp-duplicate-tools {
        display: flex;
        gap: 6px;
      }
      #gyp-batch-rename-root .gyp-duplicate-tools button {
        border: 0;
        border-radius: 8px;
        padding: 6px 8px;
        font-size: 11px;
        background: #eef2ff;
        color: #22324d;
        cursor: pointer;
      }
      #gyp-batch-rename-root .gyp-duplicate-list {
        max-height: 180px;
        overflow-x: hidden;
        overflow-y: auto;
        border-radius: 10px;
        background: #fff;
        border: 1px solid rgba(15, 23, 42, 0.08);
      }
      #gyp-batch-rename-root .gyp-duplicate-row {
        display: flex;
        align-items: flex-start;
        gap: 8px;
        padding: 8px 10px;
        border-bottom: 1px solid rgba(15, 23, 42, 0.06);
        cursor: pointer;
      }
      #gyp-batch-rename-root .gyp-duplicate-row:last-child {
        border-bottom: 0;
      }
      #gyp-batch-rename-root .gyp-duplicate-row input {
        margin-top: 2px;
        flex: 0 0 auto;
      }
      #gyp-batch-rename-root .gyp-check-field {
        flex-direction: row;
        align-items: flex-start;
        gap: 8px;
      }
      #gyp-batch-rename-root .gyp-check-field input[type="checkbox"] {
        width: auto;
        margin-top: 2px;
        flex: 0 0 auto;
      }
      #gyp-batch-rename-root .gyp-check-field span {
        line-height: 1.45;
      }
      #gyp-batch-rename-root .gyp-duplicate-name {
        font-size: 12px;
        line-height: 1.45;
        color: #23314b;
        overflow-wrap: anywhere;
        word-break: break-word;
      }
      #gyp-batch-rename-root .gyp-duplicate-empty {
        padding: 12px 10px;
        font-size: 12px;
        line-height: 1.5;
        color: #667085;
      }
      #gyp-batch-rename-root .gyp-import-details {
        margin-top: 0;
      }
      #gyp-batch-rename-root .gyp-import-summary {
        display: inline-flex;
        align-items: center;
        gap: 6px;
        font-size: 12px;
        font-weight: 600;
        color: #22324d;
      }
      #gyp-batch-rename-root .gyp-import-list {
        margin-top: 10px;
        max-height: 140px;
        overflow-x: hidden;
        overflow-y: auto;
        border-radius: 10px;
        background: #fff;
        border: 1px solid rgba(15, 23, 42, 0.08);
      }
      #gyp-batch-rename-root .gyp-import-row {
        padding: 8px 10px;
        border-bottom: 1px solid rgba(15, 23, 42, 0.06);
      }
      #gyp-batch-rename-root .gyp-import-row:last-child {
        border-bottom: 0;
      }
      #gyp-batch-rename-root .gyp-import-name {
        font-size: 12px;
        line-height: 1.45;
        color: #23314b;
        font-weight: 600;
        overflow-wrap: anywhere;
        word-break: break-word;
      }
      #gyp-batch-rename-root .gyp-import-meta,
      #gyp-batch-rename-root .gyp-import-empty {
        margin-top: 4px;
        font-size: 11px;
        line-height: 1.5;
        color: #667085;
        overflow-wrap: anywhere;
        word-break: break-word;
      }
      #gyp-batch-rename-root .gyp-import-empty {
        padding: 12px 10px;
        margin-top: 0;
      }
      #gyp-batch-rename-root .gyp-import-target {
        color: #16803c;
        font-weight: 700;
      }
      #gyp-batch-rename-root .gyp-empty-dir-list {
        max-height: 280px;
      }
      #gyp-batch-rename-root .gyp-empty-dir-row {
        display: flex;
        align-items: flex-start;
        gap: 10px;
        padding: 10px 12px;
        border-bottom: 1px solid rgba(15, 23, 42, 0.06);
        cursor: pointer;
      }
      #gyp-batch-rename-root .gyp-empty-dir-row:last-child {
        border-bottom: 0;
      }
      #gyp-batch-rename-root .gyp-empty-dir-row input {
        margin-top: 2px;
        flex: 0 0 auto;
      }
      #gyp-batch-rename-root .gyp-empty-dir-main {
        min-width: 0;
        flex: 1 1 auto;
        display: flex;
        flex-direction: column;
        gap: 4px;
      }
      #gyp-batch-rename-root .gyp-empty-dir-path {
        font-size: 12px;
        line-height: 1.5;
        color: #23314b;
        font-weight: 600;
        overflow-wrap: anywhere;
        word-break: break-word;
      }
      #gyp-batch-rename-root .gyp-empty-dir-meta {
        font-size: 11px;
        line-height: 1.55;
        color: #667085;
        overflow-wrap: anywhere;
        word-break: break-word;
      }
      #gyp-batch-rename-root .gyp-empty-dir-row[data-confidence="likely"] .gyp-empty-dir-meta {
        color: #b54708;
      }
      /* 增强输入框聚焦时的蓝色高亮 */
      #gyp-batch-rename-root .gyp-field input:focus,
      #gyp-batch-rename-root .gyp-field select:focus,
        #gyp-batch-rename-root .gyp-field textarea:focus {
        outline: none;
        border-color: #0f62fe !important;
        box-shadow: 0 0 0 3px rgba(15, 98, 254, 0.2) !important;
        background: #fff;
      }
      /* 悬停效果 */
      #gyp-batch-rename-root .gyp-field input:hover,
      #gyp-batch-rename-root .gyp-field select:hover {
        border-color: #0f62fe;
      }
      /* 修复:强制显示文字选中后的高亮颜色 */
      #gyp-batch-rename-root input::selection,
      #gyp-batch-rename-root textarea::selection {
        background-color: #0078d4 !important;
        color: #ffffff !important;
      }
      /* 针对不同浏览器的兼容 */
      #gyp-batch-rename-root input::-moz-selection,
      #gyp-batch-rename-root textarea::-moz-selection {
        background-color: #0078d4 !important;
        color: #ffffff !important;
      }
      /* 让下拉列表在悬停时也有反应 */
      #gyp-batch-rename-root .gyp-field select:hover,
      #gyp-batch-rename-root .gyp-field input:hover {
        border-color: #0f62fe;
      }
      @media (max-width: 640px) {
        #gyp-batch-rename-root {
          right: 10px;
          bottom: 10px;
        }
        #gyp-batch-rename-root .gyp-panel {
          width: min(380px, calc(100vw - 20px));
          height: calc(100vh - 92px);
          max-height: calc(100vh - 92px);
        }
        #gyp-batch-rename-root .gyp-guide-flow {
          flex-direction: column;
        }
        #gyp-batch-rename-root .gyp-guide-arrow {
          justify-content: center;
          transform: rotate(90deg);
        }
      }
    `;
    document.head.appendChild(style);

    const root = document.createElement('div');
    root.id = 'gyp-batch-rename-root';
    root.innerHTML = `
      <button
        type="button"
        class="gyp-fab"
        data-action="toggle-panel"
        data-keep-enabled="true"
        title="光鸭云盘工具"
        aria-label="光鸭云盘工具"
      >
        <span class="gyp-fab-icon" aria-hidden="true">
        <svg class="icon" viewBox="0 0 904.6 870.7" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" preserveAspectRatio="xMidYMid meet">
<style type="text/css">
	.st0{fill:#B0B0B0;}
	.st1{fill:#F6B2B1;}
	.st2{fill:#FFC41D;}
	.st3{fill:#333333;}
	.st4{fill:#FFFFFF;}
	.st5{fill:none;stroke:#333333;stroke-width:12;stroke-linecap:round;}
	.st6{stroke:#666666;stroke-width:15;stroke-linecap:round;}
	.st7{fill:none;}
</style>
<path class="st0" d="M335.2,23.7c-56.3,0-102.4,97.3-102.4,215s46.1,215,102.4,215s102.4-97.3,102.4-215S391.5,23.7,335.2,23.7z"/>
<path class="st1" d="M335.2,74.9c-35.8,0-66.6,61.4-66.6,138.2s30.7,138.2,66.6,138.2s66.6-61.4,66.6-138.2S371,74.9,335.2,74.9
	L335.2,74.9z"/>
<path class="st0" d="M609,38.7c56.3,0,102.4,97.3,102.4,215s-46.1,215-102.4,215s-102.4-97.3-102.4-215S552.7,38.7,609,38.7z"/>
<path class="st1" d="M609,89.9c35.8,0,66.6,61.4,66.6,138.2S644.8,366.4,609,366.4s-66.6-61.4-66.6-138.2S573.2,89.9,609,89.9
	L609,89.9z"/>
<path class="st0" d="M535.5,305.3c161.3,0,291.8,130.6,291.8,291.8v66.6C827.4,766.1,745.4,848,643,848H228.2
	c-102.4,0-184.3-81.9-184.3-184.3v-66.6c0-161.3,130.6-291.8,291.8-291.8h200H535.5z"/>
<path class="st2" d="M50.5,318.9L34.8,309c-8.8-5.5-8.8-18.4,0-23.9l15.7-9.9c10.3-6.5,18.3-14.5,24.8-24.8l9.9-15.7
	c5.5-8.8,18.4-8.8,23.9,0l9.9,15.7c6.5,10.3,14.5,18.3,24.8,24.8l15.7,9.9c8.8,5.5,8.8,18.4,0,23.9l-15.7,9.9
	c-10.3,6.5-18.3,14.5-24.8,24.8l-9.9,15.7c-5.5,8.8-18.4,8.8-23.9,0l-9.9-15.7C68.8,333.4,60.8,325.4,50.5,318.9z"/>
<path class="st2" d="M789,331.8l-15.7-9.9c-8.8-5.5-8.8-18.4,0-23.9l15.7-9.9c10.3-6.5,18.3-14.5,24.8-24.8l9.9-15.7
	c5.5-8.8,18.4-8.8,23.9,0l9.9,15.7c6.5,10.3,14.5,18.3,24.8,24.8L898,298c8.8,5.5,8.8,18.4,0,23.9l-15.7,9.9
	c-10.3,6.5-18.3,14.5-24.8,24.8l-9.9,15.7c-5.5,8.8-18.4,8.8-23.9,0l-9.9-15.7C807.3,346.3,799.3,338.3,789,331.8z"/>
<circle class="st3" cx="301.4" cy="429.8" r="44"/>
<circle class="st4" cx="316.4" cy="414.8" r="15"/>
<circle class="st3" cx="661.4" cy="427.9" r="44"/>
<circle class="st4" cx="646.4" cy="412.9" r="15"/>
<circle class="st1" cx="505.6" cy="458.8" r="15"/>
<path class="st5" d="M456.6,491.8c0,13.8,11.2,25,25,25s25-11.2,25-25"/>
<path class="st5" d="M506.6,491.8c0,13.8,11.2,25,25,25s25-11.2,25-25"/>
<path class="st6" d="M32.6,438.8l117,29"/>
<path class="st6" d="M32.6,535.8l117-15"/>
<path class="st6" d="M885.9,405.3l-117,29"/>
<path class="st6" d="M885.9,502.3l-117-15"/>
<circle class="st1" cx="742.4" cy="518.7" r="58"/>
<circle class="st1" cx="198.4" cy="511.8" r="58"/>
<path id="SVGID_x5F_1_x5F_" class="st7" d="M76.7,634.5c0,0-8.7,83.9,17.4,106.8c26.2,22.9,62.1,81.7,129.7,89.4"/>
<text><textPath  xlink:href="#SVGID_x5F_1_x5F_" startOffset="20.6543%">
<tspan  style="fill:#F4F5FB; font-family:'LQSshufaziti'; font-size:48px;">Serenalee</tspan></textPath>
</text>
</svg>
        </span>
      </button>
      <div class="gyp-panel" role="dialog" aria-label="光鸭云盘批量工具">
        <div class="gyp-head">
          <div>
            <div class="gyp-title-row">
              <img class="gyp-title-mark" src="https://image.868717.xyz/file/1776301692011_3.svg" alt="" aria-hidden="true" />
              <div class="gyp-title-stack">
                <div class="gyp-title">光鸭云盘工具</div>
                <div class="gyp-subtitle">批量直链下载(光鸭功能) / 分享直读(夸克、123、天翼、迅雷) / 网盘互通(夸克、123、天翼、百度、迅雷) / 磁力云批量添加(光鸭功能) / 移动整理(光鸭功能) / 批量改名(光鸭功能)</div>
                <div class="gyp-version">Serenalee (v${SCRIPT_VERSION})</div>
              </div>
            </div>
          </div>
          <button type="button" class="secondary" data-action="close-panel" data-keep-enabled="true">关闭</button>
        </div>
        <div class="gyp-status" data-role="status">等待页面捕获目录、授权和请求上下文...</div>
        <div class="gyp-progress" data-role="progress">
          <div class="gyp-progress-track">
            <div class="gyp-progress-bar" data-role="progress-bar"></div>
          </div>
          <div class="gyp-progress-text" data-role="progress-text"></div>
          <div class="gyp-progress-tools">
            <button type="button" class="secondary" data-action="pause-task" data-keep-enabled="true" disabled>暂停</button>
            <button type="button" class="danger" data-action="stop-task" data-keep-enabled="true" disabled>停止</button>
          </div>
        </div>
        <div class="gyp-sections">
          <details class="gyp-section" data-role="rename-details">
            <summary>
              <span class="gyp-section-summary">
                <span class="gyp-section-headline">
	                  <span class="gyp-section-title-line"><span class="gyp-section-icon" aria-hidden="true">📝</span><span class="gyp-section-title">批量改名(光鸭功能)</span></span>
                  <span class="gyp-section-desc">预览、执行和规则配置放在一起</span>
                </span>
              </span>
            </summary>
            <div class="gyp-section-body">
              ${buildGuideCard('批量改名怎么用', ['选规则和改名方式', '先看示例与预览', '确认后执行改名'], '建议先点“预览”,确认新名字没问题再执行。')}
              <div class="gyp-section-actions">
                <button type="button" data-action="preview">预览</button>
                <button type="button" class="secondary" data-action="refresh-preview">刷新改名预览</button>
                <button type="button" data-action="run">执行改名</button>
                <button type="button" class="secondary" data-action="state">查看状态</button>
              </div>
              <div class="gyp-section-note">先在下面调整规则,再点“预览”确认结果,最后执行改名。</div>
          <label class="gyp-field">
            <span>预处理</span>
            <select data-field="ruleMode">
              <option value="remove-leading-bracket">删除开头第一个 [] / 【】 段</option>
              <option value="replace-text">按固定文字查找并替换</option>
              <option value="none">不做预处理</option>
              <option value="custom-regex">自定义正则(高级)</option>
            </select>
            <div class="gyp-inline-help">这一步是先把原名字做一次清理。比如删掉开头的 [高清剧集网],或者删掉某段固定文字。</div>
          </label>
          <label class="gyp-field" data-role="rule-text-group">
            <span>预处理查找文本</span>
            <input data-field="ruleSearchText" placeholder="比如:1080p 或 【无删减版】" />
          </label>
          <label class="gyp-field" data-role="rule-text-group">
            <span>预处理替换成</span>
            <input data-field="ruleReplaceText" placeholder="留空表示删除找到的内容" />
            <div class="gyp-inline-help">如果只想删除一段固定文字,这里直接留空就行。</div>
          </label>
          <label class="gyp-field">
            <span>改名方式</span>
            <select data-field="outputMode">
              <option value="keep-clean">直接使用处理后的名字</option>
              <option value="add-text">增加文字</option>
              <option value="replace-text">替换文字</option>
              <option value="format">格式命名</option>
              <option value="custom-template">自定义模板(高级)</option>
            </select>
            <div class="gyp-inline-help">这里才是最终怎么命名。你想“增加、替换、格式化”,都在这里选。</div>
          </label>
          <label class="gyp-field" data-role="output-add-group">
            <span>增加内容</span>
            <input data-field="addText" placeholder="比如:4K版- 或 -收藏" />
          </label>
          <label class="gyp-field" data-role="output-add-group">
            <span>增加位置</span>
            <select data-field="addPosition">
              <option value="prefix">名称之前</option>
              <option value="suffix">名称之后</option>
            </select>
          </label>
          <label class="gyp-field" data-role="output-add-group">
            <span>增加选项</span>
            <label style="display:flex;align-items:center;gap:8px;font-size:12px;color:#344054;">
              <input type="checkbox" data-field="addIgnoreExtension" checked />
              <span>忽略后缀名(名称之后时加在扩展名前)</span>
            </label>
          </label>
          <label class="gyp-field" data-role="output-replace-group">
            <span>最终查找文本</span>
            <input data-field="outputFindText" placeholder="比如:1" />
          </label>
          <label class="gyp-field" data-role="output-replace-group">
            <span>最终替换成</span>
            <input data-field="outputReplaceText" placeholder="比如:2;留空表示删除" />
          </label>
          <label class="gyp-field" data-role="output-format-group">
            <span>名称格式</span>
            <select data-field="formatStyle">
              <option value="text-and-index">名称和索引</option>
              <option value="text-only">仅自定义名称</option>
            </select>
          </label>
          <label class="gyp-field" data-role="output-format-group">
            <span>自定义格式</span>
            <input data-field="formatText" placeholder="比如:文件" />
          </label>
          <label class="gyp-field" data-role="output-format-group">
            <span>位置</span>
            <select data-field="formatPosition">
              <option value="suffix">名称之后</option>
              <option value="prefix">名称之前</option>
            </select>
          </label>
          <label class="gyp-field" data-role="output-format-group">
            <span>开始数字为</span>
            <input data-field="startIndex" placeholder="0" />
          </label>
          <label class="gyp-field">
            <span>每次请求间隔毫秒</span>
            <input data-field="delayMs" placeholder="300" />
            <div class="gyp-inline-help">默认 300。如果接口容易失败,可以适当调大,比如 500 或 800。</div>
          </label>
          <div class="gyp-example">
            <div class="gyp-example-title">当前规则示例</div>
            <label class="gyp-field">
              <span>示例原名</span>
              <input data-field="exampleName" placeholder="这里填一个例子,下面会实时显示处理结果" />
            </label>
            <div class="gyp-inline-help" data-role="rename-example-desc"></div>
            <div class="gyp-example-row">
              <div class="gyp-example-label">预处理后</div>
              <div class="gyp-example-value" data-role="rename-example-clean"></div>
            </div>
            <div class="gyp-example-row">
              <div class="gyp-example-label">最终改名结果</div>
              <div class="gyp-example-value" data-role="rename-example-final"></div>
            </div>
          </div>
          <div class="gyp-config-actions">
            <button type="button" class="secondary" data-action="apply-config">应用配置</button>
            <button type="button" class="secondary" data-action="save-config">保存配置</button>
          </div>
          <div class="gyp-help">说明:
1. “预处理”是先把原名字清理一下,比如删掉开头的 []、删掉某段固定文字
2. “改名方式”才是最终要怎么命名,可选增加、替换、格式命名
3. 现在的“最终名字格式”其实就是以前的高级模板概念,已经放到高级区里了
4. 每次填完都看上面的示例,确认结果对了,再点“预览”或“执行改名”</div>
            </div>
          </details>
          <details class="gyp-section" data-role="duplicate-details">
            <summary>
              <span class="gyp-section-summary">
                <span class="gyp-section-headline">
	                  <span class="gyp-section-title-line"><span class="gyp-section-icon" aria-hidden="true">♻️</span><span class="gyp-section-title">重复项清理(光鸭功能)</span></span>
                  <span class="gyp-section-desc">预览、勾选、取消和删除在同一区</span>
                </span>
                <span class="gyp-section-badge" data-role="duplicate-count">删除勾选 0/0</span>
              </span>
            </summary>
            <div class="gyp-section-body">
              ${buildGuideCard('重复项清理怎么用', ['先预览重复项', '自动勾选或手动调整', '确认后删除'], '删除前可以在列表里取消不想删的项目。')}
              <div class="gyp-section-actions">
                <button type="button" class="secondary" data-action="preview-duplicates">重复项预览</button>
                <button type="button" class="secondary" data-action="select-duplicates">勾选重复项</button>
                <button type="button" class="danger" data-action="delete-duplicates">删除重复项</button>
              </div>
              <label class="gyp-field">
                <span>重复项编号</span>
                <input data-field="duplicateNumbers" placeholder="1,2,3" />
                <div class="gyp-inline-help">这里只影响重复项识别规则。只要名字最后带 (1)、(2)、(3) 就算重复;想加 4 就写 1,2,3,4。</div>
              </label>
              <div class="gyp-duplicate-panel">
                <div class="gyp-duplicate-head">
                  <div class="gyp-duplicate-title">重复项列表</div>
                  <div class="gyp-duplicate-tools">
                    <button type="button" data-action="select-all-duplicates">全选</button>
                    <button type="button" data-action="clear-all-duplicates">全不选</button>
                  </div>
                </div>
                <div class="gyp-duplicate-list" data-role="duplicate-list">
                  <div class="gyp-duplicate-empty">先点“重复项预览”,再在这里取消不想删的项目。</div>
                </div>
              </div>
            </div>
          </details>
          <details class="gyp-section" data-role="empty-dir-details">
            <summary>
              <span class="gyp-section-summary">
                <span class="gyp-section-headline">
	                  <span class="gyp-section-title-line"><span class="gyp-section-icon" aria-hidden="true">📂</span><span class="gyp-section-title">空目录扫描(光鸭功能)</span></span>
                  <span class="gyp-section-desc">扫描结果和删除勾选都放在这里</span>
                </span>
                <span class="gyp-section-badge" data-role="empty-dir-count">空目录 0 个</span>
              </span>
            </summary>
            <div class="gyp-section-body">
              ${buildGuideCard('空目录扫描怎么用', ['扫描空目录', '勾选要删除的目录', '执行删除空目录'], '只处理最里层且完全空的目录。')}
              <div class="gyp-section-actions">
                <button type="button" class="secondary" data-action="scan-empty-dirs">扫描空目录</button>
              </div>
              <div class="gyp-duplicate-panel">
                <div class="gyp-duplicate-head">
                  <div class="gyp-duplicate-title">空目录结果</div>
                  <div class="gyp-duplicate-tools">
                    <button type="button" data-action="select-all-empty-dirs">全选</button>
                    <button type="button" data-action="clear-all-empty-dirs">全不选</button>
                    <button type="button" data-action="delete-empty-dirs">删除空目录</button>
                  </div>
                </div>
                <div class="gyp-import-list gyp-empty-dir-list" data-role="empty-dir-list">
                  <div class="gyp-import-empty">点“扫描空目录”后,这里会列出当前目录树里最里层且完全空的目录。</div>
                </div>
              </div>
            </div>
          </details>
          <details class="gyp-section" data-role="move-details">
            <summary>
              <span class="gyp-section-summary">
                <span class="gyp-section-headline">
	                  <span class="gyp-section-title-line"><span class="gyp-section-icon" aria-hidden="true">📦</span><span class="gyp-section-title">移动整理(光鸭功能)</span></span>
                  <span class="gyp-section-desc">基于当前页面已勾选项做移动,不是面板里的勾选</span>
                </span>
                <span class="gyp-section-badge" data-role="move-count">当前勾选 0 项</span>
              </span>
            </summary>
            <div class="gyp-section-body">
              ${buildGuideCard('移动整理怎么用', ['先在网页列表勾选', '读取当前勾选', '选择上移或移到目标目录'], '这里读的是网页里真正勾选的文件/文件夹,不是面板里的勾选。')}
              <div class="gyp-section-actions">
                <button type="button" class="secondary" data-action="preview-move-selection" data-variant="primary-step">读取当前勾选</button>
                <button type="button" class="secondary" data-action="move-selected-up-one-level" data-variant="success-step">勾选项整体上移一层</button>
                <button type="button" class="secondary" data-action="move-folder-contents-up">拆开文件夹内容到当前目录</button>
                <button type="button" class="secondary" data-action="move-selected-to-target">勾选项移到目标目录</button>
              </div>
              <div class="gyp-section-note">这里读取的是云盘文件列表里当前页面已经勾选的文件 / 文件夹。“勾选项整体上移一层”会保留文件夹本身;“拆开文件夹内容到当前目录”会把文件夹里的直接内容提到当前目录,不保留外层文件夹,也不会自动删除空文件夹。</div>
              <label class="gyp-field">
                <span>目标目录 parentId</span>
                <input data-field="moveTargetParentId" placeholder="要移动到哪个文件夹,就填那个目录的 parentId" />
                <div class="gyp-inline-help">“勾选项移到目标目录”会把当前页面已勾选的文件 / 文件夹直接移动到这里。这个值会保存在本地。</div>
              </label>
              <div class="gyp-import-list" data-role="move-selection-list">
                <div class="gyp-import-empty">点“读取当前勾选”后,这里会显示当前页面已勾选的文件 / 文件夹。</div>
              </div>
            </div>
          </details>
          <details class="gyp-section" data-role="media-organize-details">
            <summary>
              <span class="gyp-section-summary">
                <span class="gyp-section-headline">
                  <span class="gyp-section-title-line"><span class="gyp-section-icon" aria-hidden="true">🎬</span><span class="gyp-section-title">媒体智能整理(光鸭功能)</span></span>
                  <span class="gyp-section-desc">按文件夹名优先识别电影、电视剧、综艺、动漫并移动到分类目录</span>
                </span>
                <span class="gyp-section-badge" data-role="media-organize-count">待整理 0 组</span>
              </span>
            </summary>
            <div class="gyp-section-body">
              ${buildGuideCard('媒体智能整理怎么用', ['勾选文件夹/文件', '识别并预览', '确认后自动建目录并移动'], '点击识别并预览会出现卡顿现象,属于正常读取现象,等待一会儿即可;识别准确率会因为命名有所区别,确认好再入库;每次识别数量不建议太多,不然会引起网盘卡顿或识别时间过长,建议分批次是识别入库。')}
              <div class="gyp-section-actions">
                <button type="button" class="secondary" data-action="preview-media-organize" data-variant="primary-step">识别并预览</button>
                <button type="button" class="secondary" data-action="run-media-organize" data-variant="success-step">按预览整理</button>
                <button type="button" class="secondary" data-action="clear-media-organize">清空预览</button>
              </div>
              <label class="gyp-field">
                <span>TMDB API Key(可选,但强烈建议)</span>
                <input data-field="mediaTmdbApiKey" type="password" placeholder="留空时只用本地规则粗分;填写后浏览器直连 TMDB 识别标题/年份/地区/类型" />
                <div class="gyp-inline-help">获取方式:登录 themoviedb.org 后进入 Settings -> API,申请 Developer / v3 API Key,复制生成的 API Key 填到这里。</div>
              </label>
              <label class="gyp-field">
                <span>TMDB 语言</span>
                <select data-field="mediaTmdbLanguage">
                  <option value="zh-CN">简体中文 zh-CN</option>
                  <option value="zh-HK">繁体中文 zh-HK</option>
                  <option value="zh-TW">繁体中文 zh-TW</option>
                  <option value="en-US">English en-US</option>
                </select>
              </label>
              <label class="gyp-field">
                <span>整理根目录 parentId</span>
                <input data-field="mediaRootParentId" placeholder="留空默认用当前目录;建议填“媒体库根目录”的 parentId" />
              </label>
              <label class="gyp-field gyp-check-field">
                <input data-field="mediaUseFolderNameFirst" type="checkbox" />
                <span>文件夹命名优先:适合外层是片名,里面是 1.mp4 / 2.mkv 的网盘资源</span>
              </label>
              <label class="gyp-field gyp-check-field">
                <input data-field="mediaIncludeTitleFolder" type="checkbox" />
                <span>目标目录包含片名年份层,例如 电视剧/国产剧/中国大陆/剧名 (2025)</span>
              </label>
              <label class="gyp-field gyp-check-field">
                <input data-field="mediaIncludeRegionFolder" type="checkbox" />
                <span>增加国家/地区层:仅在子分类没有表达地区时使用,避免 欧美剧/美国 这类重复路径</span>
              </label>
              <label class="gyp-field gyp-check-field">
                <input data-field="mediaIncludeSeasonFolder" type="checkbox" />
                <span>剧集/动漫/综艺带季数时增加 Season 01 层</span>
              </label>
              <label class="gyp-field gyp-check-field">
                <input data-field="mediaMoveBySourceFolder" type="checkbox" />
                <span>直接移动原始文件夹本身;默认关闭,会读取文件夹内容后把里面的文件移动到规范目录</span>
              </label>
              <label class="gyp-field gyp-check-field">
                <input data-field="mediaSkipDuplicateTargets" type="checkbox" />
                <span>目标目录已有同名项目时跳过,不覆盖、不删除源文件</span>
              </label>
              <label class="gyp-field gyp-check-field">
                <input data-field="mediaCleanupEmptySourceFolders" type="checkbox" />
                <span>移动成功后自动删除已确认为空的源文件夹</span>
              </label>
              <label class="gyp-field">
                <span>移动批大小</span>
                <input data-field="mediaBatchSize" placeholder="默认 10" />
              </label>
              <div class="gyp-inline-help">分类规则:movie -> 电影;tv + 动画类型 -> 动漫;tv + 真人秀/脱口秀 -> 综艺;其他 tv -> 电视剧。不填 TMDB Key 时,会按文件夹名里的年份、季集、综艺/动漫/国家关键词做本地粗分。</div>
              <div class="gyp-import-list" data-role="media-organize-list">
                <div class="gyp-import-empty">勾选文件夹或文件后点“识别并预览”,这里会显示分类和目标目录。</div>
              </div>
            </div>
          </details>
          <details class="gyp-section" data-role="direct-download-details">
            <summary>
              <span class="gyp-section-summary">
                <span class="gyp-section-headline">
	                  <span class="gyp-section-title-line"><span class="gyp-section-icon" aria-hidden="true">⬇️</span><span class="gyp-section-title">批量直链下载(光鸭功能)</span></span>
                  <span class="gyp-section-desc">展开文件夹、逐个取直链,绕过服务器打包</span>
                </span>
                <span class="gyp-section-badge" data-role="direct-download-count">待下载 0/0 项</span>
              </span>
            </summary>
            <div class="gyp-section-body">
              ${buildGuideCard('批量直链下载怎么用', ['打开文件夹并勾选文件', '读取当前勾选并展开', '下载勾选交给下载器'], '如果“源已修改”较多,把每批并发取链数降到 1 或 2。')}
              <div class="gyp-section-actions">
                <button type="button" class="secondary" data-action="preview-direct-download-selection" data-variant="primary-step">读取当前勾选并展开</button>
                <button type="button" class="secondary" data-action="trigger-direct-download-selection" data-variant="success-step">下载勾选</button>
                <button type="button" class="secondary" data-action="clear-direct-download">清空结果</button>
                <button type="button" class="secondary" data-action="download-direct-download-md5-size">下载光鸭MD5/Size</button>
              </div>
              <div class="gyp-section-note">这个功能只在光鸭当前文件列表里生效。更稳的用法是先打开文件夹,再勾选里面的文件;脚本会逐个请求单文件直链,不再走 create_packaging_task 打包下载,点“下载勾选”会把直链交给浏览器或用户自己的下载器接管。</div>
              <div class="gyp-inline-help">“源已修改”和并发有关:插件会按批次并发获取直链,拿到一批就立刻交给下载器;真正下载速度、多线程和断点续传由你自己的浏览器、IDM、NDM、Motrix 等下载器控制。</div>
              <label class="gyp-field">
                <span>每批并发取链数</span>
                <input data-field="directDownloadBatchSize" placeholder="3" />
                <div class="gyp-inline-help">默认 3。出现“源已修改”较多时,建议先降到 1 或 2,再点“下载勾选”重试。</div>
              </label>
              <div class="gyp-inline-help" data-role="direct-download-summary">请先打开文件夹后勾选里面的文件;点“读取当前勾选并展开”后,这里会显示最终要下载的文件列表。</div>
              <div class="gyp-import-list" data-role="direct-download-list">
                <div class="gyp-import-empty">请先打开文件夹后勾选里面的文件;点“读取当前勾选并展开”后,这里会显示最终要下载的文件列表。</div>
              </div>
            </div>
          </details>
          <details class="gyp-section" data-role="magnet-details">
            <summary>
              <span class="gyp-section-summary">
                <span class="gyp-section-headline">
	                  <span class="gyp-section-title-line"><span class="gyp-section-icon" aria-hidden="true">🧲</span><span class="gyp-section-title">磁力云批量添加(光鸭功能)</span></span>
                  <span class="gyp-section-desc">文件选择、上传入口和磁力列表都放在一起</span>
                </span>
                <span class="gyp-section-badge" data-role="magnet-file-count">磁力文本 0 个 / 磁力 0 条</span>
              </span>
            </summary>
            <div class="gyp-section-body">
              ${buildGuideCard('磁力云批量添加怎么用', ['选择TXT/JSON', '检查识别到的磁力', '开始云添加'], '文本里只要包含 magnet 链接,脚本会自动识别并按批次提交。')}
              <div class="gyp-section-actions">
                <button type="button" class="secondary" data-action="pick-magnet-files" data-keep-enabled="true" data-variant="primary-step">选择TXT/JSON</button>
                <button type="button" class="secondary" data-action="import-magnets" data-variant="success-step">开始云添加</button>
                <button type="button" class="secondary" data-action="clear-magnet-files">清空磁力TXT</button>
                <button type="button" class="secondary" data-action="list-cloud-tasks">查看云任务</button>
              </div>
              <label class="gyp-field">
                <span>云添加每批最多文件数</span>
                <input data-field="cloudBatchLimit" placeholder="500" />
                <div class="gyp-inline-help">这里只影响云添加的拆批数量。试用版常见限制是一次最多 500 个文件,脚本会按这里的值自动拆分 create_task。</div>
              </label>
              <label class="gyp-field">
                <span>云添加目录前缀</span>
                <input data-field="cloudDirPrefix" placeholder="磁力导入" />
                <div class="gyp-inline-help">这里只影响云添加时自动创建的目录名。导入本地 txt/json 后,会建立“前缀-文本名-时间戳”文件夹,避免和现有内容混在一起。</div>
              </label>
              <div class="gyp-import-list" data-role="magnet-file-list">
                <div class="gyp-import-empty">选择包含 magnet 链接的 txt 或 json 文件后,脚本会自动识别并按每批 500 文件拆分云添加。</div>
              </div>
              <input type="file" accept=".txt,.json,.log,.text,.md" multiple hidden data-role="magnet-file-input" data-keep-enabled="true" />
            </div>
          </details>
          <details class="gyp-section" data-role="share-link-details">
            <summary>
              <span class="gyp-section-summary">
                <span class="gyp-section-headline">
	                  <span class="gyp-section-title-line"><span class="gyp-section-icon" aria-hidden="true">🔗</span><span class="gyp-section-title">分享直读(夸克、123、天翼、迅雷)</span></span>
                  <span class="gyp-section-desc">粘贴分享链接、读取目录、勾选后生成或直接导入光鸭</span>
                </span>
                <span class="gyp-section-badge" data-role="share-link-count">已选 0/0</span>
              </span>
            </summary>
            <div class="gyp-section-body">
              ${buildGuideCard('分享直读怎么用', ['粘贴分享链接', '读取链接目录', '勾选后直接导入光鸭'], '适合夸克、123、天翼、迅雷分享链接;迅雷需要先打开分享页一次。')}
              <div class="gyp-section-actions">
                <button type="button" class="secondary" data-action="read-share-link" data-variant="primary-step">读取链接目录</button>
                <button type="button" class="secondary" data-action="import-share-link-to-guangya" data-variant="success-step">按勾选直接导入光鸭</button>
                <button type="button" class="secondary" data-action="select-all-share-link">全选</button>
                <button type="button" class="secondary" data-action="clear-all-share-link">全不选</button>
                <button type="button" class="secondary" data-action="generate-miaochuan-from-share-link">按勾选生成 JSON</button>
                <button type="button" class="secondary" data-action="clear-share-link">清空结果</button>
              </div>
	              <div class="gyp-section-note">这是独立于“网盘互通”的链接直读功能。已支持夸克、123 网盘、天翼云盘、迅雷云盘分享链接直读;123 / 天翼 / 迅雷会自动翻页或递归读取完整目录。迅雷需要先打开并加载对应分享页一次,让脚本捕获登录态和 pass_code_token。</div>
              <label class="gyp-field">
                <span>分享链接</span>
                <textarea data-field="shareLinkUrl" class="gyp-miaochuan-log" placeholder="可直接粘贴完整分享链接;如果复制内容里带提取码,也可以一起粘进来。"></textarea>
              </label>
              <label class="gyp-field">
                <span>提取码(可选)</span>
                <input data-field="shareLinkPasscode" placeholder="优先用你这里手填的;留空则尝试从链接或整段文本里自动识别" />
              </label>
	              <label class="gyp-field">
	                <span>夸克 Cookie(仅夸克分享直读使用)</span>
	                <textarea data-field="shareLinkQuarkCookie" class="gyp-miaochuan-log" placeholder="可选。夸克分享链接如果提示 token/MD5 获取失败,把浏览器 Network 里的完整 Cookie 粘这里;留空会先尝试复用已保存的 Cookie。迅雷不填这里,需要先打开对应迅雷分享页。"></textarea>
              </label>
              <label class="gyp-field">
                <span>光鸭 Authorization(按勾选直接导入时使用)</span>
                <textarea data-field="shareLinkGuangyaAuthorization" class="gyp-miaochuan-token" placeholder="可选。默认会先复用已保存的 Bearer Token 或当前光鸭页面自动捕获到的认证。"></textarea>
              </label>
              <label class="gyp-field">
                <span>光鸭目标 parentId(可选)</span>
                <input data-field="shareLinkGuangyaParentId" placeholder="留空导入到光鸭根目录;需要指定目录时再填" />
              </label>
              <div class="gyp-inline-help" data-role="share-link-summary">粘贴分享链接后,先点“读取链接目录”,这里会显示可勾选的文件清单。</div>
              <div class="gyp-import-list gyp-empty-dir-list" data-role="share-link-list">
                <div class="gyp-import-empty">粘贴分享链接后,先点“读取链接目录”,这里会显示可勾选的文件清单。</div>
              </div>
            </div>
          </details>
          <details class="gyp-section" data-role="miaochuan-details">
            <summary>
              <span class="gyp-section-summary">
                <span class="gyp-section-headline">
	                  <span class="gyp-section-title-line"><span class="gyp-section-icon" aria-hidden="true">⚡</span><span class="gyp-section-title">网盘互通(夸克、123、天翼、百度、迅雷)</span></span>
                  <span class="gyp-section-desc">跨网盘来源识别、生成光鸭 JSON、导入失败诊断</span>
                </span>
              </span>
            </summary>
              <div class="gyp-section-body">
                ${buildGuideCard('网盘互通怎么用', ['打开来源网盘页面', '抓取生成光鸭JSON', '直接导入光鸭或下载JSON'], '已经有秒传 JSON 时,也可以直接选择文件再生成/导入。')}
                <div class="gyp-section-actions">
                <button type="button" class="secondary" data-action="generate-miaochuan-from-page">从当前网页抓取生成</button>
                <button type="button" class="secondary" data-action="import-miaochuan-to-guangya">直接导入光鸭</button>
                <button type="button" class="secondary" data-action="pick-miaochuan-json" data-keep-enabled="true">选择秒传 JSON</button>
                <button type="button" class="secondary" data-action="generate-miaochuan-json">生成可导入 JSON</button>
                <button type="button" class="secondary" data-action="fill-miaochuan-import-box">填入页面导入框</button>
                <button type="button" class="secondary" data-action="copy-miaochuan-json">复制 JSON</button>
                <button type="button" class="secondary" data-action="download-miaochuan-json">下载 JSON</button>
                <button type="button" class="secondary" data-action="copy-miaochuan-report">复制报告</button>
                <button type="button" class="secondary" data-action="clear-miaochuan-json">清空</button>
              </div>
	              <div class="gyp-section-note">优先用“从当前网页抓取生成”:脚本会捕获或主动读取夸克 / 123 / 天翼 / 百度 / 迅雷 / 光鸭页面里的文件数据,自动生成光鸭秒传 JSON。百度、迅雷更适合在对应网盘页面勾选后使用;迅雷会走 GCID,其他多数来源走 MD5 + size。</div>
              <div class="gyp-inline-help" data-role="miaochuan-captured-count">当前网页已捕获 0 条候选文件;请先让网盘列表加载完成。</div>
              <label class="gyp-field">
                <span>夸克 Cookie(分享页主动取 MD5 时使用)</span>
                <textarea data-field="miaochuanQuarkCookie" class="gyp-miaochuan-log" placeholder="可选。夸克分享页如果提示 token/MD5 获取失败,请从浏览器 Network 里复制任意夸克请求的完整 Cookie 粘贴到这里;留空会先尝试缓存和当前页面 Cookie。"></textarea>
              </label>
              <label class="gyp-field">
                <span>光鸭 Authorization(直接导入用)</span>
                <textarea data-field="miaochuanGuangyaAuthorization" class="gyp-miaochuan-token" placeholder="可选。要在夸克 / 123 / 天翼等页面直接导入光鸭时需要这个;可先打开光鸭一次让脚本自动捕获,或粘贴 Bearer ..."></textarea>
              </label>
              <label class="gyp-field">
                <span>光鸭目标 parentId(可选)</span>
                <input data-field="miaochuanGuangyaParentId" placeholder="留空导入到光鸭根目录;如需指定目录,可填目标目录 parentId" />
              </label>
              <label class="gyp-field">
                <span>原始 JSON</span>
                <textarea data-field="miaochuanJsonInput" class="gyp-miaochuan-input" placeholder="一般不用手填。点“从当前网页抓取生成”后,这里会自动填入捕获到的原始数据;也可以手动粘贴来源 JSON。"></textarea>
              </label>
              <label class="gyp-field">
                <span>导入结果日志(可选)</span>
                <textarea data-field="miaochuanImportLog" class="gyp-miaochuan-log" placeholder="如果光鸭导入后失败,可把弹窗里的“接口调用失败 / 秒传失败 / 阶段统计”粘到这里,再重新生成诊断。"></textarea>
              </label>
              <div class="gyp-miaochuan-diagnosis" data-role="miaochuan-diagnosis">等待生成。</div>
              <div class="gyp-inline-help" data-role="miaochuan-source">来源识别:等待生成。</div>
              <label class="gyp-field">
                <span>生成后的光鸭秒传 JSON</span>
                <textarea data-role="miaochuan-output" class="gyp-miaochuan-output" readonly placeholder="点击“生成可导入 JSON”后显示。"></textarea>
              </label>
              <label class="gyp-field">
                <span>诊断报告</span>
                <textarea data-role="miaochuan-report" class="gyp-miaochuan-report" readonly placeholder="转换诊断、来源识别、失败层级分析会显示在这里。"></textarea>
              </label>
              <input type="file" accept=".json,.txt,.log,.text,.md,application/json,text/plain" hidden data-role="miaochuan-file-input" data-keep-enabled="true" />
            </div>
          </details>
          <details class="gyp-section" data-role="advanced-details">
            <summary>
              <span class="gyp-section-summary">
                <span class="gyp-section-headline">
                  <span class="gyp-section-title-line"><span class="gyp-section-icon" aria-hidden="true">🛠️</span><span class="gyp-section-title">高级与调试</span></span>
                  <span class="gyp-section-desc">手动认证、模板/正则兜底和调试信息都放在最后</span>
                </span>
              </span>
            </summary>
            <div class="gyp-section-body">
              ${buildGuideCard('高级与调试怎么用', ['自动抓不到时再打开', '刷新已捕获上下文', '必要时手填认证或规则'], '普通使用不需要改这里;它主要是兜底和排查问题用。')}
              <div class="gyp-section-note">脚本默认优先使用刚刚自动捕获到的认证和目录上下文。只有自动抓不到,或者你要用自定义模板 / 自定义正则时,再展开这里。</div>
              <div class="gyp-config-actions">
                <button type="button" class="secondary" data-action="fill-captured">刷新已捕获上下文</button>
              </div>
              <label class="gyp-field">
                <span>Authorization</span>
                <textarea data-field="authorization" placeholder="默认优先用最新自动识别;只有自动抓不到时才手填 Bearer ..."></textarea>
              </label>
              <label class="gyp-field">
                <span>DID</span>
                <input data-field="did" placeholder="默认自动识别;抓不到时再填" />
              </label>
              <label class="gyp-field">
                <span>DT</span>
                <input data-field="dt" placeholder="默认自动识别;抓不到时再填" />
              </label>
              <label class="gyp-field">
                <span>parentId</span>
                <input data-field="parentId" placeholder="当前目录 ID,平时自动识别;抓不到时再填" />
              </label>
              <label class="gyp-field">
                <span>接口单次抓取数(兜底)</span>
                <input data-field="pageSize" placeholder="默认 100;只有自动识别不到时才需要改" />
              </label>
              <label class="gyp-field">
                <span>高级模板 template</span>
                <input data-field="template" placeholder="比如:{clean} / {original} / 文件{index}" />
                <div class="gyp-inline-help" data-role="output-template-group">只有“改名方式”选了“自定义模板(高级)”时才会用到这里。</div>
              </label>
              <label class="gyp-field">
                <span>规则正则 pattern</span>
                <input data-field="rulePattern" placeholder="只有在“预处理 = 自定义正则”时才需要填" />
              </label>
              <label class="gyp-field">
                <span>规则 flags</span>
                <input data-field="ruleFlags" placeholder="一般是 u" />
              </label>
              <label class="gyp-field">
                <span>替换成 replace</span>
                <input data-field="ruleReplace" placeholder="留空表示删除匹配到的内容" />
              </label>
              <details class="gyp-debug-details">
                <summary>调试信息(一般不用)</summary>
                <div class="gyp-summary" data-role="summary"></div>
              </details>
            </div>
          </details>
        </div>
      </div>
    `;

    document.body.appendChild(root);
    reorderPanelSections(root);
    closeFeatureSections(root);

    UI.root = root;
    UI.panel = root.querySelector('.gyp-panel');
    UI.mini = root.querySelector('.gyp-fab');
    UI.status = root.querySelector('[data-role="status"]');
    UI.progressWrap = root.querySelector('[data-role="progress"]');
    UI.progressBar = root.querySelector('[data-role="progress-bar"]');
    UI.progressText = root.querySelector('[data-role="progress-text"]');
    UI.pauseTaskButton = root.querySelector('[data-action="pause-task"]');
    UI.stopTaskButton = root.querySelector('[data-action="stop-task"]');
    UI.summary = root.querySelector('[data-role="summary"]');
    UI.duplicateDetails = root.querySelector('[data-role="duplicate-details"]');
    UI.duplicateList = root.querySelector('[data-role="duplicate-list"]');
    UI.duplicateCount = root.querySelector('[data-role="duplicate-count"]');
    UI.moveDetails = root.querySelector('[data-role="move-details"]');
    UI.moveSelectionList = root.querySelector('[data-role="move-selection-list"]');
    UI.moveSelectionCount = root.querySelector('[data-role="move-count"]');
    UI.mediaOrganizeDetails = root.querySelector('[data-role="media-organize-details"]');
    UI.mediaOrganizeList = root.querySelector('[data-role="media-organize-list"]');
    UI.mediaOrganizeCount = root.querySelector('[data-role="media-organize-count"]');
    UI.directDownloadDetails = root.querySelector('[data-role="direct-download-details"]');
    UI.directDownloadList = root.querySelector('[data-role="direct-download-list"]');
    UI.directDownloadCount = root.querySelector('[data-role="direct-download-count"]');
    UI.directDownloadSummary = root.querySelector('[data-role="direct-download-summary"]');
    UI.emptyDirList = root.querySelector('[data-role="empty-dir-list"]');
    UI.emptyDirCount = root.querySelector('[data-role="empty-dir-count"]');
    UI.emptyDirDetails = root.querySelector('[data-role="empty-dir-details"]');
    UI.magnetDetails = root.querySelector('[data-role="magnet-details"]');
    UI.magnetFileInput = root.querySelector('[data-role="magnet-file-input"]');
    UI.magnetFileList = root.querySelector('[data-role="magnet-file-list"]');
    UI.magnetFileCount = root.querySelector('[data-role="magnet-file-count"]');
    UI.shareLinkDetails = root.querySelector('[data-role="share-link-details"]');
    UI.shareLinkList = root.querySelector('[data-role="share-link-list"]');
    UI.shareLinkCount = root.querySelector('[data-role="share-link-count"]');
    UI.shareLinkSummary = root.querySelector('[data-role="share-link-summary"]');
    UI.miaochuanDetails = root.querySelector('[data-role="miaochuan-details"]');
    UI.miaochuanFileInput = root.querySelector('[data-role="miaochuan-file-input"]');
    UI.miaochuanDiagnosis = root.querySelector('[data-role="miaochuan-diagnosis"]');
    UI.miaochuanOutput = root.querySelector('[data-role="miaochuan-output"]');
    UI.miaochuanReport = root.querySelector('[data-role="miaochuan-report"]');
    UI.miaochuanSource = root.querySelector('[data-role="miaochuan-source"]');
    UI.miaochuanCapturedCount = root.querySelector('[data-role="miaochuan-captured-count"]');
    UI.fields.ruleMode = root.querySelector('[data-field="ruleMode"]');
    UI.fields.outputMode = root.querySelector('[data-field="outputMode"]');
    UI.fields.ruleSearchText = root.querySelector('[data-field="ruleSearchText"]');
    UI.fields.ruleReplaceText = root.querySelector('[data-field="ruleReplaceText"]');
    UI.fields.addText = root.querySelector('[data-field="addText"]');
    UI.fields.addPosition = root.querySelector('[data-field="addPosition"]');
    UI.fields.addIgnoreExtension = root.querySelector('[data-field="addIgnoreExtension"]');
    UI.fields.outputFindText = root.querySelector('[data-field="outputFindText"]');
    UI.fields.outputReplaceText = root.querySelector('[data-field="outputReplaceText"]');
    UI.fields.formatStyle = root.querySelector('[data-field="formatStyle"]');
    UI.fields.formatText = root.querySelector('[data-field="formatText"]');
    UI.fields.formatPosition = root.querySelector('[data-field="formatPosition"]');
    UI.fields.startIndex = root.querySelector('[data-field="startIndex"]');
    UI.fields.exampleName = root.querySelector('[data-field="exampleName"]');
    UI.fields.authorization = root.querySelector('[data-field="authorization"]');
    UI.fields.did = root.querySelector('[data-field="did"]');
    UI.fields.dt = root.querySelector('[data-field="dt"]');
    UI.fields.parentId = root.querySelector('[data-field="parentId"]');
    UI.fields.pageSize = root.querySelector('[data-field="pageSize"]');
    UI.fields.template = root.querySelector('[data-field="template"]');
    UI.fields.duplicateNumbers = root.querySelector('[data-field="duplicateNumbers"]');
    UI.fields.cloudBatchLimit = root.querySelector('[data-field="cloudBatchLimit"]');
    UI.fields.cloudDirPrefix = root.querySelector('[data-field="cloudDirPrefix"]');
    UI.fields.moveTargetParentId = root.querySelector('[data-field="moveTargetParentId"]');
    UI.fields.mediaTmdbApiKey = root.querySelector('[data-field="mediaTmdbApiKey"]');
    UI.fields.mediaRootParentId = root.querySelector('[data-field="mediaRootParentId"]');
    UI.fields.mediaTmdbLanguage = root.querySelector('[data-field="mediaTmdbLanguage"]');
    UI.fields.mediaUseFolderNameFirst = root.querySelector('[data-field="mediaUseFolderNameFirst"]');
    UI.fields.mediaIncludeTitleFolder = root.querySelector('[data-field="mediaIncludeTitleFolder"]');
    UI.fields.mediaIncludeRegionFolder = root.querySelector('[data-field="mediaIncludeRegionFolder"]');
    UI.fields.mediaIncludeSeasonFolder = root.querySelector('[data-field="mediaIncludeSeasonFolder"]');
    UI.fields.mediaMoveBySourceFolder = root.querySelector('[data-field="mediaMoveBySourceFolder"]');
    UI.fields.mediaCleanupEmptySourceFolders = root.querySelector('[data-field="mediaCleanupEmptySourceFolders"]');
    UI.fields.mediaSkipDuplicateTargets = root.querySelector('[data-field="mediaSkipDuplicateTargets"]');
    UI.fields.mediaBatchSize = root.querySelector('[data-field="mediaBatchSize"]');
    UI.fields.directDownloadBatchSize = root.querySelector('[data-field="directDownloadBatchSize"]');
    UI.fields.directDownloadExportFormat = root.querySelector('[data-field="directDownloadExportFormat"]');
    UI.fields.shareLinkUrl = root.querySelector('[data-field="shareLinkUrl"]');
    UI.fields.shareLinkPasscode = root.querySelector('[data-field="shareLinkPasscode"]');
    UI.fields.shareLinkQuarkCookie = root.querySelector('[data-field="shareLinkQuarkCookie"]');
    UI.fields.shareLinkGuangyaAuthorization = root.querySelector('[data-field="shareLinkGuangyaAuthorization"]');
    UI.fields.shareLinkGuangyaParentId = root.querySelector('[data-field="shareLinkGuangyaParentId"]');
    UI.fields.miaochuanQuarkCookie = root.querySelector('[data-field="miaochuanQuarkCookie"]');
    UI.fields.miaochuanGuangyaAuthorization = root.querySelector('[data-field="miaochuanGuangyaAuthorization"]');
    UI.fields.miaochuanGuangyaParentId = root.querySelector('[data-field="miaochuanGuangyaParentId"]');
    UI.fields.miaochuanJsonInput = root.querySelector('[data-field="miaochuanJsonInput"]');
    UI.fields.miaochuanImportLog = root.querySelector('[data-field="miaochuanImportLog"]');
    UI.fields.rulePattern = root.querySelector('[data-field="rulePattern"]');
    UI.fields.ruleFlags = root.querySelector('[data-field="ruleFlags"]');
    UI.fields.ruleReplace = root.querySelector('[data-field="ruleReplace"]');
    UI.fields.delayMs = root.querySelector('[data-field="delayMs"]');
    if (UI.fields.shareLinkQuarkCookie) {
      UI.fields.shareLinkQuarkCookie.value = getStoredQuarkCookie();
    }
    if (UI.fields.miaochuanQuarkCookie) {
      UI.fields.miaochuanQuarkCookie.value = getStoredQuarkCookie();
    }
    if (UI.fields.shareLinkGuangyaAuthorization) {
      UI.fields.shareLinkGuangyaAuthorization.value = getStoredGuangyaAuthorization();
    }
    if (UI.fields.miaochuanGuangyaAuthorization) {
      UI.fields.miaochuanGuangyaAuthorization.value = getStoredGuangyaAuthorization();
    }
    renderShareLinkList();
    renderDirectDownloadList();
    renderMediaOrganizeList();

    const closePanel = () => {
      root.classList.remove('gyp-open');
    };

    const openPanel = () => {
      closeFeatureSections(root);
      root.classList.add('gyp-open');
      syncPanelFromConfig();
      updatePanelStatus();
      updateRenameModePreview();
    };

    root.addEventListener('click', async (event) => {
      const btn = event.target.closest('[data-action]');
      if (!btn) {
        return;
      }

      const action = btn.dataset.action;

      if (action === 'pause-task') {
        togglePauseActiveTask();
        return;
      }

      if (action === 'stop-task') {
        stopActiveTask();
        return;
      }

      if (action === 'toggle-panel') {
        if (root.classList.contains('gyp-open')) {
          closePanel();
        } else {
          openPanel();
        }
        return;
      }

      if (action === 'close-panel') {
        closePanel();
        return;
      }

      try {
        if (action === 'pick-magnet-files') {
          if (UI.magnetDetails) {
            UI.magnetDetails.open = true;
          }
          UI.magnetFileInput?.click();
          return;
        }

        if (action === 'clear-magnet-files') {
          STATE.magnetImportFiles = [];
          STATE.lastCloudImportSummary = null;
          if (UI.magnetFileInput) {
            UI.magnetFileInput.value = '';
          }
          renderMagnetImportList();
          updatePanelStatus('已清空待导入的磁力 txt 列表');
          return;
        }

        if (action === 'pick-miaochuan-json') {
          if (UI.miaochuanDetails) {
            UI.miaochuanDetails.open = true;
          }
          UI.miaochuanFileInput?.click();
          return;
        }

        if (action === 'clear-miaochuan-json') {
          clearMiaochuanPanel();
          return;
        }

        if (action === 'fill-captured') {
          fillPanelFromCaptured();
          return;
        }

        if (action === 'apply-config') {
          applyPanelConfig();
          return;
        }

        if (action === 'save-config') {
          applyPanelConfig();
          savePersistedConfig();
          updatePanelStatus('配置已保存到本地');
          return;
        }

        if (action === 'select-all-duplicates') {
          for (const item of STATE.duplicatePreviewItems || []) {
            STATE.duplicateSelection[item.fileId] = true;
          }
          renderDuplicatePreviewList();
          updatePanelStatus('已全选当前面板里的重复项');
          return;
        }

        if (action === 'clear-all-duplicates') {
          for (const item of STATE.duplicatePreviewItems || []) {
            STATE.duplicateSelection[item.fileId] = false;
          }
          renderDuplicatePreviewList();
          updatePanelStatus('已取消当前面板里的重复项勾选');
          return;
        }

        if (action === 'select-all-empty-dirs') {
          for (const item of STATE.lastEmptyDirScan?.emptyDirs || []) {
            STATE.emptyDirSelection[String(item.fileId || '')] = true;
          }
          renderEmptyDirScanList();
          updatePanelStatus('已全选当前面板里的空目录');
          return;
        }

        if (action === 'clear-all-empty-dirs') {
          for (const item of STATE.lastEmptyDirScan?.emptyDirs || []) {
            STATE.emptyDirSelection[String(item.fileId || '')] = false;
          }
          renderEmptyDirScanList();
          updatePanelStatus('已取消当前面板里的空目录勾选');
          return;
        }

        applyPanelConfig();
        setPanelBusy(true);

        if (action === 'preview') {
          setProgressBar({ visible: false });
          const targets = await preview();
          updatePanelStatus(`预览完成,共 ${targets.length} 个待改名`);
          return;
        }

        if (action === 'refresh-preview') {
          setProgressBar({ visible: false });
          const targets = await preview({ refresh: true });
          updatePanelStatus(`刷新预览完成,共 ${targets.length} 个待改名`);
          return;
        }

        if (action === 'run') {
          await runWithTaskControl('批量改名', async (taskControl) => {
            setProgressBar({ visible: true, percent: 0, text: '准备执行...' });
            const result = await run({
              onProgress: (state) => setProgressBar(state),
              taskControl,
            });
            updatePanelStatus(
              result.fail
                ? `执行完成,成功 ${result.ok} 个,失败 ${result.fail} 个,首个错误:${result.firstError || '(未返回详情)'}`
                : `执行完成,成功 ${result.ok} 个,失败 ${result.fail} 个`
            );
          });
          return;
        }

        if (action === 'scan-empty-dirs') {
          if (UI.emptyDirDetails) {
            UI.emptyDirDetails.open = true;
          }
          await runWithTaskControl('扫描空目录', async (taskControl) => {
            setProgressBar({ visible: true, percent: 0, indeterminate: true, text: '准备扫描当前目录树里的空目录...' });
            const result = await scanEmptyLeafDirectories({
              onProgress: (state) => setProgressBar(state),
              taskControl,
            });
            updatePanelStatus(
              result.truncated
                ? `空目录扫描完成:找到 ${result.emptyDirs.length} 个,已扫 ${result.scannedDirs} 个目录,结果可能未扫全`
                : `空目录扫描完成:找到 ${result.emptyDirs.length} 个,已扫 ${result.scannedDirs} 个目录`
            );
          });
          return;
        }

        if (action === 'delete-empty-dirs') {
          await runWithTaskControl('删除空目录', async (taskControl) => {
            setProgressBar({ visible: true, percent: 0, indeterminate: true, text: '准备删除空目录...' });
            const result = await deleteEmptyDirItems({
              onProgress: (state) => setProgressBar(state),
              taskControl,
            });
            updatePanelStatus(`空目录删除完成:成功 ${result.deleted} 个,失败 ${result.fail} 个`);
          });
          return;
        }

        if (action === 'preview-duplicates') {
          if (UI.duplicateDetails) {
            UI.duplicateDetails.open = true;
          }
          setProgressBar({ visible: false });
          const duplicates = await previewDuplicates({ refresh: true });
          setDuplicatePreview(duplicates);
          updatePanelStatus(`重复项预览完成,共 ${duplicates.length} 个;当前范围 ${getCapturedItems().length} 条已捕获记录`);
          return;
        }

        if (action === 'select-duplicates') {
          if (UI.duplicateDetails) {
            UI.duplicateDetails.open = true;
          }
          await runWithTaskControl('勾选重复项', async (taskControl) => {
            setProgressBar({ visible: true, percent: 0, text: '准备勾选重复项...' });
            const result = await selectDuplicateRows({
              refresh: true,
              onProgress: (state) => setProgressBar(state),
              taskControl,
            });
            updatePanelStatus(`重复项已处理:匹配 ${result.matched} 个,勾选 ${result.clicked} 个`);
          });
          return;
        }

        if (action === 'delete-duplicates') {
          if (UI.duplicateDetails) {
            UI.duplicateDetails.open = true;
          }
          await runWithTaskControl('删除重复项', async (taskControl) => {
            setProgressBar({ visible: true, percent: 0, indeterminate: true, text: '准备删除重复项...' });
            const result = await deleteDuplicateItems({
              refresh: true,
              onProgress: (state) => setProgressBar(state),
              taskControl,
            });
            updatePanelStatus(`重复项删除完成:成功 ${result.deleted} 个,失败 ${result.fail} 个`);
          });
          return;
        }

        if (action === 'import-magnets') {
          if (UI.magnetDetails) {
            UI.magnetDetails.open = true;
          }
          await runWithTaskControl('磁力云添加', async (taskControl) => {
            setProgressBar({ visible: true, percent: 0, indeterminate: true, text: '准备提交磁力云添加...' });
            const result = await importMagnetTextFiles({
              onProgress: (state) => setProgressBar(state),
              taskControl,
            });
            const firstFailure = result.failures && result.failures[0] ? `;首个失败:${result.failures[0].message}` : '';
            updatePanelStatus(
              `云添加提交完成:磁力成功 ${result.submittedMagnets} 条,跳过 ${result.skippedMagnets} 条,失败 ${result.failedMagnets} 条;任务批次成功 ${result.submittedTaskBatches} 个,失败 ${result.failedTaskBatches} 个${firstFailure}`
            );
            renderMagnetImportList();
          });
          return;
        }

        if (action === 'list-cloud-tasks') {
          if (UI.magnetDetails) {
            UI.magnetDetails.open = true;
          }
          setProgressBar({ visible: true, percent: 25, indeterminate: true, text: '正在读取云添加任务列表...' });
          const response = await listCloudTasks();
          if (!response.ok || !isProbablySuccess(response.payload, response)) {
            throw new Error(getErrorText(response.payload || response.text || `HTTP ${response.status}`));
          }
          const rows = extractCloudTaskRows(response.payload);
          console.table(rows.map((item) => ({
            taskId: item.taskId,
            status: item.status,
            name: item.name,
            url: item.url,
          })));
          setProgressBar({
            visible: true,
            percent: 100,
            indeterminate: false,
            text: `已读取云添加任务 ${rows.length} 条,详情已输出到控制台`,
          });
          updatePanelStatus(`已读取云添加任务 ${rows.length} 条,详情已输出到控制台`);
          return;
        }

        if (action === 'read-share-link') {
          if (UI.shareLinkDetails) {
            UI.shareLinkDetails.open = true;
          }
          await runWithTaskControl('读取分享链接', async (taskControl) => {
            setProgressBar({ visible: true, percent: 0, indeterminate: true, text: '准备读取分享链接目录...' });
            const rows = await readShareLinkFromPanel({
              onProgress: (state) => setProgressBar(state),
              taskControl,
            });
            setProgressBar({ visible: true, percent: 100, indeterminate: false, text: `分享链接读取完成:文件 ${rows.length} 项` });
          });
          return;
        }

        if (action === 'select-all-share-link') {
          if (UI.shareLinkDetails) {
            UI.shareLinkDetails.open = true;
          }
          setAllShareLinkSelection(true);
          updatePanelStatus(`已全选分享直读结果:${getSelectedShareLinkRows().length} 项`);
          return;
        }

        if (action === 'clear-all-share-link') {
          if (UI.shareLinkDetails) {
            UI.shareLinkDetails.open = true;
          }
          setAllShareLinkSelection(false);
          updatePanelStatus('已取消勾选全部分享直读结果');
          return;
        }

        if (action === 'generate-miaochuan-from-share-link') {
          if (UI.shareLinkDetails) {
            UI.shareLinkDetails.open = true;
          }
          setProgressBar({ visible: false });
          generateMiaochuanJsonFromShareLinkSelection();
          return;
        }

        if (action === 'import-share-link-to-guangya') {
          if (UI.shareLinkDetails) {
            UI.shareLinkDetails.open = true;
          }
          await runWithTaskControl('按勾选导入光鸭', async (taskControl) => {
            setProgressBar({ visible: true, percent: 0, indeterminate: true, text: '准备按勾选结果导入光鸭...' });
            const result = generateMiaochuanJsonFromShareLinkSelection();
            const summary = await importMiaochuanJsonDirectlyToGuangya({
              normalized: result.normalized,
              manualAuthorization: UI.fields.shareLinkGuangyaAuthorization?.value || '',
              parentId: UI.fields.shareLinkGuangyaParentId?.value || '',
              onProgress: (state) => setProgressBar(state),
              taskControl,
            });
            setProgressBar({ visible: true, percent: 100, indeterminate: false, text: `直接导入完成:成功 ${summary.success},失败 ${summary.transferFail + summary.mkdirFail + summary.skipped}` });
            updatePanelStatus(`分享直读导入完成:成功 ${summary.success} 条,失败 ${summary.transferFail + summary.mkdirFail + summary.skipped} 条;详情见诊断报告`);
          });
          return;
        }

        if (action === 'clear-share-link') {
          if (UI.shareLinkDetails) {
            UI.shareLinkDetails.open = true;
          }
          clearShareLinkPanel();
          return;
        }

        if (action === 'generate-miaochuan-json') {
          if (UI.miaochuanDetails) {
            UI.miaochuanDetails.open = true;
          }
          setProgressBar({ visible: false });
          await generateMiaochuanJsonFromPanel();
          return;
        }

        if (action === 'generate-miaochuan-from-page') {
          if (UI.miaochuanDetails) {
            UI.miaochuanDetails.open = true;
          }
          setProgressBar({ visible: true, percent: 0, indeterminate: true, text: '准备从当前网页读取秒传数据...' });
          await generateMiaochuanJsonFromCapturedPage({
            onProgress: (state) => setProgressBar(state),
          });
          setProgressBar({ visible: true, percent: 100, indeterminate: false, text: '秒传 JSON 生成完成' });
          return;
        }

        if (action === 'import-miaochuan-to-guangya') {
          if (UI.miaochuanDetails) {
            UI.miaochuanDetails.open = true;
          }
          await runWithTaskControl('直接导入光鸭', async (taskControl) => {
            setProgressBar({ visible: true, percent: 0, indeterminate: true, text: '准备直接导入光鸭;如当前没有 JSON 会先自动生成...' });
            const summary = await importMiaochuanJsonDirectlyToGuangya({
              fromCurrentPage: true,
              onProgress: (state) => setProgressBar(state),
              taskControl,
            });
            setProgressBar({ visible: true, percent: 100, indeterminate: false, text: `直接导入完成:成功 ${summary.success},失败 ${summary.transferFail + summary.mkdirFail + summary.skipped}` });
            updatePanelStatus(`直接导入光鸭完成:成功 ${summary.success} 条,失败 ${summary.transferFail + summary.mkdirFail + summary.skipped} 条;详情见诊断报告`);
          });
          return;
        }

        if (action === 'fill-miaochuan-import-box') {
          if (UI.miaochuanDetails) {
            UI.miaochuanDetails.open = true;
          }
          await fillMiaochuanPageImportBox();
          return;
        }

        if (action === 'copy-miaochuan-json') {
          const text = STATE.lastMiaochuanJsonResult?.outputText || UI.miaochuanOutput?.value || '';
          await copyMiaochuanText(text);
          updatePanelStatus('已复制生成后的光鸭秒传 JSON');
          return;
        }

        if (action === 'copy-miaochuan-report') {
          const text = STATE.lastMiaochuanJsonResult?.reportText || UI.miaochuanReport?.value || '';
          await copyMiaochuanText(text);
          updatePanelStatus('已复制秒传 JSON 诊断报告');
          return;
        }

        if (action === 'download-miaochuan-json') {
          const text = STATE.lastMiaochuanJsonResult?.outputText || UI.miaochuanOutput?.value || '';
          if (!text) {
            throw new Error('当前没有可下载的 JSON,请先生成。');
          }
          downloadMiaochuanText(`光鸭秒传_${new Date().toISOString().slice(0, 19).replace(/[T:]/g, '-')}.json`, text);
          updatePanelStatus('已下载生成后的光鸭秒传 JSON');
          return;
        }

        if (action === 'preview-move-selection') {
          if (UI.moveDetails) {
            UI.moveDetails.open = true;
          }
          setProgressBar({ visible: false });
          await waitForUiPaint(1);
          const selection = await collectCheckedPageSelectionPreviewItems();
          const items = selection.items || [];
          setMoveSelectionPreview(items, selection.meta);
          if (!items.length) {
            updatePanelStatus('当前页面没有勾选任何文件或文件夹');
          } else if (selection.meta?.partial) {
            updatePanelStatus(selection.meta.warning || `页面显示已选 ${selection.meta?.expectedCount || 0} 项,但当前只识别到 ${selection.meta?.visibleCount || items.length} 项`);
          } else {
            updatePanelStatus(selection.meta?.warning || `已读取当前页面勾选 ${Math.max(items.length, Number(selection.meta?.expectedCount || 0))} 项`);
          }
          return;
        }

        if (action === 'preview-media-organize') {
          if (UI.mediaOrganizeDetails) {
            UI.mediaOrganizeDetails.open = true;
          }
          await runWithTaskControl('媒体智能识别预览', async (taskControl) => {
            setProgressBar({ visible: true, percent: 0, indeterminate: true, text: '准备读取勾选项并识别媒体...' });
            const groups = await previewMediaOrganizeSelection({
              onProgress: (state) => setProgressBar(state),
              taskControl,
            });
            setProgressBar({ visible: true, percent: 100, indeterminate: false, text: `媒体识别完成:${groups.length} 组` });
            updatePanelStatus(`媒体智能整理预览完成:${groups.length} 组;请核对目标目录后再执行整理`);
          });
          return;
        }

        if (action === 'run-media-organize') {
          if (UI.mediaOrganizeDetails) {
            UI.mediaOrganizeDetails.open = true;
          }
          await runWithTaskControl('媒体智能整理', async (taskControl) => {
            setProgressBar({ visible: true, percent: 0, indeterminate: true, text: '准备按预览结果整理媒体...' });
            const result = await executeMediaOrganizePlan({
              onProgress: (state) => setProgressBar(state),
              taskControl,
            });
            renderMediaOrganizeList();
            const failureText = result.failures?.length ? `;首个失败:${result.failures[0]}` : '';
            updatePanelStatus(`媒体智能整理完成:成功 ${result.ok} 项,失败 ${result.fail} 项${failureText}`);
          });
          return;
        }

        if (action === 'clear-media-organize') {
          STATE.mediaOrganizePreviewItems = [];
          STATE.mediaOrganizePlan = null;
          STATE.mediaOrganizeWarning = '';
          renderMediaOrganizeList();
          updatePanelStatus('已清空媒体智能整理预览');
          return;
        }

        if (action === 'preview-direct-download-selection') {
          if (UI.directDownloadDetails) {
            UI.directDownloadDetails.open = true;
          }
          await runWithTaskControl('读取批量直链下载勾选', async (taskControl) => {
            setProgressBar({ visible: true, percent: 0, indeterminate: true, text: '准备读取当前勾选并展开文件夹...' });
            await waitForUiPaint(1);
            const files = await previewDirectDownloadSelection({
              onProgress: (state) => setProgressBar(state),
              taskControl,
            });
            setProgressBar({ visible: true, percent: 100, indeterminate: false, text: `已展开待下载文件 ${files.length} 项` });
          });
          return;
        }

        if (action === 'trigger-direct-download-selection') {
          if (UI.directDownloadDetails) {
            UI.directDownloadDetails.open = true;
          }
          await runWithTaskControl('下载批量直链勾选', async (taskControl) => {
            setProgressBar({ visible: true, percent: 0, indeterminate: true, text: '准备展开文件夹并触发浏览器下载...' });
            await waitForUiPaint(1);
            const result = await triggerDirectDownloadsFromCheckedItems({
              onProgress: (state) => setProgressBar(state),
              taskControl,
              reusePreview: false,
            });
            setProgressBar({
              visible: true,
              percent: 100,
              indeterminate: false,
              text: `已触发 ${result.count} 个浏览器/下载器任务${result.failedCount ? `,失败 ${result.failedCount} 个` : ''}`,
            });
          });
          return;
        }

        if (action === 'download-direct-download-md5-size') {
          if (UI.directDownloadDetails) {
            UI.directDownloadDetails.open = true;
          }
          await runWithTaskControl('下载光鸭 MD5/Size', async (taskControl) => {
            setProgressBar({ visible: true, percent: 0, indeterminate: true, text: '准备读取当前勾选并导出光鸭 MD5/Size...' });
            await waitForUiPaint(1);
            const result = await downloadDirectDownloadMd5SizeSelection({
              onProgress: (state) => setProgressBar(state),
              taskControl,
              reusePreview: false,
            });
            const missingText = [
              result.missingMd5 ? `${result.missingMd5} 项缺少 MD5` : '',
              result.missingSize ? `${result.missingSize} 项缺少 size` : '',
            ].filter(Boolean).join(',');
            setProgressBar({
              visible: true,
              percent: 100,
              indeterminate: false,
              text: `已导出 ${result.count} 项光鸭 MD5/Size 到 ${result.filename}${missingText ? `;${missingText}` : ''}`,
            });
          });
          return;
        }

        if (action === 'clear-direct-download') {
          if (UI.directDownloadDetails) {
            UI.directDownloadDetails.open = true;
          }
          clearDirectDownloadPanel();
          return;
        }

        if (action === 'move-folder-contents-up') {
          if (UI.moveDetails) {
            UI.moveDetails.open = true;
          }
          await runWithTaskControl('拆开文件夹内容到当前目录', async (taskControl) => {
            setProgressBar({ visible: true, percent: 0, indeterminate: true, text: '准备读取已勾选文件夹内容...' });
            const result = await moveCheckedFolderContentsToCurrentDirectory({
              onProgress: (state) => setProgressBar(state),
              taskControl,
            });
            updatePanelStatus(
              result.fail
                ? `拆开文件夹内容完成:成功 ${result.ok} 项,失败 ${result.fail} 项;读取文件夹 ${result.folders?.length || 0} 个`
                : `拆开文件夹内容完成:成功 ${result.ok} 项,失败 ${result.fail} 项;读取文件夹 ${result.folders?.length || 0} 个;空文件夹不会自动删除`
            );
          });
          return;
        }

        if (action === 'move-selected-up-one-level') {
          if (UI.moveDetails) {
            UI.moveDetails.open = true;
          }
          await runWithTaskControl('勾选项整体上移一层', async (taskControl) => {
            setProgressBar({ visible: true, percent: 0, indeterminate: true, text: '准备把当前勾选项整体上移一层...' });
            const result = await moveCheckedItemsUpOneLevel({
              onProgress: (state) => setProgressBar(state),
              taskControl,
            });
            updatePanelStatus(
              result.fail
                ? `勾选项整体上移一层完成:成功 ${result.ok} 项,失败 ${result.fail} 项;目标目录 ${result.targetParentId || '(未识别)'}`
                : `勾选项整体上移一层完成:成功 ${result.ok} 项,失败 ${result.fail} 项;目标目录 ${result.targetParentId || '(未识别)'}`
            );
          });
          return;
        }

        if (action === 'move-selected-to-target') {
          if (UI.moveDetails) {
            UI.moveDetails.open = true;
          }
          await runWithTaskControl('移动勾选项到目标目录', async (taskControl) => {
            setProgressBar({ visible: true, percent: 0, indeterminate: true, text: '准备移动当前勾选项...' });
            const result = await moveCheckedItemsToTargetDirectory({
              onProgress: (state) => setProgressBar(state),
              taskControl,
            });
            updatePanelStatus(
              result.fail
                ? `移动勾选项完成:成功 ${result.ok} 项,失败 ${result.fail} 项;目标目录 ${result.targetParentId || CONFIG.move.targetParentId || '(未识别)'}`
                : `移动勾选项完成:成功 ${result.ok} 项,失败 ${result.fail} 项;目标目录 ${result.targetParentId || CONFIG.move.targetParentId || '(未识别)'}`
            );
          });
          return;
        }

        if (action === 'state') {
          setProgressBar({ visible: false });
          console.log(LOG_PREFIX, exportState());
          updatePanelStatus('状态已输出到控制台');
        }
      } catch (err) {
        if (isTaskAbortError(err)) {
          setProgressBar({ visible: true, percent: 100, indeterminate: false, text: err.message || '已停止当前任务' });
          updatePanelStatus(err.message || '已停止当前任务');
        } else {
          fail('面板操作失败:', err);
          setProgressBar({ visible: true, percent: 100, text: `失败:${err.message || err}` });
          updatePanelStatus(`失败:${err.message || err}`);
        }
      } finally {
        setPanelBusy(false);
      }
    });

    root.addEventListener('input', (event) => {
      if (event.target.closest('[data-field]')) {
        updateRenameModePreview();
      }
    });

    root.addEventListener('change', (event) => {
      if (event.target.closest('[data-field]')) {
        updateRenameModePreview();
      }

      if (event.target === UI.fields.directDownloadExportFormat) {
        CONFIG.download.exportFormat = getDirectDownloadExportFormat();
        savePersistedConfig();
        if (STATE.lastDirectDownloadSummary?.entries?.length) {
          refreshDirectDownloadOutputFromSummary();
          updatePanelStatus(`已切换批量直链导出格式为 ${getDirectDownloadExportFormatLabel(CONFIG.download.exportFormat)}`);
        }
        return;
      }

      if (event.target === UI.magnetFileInput) {
        const files = Array.from(UI.magnetFileInput?.files || []);
        if (!files.length) {
          return;
        }

        setProgressBar({
          visible: true,
          percent: 0,
          indeterminate: true,
          text: `正在识别 ${files.length} 个本地磁力文本...`,
        });

        readMagnetImportFiles(files, {
          onProgress: (state) => setProgressBar(state),
        })
          .then((entries) => {
            setMagnetImportFiles(entries, { append: true });
            const stats = getSelectedMagnetImportStats();
            setProgressBar({
              visible: true,
              percent: 100,
              indeterminate: false,
              text: `已识别磁力文本 ${stats.fileCount} 个,共 ${stats.magnetCount} 条磁力`,
            });
            updatePanelStatus(`已识别磁力文本 ${stats.fileCount} 个,共 ${stats.magnetCount} 条磁力`);
          })
          .catch((err) => {
            fail('读取磁力文本失败:', err);
            setProgressBar({
              visible: true,
              percent: 100,
              indeterminate: false,
              text: `读取磁力文本失败:${getErrorText(err)}`,
            });
            updatePanelStatus(`读取磁力文本失败:${getErrorText(err)}`);
          })
          .finally(() => {
            if (UI.magnetFileInput) {
              UI.magnetFileInput.value = '';
            }
          });
        return;
      }

      if (event.target === UI.miaochuanFileInput) {
        const file = UI.miaochuanFileInput?.files?.[0] || null;
        if (!file) {
          return;
        }
        file.text()
          .then(async (text) => {
            if (UI.fields.miaochuanJsonInput) {
              UI.fields.miaochuanJsonInput.value = text;
            }
            if (UI.miaochuanDetails) {
              UI.miaochuanDetails.open = true;
            }
            const result = await generateMiaochuanJsonFromPanel();
            updatePanelStatus(`已读取并生成秒传 JSON:${file.name},${result.normalized.files.length} 项`);
          })
          .catch((err) => {
            fail('读取秒传 JSON 文件失败:', err);
            updatePanelStatus(`读取秒传 JSON 文件失败:${getErrorText(err)}`);
          })
          .finally(() => {
            if (UI.miaochuanFileInput) {
              UI.miaochuanFileInput.value = '';
            }
          });
        return;
      }

      const shareLinkInput = event.target.closest('[data-action="toggle-share-link-file"]');
      if (shareLinkInput) {
        const shareLinkKey = shareLinkInput.dataset.shareLinkKey || '';
        if (!shareLinkKey) {
          return;
        }
        STATE.shareLinkSelection[shareLinkKey] = Boolean(shareLinkInput.checked);
        renderShareLinkList();
        updatePanelStatus(`已更新分享直读勾选:${getSelectedShareLinkRows().length}/${(STATE.shareLinkRows || []).length} 项`);
        return;
      }

      const duplicateInput = event.target.closest('[data-action="toggle-duplicate"]');
      if (duplicateInput) {
        const fileId = duplicateInput.dataset.fileId || '';
        if (!fileId) {
          return;
        }
        STATE.duplicateSelection[fileId] = Boolean(duplicateInput.checked);
        renderDuplicatePreviewList();
        updatePanelStatus('已更新删除勾选清单');
        return;
      }

      const emptyDirInput = event.target.closest('[data-action="toggle-empty-dir"]');
      if (!emptyDirInput) {
        return;
      }
      const emptyDirId = emptyDirInput.dataset.fileId || '';
      if (!emptyDirId) {
        return;
      }
      STATE.emptyDirSelection[emptyDirId] = Boolean(emptyDirInput.checked);
      renderEmptyDirScanList();
      updatePanelStatus('已更新空目录删除勾选清单');
    });

    document.addEventListener('pointerdown', (event) => {
      if (!root.classList.contains('gyp-open')) {
        return;
      }
      if (root.contains(event.target)) {
        return;
      }
      closePanel();
    });

    document.addEventListener('keydown', (event) => {
      if (event.key === 'Escape') {
        closePanel();
      }
    });

    syncPanelFromConfig();
    renderDuplicatePreviewList();
    renderMoveSelectionList();
    renderEmptyDirScanList();
    renderMagnetImportList();
    renderMiaochuanJsonResult(null);
    renderMiaochuanCaptureStatus();
    updateRenameModePreview();
    syncTaskControlUi();
  }

  function mountPanelWhenReady() {
    if (UI.root) {
      return;
    }
    if (document.body) {
      createPanel();
      return;
    }

    const tryMount = () => {
      if (document.body && !UI.root) {
        createPanel();
      }
    };

    document.addEventListener('DOMContentLoaded', tryMount, { once: true });
    window.addEventListener('load', tryMount, { once: true });
    const timer = window.setInterval(() => {
      if (UI.root) {
        window.clearInterval(timer);
        return;
      }
      if (document.body) {
        createPanel();
        window.clearInterval(timer);
      }
    }, 300);
  }

  function handleCapture(detail) {
    if (!detail || typeof detail !== 'object') {
      return;
    }

    const url = String(detail.url || '');
    const requestBody = safeJsonParse(detail.requestBody);
    const responseBody = safeJsonParse(detail.responseText);

    if (responseBody && typeof responseBody === 'object') {
      captureMiaochuanSourcePayload(url, requestBody, responseBody);
    }

    if (/api-pan\.xunlei\.com/i.test(url)) {
      const headers = sanitizeHeaders(detail.headers);
      if (headers.authorization || headers['x-device-id'] || headers['x-captcha-token'] || headers['x-client-id']) {
        STATE.lastXunleiHeaders = {
          ...(STATE.lastXunleiHeaders || {}),
          ...headers,
        };
      }
    }

    if (!url.includes(CONFIG.request.apiHost)) {
      return;
    }

    STATE.lastApiHeaders = sanitizeHeaders(detail.headers);
    mergeHeaders(detail.headers);

    if (isLikelyListCapture(url, requestBody, responseBody)) {
      STATE.lastListHeaders = sanitizeHeaders(detail.headers);
      STATE.lastListCapturedAt = Date.now();
      if (requestBody && typeof requestBody === 'object') {
        STATE.lastListBody = sanitizeListBody(requestBody);
        STATE.lastCapturedParentId = getParentIdFromListBody(STATE.lastListBody) || STATE.lastCapturedParentId;
      }

      if (responseBody && typeof responseBody === 'object') {
        const items = extractItemsFromPayload(responseBody);
        STATE.lastListUrl = url;
        STATE.lastListResponse = responseBody;
        if (items.length) {
          const merged = mergeCapturedItems(getParentIdFromListBody(requestBody || STATE.lastListBody), items, {
            listUrl: url,
            requestBody: STATE.lastListBody,
          });
          log(`已捕获列表响应:本批 ${items.length} 项,当前目录累计 ${merged.total} 项(共 ${merged.batchCount} 批)。`);
          syncPanelFromConfig({ fillEmptyOnly: true });
          updatePanelStatus(`已累计当前目录 ${merged.total} 项`);
        }
      }
    }

    if (url.includes(CONFIG.request.renamePath) || /\/rename(?:\?|$)/i.test(url)) {
      STATE.lastRenameRequest = {
        url,
        headers: sanitizeHeaders(detail.headers),
        requestBody,
        responseBody,
      };
      updatePanelStatus('已捕获改名请求上下文');
    }
  }

  function injectNetworkHook() {
    const code = `
      (() => {
        if (window.__gypBatchRenameHookInstalled) {
          return;
        }
        window.__gypBatchRenameHookInstalled = true;

        const EVENT_NAME = ${JSON.stringify(CAPTURE_EVENT)};
        const REQUEST_EVENT = ${JSON.stringify(PAGE_REQUEST_EVENT)};
        const RESPONSE_EVENT = ${JSON.stringify(PAGE_RESPONSE_EVENT)};
        const API_HOST = ${JSON.stringify(CONFIG.request.apiHost)};
        const RENAME_PATH = ${JSON.stringify(CONFIG.request.renamePath)};
        const CAPTURE_HOSTS = [
          'api.guangyapan.com',
          'guangyapan.com',
          'pan.quark.cn',
          'drive.quark.cn',
          'drive-pc.quark.cn',
          'pc-api.uc.cn',
	          'cloud.189.cn',
	          '123pan.com',
	          '123pan.cn',
	          'pan.baidu.com',
	          'yun.baidu.com',
	          'pan.xunlei.com',
	          'api-pan.xunlei.com',
	        ];

        const shouldCapture = (url) => {
          if (typeof url !== 'string' || !url) {
            return false;
          }
          try {
            const parsed = new URL(url, location.href);
            const host = parsed.hostname.toLowerCase();
            return CAPTURE_HOSTS.some((item) => host === item || host.endsWith('.' + item));
          } catch (err) {
            return String(url).includes(API_HOST);
          }
        };

        const normalizeHeaders = (headersLike) => {
          const out = {};
          if (!headersLike) {
            return out;
          }

          if (headersLike instanceof Headers) {
            for (const [key, value] of headersLike.entries()) {
              out[String(key).toLowerCase()] = value;
            }
            return out;
          }

          if (Array.isArray(headersLike)) {
            for (const [key, value] of headersLike) {
              out[String(key).toLowerCase()] = value;
            }
            return out;
          }

          if (typeof headersLike === 'object') {
            for (const [key, value] of Object.entries(headersLike)) {
              out[String(key).toLowerCase()] = value;
            }
          }
          return out;
        };

        const emit = (detail) => {
          window.dispatchEvent(new CustomEvent(EVENT_NAME, { detail }));
        };

        const emitResponse = (detail) => {
          window.dispatchEvent(new CustomEvent(RESPONSE_EVENT, { detail }));
        };

        const originalFetch = window.fetch.bind(window);
        const buildNativeRequestOptions = (optionsLike) => {
          const source = optionsLike && typeof optionsLike === 'object' ? optionsLike : {};
          const out = {};
          const stringFields = ['method', 'mode', 'credentials', 'cache', 'redirect', 'referrer', 'referrerPolicy'];

          for (const key of stringFields) {
            if (typeof source[key] === 'string' && source[key]) {
              out[key] = String(source[key]);
            }
          }

          if (typeof source.keepalive === 'boolean') {
            out.keepalive = source.keepalive;
          }

          const headers = normalizeHeaders(source.headers);
          if (Object.keys(headers).length) {
            out.headers = { ...headers };
          }

          if (typeof source.body === 'string') {
            out.body = source.body;
          }

          return out;
        };
        const requestViaXhr = (url, optionsLike) =>
          new Promise((resolve, reject) => {
            const options = buildNativeRequestOptions(optionsLike);
            const xhr = new XMLHttpRequest();

            xhr.open(options.method || 'GET', String(url), true);
            xhr.withCredentials = options.credentials === 'include';
            xhr.timeout = 30000;

            const headers = normalizeHeaders(options.headers);
            for (const [key, value] of Object.entries(headers)) {
              try {
                xhr.setRequestHeader(key, value);
              } catch (err) {
                reject(new Error(\`XHR setRequestHeader failed for \${key}: \${err && err.message ? err.message : err}\`));
                return;
              }
            }

            xhr.onload = () => {
              resolve({
                ok: xhr.status >= 200 && xhr.status < 300,
                status: xhr.status,
                text: typeof xhr.responseText === 'string' ? xhr.responseText : '',
                via: 'xhr',
              });
            };
            xhr.onerror = () => reject(new Error('XHR network error'));
            xhr.ontimeout = () => reject(new Error('XHR timeout'));
            xhr.send(typeof options.body === 'string' ? options.body : null);
          });

        window.addEventListener(REQUEST_EVENT, async (event) => {
          const detail = event.detail || {};
          if (!detail.requestId || !detail.url) {
            return;
          }

          try {
            const requestUrl = String(detail.url);
            const requestOptions = buildNativeRequestOptions(detail.options);
            try {
              const response = await originalFetch(requestUrl, requestOptions);
              const text = await response.clone().text();
              emitResponse({
                requestId: detail.requestId,
                ok: response.ok,
                status: response.status,
                text,
                via: 'fetch',
              });
              return;
            } catch (fetchErr) {
              try {
                const fallback = await requestViaXhr(requestUrl, requestOptions);
                emitResponse({
                  requestId: detail.requestId,
                  ok: fallback.ok,
                  status: fallback.status,
                  text: fallback.text,
                  via: fallback.via,
                  fallbackFrom: String(fetchErr && fetchErr.message ? fetchErr.message : fetchErr),
                });
                return;
              } catch (xhrErr) {
                emitResponse({
                  requestId: detail.requestId,
                  error: \`fetch failed: \${String(fetchErr && fetchErr.message ? fetchErr.message : fetchErr)} | xhr failed: \${String(xhrErr && xhrErr.message ? xhrErr.message : xhrErr)}\`,
                });
                return;
              }
            }
          } catch (err) {
            emitResponse({
              requestId: detail.requestId,
              error: String(err && err.message ? err.message : err),
            });
          }
        });

        window.fetch = async function patchedFetch(input, init) {
          const url = typeof input === 'string' ? input : (input && input.url) || '';
          const requestHeaders = normalizeHeaders((init && init.headers) || (input && input.headers));
          const requestBody = init && typeof init.body === 'string' ? init.body : '';
          const response = await originalFetch(input, init);

          if (shouldCapture(url)) {
            try {
              const cloned = response.clone();
              const responseText = await cloned.text();
              emit({
                type: 'fetch',
                url,
                headers: requestHeaders,
                requestBody,
                responseText,
                status: response.status,
              });
            } catch (err) {
              emit({
                type: 'fetch',
                url,
                headers: requestHeaders,
                requestBody,
                responseText: '',
                status: response.status,
                captureError: String(err),
              });
            }
          }

          return response;
        };

        const rawOpen = XMLHttpRequest.prototype.open;
        const rawSetHeader = XMLHttpRequest.prototype.setRequestHeader;
        const rawSend = XMLHttpRequest.prototype.send;

        XMLHttpRequest.prototype.open = function patchedOpen(method, url) {
          this.__gypCapture = {
            method,
            url,
            headers: {},
            requestBody: '',
          };
          return rawOpen.apply(this, arguments);
        };

        XMLHttpRequest.prototype.setRequestHeader = function patchedSetHeader(name, value) {
          if (this.__gypCapture) {
            this.__gypCapture.headers[String(name).toLowerCase()] = value;
          }
          return rawSetHeader.apply(this, arguments);
        };

        XMLHttpRequest.prototype.send = function patchedSend(body) {
          if (this.__gypCapture && typeof body === 'string') {
            this.__gypCapture.requestBody = body;
          }

          this.addEventListener('load', function onLoad() {
            const url = this.responseURL || (this.__gypCapture && this.__gypCapture.url) || '';
            if (!shouldCapture(url)) {
              return;
            }

            emit({
              type: 'xhr',
              url,
              headers: this.__gypCapture ? this.__gypCapture.headers : {},
              requestBody: this.__gypCapture ? this.__gypCapture.requestBody : '',
              responseText: this.responseText || '',
              status: this.status,
            });
          });

          return rawSend.apply(this, arguments);
        };
      })();
    `;

    const script = document.createElement('script');
    script.textContent = code;
    (document.documentElement || document.head || document.body).appendChild(script);
    script.remove();
  }

  function registerMenu() {
    if (typeof GM_registerMenuCommand !== 'function') {
      return;
    }

    GM_registerMenuCommand('光鸭云盘:预览当前已捕获列表', () => {
      preview().catch((err) => fail('预览失败:', err));
    });

    GM_registerMenuCommand('光鸭云盘:重新拉取当前目录并预览', () => {
      preview({ refresh: true }).catch((err) => fail('预览失败:', err));
    });

    GM_registerMenuCommand('光鸭云盘:执行批量改名', () => {
      run().catch((err) => fail('执行失败:', err));
    });

    GM_registerMenuCommand('光鸭云盘:扫描最里层空目录', () => {
      scanEmptyLeafDirectories().catch((err) => fail('空目录扫描失败:', err));
    });

    GM_registerMenuCommand('光鸭云盘:删除已勾选空目录', () => {
      deleteEmptyDirItems().catch((err) => fail('删除空目录失败:', err));
    });

    GM_registerMenuCommand('光鸭云盘:预览重复项', () => {
      previewDuplicates({ refresh: true }).catch((err) => fail('重复项预览失败:', err));
    });

    GM_registerMenuCommand('光鸭云盘:删除重复项', () => {
      deleteDuplicateItems({ refresh: true }).catch((err) => fail('删除重复项失败:', err));
    });

    GM_registerMenuCommand('光鸭云盘:查看云添加任务', () => {
      listCloudTasks()
        .then((response) => {
          console.table(extractCloudTaskRows(response.payload));
        })
        .catch((err) => fail('读取云添加任务失败:', err));
    });

    GM_registerMenuCommand('光鸭云盘:从当前网页生成秒传 JSON', async () => {
      try {
        const result = await generateMiaochuanJsonFromCapturedPage();
        console.log(LOG_PREFIX, '秒传 JSON 已生成:', result.normalized);
      } catch (err) {
        fail('从当前网页生成秒传 JSON 失败:', err);
      }
    });

    GM_registerMenuCommand('光鸭云盘:读取当前页面勾选', () => {
      console.table(buildCheckedPageSelectionPreviewItems());
    });

    GM_registerMenuCommand('光鸭云盘:查看捕获状态', () => {
      console.log(LOG_PREFIX, exportState());
    });
  }

  window.addEventListener(CAPTURE_EVENT, (event) => {
    handleCapture(event.detail);
  });

  loadPersistedConfig();
  injectNetworkHook();
  registerMenu();
  mountPanelWhenReady();

  const api = {
    config: CONFIG,
    state: STATE,
    preview,
    previewDuplicates,
    run,
    fetchCurrentList,
    exportState,
    selectDuplicateRows,
    deleteDuplicateItems,
    deleteEmptyDirItems,
    importMagnetTextFiles,
    listCloudTasks,
    previewDirectDownloadSelection,
    generateDirectDownloadLinksFromCheckedItems,
    downloadDirectDownloadMd5SizeSelection,
    triggerDirectDownloadsFromCheckedItems,
    readShareLinkFromPanel,
    generateMiaochuanJsonFromShareLinkSelection,
    generateMiaochuanJsonFromCapturedPage,
    generateMiaochuanJsonFromPanel,
    normalizeMiaochuanPayload,
    buildCheckedPageSelectionPreviewItems,
    moveCheckedFolderContentsToCurrentDirectory,
    moveCheckedItemsUpOneLevel,
    moveCheckedItemsToTargetDirectory,
    scanEmptyLeafDirectories,
    extractMagnetLinks,
    applyPanelConfig,
    savePersistedConfig,
  };

  const pageWindow = getPageWindowObject();
  pageWindow.gypBatchRenamer = api;

  log('脚本已加载。页面右下角会出现光鸭云盘悬浮面板,也可以在控制台运行 gypBatchRenamer.preview() / gypBatchRenamer.run() / gypBatchRenamer.deleteDuplicateItems() / gypBatchRenamer.importMagnetTextFiles() / gypBatchRenamer.scanEmptyLeafDirectories()。');
})();