Pixiv AI Tag

对Pixiv中的AI生成图像添加一个标注

// ==UserScript==
// @license MIT
// @name        Pixiv AI Tag
// @description 对Pixiv中的AI生成图像添加一个标注
// @author      BAKAOLC
// @version     1.0.0
// @icon        http://www.pixiv.net/favicon.ico
// @match       *://www.pixiv.net/*
// @namespace   none
// @grant       GM.setValue
// @grant       GM.getValue
// @grant       GM.deleteValue
// @grant       GM.listValues
// @grant       GM.xmlHttpRequest
// @supportURL  https://github.com/BAKAOLC/Tampermonkey-Script
// @homepageURL https://github.com/BAKAOLC/Tampermonkey-Script
// @noframes
// ==/UserScript==

(function () {
    'use strict';


    // ============= 配置常量 =============
    const CONFIG = {
        QUERY_INTERVAL: 500, // 查询间隔(毫秒)
        LOG_LEVEL: 'info', // 日志级别: 'debug', 'info', 'warn', 'error'

        // 缓存配置
        CACHE: {
            ILLUST_EXPIRE_TIME: 60 * 60 * 1000,      // 插画缓存1小时
            CLEANUP_INTERVAL: 10 * 60 * 1000,        // 清理间隔10分钟
            MAX_ENTRIES: 1000                        // 最大缓存条目数
        },

        // 跨标签页同步配置
        CROSS_TAB_SYNC: {
            LOCK_EXPIRE_TIME: 15 * 1000,           // 锁过期时间15秒
            REQUEST_INTERVAL: 1000,                 // 跨标签页请求间隔1秒
            HEARTBEAT_INTERVAL: 3 * 1000,           // 心跳间隔3秒
            HEARTBEAT_EXPIRE_TIME: 10 * 1000        // 心跳过期时间10秒
        },

        // 速率限制配置
        RATE_LIMIT: {
            INITIAL_DELAY: 5000,     // 初始重试延迟5秒
            MAX_DELAY: 60000,        // 最大重试延迟1分钟
            BACKOFF_MULTIPLIER: 2    // 退避倍数
        },

        // AI标签列表
        AI_TAGS: [
            'AI', 'AI-generated', 'AI绘画', 'AI絵', 'AI生成', 'AI生成作品', 'AI作成',
            'AIartwork', 'AIgenerated', 'AIアート', 'AIイラスト', 'AIのべりすと',
            'NovelAI', 'StableDiffusion', 'MidJourney', 'DALL-E', 'Diffusion',
            'stable_diffusion', 'novel_ai', 'midjourney', 'dall_e'
        ],

        // 用户配置(可通过脚本修改)
        USER_CONFIG: {
            query_delay: 0,        // 查询间隔,时间单位为毫秒,0代表无延时
            remove_image: 0,       // 是否移除AI作品的预览图 0:不移除 1:仅屏蔽图像显示 2:从网页中移除
            show_ai_possible: true, // 是否显示可能是AI的标签
            enable_tag_detection: true, // 是否启用标签检测
            enable_auto_cache: true     // 是否启用自动缓存
        }
    };

    // 页面选择器配置 - 使用更通用的匹配方式
    const SELECTORS = {
        // 通用选择器:所有包含图像且链接到artwork的a标签
        ARTWORK_LINKS: 'a[href*="/artworks/"]:not(.add_ai_tag)',

        // 图像容器选择器:用于查找包含图像的链接
        IMAGE_CONTAINERS: [
            'a[href*="/artworks/"] img',           // 直接包含图像的链接
            'a[href*="/artworks/"] canvas',        // 包含canvas的链接
            'a[href*="/artworks/"] svg',           // 包含svg的链接
            'a[href*="/artworks/"] [style*="background-image"]' // 背景图像
        ],

        // 用于移除图像的父级深度配置(保留原有逻辑)
        REMOVE_PARENT_DEPTH: 4
    };

    // ============= 工具函数 =============
    const Utils = {
        sleep(ms) {
            return new Promise(resolve => setTimeout(resolve, ms));
        },

        // 等待DOM元素出现
        waitForElement(selector, timeout = 10000, interval = 100) {
            return new Promise((resolve, reject) => {
                const startTime = Date.now();

                const check = () => {
                    const element = document.querySelector(selector);
                    if (element) {
                        resolve(element);
                        return;
                    }

                    if (Date.now() - startTime >= timeout) {
                        reject(new Error(`Timeout waiting for element: ${selector}`));
                        return;
                    }

                    setTimeout(check, interval);
                };

                check();
            });
        },

        // 等待页面数据加载完成
        waitForPageData(illustId, timeout = 15000) {
            return new Promise((resolve, reject) => {
                const startTime = Date.now();

                const check = () => {
                    // 检查多种数据源
                    const conditions = [
                        // 检查preload-data脚本
                        () => {
                            const scripts = document.querySelectorAll('script');
                            for (const script of scripts) {
                                if (script.textContent && script.textContent.includes('preload-data')) {
                                    const patterns = [
                                        /{"timestamp".*?}(?=<\/script>)/,
                                        /{"timestamp"[^}]*}[^{]*{[^}]*"illust"[^}]*}/,
                                        /{[^}]*"illust"[^}]*}/
                                    ];

                                    for (const pattern of patterns) {
                                        const match = script.textContent.match(pattern);
                                        if (match) {
                                            try {
                                                const data = JSON.parse(match[0]);
                                                if (data.illust?.[illustId]) {
                                                    return { type: 'preload-data', data: data };
                                                }
                                            } catch (e) {
                                                // 继续尝试
                                            }
                                        }
                                    }
                                }
                            }
                            return null;
                        },

                        // 检查全局变量
                        () => {
                            const globalVars = ['__INITIAL_STATE__', '__PRELOADED_STATE__', 'pixiv'];
                            for (const varName of globalVars) {
                                if (window[varName]) {
                                    const data = window[varName];
                                    const illust = data.illust?.[illustId] || data.preload?.illust?.[illustId];
                                    if (illust) {
                                        return { type: 'global', data: data, varName: varName };
                                    }
                                }
                            }
                            return null;
                        }
                    ];

                    for (const condition of conditions) {
                        try {
                            const result = condition();
                            if (result) {
                                resolve(result);
                                return;
                            }
                        } catch (e) {
                            // 忽略错误,继续检查
                        }
                    }

                    if (Date.now() - startTime >= timeout) {
                        reject(new Error(`Timeout waiting for page data for illust ${illustId}`));
                        return;
                    }

                    setTimeout(check, 200);
                };

                check();
            });
        },

        log(message, level = 'info') {
            const levels = { debug: 0, info: 1, warn: 2, error: 3 };
            const configLevel = levels[CONFIG.LOG_LEVEL] !== undefined ? levels[CONFIG.LOG_LEVEL] : 1;
            const messageLevel = levels[level] !== undefined ? levels[level] : 1;

            // 只输出等于或高于配置级别的日志
            if (messageLevel < configLevel) return;

            const prefix = '[Pixiv AI Tag]';
            const timestamp = new Date().toLocaleTimeString();
            switch (level) {
                case 'error':
                    console.error(`${prefix} [${timestamp}] ${message}`);
                    break;
                case 'warn':
                    console.warn(`${prefix} [${timestamp}] ${message}`);
                    break;
                case 'debug':
                    console.debug(`${prefix} [${timestamp}] ${message}`);
                    break;
                default:
                    console.log(`${prefix} [${timestamp}] ${message}`);
            }
        },

        safeQuerySelector(selector, context = document) {
            try {
                return context.querySelector(selector);
            } catch (error) {
                this.log(`Invalid selector: ${selector}`, 'error');
                return null;
            }
        },

        safeQuerySelectorAll(selector, context = document) {
            try {
                return context.querySelectorAll(selector);
            } catch (error) {
                this.log(`Invalid selector: ${selector}`, 'error');
                return [];
            }
        },

        // 检查标签中是否包含AI相关标签
        checkAITags(tags) {
            if (!tags || !Array.isArray(tags)) return false;

            const tagStrings = tags.map(tag => {
                if (typeof tag === 'string') {
                    return tag;
                } else if (tag && typeof tag === 'object' && tag.tag) {
                    return tag.tag;
                }
                return '';
            }).filter(tag => tag.length > 0);

            // 检查是否有任何标签匹配AI标签列表
            for (const aiTag of CONFIG.AI_TAGS) {
                for (const tagString of tagStrings) {
                    const lowerTag = tagString.toLowerCase();
                    const lowerAiTag = aiTag.toLowerCase();

                    // 精确匹配或者作为独立单词匹配
                    if (lowerTag === lowerAiTag ||
                        lowerTag.includes(`_${lowerAiTag}_`) ||
                        lowerTag.startsWith(`${lowerAiTag}_`) ||
                        lowerTag.endsWith(`_${lowerAiTag}`) ||
                        (lowerAiTag.length >= 3 && lowerTag.includes(lowerAiTag) &&
                            !lowerTag.match(new RegExp(`[a-z]${lowerAiTag.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}[a-z]`)))) {
                        Utils.log(`Found AI tag match: "${tagString}" matches "${aiTag}"`, 'debug');
                        return true;
                    }
                }
            }

            return false;
        },

        // 获取父节点
        getParentNodeWithDepth(node, depth) {
            while (depth > 0) {
                if (node.parentNode)
                    node = node.parentNode;
                else
                    return null;
                depth--;
            }
            return node;
        }
    };

    // ============= 跨标签页缓存管理器 =============
    class CrossTabCacheManager {
        constructor() {
            this.cachePrefix = 'pixiv_ai_cache_';
            this.lastCleanup = 0;
            this.initializeCleanup();
        }

        getCacheKey(type, id) {
            return `${this.cachePrefix}${type}_${id}`;
        }

        async getCache(type, id) {
            try {
                const key = this.getCacheKey(type, id);
                const data = await GM.getValue(key, null);

                if (!data) return null;

                const parsed = JSON.parse(data);
                const now = Date.now();

                if (now - parsed.timestamp > CONFIG.CACHE.ILLUST_EXPIRE_TIME) {
                    await GM.deleteValue(key);
                    return null;
                }

                Utils.log(`Cache hit for ${type}:${id}`, 'debug');
                return parsed.data;
            } catch (error) {
                Utils.log(`Cache get error for ${type}:${id}: ${error.message}`, 'error');
                return null;
            }
        }

        async setCache(type, id, data) {
            try {
                const key = this.getCacheKey(type, id);
                const cacheData = {
                    data: data,
                    timestamp: Date.now(),
                    type: type,
                    id: id
                };

                await GM.setValue(key, JSON.stringify(cacheData));
                Utils.log(`Cache set for ${type}:${id}`, 'debug');
                this.scheduleCleanup();
            } catch (error) {
                Utils.log(`Cache set error for ${type}:${id}: ${error.message}`, 'error');
            }
        }

        initializeCleanup() {
            this.scheduleCleanup();
        }

        scheduleCleanup() {
            const now = Date.now();
            if (now - this.lastCleanup > CONFIG.CACHE.CLEANUP_INTERVAL) {
                setTimeout(() => this.cleanupExpiredCache(), 1000);
            }
        }

        async cleanupExpiredCache() {
            try {
                const now = Date.now();
                this.lastCleanup = now;

                const keys = await GM.listValues();
                const cacheKeys = keys.filter(key => key.startsWith(this.cachePrefix));
                let cleanedCount = 0;

                for (const key of cacheKeys) {
                    try {
                        const data = await GM.getValue(key, null);
                        if (!data) continue;

                        const parsed = JSON.parse(data);
                        if (now - parsed.timestamp > CONFIG.CACHE.ILLUST_EXPIRE_TIME) {
                            await GM.deleteValue(key);
                            cleanedCount++;
                        }
                    } catch (error) {
                        await GM.deleteValue(key);
                        cleanedCount++;
                    }
                }

                if (cleanedCount > 0) {
                    Utils.log(`Cleaned up ${cleanedCount} expired cache entries`, 'debug');
                }
            } catch (error) {
                Utils.log(`Cache cleanup error: ${error.message}`, 'error');
            }
        }
    }

    // ============= 跨标签页同步管理器 =============
    class CrossTabSyncManager {
        constructor() {
            this.tabId = this.generateTabId();
            this.lockKey = 'pixiv_ai_request_lock';
            this.lastRequestKey = 'pixiv_ai_last_request';
            this.heartbeatKey = 'pixiv_ai_heartbeat';
            this.heartbeatInterval = null;

            this.cleanupExpiredLocks();
            this.startHeartbeat();
            this.setupCleanupOnUnload();
        }

        generateTabId() {
            return `tab_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
        }

        startHeartbeat() {
            this.updateHeartbeat();
            this.heartbeatInterval = setInterval(async () => {
                await this.updateHeartbeat();
                await this.checkDeadTabs();
            }, CONFIG.CROSS_TAB_SYNC.HEARTBEAT_INTERVAL);
        }

        async updateHeartbeat() {
            try {
                const heartbeatData = await GM.getValue(this.heartbeatKey, '{}');
                const heartbeats = JSON.parse(heartbeatData);
                heartbeats[this.tabId] = { timestamp: Date.now(), isActive: true };
                await GM.setValue(this.heartbeatKey, JSON.stringify(heartbeats));
            } catch (error) {
                Utils.log(`Error updating heartbeat: ${error.message}`, 'error');
            }
        }

        async executeRequestSynchronized(requestFunction, requestData) {
            const maxRetries = 3;
            let retries = 0;

            while (retries < maxRetries) {
                try {
                    const lockAcquired = await this.acquireLock();

                    if (!lockAcquired) {
                        await Utils.sleep(200);
                        retries++;
                        continue;
                    }

                    try {
                        await this.shouldWaitForOtherTabs();
                        const result = await requestFunction(requestData);
                        await this.recordRequestTime();
                        return result;
                    } finally {
                        await this.releaseLock();
                    }
                } catch (error) {
                    await this.releaseLock();
                    retries++;
                    if (retries >= maxRetries) {
                        throw error;
                    }
                    await Utils.sleep(1000 * retries);
                }
            }

            throw new Error('Failed to execute synchronized request after retries');
        }

        async acquireLock() {
            try {
                const now = Date.now();
                const lockData = await GM.getValue(this.lockKey, null);

                if (lockData) {
                    const lock = JSON.parse(lockData);
                    if (lock.tabId === this.tabId) {
                        lock.timestamp = now;
                        await GM.setValue(this.lockKey, JSON.stringify(lock));
                        return true;
                    }

                    if (now - lock.timestamp < CONFIG.CROSS_TAB_SYNC.LOCK_EXPIRE_TIME) {
                        return false;
                    } else {
                        await GM.deleteValue(this.lockKey);
                    }
                }

                const newLock = { tabId: this.tabId, timestamp: now };
                await GM.setValue(this.lockKey, JSON.stringify(newLock));
                return true;
            } catch (error) {
                Utils.log(`Error acquiring lock: ${error.message}`, 'error');
                return false;
            }
        }

        async releaseLock() {
            try {
                const lockData = await GM.getValue(this.lockKey, null);
                if (lockData) {
                    const lock = JSON.parse(lockData);
                    if (lock.tabId === this.tabId) {
                        await GM.deleteValue(this.lockKey);
                    }
                }
            } catch (error) {
                Utils.log(`Error releasing lock: ${error.message}`, 'error');
            }
        }

        async shouldWaitForOtherTabs() {
            try {
                const lastRequestTime = await GM.getValue(this.lastRequestKey, 0);
                const now = Date.now();
                const timeSinceLastRequest = now - lastRequestTime;

                if (timeSinceLastRequest < CONFIG.CROSS_TAB_SYNC.REQUEST_INTERVAL) {
                    const waitTime = CONFIG.CROSS_TAB_SYNC.REQUEST_INTERVAL - timeSinceLastRequest;
                    await Utils.sleep(waitTime);
                }
            } catch (error) {
                Utils.log(`Error checking request interval: ${error.message}`, 'error');
            }
        }

        async recordRequestTime() {
            try {
                await GM.setValue(this.lastRequestKey, Date.now());
            } catch (error) {
                Utils.log(`Error recording request time: ${error.message}`, 'error');
            }
        }

        async cleanupExpiredLocks() {
            try {
                const now = Date.now();
                const lockData = await GM.getValue(this.lockKey, null);

                if (lockData) {
                    const lock = JSON.parse(lockData);
                    if (now - lock.timestamp > CONFIG.CROSS_TAB_SYNC.LOCK_EXPIRE_TIME) {
                        await GM.deleteValue(this.lockKey);
                    }
                }
            } catch (error) {
                Utils.log(`Error cleaning up locks: ${error.message}`, 'error');
            }
        }

        setupCleanupOnUnload() {
            const cleanup = async () => {
                try {
                    await this.releaseLock();
                    if (this.heartbeatInterval) {
                        clearInterval(this.heartbeatInterval);
                    }
                } catch (error) {
                    // 忽略错误
                }
            };

            window.addEventListener('beforeunload', cleanup);
            window.addEventListener('unload', cleanup);
        }

        async checkDeadTabs() {
            // 简化版本,只清理过期锁
            await this.cleanupExpiredLocks();
        }
    }

    // ============= API 客户端 =============
    class APIClient {
        constructor(syncManager) {
            this.syncManager = syncManager;
        }

        async fetchPixivIllust(id) {
            const requestFunction = async () => {
                return new Promise((resolve, reject) => {
                    GM.xmlHttpRequest({
                        method: 'GET',
                        url: `https://www.pixiv.net/ajax/illust/${id}`,
                        headers: {
                            'Accept': 'application/json',
                            'Referer': 'https://www.pixiv.net/'
                        },
                        onload: function (response) {
                            if (response.status >= 200 && response.status < 300) {
                                try {
                                    const data = JSON.parse(response.responseText);
                                    resolve({ json: () => Promise.resolve(data), ok: true });
                                } catch (error) {
                                    reject(new Error(`JSON parse error: ${error.message}`));
                                }
                            } else {
                                reject(new Error(`HTTP ${response.status}: ${response.statusText}`));
                            }
                        },
                        onerror: function (error) {
                            reject(new Error(`Network error: ${error.message || 'Unknown error'}`));
                        },
                        ontimeout: function () {
                            reject(new Error('Request timeout'));
                        },
                        timeout: 10000
                    });
                });
            };

            return await this.syncManager.executeRequestSynchronized(requestFunction, { id });
        }
    }

    // ============= Pixiv页面自动记录器 =============
    class PixivAutoRecorder {
        constructor(cacheManager) {
            this.cacheManager = cacheManager;
            this.isPixivPage = location.hostname === 'www.pixiv.net';
        }

        async initialize() {
            if (!this.isPixivPage || !CONFIG.USER_CONFIG.enable_auto_cache) {
                Utils.log('Pixiv auto recorder skipped (not pixiv page or disabled)', 'debug');
                return;
            }

            Utils.log('Initializing Pixiv auto recorder...', 'debug');
            try {
                await this.recordCurrentPage();
                Utils.log('Pixiv auto recorder initialized successfully', 'debug');
            } catch (error) {
                Utils.log(`Pixiv auto recorder error: ${error.message}`, 'error');
                // 不要抛出错误,让脚本继续运行
            }
        }

        async recordCurrentPage() {
            try {
                const url = location.href;

                if (url.includes('/artworks/')) {
                    await this.recordIllustPage();
                }
            } catch (error) {
                Utils.log(`Error recording current page: ${error.message}`, 'error');
            }
        }

        async recordIllustPage() {
            try {
                const match = location.href.match(/\/artworks\/(\d+)/);
                if (!match) return;

                const illustId = match[1];
                Utils.log(`Recording illust page: ${illustId}`, 'debug');

                let pageData = null;
                try {
                    pageData = await Utils.waitForPageData(illustId, 10000);
                } catch (error) {
                    Utils.log(`Timeout waiting for page data: ${error.message}`, 'warn');
                }

                if (pageData && pageData.type !== 'basic') {
                    let illust = null;

                    if (pageData.type === 'preload-data') {
                        illust = pageData.data.illust?.[illustId];
                    } else if (pageData.type === 'global') {
                        illust = pageData.data.illust?.[illustId] || pageData.data.preload?.illust?.[illustId];
                    }

                    if (illust) {
                        await this.processIllustData(illustId, illust);
                    }
                }
            } catch (error) {
                Utils.log(`Error recording illust page: ${error.message}`, 'error');
            }
        }

        async processIllustData(illustId, illust) {
            try {
                const tags = illust.tags?.tags || [];
                const isAIByType = illust.aiType === 2;
                const isAIPossibleByType = illust.aiType >= 2;
                const isAIByTags = CONFIG.USER_CONFIG.enable_tag_detection ? Utils.checkAITags(tags) : false;

                const cacheData = {
                    ai: isAIByType || isAIByTags,
                    ai_is_possible: isAIPossibleByType || isAIByTags,
                    user_id: illust.userId,
                    title: illust.title,
                    tags: tags,
                    aiType: illust.aiType,
                    isAIByTags: isAIByTags
                };

                await this.cacheManager.setCache('pixiv_illust', illustId, cacheData);
                Utils.log(`Auto-recorded illust ${illustId} (AI: ${cacheData.ai})`, 'debug');
            } catch (error) {
                Utils.log(`Error processing illust data: ${error.message}`, 'error');
            }
        }
    }

    // ============= DOM 操作工具 =============
    class DOMUtils {
        static addStyles() {
            if (document.getElementById('pixiv-ai-tag-styles')) return;

            const styles = `
.add_ai_tag_view {
    padding: 0px 6px;
    border-radius: 3px;
    color: rgb(255, 255, 255);
    background: rgb(96, 64, 255);
    font-weight: bold;
    font-size: 10px;
    line-height: 16px;
    user-select: none;
}

                .add_ai_possible_tag_view {
                    background: rgb(96, 64, 127);
                }

                .add_ai_tag_view.ai-by-tags {
                    background: rgb(255, 96, 64);
                }

                .add_ai_possible_tag_view.ai-by-tags {
                    background: rgb(127, 64, 96);
                }
            `;

            const styleElement = document.createElement('style');
            styleElement.id = 'pixiv-ai-tag-styles';
            styleElement.textContent = styles;
            document.head.appendChild(styleElement);
        }

        static addTag(node, text, className = 'add_ai_tag_view', isAIByTags = false) {
            // 只对包含图片的链接添加标签
            const img = node.querySelector('img');
            if (!img) {
                return false; // 不是图片链接,跳过
            }

            // 检查是否已经有AI标签,避免重复添加
            if (node.querySelector('.add_ai_tag_view, .add_ai_possible_tag_view')) {
                return true; // 已存在标签,视为成功
            }

            const finalClassName = className + (isAIByTags ? ' ai-by-tags' : '');

            // 查找合适的图片容器 - 尝试多个可能的父级
            let imgContainer = img.parentElement;

            // 如果直接父级没有合适的定位,向上查找
            while (imgContainer && imgContainer !== node) {
                const style = window.getComputedStyle(imgContainer);
                if (style.position === 'relative' || imgContainer.classList.contains('sc-324476b7-9')) {
                    break;
                }
                imgContainer = imgContainer.parentElement;
            }

            // 如果没找到合适的容器,使用图片的直接父级
            if (!imgContainer || imgContainer === node) {
                imgContainer = img.parentElement;
            }

            if (!imgContainer) {
                return false; // 失败
            }

            // 设置容器为相对定位
            imgContainer.style.position = 'relative';

            // 固定放在左下角,避免与其他元素重叠
            const position = 'bottom: 4px; left: 4px;';

            const tagHtml = `<div class="${finalClassName}" style="position: absolute; ${position} z-index: 10;">${text}</div>`;
            imgContainer.insertAdjacentHTML('afterbegin', tagHtml);

            // 标记节点为已处理
            node.dataset.tagAdded = 'true';

            return true; // 成功
        }
    }

    // ============= 查询数据管理器 =============
    class QueryDataManager {
        constructor(cacheManager, apiClient) {
            this.cacheManager = cacheManager;
            this.apiClient = apiClient;
            this.data = { pixiv_illust: {} };
        }

        getOrCreate(type, id) {
            if (!this.data[type][id]) {
                this.data[type][id] = {
                    nodes: [],
                    querying: false,
                    ai: null,
                    ai_is_possible: null
                };
            }
            return this.data[type][id];
        }

        async addNode(type, id, node) {
            const entry = this.getOrCreate(type, id);

            // 总是添加节点,因为同一个作品可能有多个链接(图片链接和标题链接)
            if (!entry.nodes.includes(node)) {
                entry.nodes.push(node);
            }

            // 预检查缓存
            const cachedData = await this.cacheManager.getCache(type, id);
            if (cachedData) {
                // 静默使用缓存数据
                // 只对当前节点应用缓存数据
                this.applyCachedData(type, id, [node], cachedData);
                // 移除已成功添加标签的节点
                entry.nodes = entry.nodes.filter(n => !n.dataset.tagAdded);
            } else if (!entry.querying) {
                // 静默排队API请求
            }
        }

        applyCachedData(type, id, nodes, cachedData) {
            if (type === 'pixiv_illust') {
                // 延迟处理,给DOM一些时间完全渲染
                setTimeout(() => {
                    nodes.forEach(node => {
                        if (cachedData.ai) {
                            const success = DOMUtils.addTag(node, 'AI', 'add_ai_tag_view', cachedData.isAIByTags);
                            if (success) {
                                this.handleImageRemoval(node);
                            } else {
                                // 如果失败,再次尝试
                                setTimeout(() => {
                                    DOMUtils.addTag(node, 'AI', 'add_ai_tag_view', cachedData.isAIByTags);
                                }, 1000);
                            }
                        } else if (cachedData.ai_is_possible && CONFIG.USER_CONFIG.show_ai_possible) {
                            const success = DOMUtils.addTag(node, 'AI?', 'add_ai_possible_tag_view', cachedData.isAIByTags);
                            if (!success) {
                                setTimeout(() => {
                                    DOMUtils.addTag(node, 'AI?', 'add_ai_possible_tag_view', cachedData.isAIByTags);
                                }, 1000);
                            }
                        }
                    });
                }, 100); // 100ms延迟
            }
        }

        handleImageRemoval(node) {
            try {
                switch (CONFIG.USER_CONFIG.remove_image) {
                    case 1:
                        // 替换所有图像内容为文本
                        const images = node.querySelectorAll('img, canvas, svg');
                        images.forEach(img => {
                            img.outerHTML = "<h5>AI Artwork</h5>";
                        });

                        // 处理背景图像
                        const bgElements = node.querySelectorAll('[style*="background-image"]');
                        bgElements.forEach(el => {
                            el.style.backgroundImage = 'none';
                            if (!el.textContent.trim()) {
                                el.innerHTML = "<h5>AI Artwork</h5>";
                            }
                        });
                        break;

                    case 2:
                        // 移除整个容器
                        const parent = Utils.getParentNodeWithDepth(node, SELECTORS.REMOVE_PARENT_DEPTH);
                        if (parent && parent.parentNode) {
                            parent.parentNode.removeChild(parent);
                        }
                        break;
                }
            } catch (error) {
                Utils.log(`Error handling image removal: ${error.message}`, 'error');
            }
        }

        getQueuedItems() {
            const queued = [];
            for (const [type, items] of Object.entries(this.data)) {
                for (const [id, data] of Object.entries(items)) {
                    if (data.nodes.length > 0 && !data.querying) {
                        queued.push({ type, id, data });
                    }
                }
            }
            return queued;
        }

        async processPixivIllust(id, nodes) {
            const entry = this.getOrCreate('pixiv_illust', id);

            try {
                const cachedData = await this.cacheManager.getCache('pixiv_illust', id);

                if (cachedData) {
                    Utils.log(`Using cached data for illust ${id}`, 'debug');
                    this.applyCachedData('pixiv_illust', id, nodes, cachedData);
                    return false;
                }

                if (entry.ai === null) {
                    entry.querying = true;
                    Utils.log(`Fetching data for illust ${id}`, 'debug');

                    const response = await this.apiClient.fetchPixivIllust(id);
                    const json = await response.json();

                    if (!json?.body) {
                        throw new Error('Invalid response');
                    }

                    const { aiType } = json.body;
                    const tags = json.body.tags?.tags || [];

                    const isAIByType = aiType === 2;
                    const isAIPossibleByType = aiType >= 2;
                    const isAIByTags = CONFIG.USER_CONFIG.enable_tag_detection ? Utils.checkAITags(tags) : false;

                    const cacheData = {
                        ai: isAIByType || isAIByTags,
                        ai_is_possible: isAIPossibleByType || isAIByTags,
                        user_id: json.body.userId,
                        title: json.body.title || '',
                        tags: tags,
                        aiType: aiType,
                        isAIByTags: isAIByTags
                    };

                    entry.ai = cacheData.ai;
                    entry.ai_is_possible = cacheData.ai_is_possible;

                    await this.cacheManager.setCache('pixiv_illust', id, cacheData);
                    this.applyCachedData('pixiv_illust', id, nodes, cacheData);

                    Utils.log(`Processed illust ${id}: AI=${cacheData.ai}`, 'debug');
                    entry.querying = false;
                    return true;
                } else {
                    this.applyCachedData('pixiv_illust', id, nodes, {
                        ai: entry.ai,
                        ai_is_possible: entry.ai_is_possible
                    });
                    return false;
                }
            } catch (error) {
                entry.querying = false;
                Utils.log(`Error processing illust ${id}: ${error.message}`, 'error');
                return false;
            }
        }
    }

    // ============= URL 处理器 =============
    class URLProcessor {
        constructor(queryDataManager) {
            this.queryDataManager = queryDataManager;
        }

        extractPixivIllustId(url) {
            const match = url.match(/\/artworks\/(\d+)/);
            return match ? match[1] : null;
        }

        async processNode(node) {
            if (!node?.href) return;

            if (node.classList.contains('add_ai_tag')) return;

            node.classList.add('add_ai_tag');
            const url = node.href;

            if (/pixiv\.net/.test(url) && /artworks/.test(url)) {
                const id = this.extractPixivIllustId(url);
                if (id) {
                    await this.queryDataManager.addNode('pixiv_illust', id, node);
                }
            }
        }
    }

    // ============= 主应用类 =============
    class PixivAITagEnhanced {
        constructor() {
            this.cacheManager = new CrossTabCacheManager();
            this.syncManager = new CrossTabSyncManager();
            this.apiClient = new APIClient(this.syncManager);
            this.queryDataManager = new QueryDataManager(this.cacheManager, this.apiClient);
            this.urlProcessor = new URLProcessor(this.queryDataManager);
            this.pixivAutoRecorder = new PixivAutoRecorder(this.cacheManager);

            this.isRunning = false;
            this.observer = null;
            this.queryInterval = null;
        }

        async initialize() {
            try {
                Utils.log('Initializing Pixiv AI Tag Enhanced...', 'debug');

                DOMUtils.addStyles();
                await this.pixivAutoRecorder.initialize();
                this.setupObserver();
                this.startQueryLoop();
                this.startMaintenanceLoop();
                // 移除定期扫描,MutationObserver已经足够

                Utils.log('Initialization completed', 'debug');

                // 等待页面完全加载后扫描
                this.waitForPageLoad();

            } catch (error) {
                Utils.log(`Initialization error: ${error.message}`, 'error');
                console.error('Full initialization error:', error);
            }
        }

        async waitForPageLoad() {
            // 如果页面已经完全加载
            if (document.readyState === 'complete') {
                Utils.log('Page already loaded, scanning immediately', 'debug');
                await this.scanDocument();
                return;
            }

            // 等待页面加载完成
            const loadPromise = new Promise((resolve) => {
                if (document.readyState === 'complete') {
                    resolve();
                } else {
                    window.addEventListener('load', resolve, { once: true });
                }
            });

            // 等待DOM内容加载完成(备用)
            const domPromise = new Promise((resolve) => {
                if (document.readyState !== 'loading') {
                    resolve();
                } else {
                    document.addEventListener('DOMContentLoaded', resolve, { once: true });
                }
            });

            try {
                // 等待页面加载完成,最多等待10秒
                await Promise.race([
                    loadPromise,
                    new Promise(resolve => setTimeout(resolve, 10000))
                ]);

                Utils.log('Page load completed, starting scan', 'debug');
                await this.scanDocument();

                // 如果还没有找到链接,再等待一下(可能是SPA应用)
                const artworkCount = document.querySelectorAll('a[href*="/artworks/"]:not(.add_ai_tag)').length;
                if (artworkCount === 0) {
                    Utils.log('No artwork links found, waiting for SPA content...', 'debug');
                    await Utils.sleep(2000);
                    await this.scanDocument();
                }

            } catch (error) {
                Utils.log(`Error waiting for page load: ${error.message}`, 'error');
                // 即使出错也尝试扫描
                await this.scanDocument();
            }
        }

        async scanDocument() {
            try {
                Utils.log('Starting document scan...', 'debug');

                const artworkLinks = document.querySelectorAll('a[href*="/artworks/"]:not(.add_ai_tag)');
                if (artworkLinks.length > 0) {
                    Utils.log(`Found ${artworkLinks.length} new artwork links`, 'debug');
                }

                if (artworkLinks.length > 0) {
                    // 优先处理包含图片的链接,避免重复处理同一作品
                    const processedIds = new Set();
                    let processed = 0;

                    for (const link of artworkLinks) {
                        const id = this.urlProcessor.extractPixivIllustId(link.href);
                        if (id && !processedIds.has(id)) {
                            // 优先选择包含图片的链接
                            if (this.hasImageContent(link)) {
                                await this.urlProcessor.processNode(link);
                                processedIds.add(id);
                                processed++;
                            }
                        }
                    }

                    // 处理没有图片但还未处理的链接
                    for (const link of artworkLinks) {
                        const id = this.urlProcessor.extractPixivIllustId(link.href);
                        if (id && !processedIds.has(id)) {
                            await this.urlProcessor.processNode(link);
                            processedIds.add(id);
                            processed++;
                        }
                    }

                    Utils.log(`Processed ${processed} unique artwork links`, 'debug');
                } else {
                    Utils.log('No new artwork links found', 'debug');
                }
            } catch (error) {
                Utils.log(`Document scan error: ${error.message}`, 'error');
                console.error('Scan error details:', error);
            }
        }

        // 高效的增量扫描 - 只处理新节点
        async scanNewNodes(nodes) {
            try {
                const processedIds = new Set();
                let processed = 0;

                // 收集所有artwork链接
                const allLinks = [];
                for (const node of nodes) {
                    if (node.nodeType === 1) { // Element node
                        // 检查节点本身是否是artwork链接
                        if (node.matches && node.matches('a[href*="/artworks/"]:not(.add_ai_tag)')) {
                            allLinks.push(node);
                        }

                        // 检查节点内部的artwork链接
                        const artworkLinks = node.querySelectorAll('a[href*="/artworks/"]:not(.add_ai_tag)');
                        allLinks.push(...artworkLinks);
                    }
                }

                // 优先处理包含图片的链接
                for (const link of allLinks) {
                    const id = this.urlProcessor.extractPixivIllustId(link.href);
                    if (id && !processedIds.has(id) && this.hasImageContent(link)) {
                        await this.urlProcessor.processNode(link);
                        processedIds.add(id);
                        processed++;
                    }
                }

                // 处理剩余的链接
                for (const link of allLinks) {
                    const id = this.urlProcessor.extractPixivIllustId(link.href);
                    if (id && !processedIds.has(id)) {
                        await this.urlProcessor.processNode(link);
                        processedIds.add(id);
                        processed++;
                    }
                }

                if (processed > 0) {
                    Utils.log(`Incrementally processed ${processed} unique artwork links`, 'debug');
                }

                return processed;
            } catch (error) {
                Utils.log(`Incremental scan error: ${error.message}`, 'error');
                return 0;
            }
        }

        // 检查链接是否包含图像内容
        hasImageContent(link) {
            if (!link) return false;

            // 检查是否包含img标签
            if (link.querySelector('img')) return true;

            // 检查是否包含canvas
            if (link.querySelector('canvas')) return true;

            // 检查是否包含svg
            if (link.querySelector('svg')) return true;

            // 检查是否有背景图像
            const elementsWithBg = link.querySelectorAll('[style*="background-image"]');
            if (elementsWithBg.length > 0) return true;

            // 检查CSS背景图像
            const computedStyle = window.getComputedStyle(link);
            if (computedStyle.backgroundImage && computedStyle.backgroundImage !== 'none') return true;

            // 检查子元素的背景图像
            const children = link.querySelectorAll('*');
            for (const child of children) {
                const childStyle = window.getComputedStyle(child);
                if (childStyle.backgroundImage && childStyle.backgroundImage !== 'none') {
                    return true;
                }
            }

            return false;
        }

        setupObserver() {
            if (this.observer) {
                this.observer.disconnect();
            }

            // 测试MutationObserver是否工作
            Utils.log('Setting up MutationObserver...', 'debug');

            this.observer = new MutationObserver(async (mutations) => {
                Utils.log(`🔍 MutationObserver triggered! ${mutations.length} mutations detected`, 'debug');

                const newNodes = [];
                let totalAddedNodes = 0;

                for (const mutation of mutations) {
                    Utils.log(`  Mutation type: ${mutation.type}, added: ${mutation.addedNodes.length}, removed: ${mutation.removedNodes.length}`, 'debug');

                    if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
                        totalAddedNodes += mutation.addedNodes.length;

                        for (const node of mutation.addedNodes) {
                            if (node.nodeType === 1) { // Element node
                                // 检查是否包含artwork链接
                                const hasArtworkLink = (node.matches && node.matches('a[href*="/artworks/"]')) ||
                                    (node.querySelector && node.querySelector('a[href*="/artworks/"]'));

                                if (hasArtworkLink) {
                                    newNodes.push(node);
                                    Utils.log(`  📎 Found node with artwork links: ${node.tagName}`, 'debug');
                                }
                            }
                        }
                    }
                }

                Utils.log(`  Total added nodes: ${totalAddedNodes}, artwork nodes: ${newNodes.length}`, 'debug');

                if (newNodes.length > 0) {
                    Utils.log(`🎯 Processing ${newNodes.length} new nodes with artwork links`, 'debug');
                    await this.scanNewNodes(newNodes);
                }
            });

            // 检查document.body是否存在
            if (!document.body) {
                Utils.log('⚠️ document.body not found, waiting...', 'warn');
                setTimeout(() => this.setupObserver(), 100);
                return;
            }

            try {
                this.observer.observe(document.body, {
                    childList: true,
                    subtree: true
                });

                Utils.log('✅ MutationObserver setup completed, observing document.body', 'debug');

                // 测试observer是否真的在工作
                setTimeout(() => {
                    Utils.log('🧪 Testing MutationObserver by adding a test element...', 'debug');
                    const testDiv = document.createElement('div');
                    testDiv.id = 'pixiv-ai-test';
                    testDiv.style.display = 'none';
                    document.body.appendChild(testDiv);

                    setTimeout(() => {
                        if (document.getElementById('pixiv-ai-test')) {
                            document.body.removeChild(testDiv);
                        }
                    }, 1000);
                }, 2000);

            } catch (error) {
                Utils.log(`❌ Failed to setup MutationObserver: ${error.message}`, 'error');
            }
        }

        startQueryLoop() {
            if (this.isRunning) return;

            this.isRunning = true;

            const interval = CONFIG.USER_CONFIG.query_delay > 0 ?
                CONFIG.USER_CONFIG.query_delay : CONFIG.QUERY_INTERVAL;

            this.queryInterval = setInterval(async () => {
                if (this.isRunning) {
                    await this.processQueuedQueries();
                }
            }, interval);
        }

        async processQueuedQueries() {
            const queuedItems = this.queryDataManager.getQueuedItems();
            if (queuedItems.length === 0) return;

            for (const { type, id, data } of queuedItems) {
                const nodes = [...data.nodes];
                data.nodes = [];

                try {
                    if (type === 'pixiv_illust') {
                        await this.queryDataManager.processPixivIllust(id, nodes);
                    }
                } catch (error) {
                    if (error.message.includes('429')) {
                        data.nodes.unshift(...nodes);
                        Utils.log(`Rate limited for ${type}:${id}, re-queuing`, 'warn');
                    }
                    Utils.log(`Error processing ${type}:${id}: ${error.message}`, 'error');
                }

                await Utils.sleep(100);
            }
        }

        startMaintenanceLoop() {
            setInterval(async () => {
                try {
                    await this.cacheManager.cleanupExpiredCache();
                } catch (error) {
                    Utils.log(`Maintenance error: ${error.message}`, 'error');
                }
            }, CONFIG.CACHE.CLEANUP_INTERVAL);
        }

        startPeriodicScan() {
            // 备用的定期扫描,以防MutationObserver不工作
            setInterval(async () => {
                try {
                    const newLinks = document.querySelectorAll('a[href*="/artworks/"]:not(.add_ai_tag)').length;
                    if (newLinks > 0) {
                        Utils.log(`🔄 Periodic scan found ${newLinks} new artwork links`, 'info');
                        await this.scanDocument();
                    }
                    // 移除了"没有找到新链接"的日志,减少噪音
                } catch (error) {
                    Utils.log(`Periodic scan error: ${error.message}`, 'error');
                }
            }, 5000); // 每5秒检查一次
        }

        async stop() {
            this.isRunning = false;

            if (this.queryInterval) {
                clearInterval(this.queryInterval);
                this.queryInterval = null;
            }

            if (this.observer) {
                this.observer.disconnect();
                this.observer = null;
            }

            await this.syncManager.stop();
            Utils.log('Pixiv AI Tag Enhanced stopped', 'info');
        }
    }

    // ============= 初始化 =============
    let enhancer = null;


    async function initialize() {
        try {
            if (enhancer) {
                await enhancer.stop();
            }

            enhancer = new PixivAITagEnhanced();
            await enhancer.initialize();

        } catch (error) {
            Utils.log(`Initialization failed: ${error.message}`, 'error');
            console.error('Full error:', error);
        }
    }

    // 确保在DOM准备好后初始化
    if (document.readyState === 'loading') {
        Utils.log('Document still loading, waiting for DOMContentLoaded', 'debug');
        document.addEventListener('DOMContentLoaded', initialize);
    } else {
        Utils.log('Document ready, initializing immediately', 'debug');
        initialize();
    }

})();