Greasy Fork is available in English.

知乎收藏夹 Pro

(1) 使用 AI 为知乎收藏夹一键生成描述。(2) 使用 AI 整理与重分类收藏夹。(3) [todo]替换知乎收藏按钮,直接用AI辅助分类

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         知乎收藏夹 Pro
// @license      MIT
// @namespace    http://tampermonkey.net/
// @version      0.4.3
// @description  (1) 使用 AI 为知乎收藏夹一键生成描述。(2) 使用 AI 整理与重分类收藏夹。(3) [todo]替换知乎收藏按钮,直接用AI辅助分类
// @author       https://github.com/ienone
// @match        https://www.zhihu.com/collection/*
// @match        https://www.zhihu.com/collections/mine*
// @match        https://www.zhihu.com/people/*/collections*
// @icon         https://static.zhihu.com/heifetz/favicon.ico
// @connect      api.deepseek.com
// @connect      zhuanlan.zhihu.com
// @grant        GM_xmlhttpRequest
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addStyle
// @grant        GM_registerMenuCommand
// @grant        GM_getResourceText
// @resource     CHART_JS https://cdn.jsdelivr.net/npm/chart.js
// ==/UserScript==



/* jshint esversion: 11 */

(function() {
    'use strict';

    const ZHIHU_BLUE = '#056DE8';
    let moveHistory = []; // 用于存储所有成功的移动操作
    let progressDashboardState = {}; // 存储仪表盘的所有状态
    let chartInstances = {}; // 存储Chart.js实例

    /**
     * 生成一系列颜色用于图表
     */
    function generateColors(count) {
        const colors = [];
        const baseHue = 200; // 知乎蓝的色相
        for (let i = 0; i < count; i++) {
            // 使用黄金分割角来生成视觉上分散的颜色
            const hue = (baseHue + (i * 137.508)) % 360;
            colors.push(`hsl(${hue}, 70%, 60%)`);
        }
        return colors;
    }

    // --- 1. 自定义 CSS ---
    GM_addStyle(`

        /* --- Shimmer 加载动画效果 --- */
        .zcp-shimmer {
            position: relative;
            overflow: hidden;
        }
        .zcp-shimmer::after {
            content: '';
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: linear-gradient(100deg, rgba(255,255,255,0) 20%, rgba(255,255,255,0.3) 50%, rgba(255,255,255,0) 80%);
            transform: translateX(-100%);
            animation: zcp-shimmer-animation 1.5s infinite;
        }
        @keyframes zcp-shimmer-animation {
            100% {
                transform: translateX(100%);
            }
        }

        /* --- AI 功能按钮 --- */
        button#zcp-ai-btn.zcp-ai-button {
            display: inline-flex;
            align-items: center;
            justify-content: center;
            vertical-align: middle;
            height: 36px;
            background-color: ${ZHIHU_BLUE} !important;
            color: white !important;
            border: none !important;
            border-radius: 12px;
            cursor: pointer;
            padding: 0 16px;
            margin-left: 12px;
            box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.08) !important;
            transition: transform 0.2s ease, box-shadow 0.2s ease !important;
        }

        button#zcp-ai-btn.zcp-ai-button:hover {
            transform: translateY(-2px) !important;
            box-shadow: 0 5px 10px rgba(0, 0, 0, 0.12), 0 3px 6px rgba(0, 0, 0, 0.1) !important;
        }

        button#zcp-ai-btn.zcp-ai-button:active {
            transform: translateY(1px) !important;
            box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.2) !important;
        }

        button#zcp-ai-btn.zcp-ai-button:disabled {
            background-color: #A0A0A0 !important;
            cursor: not-allowed !important;
            transform: none !important;
            box-shadow: none !important;
            opacity: 0.8;
        }

        button#zcp-ai-btn.zcp-ai-button .zcp-ai-icon {
            width: 20px;
            height: 20px;
            margin-right: 6px;
            fill: currentColor;
        }
        
        .zcp-spinner {
            width: 22px;
            height: 22px;
            border: 3px solid rgba(255, 255, 255, 0.3);
            border-radius: 50%;
            border-top-color: #ffffff;
            animation: zcp-spin 1s ease-in-out infinite;
        }
        @keyframes zcp-spin { to { transform: rotate(360deg); } }

        /* --- 模态框样式 --- */
        .zcp-modal-overlay {
            position: fixed; top: 0; left: 0; width: 100vw; height: 100vh;
            background-color: rgba(0, 0, 0, 0.1);
            backdrop-filter: blur(8px);
            -webkit-backdrop-filter: blur(8px);
            display: flex; justify-content: center; align-items: center; z-index: 9999;
        }
        .zcp-modal-container {
            background-color: #ffffff;
            padding: 28px;
            border-radius: 24px;
            box-shadow: 0 12px 40px rgba(0,0,0,0.15);
            width: 90%;
            max-width: 520px;
        }
        .zcp-modal-header {
            font-size: 22px; color: #1a1a1a; font-weight: 600;
            margin-bottom: 24px; text-align: center;
        }

        /* --- [修改] 供用户修改的文本框样式 --- */
        .zcp-modal-content textarea {
            width: 100%;
            min-height: 120px;
            border-radius: 12px;
            border: 1px solid #EAEAEA;
            padding: 14px;
            font-size: 16px;
            resize: vertical;
            box-sizing: border-box;
            background-color: #FDFDFD;
            color: #333;
            /* [修改] 为常态文本框添加入下沉效果 */
            box-shadow: inset 0 2px 4px rgba(0,0,0,0.06);
            transition: box-shadow 0.2s, border-color 0.2s, background-color 0.2s;
            scrollbar-width: none; /* Firefox */
            -ms-overflow-style: none;  /* IE and Edge */
        }
        .zcp-modal-content textarea::-webkit-scrollbar { display: none; }

        /* [修改] 输入时下沉效果加深 */
        .zcp-modal-content textarea:focus {
            outline: none;
            border-color: #D0D0D0;
            background-color: #fff;
            /* 下沉效果更明显 */
            box-shadow: inset 0 3px 6px rgba(0,0,0,0.08);
        }

        .zcp-modal-actions {
            display: flex; justify-content: flex-end;
            margin-top: 24px; gap: 12px;
        }

        /* --- 模态框按钮 --- */
        .zcp-modal-button {
            padding: 10px 24px;
            border-radius: 10px;
            border: none;
            cursor: pointer;
            font-size: 14px;
            font-weight: 500;
            transition: transform 0.15s ease, box-shadow 0.15s ease, opacity 0.15s;
        }
        
        /* [修改] 统一并强化所有按钮的按下(active)效果 */
        .zcp-modal-button:active {
            transform: translateY(1px); /* 轻微下移 */
        }

        .zcp-modal-button.primary {
            background-color: ${ZHIHU_BLUE};
            color: white;
            box-shadow: 0 2px 5px rgba(5, 109, 232, 0.3);
        }
        .zcp-modal-button.primary:hover {
            opacity: 0.9;
            box-shadow: 0 4px 8px rgba(5, 109, 232, 0.35);
        }
        .zcp-modal-button.primary:active {
            /* 使用内阴影来创建清晰的“按下”感 */
            box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.2);
        }

        .zcp-modal-button.secondary {
            background-color: #f0f2f5;
            color: #333;
            border: 1px solid #EAEAEA;
        }
        .zcp-modal-button.secondary:hover {
            border-color: #DDD;
            background-color: #E9E9E9;
        }
        .zcp-modal-button.secondary:active {
            background-color: #E2E2E2;
            /* 使用内阴影来创建清晰的“按下”感 */
            box-shadow: inset 0 2px 4px rgba(0,0,0,0.08);
        }


    /* --- 功能二:整理 UI --- */

    /* 整理入口按钮 */
    button#zcp-organize-btn {
        display: inline-flex;
        align-items: center;
        justify-content: center;
        vertical-align: middle;
        height: 36px;
        background-color: ${ZHIHU_BLUE} !important;
        color: white !important;
        border: none !important;
        border-radius: 12px;
        cursor: pointer;
        padding: 0 16px;
        margin-right: 20px; /*  与右侧按钮拉开距离 */
        margin-left: auto; 
        box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.08) !important;
        transition: transform 0.2s ease, box-shadow 0.2s ease !important;
    }
    button#zcp-organize-btn:hover {
        transform: translateY(-2px) !important;
        box-shadow: 0 5px 10px rgba(0, 0, 0, 0.12), 0 3px 6px rgba(0, 0, 0, 0.1) !important;
    }
    button#zcp-organize-btn:active {
        transform: translateY(1px) !important;
        box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.2) !important;
    }
    button#zcp-organize-btn .zcp-ai-icon {
        width: 18px; height: 18px; fill: currentColor; margin-right: 6px;
    }
    button#zcp-organize-btn:disabled {
        background-color: #A0A0A0 !important;
        cursor: not-allowed !important;
        transform: none !important;
        box-shadow: none !important;
        opacity: 0.8;
    }

    /* 整理模态框 - 设置界面 */
    .zcp-organize-settings .zcp-fieldset {
        margin-bottom: 20px; border: 1px solid #e9e9e9; padding: 12px 16px;
        border-radius: 12px; background: #fcfcfc;
    }
    .zcp-organize-settings legend { font-weight: 600; padding: 0 8px; color: #333; }
    .zcp-collection-list { max-height: 150px; overflow-y: auto; padding: 5px; }
    .zcp-collection-list label { display: block; margin-bottom: 8px; cursor: pointer; padding: 4px 8px; border-radius: 6px; transition: background-color 0.2s; }
    .zcp-collection-list label:hover { background-color: #f0f2f5; }
    .zcp-collection-list input { margin-right: 10px; }

    /* --- 统一自定义复选框样式 --- */
    .zcp-custom-checkbox {
        display: inline-flex;
        align-items: center;
        cursor: pointer;
        gap: 8px;
        padding: 4px; /* 增加点击区域 */
        border-radius: 6px;
        transition: background-color 0.2s;
    }
    .zcp-custom-checkbox:hover {
        background-color: #f0f2f5;
    }
    .zcp-custom-checkbox input[type="checkbox"] {
        position: absolute;
        opacity: 0;
        width: 0;
        height: 0;
    }
    .zcp-checkbox-visual {
        display: inline-flex;
        align-items: center;
        justify-content: center;
        flex-shrink: 0;
        width: 16px; /* 统一尺寸 */
        height: 16px;
        border: 2px solid #ccc; /* 默认边框 */
        border-radius: 5px;
        background-color: transparent;
        transition: all 0.2s ease-out;
    }
    .zcp-custom-checkbox:hover .zcp-checkbox-visual {
        border-color: #999;
    }
    /* 选中时,用伪元素创建内部小圆角正方形 */
    .zcp-checkbox-visual::after {
        content: '';
        display: block;
        width: 12px;
        height: 12px;
        background-color: ${ZHIHU_BLUE};
        border-radius: 2px; /* 小圆角正方形 */
        transform: scale(0);
        transition: transform 0.2s ease-in-out;
    }
    .zcp-custom-checkbox input[type="checkbox"]:checked + .zcp-checkbox-visual::after {
        transform: scale(1);
    }
    
    /* 针对收藏夹列表调整间距和大小 */
    .zcp-collection-list .zcp-custom-checkbox {
        width: 100%;
        gap: 12px;
    }

    .zcp-options-grid { display: flex; justify-content: space-between; align-items: center; gap: 16px; }
    .zcp-options-grid input[type="number"] {
        border-radius: 8px;
        border: 1px solid #EAEAEA;
        padding: 6px 8px;
        font-size: 14px;
        box-sizing: border-box;
        background-color: #FDFDFD;
        box-shadow: inset 0 2px 4px rgba(0,0,0,0.06);
        transition: box-shadow 0.2s, border-color 0.2s;
        text-align: center;
        -moz-appearance: textfield; /* Firefox */
    }
    .zcp-options-grid input[type="number"]::-webkit-outer-spin-button,
    .zcp-options-grid input[type="number"]::-webkit-inner-spin-button {
        -webkit-appearance: none;
        margin: 0;
    }
    .zcp-options-grid input[type="number"]:focus {
        outline: none;
        border-color: #D0D0D0;
        box-shadow: inset 0 3px 6px rgba(0,0,0,0.08);
    }

    /* 模态框尺寸过渡动画 */
    .zcp-modal-container {
        transition: max-width 0.5s ease-in-out, max-height 0.5s ease-in-out;
    }
    .zcp-modal-container.dashboard-mode {
        max-width: 1200px;
        width: 95%;
    }

    /* 整理模态框 - 仪表盘(Dashboard)总布局 */
    .zcp-dashboard-container {
        display: flex;
        gap: 24px;
        height: 60vh; /* 建议高度 */
        min-height: 500px;
    }
    .zcp-dashboard-left {
        width: 35%;
        display: flex;
        flex-direction: column;
        gap: 15px;
    }
    .zcp-dashboard-right {
        width: 65%;
        display: flex;
        flex-direction: column;
    }

    /* 图表容器样式 */
    .zcp-chart-container {
        background: #f9f9f9;
        border: 1px solid #eee;
        border-radius: 12px;
        padding: 15px;
        flex-grow: 1;
        display: flex;
        flex-direction: column;
    }
    .zcp-chart-container h3 {
        margin: 0 0 10px 5px;
        font-size: 14px;
        font-weight: 600;
        color: #333;
    }
    .zcp-chart-wrapper {
        position: relative;
        flex-grow: 1;
    }
    .zcp-chart-container canvas {
        cursor: pointer;
    }

    /* 日志区域新样式 */
    .zcp-progress-log {
        height: 100%; /* 占满右侧所有可用空间 */
        overflow-y: auto;
        background-color: #fdfdfd;
        border: 1px solid #eee;
        border-radius: 12px;
        padding: 10px;
        font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
        font-size: 13px;
        line-height: 1.6;
        color: #444;
    }

    /* 日志条目新样式 */
    .zcp-log-item {
        padding: 8px 12px;
        border-bottom: 1px solid #f0f0f0;
        display: flex;
        flex-direction: column;
        gap: 4px;
    }
    .zcp-log-item:last-child {
        border-bottom: none;
    }
    .zcp-log-title {
        font-weight: 600;
        color: #1a1a1a;
    }
    .zcp-log-title a {
        color: inherit;
        text-decoration: none;
        transition: color 0.2s;
    }
    .zcp-log-title a:hover {
        color: ${ZHIHU_BLUE};
        text-decoration: underline;
    }
    .zcp-log-path {
        display: flex;
        align-items: center;
        gap: 8px;
        font-size: 12px;
        color: #666;
    }
    .zcp-log-path .zcp-log-collection {
        background: #f0f2f5;
        padding: 2px 6px;
        border-radius: 4px;
    }
    .zcp-log-path .zcp-log-arrow {
        color: #999;
    }
    .zcp-log-path .zcp-log-status-text {
        font-style: italic;
    }
    .zcp-log-item.status-error .zcp-log-path .zcp-log-status-text {
        color: #dc3545;
        font-weight: bold;
    }

    /* 状态颜色应用 */
    .zcp-log-item.status-success .zcp-log-path .zcp-log-collection.target {
        background-color: #e6ffed;
        border: 1px solid #b7eb8f;
    }
    .zcp-log-item.status-skipped .zcp-log-path {
        color: #087a91;
    }
    .zcp-log-item.status-dryrun .zcp-log-path {
        color: #6c757d;
    }
    
    .zcp-undo-btn, .zcp-redo-btn {
        background: #e9e9e9; border: 1px solid #ddd; color: #555;
        padding: 2px 8px; font-size: 11px; border-radius: 5px; cursor: pointer;
        transition: all 0.2s; margin-left: auto; /* 推到最右边 */
    }
    .zcp-undo-btn:hover { background: #dcdcdc; border-color: #ccc; }
    .zcp-redo-btn { background: #e6f7ff; border: 1px solid #91d5ff; color: #096dd9; }
    .zcp-redo-btn:hover { background: #bae7ff; border-color: #69c0ff; }
    .zcp-undo-btn:disabled, .zcp-redo-btn:disabled {
        background: #f5f5f5; color: #aaa; cursor: not-allowed; border-color: #eee;
    }

    /* 底部按钮区域样式 */
    .zcp-modal-actions.dashboard-mode {
        justify-content: space-between;
        align-items: center;
    }
    .zcp-modal-actions .zcp-progress-stats {
        font-size: 13px; color: #666;
    }
    `);


    // --- 2. 核心 API 调用与工具函数 ---

    /**
     * 获取知乎 API 请求所需的 headers
     */
    function getZhihuApiHeaders() {
        const xsrfToken = document.cookie.split('; ').find(row => row.startsWith('_xsrf='))?.split('=')[1];
        if (!xsrfToken) {
            throw new Error('无法找到 _xsrf token,请确保您已登录知乎。');
        }
        return {
            'Content-Type': 'application/json',
            'x-xsrftoken': xsrfToken,
        };
    }

    /**
     * 调用 DeepSeek API
     */
    async function callDeepSeek(prompt) {
        const apiKey = await GM_getValue('deepseek_api_key', '');
        if (!apiKey) {
            alert('请先在油猴脚本菜单中设置 DeepSeek API Key!');
            throw new Error('DeepSeek API Key 未设置');
        }

        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: 'POST',
                url: 'https://api.deepseek.com/chat/completions',
                headers: {
                    'Content-Type': 'application/json',
                    'Authorization': `Bearer ${apiKey}`,
                },
                data: JSON.stringify({
                    model: 'deepseek-chat',
                    messages: [
                        { "role": "system", "content": "你是一位专业的知识库管理员和内容分析专家。" },
                        { "role": "user", "content": prompt }
                    ],
                    temperature: 0.7,
                }),
                onload: function(response) {
                    if (response.status >= 200 && response.status < 300) {
                        const result = JSON.parse(response.responseText);
                        resolve(result.choices[0].message.content.trim());
                    } else {
                        console.error('DeepSeek API Error:', response);
                        reject(`API 请求失败: ${response.status}`);
                    }
                },
                onerror: function(error) {
                    console.error('Network Error calling DeepSeek:', error);
                    reject('网络错误,无法连接到 DeepSeek API');
                }
            });
        });
    }
    // --- 功能二:API 封装与工具函数 ---

    /**
     * 从页面 URL 中获取当前用户的 ID
     */
    function getUserId() {
        const match = window.location.pathname.match(/\/people\/([^/]+)/);
        if (!match) throw new Error("无法在URL中找到用户ID");
        return match[1];
    }
    /**
     * "API"-1: 获取用户所有收藏夹 (已修改为从页面DOM抓取,不再使用API)
     */
    async function fetchAllUserCollections() {
        console.log('[知乎收藏夹 Pro] 正在从当前页面抓取收藏夹列表...');

        const itemElements = document.querySelectorAll('.SelfCollectionItem-innerContainer');

        if (itemElements.length === 0) {
            throw new Error('在当前页面上没有找到任何收藏夹。请确保您在 "我的收藏" (collections/mine) 页面,且收藏夹列表已完全加载。');
        }

        const allCollections = Array.from(itemElements).map(item => {
            const titleElement = item.querySelector('a.SelfCollectionItem-title');
            const descriptionElement = item.querySelector('.SelfCollectionItem-description');

            if (!titleElement || !titleElement.href) {
                console.warn('[知乎收藏夹 Pro] 跳过一个无效的收藏夹元素 (缺少标题链接)。');
                return null; // 跳过缺少标题链接的元素
            }

            // 提取标题:通过获取第一个文本节点来提取
            // 例如,从 `<a>文化<span>...</span></a>` 中正确提取 "文化"
            const title = (titleElement.childNodes[0] && titleElement.childNodes[0].nodeType === Node.TEXT_NODE)
                ? titleElement.childNodes[0].nodeValue.trim()
                : titleElement.textContent.trim();
            
            // 从链接中提取 URL 和收藏夹 ID
            const url = titleElement.href;
            const idMatch = url.match(/\/collection\/(\d+)/);
            const id = idMatch ? idMatch[1] : null;

            // 提取描述,如果存在的话
            const description = descriptionElement ? descriptionElement.textContent.trim() : '';

            if (id && title) {
                return { id, title, description };
            }
            
            console.warn(`[知乎收藏夹 Pro] 解析一个收藏夹时失败,标题: "${title}", URL: "${url}"`);
            return null;
        }).filter(Boolean); // 过滤掉所有解析失败的 null 条目

        if (allCollections.length === 0) {
             throw new Error('成功找到收藏夹的HTML元素,但未能解析出任何有效的收藏夹信息。页面结构可能已更新。');
        }

        console.log(`[知乎收藏夹 Pro] 成功从页面抓取 ${allCollections.length} 个收藏夹。`);
        return allCollections;
    }
    /**
     * API-2: 获取单个收藏夹的所有内容
     */
    async function fetchCollectionItems(collectionId) {
        let allItems = [];
        let nextUrl = `/api/v4/collections/${collectionId}/items?limit=20&offset=0`;

        while (nextUrl) {
            const response = await new Promise((resolve, reject) => {
                GM_xmlhttpRequest({
                    method: 'GET',
                    url: nextUrl,
                    headers: getZhihuApiHeaders(),
                    onload: res => {
                        if (res.status === 200) resolve(JSON.parse(res.responseText));
                        else reject(new Error(`获取收藏夹内容失败: ${res.status}`));
                    },
                    onerror: err => reject(new Error('网络错误'))
                });
            });
            allItems = allItems.concat(response.data);
            nextUrl = response.paging.is_end ? null : response.paging.next;
        }
        return allItems;
    }

    /**
     * API-3: 抓取文章/回答正文
     */
    async function scrapeContent(url) {
        const html = await new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: 'GET',
                url: url,
                onload: res => {
                    if (res.status === 200) resolve(res.responseText);
                    else reject(new Error(`抓取内容失败: ${res.status} for ${url}`));
                },
                onerror: err => reject(new Error('网络错误'))
            });
        });
        const parser = new DOMParser();
        const doc = parser.parseFromString(html, 'text/html');
        const contentElement = doc.querySelector('.RichText.ztext');
        return contentElement ? contentElement.innerText.trim() : '正文抓取失败';
    }

    /**
     * API-4: 添加内容到收藏夹
     */
    async function addToCollection(contentId, contentType, targetCollectionId) {
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: 'POST',
                url: `/api/v4/collections/${targetCollectionId}/contents?content_id=${contentId}&content_type=${contentType}`,
                headers: getZhihuApiHeaders(),
                data: '{}',
                onload: res => {
                    if (res.status === 200 && JSON.parse(res.responseText).success) resolve(true);
                    else reject(new Error(`添加失败: ${res.responseText}`));
                },
                onerror: err => reject(new Error('网络错误'))
            });
        });
    }

    /**
     * API-5: 从收藏夹移除内容
     */
    async function removeFromCollection(contentId, contentType, sourceCollectionId) {
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: 'DELETE',
                url: `/api/v4/collections/${sourceCollectionId}/contents/${contentId}?content_type=${contentType}`,
                headers: getZhihuApiHeaders(),
                onload: res => {
                    if (res.status === 200 && JSON.parse(res.responseText).success) resolve(true);
                    else reject(new Error(`移除失败: ${res.responseText}`));
                },
                onerror: err => reject(new Error('网络错误'))
            });
        });
    }

    /**
     * 为整理功能构建 AI Prompt
     */
    function buildOrganizationPrompt(articleContent, articleTitle, targetCollections) {
        const collectionInfo = targetCollections
            .map(c => `- ${c.title}: ${c.description || '无描述'}`)
            .join('\n');

        return `你是一位图书管理员,任务是将一篇文章精准地分类到一个最合适的收藏夹中。

这是待分类的文章:
标题:${articleTitle}
正文摘要(前500字):${articleContent.substring(0, 500)}...

这是你的可用收藏夹列表和它们的简介:
${collectionInfo}

请分析文章内容,并从上面的列表中,选择一个最匹配的收藏夹。
你的回答必须只包含你选择的收藏夹的 **完整标题**,不要添加任何解释、引号或其他文字。

例如,如果最匹配的是“技术视野”,你就只回答“技术视野”。

你选择的收藏夹标题是:`;
    }


    // --- 功能二:UI 渲染与流程控制 ---

    /**
     * 注入“整理”按钮
     */
    function injectOrganizeButton(container) {
        const organizeButton = document.createElement('button');
        organizeButton.id = 'zcp-organize-btn';
        organizeButton.className = 'zcp-ai-button';
        const svgIcon = `<svg class="zcp-ai-icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M399.825455 247.901091a23.272727 23.272727 0 0 1 43.659636 0L493.381818 382.743273a23.272727 23.272727 0 0 0 13.730909 13.730909l134.842182 49.896727a23.272727 23.272727 0 0 1 0 43.659636L507.112727 539.927273a23.272727 23.272727 0 0 0-13.730909 13.730909l-49.896727 134.842182a23.272727 23.272727 0 0 1-43.659636 0l-49.896728-134.842182a23.272727 23.272727 0 0 0-13.730909-13.730909l-134.842182-49.896728a23.272727 23.272727 0 0 1 0-43.659636l134.842182-49.896727a23.272727 23.272727 0 0 0 13.730909-13.730909L399.825455 247.901091zM738.769455 584.890182a9.309091 9.309091 0 0 1 17.454545 0l27.461818 74.333091a9.309091 9.309091 0 0 0 5.538909 5.492363l74.286546 27.461819a9.309091 9.309091 0 0 1 0 17.50109l-74.286546 27.461819a9.309091 9.309091 0 0 0-5.492363 5.538909l-27.508364 74.286545a9.309091 9.309091 0 0 1-17.454545 0l-27.508364-74.286545a9.309091 9.309091 0 0 0-5.492364-5.492364l-74.333091-27.508364a9.309091 9.309091 0 0 1 0-17.454545l74.333091-27.508364a9.309091 9.309091 0 0 0 5.492364-5.492363l27.461818-74.333091z"></path></svg>`;
        organizeButton.innerHTML = `${svgIcon}<span>整理</span>`;
        container.appendChild(organizeButton);

        organizeButton.addEventListener('click', handleOrganizeClick);
        // 定位到“新建收藏夹”按钮
        const newCollectionButton = container.querySelector('.CollectionsHeader-addFavlistButton, .css-10dextj'); // 兼容新旧class

        if (newCollectionButton) {
            // 将按钮插入到“新建收藏夹”按钮之前
            container.insertBefore(organizeButton, newCollectionButton);
        } else {
            // 如果找不到,作为备选方案,还是添加到容器末尾
            container.appendChild(organizeButton);
        }

        organizeButton.addEventListener('click', handleOrganizeClick);
    }
    /**
     * 点击“整理”按钮后的处理
     */
    async function handleOrganizeClick(event) {
        const button = event.currentTarget;
        button.disabled = true;
        button.querySelector('span').textContent = '加载中...';

        try {
            const collections = await fetchAllUserCollections();
            showOrganizeSettingsModal(collections);
        } catch (error) {
            alert(`加载收藏夹列表失败: ${error.message}`);
        } finally {
            button.disabled = false;
            button.querySelector('span').textContent = '整理';
        }
    }

    /**
     * 显示整理的设置模态框
     */
    function showOrganizeSettingsModal(collections) {
        const overlay = document.createElement('div');
        overlay.className = 'zcp-modal-overlay';
        overlay.addEventListener('click', (e) => { if (e.target === overlay) closeModal(); });

        const createCollectionCheckbox = (c, name) => `
            <label class="zcp-custom-checkbox" title="${c.description || '无描述'}">
                <input type="checkbox" name="${name}" value="${c.id}">
                <span class="zcp-checkbox-visual"></span>
                <span>${c.title}</span>
            </label>`;

        const collectionOptions = collections.map(c => createCollectionCheckbox(c, 'source-collection')).join('');
        const targetCollectionOptions = collections.map(c => createCollectionCheckbox(c, 'target-collection')).join('');

        overlay.innerHTML = `
            <div class="zcp-modal-container">
                <div class="zcp-modal-header">整理收藏夹</div>
                <div class="zcp-modal-content zcp-organize-settings">
                    <fieldset class="zcp-fieldset">
                        <legend>1. 选择源收藏夹 (待整理)</legend>
                        <div class="zcp-collection-list" id="zcp-source-list">${collectionOptions}</div>
                    </fieldset>
                    <fieldset class="zcp-fieldset">
                        <legend>2. 选择目标收藏夹 (分类目的地)</legend>
                        <div class="zcp-collection-list" id="zcp-target-list">${targetCollectionOptions}</div>
                    </fieldset>
                    <fieldset class="zcp-fieldset">
                        <legend>3. 设置</legend>
                        <div class="zcp-options-grid">
                            <label class="zcp-custom-checkbox">
                                <input type="checkbox" id="zcp-dry-run" checked>
                                <span class="zcp-checkbox-visual"></span>
                                <span>试运行 (Dry Run)</span>
                            </label>
                            <label style="display: flex; align-items: center; gap: 8px;">
                                <span>并发数:</span>
                                <input type="number" id="zcp-concurrency" value="3" min="1" max="5" style="width: 50px;">
                            </label>
                        </div>
                    </fieldset>
                </div>
                <div class="zcp-modal-actions">
                    <button id="zcp-cancel-btn" class="zcp-modal-button secondary">取消</button>
                    <button id="zcp-start-btn" class="zcp-modal-button primary">开始整理</button>
                </div>
            </div>`;

        document.body.appendChild(overlay);

        const closeModal = () => document.body.removeChild(overlay);
        overlay.querySelector('#zcp-cancel-btn').addEventListener('click', closeModal);
        overlay.querySelector('#zcp-start-btn').addEventListener('click', () => {
             startOrganizationProcess(collections, overlay);
        });
    }

    /**
     * 开始整理流程,构建任务队列并启动 workers
     */
    async function startOrganizationProcess(allCollectionsData, modalOverlay) {
        // 1. 从UI获取设置
        const getCheckedValues = name => Array.from(modalOverlay.querySelectorAll(`input[name="${name}"]:checked`)).map(cb => cb.value);
        const sourceIds = getCheckedValues('source-collection');
        const targetIds = getCheckedValues('target-collection');
        const isDryRun = modalOverlay.querySelector('#zcp-dry-run').checked;
        const concurrency = parseInt(modalOverlay.querySelector('#zcp-concurrency').value, 10);

        if (sourceIds.length === 0 || targetIds.length === 0) {
            alert('请至少选择一个源收藏夹和一个目标收藏夹!');
            return;
        }

        const startBtn = modalOverlay.querySelector('#zcp-start-btn');
        const originalBtnText = startBtn.textContent; // 保存原始文本
        
        // [修改] 禁用按钮,更新文本,并添加 Shimmer 效果
        startBtn.disabled = true;
        startBtn.textContent = '正在构建任务...';
        startBtn.classList.add('zcp-shimmer');

        // 重置历史记录
        moveHistory = [];

        // 2. 构建任务队列
        const taskQueue = [];
        try {
            for (const sourceId of sourceIds) {
                const items = await fetchCollectionItems(sourceId);
                items.forEach((item, index) => {
                    if (item.content) {
                        taskQueue.push({
                            id: `task-${sourceId}-${index}`,
                            contentId: item.content.id,
                            contentType: item.content.type,
                            title: item.content.question ? item.content.question.title : item.content.title,
                            url: item.content.url.replace('http:', 'https:'),
                            sourceCollectionId: sourceId,
                            status: 'pending', // 初始状态
                        });
                    }
                });
            }
        } catch(e) {
            alert(`构建任务失败: ${e.message}`);
            // [修改] 在出错时恢复按钮状态
            startBtn.disabled = false;
            startBtn.textContent = originalBtnText; // 恢复原始文本
            startBtn.classList.remove('zcp-shimmer'); // 移除流光效果
            return;
        }

        if (taskQueue.length === 0) {
            alert('选中的源收藏夹中没有内容可供整理。');
            startBtn.disabled = false;
            startBtn.textContent = '开始整理';
            return;
        }

        // 3. 初始化仪表盘的全局状态对象
        const sourceCollections = allCollectionsData.filter(c => sourceIds.includes(c.id));
        const targetCollections = allCollectionsData.filter(c => targetIds.includes(c.id));
        // 合并并去重所有涉及的收藏夹
        const allInvolvedCollections = [...sourceCollections, ...targetCollections]
            .filter((v, i, a) => a.findIndex(t => (t.id === v.id)) === i);

        progressDashboardState = {
            stats: {
                moved: 0,
                skipped: 0,
                error: 0,
                pending: taskQueue.length
            },
            collections: {}, // 用于存储增减统计
            logs: taskQueue.map(t => ({ ...t, message: '' })), // 扩展任务队列为日志对象
            // 将 filter 从字符串改为对象,以支持更复杂的筛选
            filter: { type: 'all', value: null }, 
            isDryRun: isDryRun,
            allCollectionsData: allCollectionsData, // 缓存所有收藏夹信息
            targetCollectionsData: targetCollections // 缓存目标收藏夹信息
        };

        allInvolvedCollections.forEach(c => {
            progressDashboardState.collections[c.id] = { title: c.title, added: 0, removed: 0 };
        });

        // 4. 切换到仪表盘UI并初始化
        await showDashboardUI(modalOverlay);

        // 5. 启动 Workers
        const workers = [];
        for (let i = 0; i < concurrency; i++) {
            // 将 taskQueue 传递给 worker
            workers.push(worker(taskQueue));
        }

        await Promise.all(workers);

        // 6. 任务结束
        const bulkActionButton = modalOverlay.querySelector('#zcp-bulk-action-btn');
        if (bulkActionButton) {
            if (moveHistory.length > 0 && !progressDashboardState.isDryRun) {
                bulkActionButton.textContent = '一键撤销';
                bulkActionButton.dataset.action = 'undo'; // [重要] 设置初始状态
                bulkActionButton.disabled = false;
                // 添加点击事件处理器
                bulkActionButton.addEventListener('click', () => handleBulkAction(modalOverlay));
            } else {
                bulkActionButton.textContent = '全部完成';
                bulkActionButton.disabled = true; // 保持禁用
            }
        }
    }
        
    async function showDashboardUI(modalOverlay) {
        const modalContainer = modalOverlay.querySelector('.zcp-modal-container');
        // 触发模态框放大动画
        modalContainer.classList.add('dashboard-mode');

        modalContainer.innerHTML = `
            <div class="zcp-modal-header">整理进度</div>
            <div class="zcp-modal-content zcp-dashboard-container">
                <div class="zcp-dashboard-left">
                    <div class="zcp-chart-container">
                        <h3>整理状态</h3>
                        <div class="zcp-chart-wrapper"><canvas id="zcp-status-chart"></canvas></div>
                    </div>
                    <div class="zcp-chart-container">
                        <h3>收藏夹增减情况</h3>
                        <div class="zcp-chart-wrapper"><canvas id="zcp-collections-chart"></canvas></div>
                    </div>
                </div>
                <div class="zcp-dashboard-right">
                    <div class="zcp-progress-log"></div>
                </div>
            </div>
            <div class="zcp-modal-actions dashboard-mode">
                <div class="zcp-progress-stats">0 / ${progressDashboardState.logs.length}</div>
                <div style="display: flex; gap: 12px;">
                    <button id="zcp-bulk-action-btn" class="zcp-modal-button secondary" disabled>处理中...</button>
                    <button id="zcp-close-progress-btn" class="zcp-modal-button primary">关闭</button>
                </div>
            </div>`;
        
        modalOverlay.querySelector('#zcp-close-progress-btn').addEventListener('click', () => document.body.removeChild(modalOverlay));

        // 为日志容器添加事件委托,修复撤销/重做按钮的点击事件
        const logContainer = modalOverlay.querySelector('.zcp-progress-log');
        if (logContainer) {
            logContainer.addEventListener('click', handleToggleActionClick);
        }

        // 检查 Chart 是否真的存在,以防 @require 失败
        if (typeof Chart === 'undefined') {
            console.error("Chart.js 未能通过 @require 加载!请检查油猴脚本设置或网络。");
            modalContainer.querySelector('.zcp-dashboard-left').innerHTML = '<p style="color:red;">无法加载图表库,请检查油猴脚本设置或网络。</p>';
            return;
        }

        // 直接初始化图表
        initializeCharts();
        updateDashboardUI(); // 首次渲染
    }

    function initializeCharts() {

        // 为两个图表添加了统一的、正确的点击处理逻辑
        const resetFilter = () => {
            progressDashboardState.filter = { type: 'all', value: null };
            updateDashboardUI();
        };

        // 状态饼图
        const statusCtx = document.getElementById('zcp-status-chart').getContext('2d');
        chartInstances.status = new Chart(statusCtx, {
            type: 'doughnut',
            data: {
                labels: ['已移动', '已跳过', '错误', '待处理'],
                datasets: [{ data: [0, 0, 0, 0], backgroundColor: ['#28a745', '#087a91', '#dc3545', '#cccccc'] }]
            },
            options: {
                responsive: true,
                maintainAspectRatio: false,
                onClick: (e, elements) => {
                    if (elements.length > 0) { // 点击了扇区
                        const label = chartInstances.status.data.labels[elements[0].index];
                        const filterMap = {'已移动': 'success', '已跳过': 'skipped', '错误': 'error', '待处理': 'pending', '建议移动': 'dryrun'};
                        
                        const statusValue = filterMap[label];
                        if (statusValue) {
                            progressDashboardState.filter = { type: 'status', value: statusValue };
                            updateDashboardUI();
                        }
                    } else { // 点击了空白处
                        resetFilter();
                    }
                }
            }
        });

        // 收藏夹柱状图
        const collectionsCtx = document.getElementById('zcp-collections-chart').getContext('2d');
        const collectionLabels = Object.values(progressDashboardState.collections).map(c => c.title);
        const collectionColors = generateColors(collectionLabels.length);

        chartInstances.collections = new Chart(collectionsCtx, {
            type: 'bar',
            data: {
                labels: collectionLabels,
                // 分别表示移入和移出
                // 使用静态颜色
                datasets: [
                    {
                        label: '移入',
                        data: [],
                        backgroundColor: ZHIHU_BLUE, // 为“移入”设置固定的知乎蓝
                        borderColor: '#045bc7'      // 设置一个匹配的、稍暗的边框色
                    },
                    {
                        label: '移出',
                        data: [],
                        backgroundColor: '#ff9c38', // 为“移出”设置固定的警示橙色
                        borderColor: '#e08321'      // 设置一个匹配的、稍暗的边框色
                    }
                ]
            },
            options: {
                responsive: true,
                maintainAspectRatio: false,
                indexAxis: 'y',
                // 设置坐标轴为堆叠模式,使正负条形图对齐
                scales: {
                    x: {
                        stacked: true,
                        grid: { color: '#f0f0f0' }
                    },
                    y: {
                        stacked: true,
                        grid: { display: false }
                    }
                },
                plugins: {
                    // 重新显示图例,并自定义工具提示
                    legend: {
                        display: true,
                        position: 'top',
                    },
                    tooltip: {
                        callbacks: {
                            label: function(context) {
                                // 为负值(移出)取绝对值显示
                                return ` ${context.dataset.label}: ${Math.abs(context.raw)}`;
                            }
                        }
                    }
                },
                onClick: (e, elements) => {
                    // 移除试运行禁用,允许在任何模式下筛选
                    if (elements.length > 0) {
                        const collectionTitle = chartInstances.collections.data.labels[elements[0].index];
                        const collectionId = Object.keys(progressDashboardState.collections).find(
                            id => progressDashboardState.collections[id].title === collectionTitle
                        );
                        if (collectionId) {
                            progressDashboardState.filter = { type: 'collection', value: collectionId };
                            updateDashboardUI();
                        }
                    } else {
                        resetFilter();
                    }
                }
            }
        });
    }

    function updateDashboardUI() {
        if (typeof Chart === 'undefined') return;
        const state = progressDashboardState;

        // 更新状态图
        const statusData = chartInstances.status.data.datasets[0].data;
        statusData[0] = state.stats.moved;
        statusData[1] = state.stats.skipped;
        statusData[2] = state.stats.error;
        statusData[3] = state.stats.pending;
        chartInstances.status.update();
        
        // 更新收藏夹图
        const collectionsChart = chartInstances.collections;
        const collectionsData = Object.values(state.collections);
        collectionsChart.data.datasets[0].data = collectionsData.map(c => c.added);
        collectionsChart.data.datasets[1].data = collectionsData.map(c => -c.removed); // 移出数据设为负值
        collectionsChart.update();

        // 更新日志
        const logContainer = document.querySelector('.zcp-progress-log');
        if (!logContainer) return;

        const filter = state.filter;
        let filteredLogs;

        if (filter.type === 'all' || !filter.type) {
            filteredLogs = state.logs;
        } else if (filter.type === 'status') {
            if (filter.value === 'dryrun') {
                // '建议移动' 包含了所有 dryrun 状态下非 skipped 的条目
                filteredLogs = state.logs.filter(log => log.status === 'dryrun');
            } else {
                filteredLogs = state.logs.filter(log => log.status === filter.value);
            }
        } else if (filter.type === 'collection') {
            filteredLogs = state.logs.filter(log =>
                log.sourceCollectionId === filter.value ||
                log.targetCollectionId === filter.value
            );
        } else {
            filteredLogs = state.logs; 
        }

        logContainer.innerHTML = filteredLogs.map(log => {
            const sourceTitle = state.allCollectionsData.find(c => c.id === log.sourceCollectionId)?.title || '未知';
            let pathHtml = '';
            if (log.status === 'success' || (log.status === 'dryrun' && log.targetCollectionId)) {
                const targetTitle = state.allCollectionsData.find(c => c.id === log.targetCollectionId)?.title || '未知';
                const dryRunTag = log.status === 'dryrun' ? '[试运行] ' : '';
                pathHtml = `
                    <div class="zcp-log-path">
                        <span class="zcp-log-collection source">${sourceTitle}</span>
                        <span class="zcp-log-arrow">→</span>
                        <span class="zcp-log-collection target">${targetTitle}</span>
                        ${log.status === 'success' ? `
                        <button class="${moveHistory.find(m => m.contentId === log.contentId)?.undone ? 'zcp-redo-btn' : 'zcp-undo-btn'}" 
                                data-content-id="${log.contentId}">
                            ${moveHistory.find(m => m.contentId === log.contentId)?.undone ? '重做' : '撤销'}
                        </button>` : `<span class="zcp-log-status-text">${dryRunTag}</span>`}
                    </div>`;
            } else if (log.status === 'skipped') {
                pathHtml = `<div class="zcp-log-path"><span class="zcp-log-status-text">分类未变: ${sourceTitle}</span></div>`;
            } else if (log.status === 'error') {
                pathHtml = `<div class="zcp-log-path"><span class="zcp-log-status-text">错误: ${log.message}</span></div>`;
            }

            return `<div class="zcp-log-item status-${log.status}" id="${log.id}">
                        <div class="zcp-log-title"><a href="${log.url}" target="_blank">${log.title}</a></div>
                        ${pathHtml}
                    </div>`;
        }).join('');

        // 更新进度条
        const completed = state.stats.moved + state.stats.skipped + state.stats.error;
        document.querySelector('.zcp-progress-stats').textContent = `${completed} / ${state.logs.length}`;
    }

    async function handleToggleActionClick(event) {
        const button = event.target.closest('.zcp-undo-btn, .zcp-redo-btn'); // 使用 closest 确保点到图标也能触发
        if (!button) return;

        const { contentId } = button.dataset;
        const moveRecord = moveHistory.find(m => m.contentId === contentId);
        if (!moveRecord) return;

        const isUndoAction = button.classList.contains('zcp-undo-btn');
        const originalText = button.textContent;
        
        button.disabled = true;
        button.textContent = isUndoAction ? '撤销中...' : '重做中...';
        button.classList.add('zcp-shimmer'); 

        try {
            if (isUndoAction) {
                // 执行撤销:移回 source
                await addToCollection(moveRecord.contentId, moveRecord.contentType, moveRecord.sourceCollectionId);
                await removeFromCollection(moveRecord.contentId, moveRecord.contentType, moveRecord.targetCollectionId);
                
                // 更新UI和状态
                button.textContent = '重做';
                button.classList.remove('zcp-undo-btn');
                button.classList.add('zcp-redo-btn');
                moveRecord.undone = true; // 标记为已撤销
            } else {
                // 执行重做:移到 target
                await addToCollection(moveRecord.contentId, moveRecord.contentType, moveRecord.targetCollectionId);
                await removeFromCollection(moveRecord.contentId, moveRecord.contentType, moveRecord.sourceCollectionId);

                // 更新UI和状态
                button.textContent = '撤销';
                button.classList.remove('zcp-redo-btn');
                button.classList.add('zcp-undo-btn');
                moveRecord.undone = false; // 标记为未撤销(即已重做)
            }
        } catch (error) {
            console.error(`${originalText}失败:`, error);
            button.textContent = `${originalText}失败`;
        } finally {
            button.disabled = false;
            button.classList.remove('zcp-shimmer'); //
        }
    }

    // 处理一键撤销/重做的函数
    async function handleBulkAction(modalOverlay) {
        const bulkButton = modalOverlay.querySelector('#zcp-bulk-action-btn');
        const action = bulkButton.dataset.action; // 'undo' or 'redo'

        if (!action) return;

        bulkButton.disabled = true;
        bulkButton.textContent = action === 'undo' ? '正在一键撤销...' : '正在一键重做...';
        bulkButton.classList.add('zcp-shimmer'); // 

        const itemsToProcess = moveHistory.filter(move => action === 'undo' ? !move.undone : move.undone);
        let successCount = 0;
        let hasError = false;

        for (const move of itemsToProcess) {
            try {
                if (action === 'undo') {
                    await addToCollection(move.contentId, move.contentType, move.sourceCollectionId);
                    await removeFromCollection(move.contentId, move.contentType, move.targetCollectionId);
                    move.undone = true;
                } else { // redo
                    await addToCollection(move.contentId, move.contentType, move.targetCollectionId);
                    await removeFromCollection(move.contentId, move.contentType, move.sourceCollectionId);
                    move.undone = false;
                }
                successCount++;
            } catch (error) {
                console.error(`批量${action}失败于: ${move.contentId}`, error);
                hasError = true;
                break; // 一旦出错就停止
            }
        }

        console.log(`成功批量${action}了 ${successCount} 项。`);
        
        // 批量更新UI
        updateDashboardUI();

        // 更新按钮状态
        if (hasError) {
             bulkButton.textContent = `操作中断,请重试`;
        } else {
            if (action === 'undo') {
                bulkButton.textContent = '一键重做';
                bulkButton.dataset.action = 'redo';
            } else {
                bulkButton.textContent = '一键撤销';
                bulkButton.dataset.action = 'undo';
            }
        }
        bulkButton.disabled = false;
        bulkButton.classList.remove('zcp-shimmer');
    }

    /**
     * Worker 函数,并发处理任务
     */
    async function worker(taskQueue) {
        let task;
        // 使用更稳健的循环模式,将“取任务”和“检查是否存在”合并
        // 当 taskQueue 为空时, taskQueue.shift() 返回 undefined, 循环会自动终止。
        // 杜绝了多个 worker 竞争最后一个任务的 race condition。
        while ((task = taskQueue.shift())) {

            const logEntry = progressDashboardState.logs.find(l => l.id === task.id);
            
            try {
                const contentText = await scrapeContent(task.url);
                if (contentText === '正文抓取失败') throw new Error('正文抓取失败');

                const targetCollections = Object.values(progressDashboardState.collections)
                    .map(c => progressDashboardState.allCollectionsData.find(ac => ac.title === c.title));
                const prompt = buildOrganizationPrompt(contentText, task.title, targetCollections);
                const recommendedTitle = await callDeepSeek(prompt);
                
                const targetCollection = progressDashboardState.allCollectionsData.find(c => c.title === recommendedTitle);
                if (!targetCollection) throw new Error(`AI返回无效收藏夹名: "${recommendedTitle}"`);
                
                logEntry.targetCollectionId = targetCollection.id;

                if (targetCollection.id === task.sourceCollectionId) {
                    logEntry.status = 'skipped';
                } else if (progressDashboardState.isDryRun) {
                    logEntry.status = 'dryrun';
                } else {
                    await addToCollection(task.contentId, task.contentType, targetCollection.id);
                    await removeFromCollection(task.contentId, task.contentType, task.sourceCollectionId);
                    logEntry.status = 'success';
                    moveHistory.push({
                        contentId: task.contentId, contentType: task.contentType,
                        sourceCollectionId: task.sourceCollectionId,
                        targetCollectionId: targetCollection.id, undone: false
                    });
                }
            } catch (error) {
                console.error(`任务失败 [${task.title}]:`, error);
                logEntry.status = 'error';
                logEntry.message = error.message;
            } finally {
                // 更新统计数据
                progressDashboardState.stats.pending--;
                
                // 简化和修正统计逻辑
                if (logEntry.status === 'success' || (logEntry.status === 'dryrun' && logEntry.targetCollectionId !== task.sourceCollectionId)) {
                    // 在 dryrun 模式下,只有当目标与源不同时,才算作 '建议移动'
                    progressDashboardState.stats.moved++;
                    progressDashboardState.collections[task.sourceCollectionId].removed++;
                    if(logEntry.targetCollectionId) {
                        progressDashboardState.collections[logEntry.targetCollectionId].added++;
                    }
                } else if (logEntry.status === 'skipped') {
                    progressDashboardState.stats.skipped++;
                } else if (logEntry.status === 'error') {
                    progressDashboardState.stats.error++;
                }

                // 为了让饼图标签在试运行时显示正确,临时修改
                if (progressDashboardState.isDryRun) {
                    chartInstances.status.data.labels[0] = '建议移动';
                }
                
                // 调度UI更新
                requestAnimationFrame(updateDashboardUI);
            }
        }
    }

    // --- 3. 功能一:AI 生成描述 ---

    /**
     * 脚本主入口,检测页面并注入按钮
     */
    function init() {
        const path = window.location.pathname;

        const observer = new MutationObserver((mutationsList, obs) => {
            // 路由分发

            if (path.startsWith('/collections/mine')) {
                // 使用稳定、可读的 CSS class 选择器
                const actionsContainer = document.querySelector('.CollectionsHeader-mainContent'); 
                if (actionsContainer && !document.getElementById('zcp-organize-btn')) {
                    injectOrganizeButton(actionsContainer);
                    obs.disconnect();
                }
            }
            // 匹配个人收藏夹列表页: /people/xxx/collections
            else if (path.includes('/people/') && path.includes('/collections')) {
                // 优先选用新版 class
                let actionsContainer = document.querySelector('.CollectionsHeader-mainContent');
                if (!actionsContainer) {
                    actionsContainer = document.querySelector('.Profile-main .Profile-sideColumn');
                }
                if (actionsContainer && !document.getElementById('zcp-organize-btn')) {
                    // 如果是 Profile-sideColumn,插入一个容器
                    if (actionsContainer.classList.contains('Profile-sideColumn')) {
                        const btnContainer = document.createElement('div');
                        btnContainer.style.marginBottom = '12px';
                        actionsContainer.prepend(btnContainer);
                        injectOrganizeButton(btnContainer);
                    } else {
                        injectOrganizeButton(actionsContainer);
                    }
                    obs.disconnect();
                }
            }
            // 匹配单个收藏夹详情页: /collection/xxx
            else if (path.startsWith('/collection/')) {
                const actionsContainer = document.querySelector('.CollectionDetailPageHeader-actions');
                if (actionsContainer && !document.getElementById('zcp-ai-btn')) {
                    injectAIButton(actionsContainer);
                    obs.disconnect();
                }
            }
        });

        observer.observe(document.body, { childList: true, subtree: true });
    }
    /**
     * 注入 AI 按钮到页面
     */
    function injectAIButton(container) {
        const aiButton = document.createElement('button');
        aiButton.id = 'zcp-ai-btn';
        aiButton.className = 'zcp-ai-button';
        aiButton.title = " AI 生成描述";

        // 使用您提供的新 SVG 图标,并进行优化
        const svgIcon = `
            <svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24" fill="currentColor">
                <path d="M19 9l1.25-2.75L23 5l-2.75-1.25L19 1l-1.25 2.75L15 5l2.75 1.25L19 9zm-7.5.5L9 4 6.5 9.5 1 12l5.5 2.5L9 20l2.5-5.5L17 12l-5.5-2.5zM19 15l-1.25 2.75L15 19l2.75 1.25L19 23l1.25-2.75L23 19l-2.75-1.25L19 15z"/>
            </svg>
        `;
        const textSpan = document.createElement('span');
        textSpan.textContent = 'AI 描述';
        textSpan.style.fontSize = '14px';

        textSpan.style.display = 'inline-flex';
        textSpan.style.alignItems = 'center';
        // --------------------
        aiButton.innerHTML = svgIcon;
        aiButton.appendChild(textSpan);


        container.appendChild(aiButton);
        aiButton.addEventListener('click', handleGenerateDescription);
    }

    /**
     * 点击 AI 按钮后的主处理函数
     */
    async function handleGenerateDescription(event) {
        const button = event.currentTarget;
        const originalContent = button.innerHTML;

        // 步骤 1: 立即锁定按钮尺寸并显示加载动画
        const { width, height } = button.getBoundingClientRect();
        button.style.width = `${width}px`;
        button.style.height = `${height}px`;
        button.innerHTML = `<div class="zcp-spinner"></div>`;
        button.disabled = true;

        await new Promise(resolve => setTimeout(resolve, 50)); 

        // 步骤 3: 现在开始执行耗时的抓取和 AI 调用
        console.log('%c[知乎收藏夹 Pro] 开始生成描述...', 'color: white; background-color: #056DE8; padding: 2px 5px; border-radius: 3px;');
        try {
            console.log("开始采集文章...");
            const articles = await scrapeCollectionPage();
            if (articles.length === 0) {
                alert('未能采集到页面上的任何文章内容,请确保页面上有文章列表。');
                // finally 块会自动处理按钮的恢复
                return;
            }
            console.log(`%c[知乎收藏夹 Pro] 数据采集完成:`, 'color: #056DE8; font-weight: bold;');
            console.log(`- 成功采集到 ${articles.length} 篇文章,将用于生成描述。`);

            console.log("构建 Prompt...");
            const prompt = buildDescriptionPrompt(articles);

            console.log("调用 AI API...");
            const generatedDescription = await callDeepSeek(prompt);
            console.log("AI 已生成描述。");

            showDescriptionConfirmModal(generatedDescription);

        } catch (error) {
            console.error('生成描述失败:', error);
            alert(`生成描述时出错: ${error.message || error}`);
        } finally {
            // 步骤 4: 无论成功或失败,最后都恢复按钮的原始状态
            button.innerHTML = originalContent;
            button.disabled = false;
            button.style.width = '';
            button.style.height = '';
        }
    }
    /**
     * 从收藏夹页面抓取文章数据
     */
    async function scrapeCollectionPage() {
        const articlesData = [];
        // 使用更稳定的 class 选择器
        const itemElements = document.querySelectorAll('.CollectionDetailPageItem');
        const itemsToProcess = Array.from(itemElements).slice(0, 18); // 最多处理18篇(1页)

        //  添加日志,告知开始处理单篇文章
        console.log('- 开始逐篇处理文章内容...');
        
        for (const item of itemsToProcess) {
            try {
                // 如果文章内容是折叠的,点击“阅读全文”
                const moreButton = item.querySelector('.ContentItem-more');
                if (moreButton) {
                    // 使用一个 Promise 来等待内容加载
                    await new Promise(resolve => {
                        const contentObserver = new MutationObserver(() => {
                            // 当 "阅读全文" 按钮消失时,我们认为内容已加载
                            if (!item.querySelector('.ContentItem-more')) {
                                contentObserver.disconnect();
                                resolve();
                            }
                        });
                        contentObserver.observe(item, { childList: true, subtree: true });
                        moreButton.click();
                        // 设置一个超时,防止无限等待
                        setTimeout(() => { contentObserver.disconnect(); resolve(); }, 2000);
                    });
                }
                
                const titleElement = item.querySelector('.ContentItem-title a');
                const contentElement = item.querySelector('.RichText.ztext');
                
                if (titleElement && contentElement) {
                    const title = titleElement.innerText.trim();
                    const content = contentElement.innerText.trim();

                    // 打印单篇文章的标题和字数
                    console.log(`  - 已处理: "${title}" (内容字数: ${content.length})`);

                    articlesData.push({ title, content });
                }
            } catch (e) {
                console.warn("处理单个文章时出错,已跳过:", e);
            }
        }
        return articlesData;
    }

    /**
     * 构建发送给 AI 的 Prompt
     */
    function buildDescriptionPrompt(articles) {
        let articleText = articles
            .map(a => `- 标题: ${a.title}\n  正文摘要: ${a.content.substring(0, 250).replace(/\s+/g, ' ')}...`)
            .join('\n\n');
        return `根据以下来自知乎收藏夹的文章标题和正文,为这个收藏夹生成一段话精炼的描述。不要列举介绍各篇文章的主题,宏观一点。介绍是给自己看的,直接介绍,开头不需要诸如"本收藏夹……",不需要诸如"特别适合xxx的人"的话,字数不超过50字,不需要在最后输出字数统计。

文章列表:
${articleText}

请生成描述:`;
    }

    /**
     * 显示包含 AI 生成描述的确认模态框
     */
    function showDescriptionConfirmModal(description) {
        // 创建模态框
        const overlay = document.createElement('div');
        overlay.className = 'zcp-modal-overlay';

        overlay.innerHTML = `
            <div class="zcp-modal-container">
                <div class="zcp-modal-header">AI 生成的描述</div>
                <div class="zcp-modal-content">
                    <textarea id="zcp-desc-textarea">${description}</textarea>
                </div>
                <div class="zcp-modal-actions">
                    <button id="zcp-cancel-btn" class="zcp-modal-button secondary">取消</button>
                    <button id="zcp-apply-btn" class="zcp-modal-button primary">应用</button>
                </div>
            </div>
        `;

        document.body.appendChild(overlay);

        // 添加事件监听
        const closeModal = () => document.body.removeChild(overlay);
        overlay.querySelector('#zcp-cancel-btn').addEventListener('click', closeModal);
        overlay.querySelector('#zcp-apply-btn').addEventListener('click', async () => {
            const newDescription = overlay.querySelector('#zcp-desc-textarea').value;
            try {
                await applyDescriptionChange(newDescription);
                alert("收藏夹描述更新成功!");
                closeModal();
            } catch (e) {
                 alert(`更新失败: ${e.message}`);
            }
        });
    }

    /**
     * 调用知乎 API,应用描述更改
     */
    async function applyDescriptionChange(newDescription) {
        const collectionId = window.location.pathname.split('/').pop();
        const title = document.querySelector('.CollectionDetailPageHeader-title').innerText;

        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: 'PUT',
                url: `/api/v4/collections/${collectionId}`,
                headers: getZhihuApiHeaders(),
                data: JSON.stringify({
                    title: title, // 知乎API要求必须同时提交标题
                    description: newDescription,
                }),
                onload: function(response) {
                    if (response.status === 200) {
                        const descElement = document.querySelector('.CollectionDetailPageHeader-description');
                        if (descElement) {
                           descElement.innerText = newDescription;
                        } else {
                           // 如果原先没有描述,刷新页面以显示新描述
                           window.location.reload();
                        }
                        resolve(response);
                    } else {
                        console.error('更新收藏夹描述失败:', response);
                        try {
                           const errorInfo = JSON.parse(response.responseText);
                           reject(new Error(errorInfo.error.message || `HTTP ${response.status}`));
                        } catch(e) {
                           reject(new Error(`请求失败,HTTP 状态码: ${response.status}`));
                        }
                    }
                },
                onerror: (err) => reject(new Error('网络请求错误'))
            });
        });
    }

    // --- 4. 用户设置 ---
    GM_registerMenuCommand('设置 DeepSeek API Key', () => {
        const currentKey = GM_getValue('deepseek_api_key', '');
        const newKey = prompt('请输入你的 DeepSeek API Key:', currentKey);
        if (newKey !== null) { // 允许用户设置为空
            GM_setValue('deepseek_api_key', newKey);
            alert('API Key 已保存!');
        }
    });

    // --- 启动脚本 (处理 Chart.js 的异步加载) ---

    // 1. 读取 Chart.js 的代码
    const chartJsCode = GM_getResourceText('CHART_JS');

    if (chartJsCode) {
        try {
            // 2. 在脚本的沙箱环境中直接执行代码
            eval(chartJsCode);

            // 3. 验证一下
            if (typeof Chart !== 'undefined') {
                console.log('[知乎收藏夹 Pro] Chart.js 库加载并执行成功!');
                // 4. 立即启动主逻辑
                init();
            } else {
                throw new Error('Chart object not found after eval.');
            }

        } catch (e) {
            console.error('[知乎收藏夹 Pro] 执行依赖库时出错:', e);
            alert('知乎收藏夹 Pro:加载依赖库时发生错误,请查看控制台。');
        }
    } else {
        alert('知乎收藏夹 Pro:无法获取依赖库 Chart.js 的内容,脚本无法运行。');
    }

})();