Greasy Fork

Greasy Fork is available in English.

工信部官网miit ICP备案批量查询小工具

工业和信息化部政务服务平台,批量获取公司备案信息,手动查询一次公司名称后,批量获取该公司名下所有网站、APP、小程序、快应用备案内容,一键复制结果

// ==UserScript==
// @name         工信部官网miit ICP备案批量查询小工具
// @namespace    https://beian.miit.gov.cn
// @version      0.6
// @description  工业和信息化部政务服务平台,批量获取公司备案信息,手动查询一次公司名称后,批量获取该公司名下所有网站、APP、小程序、快应用备案内容,一键复制结果
// @match        *://beian.miit.gov.cn/*
// @grant        unsafeWindow
// @grant        GM_setClipboard
// @run-at       document-start
// @author       ejfkdev and Gemini 2.5 pro
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    const TARGET_API_URL = 'https://hlwicpfwc.miit.gov.cn/icpproject_query/api/icpAbbreviateInfo/queryByCondition';
    const UI_WRAPPER_ID = 'miit-userscript-ui-wrapper-v06'; // Version update in ID
    const FETCH_BUTTON_ID = 'miit-fetch-data-button-v06';
    const PROGRESS_DIV_ID = 'miit-progress-div-v06';
    const TEXTAREAS_TABLE_ID = 'miit-textareas-table-v06';
    const UNIT_NAME_INPUT_SELECTOR = 'input.el-input__inner';

    const TEXTAREA_CONFIG = [
        { id: 'miit-ta-website-v06', labelText: '网站', serviceType: 1, dataField: 'domain', countSpanId: 'miit-count-website-v06', copyButtonId: 'miit-copy-website-v06' },
        { id: 'miit-ta-app-v06', labelText: 'APP', serviceType: 6, dataField: 'serviceName', countSpanId: 'miit-count-app-v06', copyButtonId: 'miit-copy-app-v06' },
        { id: 'miit-ta-mini_program-v06', labelText: '小程序', serviceType: 7, dataField: 'serviceName', countSpanId: 'miit-count-mini_program-v06', copyButtonId: 'miit-copy-mini_program-v06' },
        { id: 'miit-ta-quick_app-v06', labelText: '快应用', serviceType: 8, dataField: 'serviceName', countSpanId: 'miit-count-quick_app-v06', copyButtonId: 'miit-copy-quick_app-v06' }
    ];
    const REQUIRED_MONITORED_HEADERS = ['sign', 'token', 'uuid'];

    let monitoredRequestData = { sign: null, token: null, uuid: null };
    let uiInjected = false;
    let isFetchingAllData = false;

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

    function headersToObject(headersSource) {
        const obj = {};
        if (headersSource instanceof Headers) {
            headersSource.forEach((value, key) => { obj[key.toLowerCase()] = value; });
        } else if (Array.isArray(headersSource)) {
            headersSource.forEach(([key, value]) => { obj[key.toLowerCase()] = value; });
        } else if (typeof headersSource === 'object' && headersSource !== null) {
            for (const key in headersSource) {
                obj[key.toLowerCase()] = headersSource[key];
            }
        }
        return obj;
    }

    async function fetchDataForServiceType(serviceConfig, unitName, baseApiHeaders, progressDivElement, textareaElement, labelCountSpanElement) {
        const collectedItems = new Set();
        let currentPage = 1;
        let totalPages = 1;
        const pageSize = "40";

        textareaElement.value = "";
        if (labelCountSpanElement) labelCountSpanElement.textContent = " (0)";

        progressDivElement.textContent = `进度: 即将开始获取 ${serviceConfig.labelText} 数据...`;

        while (currentPage <= totalPages && isFetchingAllData) {
            const payloadForPage = {
                pageNum: String(currentPage),
                pageSize: pageSize,
                unitName: unitName,
                serviceType: serviceConfig.serviceType
            };
            const currentHeaders = { ...baseApiHeaders,
                'sign': monitoredRequestData.sign,
                'token': monitoredRequestData.token,
                'uuid': monitoredRequestData.uuid,
                'content-type': 'application/json'
            };
             const browserControlledHeaders = ['host', 'connection', 'content-length', 'cookie'];
             Object.keys(currentHeaders).forEach(key => {
                const lowerKey = key.toLowerCase();
                if (browserControlledHeaders.includes(lowerKey) ||
                    (lowerKey.startsWith('sec-') && !['sec-ch-ua', 'sec-ch-ua-mobile', 'sec-ch-ua-platform', 'sec-fetch-dest', 'sec-fetch-mode', 'sec-fetch-site', 'sec-gpc'].includes(lowerKey) )) {
                    if (!['user-agent', 'origin', 'referer'].includes(lowerKey) || !baseApiHeaders[lowerKey]) {
                         delete currentHeaders[key];
                    }
                }
            });

            progressDivElement.textContent = `进度: 正在获取 ${serviceConfig.labelText} 数据 (第 ${currentPage}${totalPages > 1 && currentPage > 1 ? '/'+totalPages : ''} 页)...`;
            console.log(`[MIIT Helper] Fetching ${serviceConfig.labelText} - Page ${currentPage} - Payload:`, payloadForPage);

            try {
                const response = await unsafeWindow.fetch(TARGET_API_URL, {
                    method: 'POST',
                    headers: currentHeaders,
                    body: JSON.stringify(payloadForPage),
                    mode: 'cors',
                    credentials: 'include',
                    referrer: currentHeaders['referer'] || 'https://beian.miit.gov.cn/',
                    referrerPolicy: 'strict-origin-when-cross-origin'
                });

                if (!response.ok) {
                    let errorText = `HTTP错误! 状态: ${response.status} ${response.statusText}`;
                    try { const errorData = await response.text(); errorText += `, 响应: ${errorData.substring(0,200)}`; } catch (e) {}
                    throw new Error(errorText);
                }
                const responseData = await response.json();

                if (responseData.success && responseData.code === 200) {
                    const params = responseData.params || {};
                    if (currentPage === 1) {
                        totalPages = parseInt(params.pages, 10) || 1;
                        console.log(`[MIIT Helper] ${serviceConfig.labelText} - 总页数: ${totalPages}`);
                        if (totalPages === 0 && params.list && params.list.length > 0) totalPages = 1;
                        if (totalPages === 0 || (!params.list || params.list.length === 0 && totalPages <=1) ) {
                            if (params.list && params.list.length > 0) { /* continue */ } else {
                                progressDivElement.textContent = `进度: ${serviceConfig.labelText} - 服务器报告0页或无数据。`;
                                if ((!params.list || params.list.length === 0)) break;
                            }
                        }
                    }

                    const itemListOnPage = params.list || [];
                    let pageItemsCount = 0;
                    for (const item of itemListOnPage) {
                        const valueToCollect = item[serviceConfig.dataField];
                        if (valueToCollect) {
                            collectedItems.add(valueToCollect);
                            pageItemsCount++;
                        }
                    }
                    console.log(`[MIIT Helper] 从第 ${currentPage} 页 (${serviceConfig.labelText}) 提取到 ${pageItemsCount} 个条目。`);

                    textareaElement.value = Array.from(collectedItems).sort().join('\n');
                    if (labelCountSpanElement) labelCountSpanElement.textContent = ` (${collectedItems.size})`;

                    if (currentPage >= totalPages) break;
                    currentPage++;
                } else {
                    const errMsg = `[MIIT Helper] 第 ${currentPage} 页 (${serviceConfig.labelText}) API错误: ${responseData.msg || '未知API错误'}`;
                    progressDivElement.textContent = `进度: ${errMsg}`;
                    console.error(errMsg, responseData);
                    throw new Error(errMsg);
                }
            } catch (e) {
                const errMsg = `[MIIT Helper] 请求第 ${currentPage} 页 (${serviceConfig.labelText}) 时出错: ${e.message ? e.message.substring(0,150) : String(e).substring(0,150)}`;
                progressDivElement.textContent = `错误: ${errMsg}`;
                console.error(errMsg, e);
                isFetchingAllData = false;
                const fetchButton = document.getElementById(FETCH_BUTTON_ID);
                if (fetchButton) {
                    fetchButton.disabled = !(monitoredRequestData.sign && monitoredRequestData.token && monitoredRequestData.uuid);
                    fetchButton.style.backgroundColor = fetchButton.disabled ? '#aaa' : '#4CAF50'; // 更新按钮颜色
                }
                throw e;
            }
            if (currentPage <= totalPages && isFetchingAllData) { // 只有当还有下一页且未被中断时才延迟
                 await delay(10000); // 每个分页请求间隔调整到10秒
            }
        }
        console.log(`[MIIT Helper] ${serviceConfig.labelText} 数据提取完成,共 ${collectedItems.size} 条。`);
    }

    async function handleFetchAllData() {
        if (isFetchingAllData) {
            alert("已有一个获取任务正在进行中,请稍候...");
            return;
        }
        isFetchingAllData = true;
        const fetchButton = document.getElementById(FETCH_BUTTON_ID);
        if(fetchButton) fetchButton.disabled = true;


        const progressDiv = document.getElementById(PROGRESS_DIV_ID);
        const unitNameInputElement = document.querySelector(UNIT_NAME_INPUT_SELECTOR);

        if (!unitNameInputElement || !unitNameInputElement.value.trim()) {
            const msg = "错误: 请在页面顶部的输入框中输入单位名称 (例如 科大讯飞股份有限公司)。";
            if (progressDiv) progressDiv.textContent = msg;
            console.error(`[MIIT Helper] ${msg}`);
            alert(msg);
            isFetchingAllData = false;
            if(fetchButton) {
                 fetchButton.disabled = false; // 出错,重新启用按钮 (如果头部已捕获)
                 fetchButton.style.backgroundColor = (monitoredRequestData.sign && monitoredRequestData.token && monitoredRequestData.uuid) ? '#4CAF50' : '#aaa';
            }
            return;
        }
        const unitName = unitNameInputElement.value.trim();

        if (!monitoredRequestData.sign || !monitoredRequestData.token || !monitoredRequestData.uuid) {
            const msg = "错误: 必需的请求头 (sign, token, uuid) 尚未从页面请求中捕获。请先在页面上进行一次查询(可能需要验证码)以捕获这些信息。";
            if (progressDiv) progressDiv.textContent = msg;
            console.error(`[MIIT Helper] ${msg}`);
            alert(msg);
            isFetchingAllData = false;
            // 按钮状态由 updateUIAfterHeadersCaptured 控制,这里不直接启用
            return;
        }

        console.log(`[MIIT Helper] '获取备案数据' 按钮被点击。单位名称: ${unitName}`);
        if (progressDiv) progressDiv.textContent = '进度: 初始化中...';

        TEXTAREA_CONFIG.forEach(cfg => {
            const ta = document.getElementById(cfg.id);
            if (ta) ta.value = "";
            const lbl = document.getElementById(cfg.countSpanId);
            if (lbl) lbl.textContent = " (0)";
        });

        const baseApiHeadersFromExample = {
            "accept": "application/json, text/plain, */*",
            "accept-language": "zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7",
            "cache-control": "no-cache", "pragma": "no-cache",
            "origin": "https://beian.miit.gov.cn",
            "referer": "https://beian.miit.gov.cn/"
        };
        baseApiHeadersFromExample['user-agent'] = navigator.userAgent;

        await delay(3000);

        for (let i = 0; i < TEXTAREA_CONFIG.length; i++) {
            if (!isFetchingAllData) break; // 如果中途出错,则不再继续
            const serviceConfig = TEXTAREA_CONFIG[i];
            const textareaElement = document.getElementById(serviceConfig.id);
            const labelCountSpanElement = document.getElementById(serviceConfig.countSpanId);

            try {
                await fetchDataForServiceType(serviceConfig, unitName, baseApiHeadersFromExample, progressDiv, textareaElement, labelCountSpanElement);
                if (i < TEXTAREA_CONFIG.length - 1 && isFetchingAllData) {
                    progressDiv.textContent = `进度: ${serviceConfig.labelText} 完成。等待10秒后获取下一组...`;
                    await delay(10000);
                }
            } catch (error) {
                console.error(`[MIIT Helper] 获取 ${serviceConfig.labelText} 数据时发生严重错误,后续任务已中止。`, error);
                // isFetchingAllData 和按钮状态已在 fetchDataForServiceType 中处理
                break;
            }
        }

        if (isFetchingAllData) {
            if (progressDiv) progressDiv.textContent = '进度: 所有数据类型获取完毕!(已去重)';
        }
        isFetchingAllData = false;
        if(fetchButton) {
            fetchButton.disabled = !(monitoredRequestData.sign && monitoredRequestData.token && monitoredRequestData.uuid);
            fetchButton.style.backgroundColor = fetchButton.disabled ? '#aaa' : '#4CAF50';
        }
    }

    function createCopyButton(textareaId, buttonText = "复制内容") {
        const button = document.createElement('button');
        button.textContent = buttonText;
        button.setAttribute('style', 'margin-top: 5px; padding: 3px 8px; font-size: 11px; cursor: pointer; background-color: #f0f0f0; border: 1px solid #ccc; border-radius: 3px;');
        button.onmouseover = function() { this.style.backgroundColor = '#e0e0e0'; };
        button.onmouseout = function() { this.style.backgroundColor = '#f0f0f0'; };
        button.onclick = function() {
            const textarea = document.getElementById(textareaId);
            if (textarea && textarea.value) {
                GM_setClipboard(textarea.value, 'text');
                const originalText = this.textContent;
                this.textContent = '已复制!';
                this.disabled = true;
                setTimeout(() => {
                    this.textContent = originalText;
                    this.disabled = false;
                }, 2000);
            } else {
                alert("没有内容可复制。");
            }
        };
        return button;
    }

    function tryInjectUI() {
        if (document.getElementById(UI_WRAPPER_ID)) {
            updateUIAfterHeadersCaptured(); // 如果UI已存在,仅更新其状态
            return;
        }

        const targetContainer = document.querySelector('div.listcont');
        if (!targetContainer) {
            setTimeout(tryInjectUI, 2000);
            return;
        }

        console.log("[MIIT Helper] 正在注入初始UI...");

        const wrapperDiv = document.createElement('div');
        wrapperDiv.id = UI_WRAPPER_ID;
        wrapperDiv.setAttribute('style', 'border: 1px solid #ccc; padding: 15px; margin-top: 20px; margin-bottom: 20px; background-color: #f9f9f9; font-family: sans-serif;');

        const button = document.createElement('button');
        button.id = FETCH_BUTTON_ID;
        button.textContent = '批量获取所有备案数据';
        // 初始样式为禁用
        button.setAttribute('style', 'padding: 10px 18px; font-size: 16px; cursor: not-allowed; background-color: #aaa; color: white; border: none; border-radius: 4px; transition: background-color 0.3s ease;');
        button.disabled = true;
        button.onclick = handleFetchAllData;
        wrapperDiv.appendChild(button);

        const progressDiv = document.createElement('div');
        progressDiv.id = PROGRESS_DIV_ID;
        progressDiv.textContent = '提示: 请先在页面顶部的输入框进行一次查询(可能需要输入验证码),本工具捕获到必要信息后,上方按钮将启用。';
        progressDiv.setAttribute('style', 'margin-top: 10px; margin-bottom:10px; min-height: 1.2em; font-weight: bold; color: #777;');
        wrapperDiv.appendChild(progressDiv);

        const table = document.createElement('table');
        table.id = TEXTAREAS_TABLE_ID;
        table.setAttribute('style', 'width: 100%; margin-top: 10px; border-collapse: collapse; display: none;');

        const trLabels = table.insertRow();
        TEXTAREA_CONFIG.forEach(cfg => {
            const th = document.createElement('th');
            th.setAttribute('style', 'width: 25%; text-align: left; padding: 8px; border: 1px solid #ddd; background-color: #f0f0f0; font-size: 14px;');
            th.textContent = cfg.labelText;
            const countSpan = document.createElement('span');
            countSpan.id = cfg.countSpanId;
            countSpan.textContent = " (0)";
            countSpan.style.fontWeight = 'normal';
            countSpan.style.fontSize = '12px';
            th.appendChild(countSpan);
            trLabels.appendChild(th);
        });

        const trTextareas = table.insertRow();
        TEXTAREA_CONFIG.forEach(cfg => {
            const td = trTextareas.insertCell();
            td.setAttribute('style', 'width: 25%; padding: 5px; border: 1px solid #ddd; vertical-align: top; text-align: center;');
            const textarea = document.createElement('textarea');
            textarea.id = cfg.id;
            textarea.setAttribute('style', 'width: 98%; height: 200px; box-sizing: border-box; font-size: 12px; padding: 5px; border: 1px solid #ccc; margin-bottom: 5px;');
            textarea.readOnly = true;
            td.appendChild(textarea);
            td.appendChild(createCopyButton(cfg.id, `复制 ${cfg.labelText}`));
        });
        wrapperDiv.appendChild(table);

        if (targetContainer.firstChild) {
            targetContainer.insertBefore(wrapperDiv, targetContainer.firstChild);
        } else {
            targetContainer.appendChild(wrapperDiv);
        }

        uiInjected = true;
        console.log("[MIIT Helper] UI框架注入成功。");
        updateUIAfterHeadersCaptured(); // 根据当前是否有头部信息更新按钮状态
    }

    function updateUIAfterHeadersCaptured() {
        if (!uiInjected) { // 如果UI还未注入,先尝试注入
             // 在监控到头部后,如果页面已加载,可以尝试注入
            if (document.readyState === "complete" || document.readyState === "interactive") {
                tryInjectUI();
            } else {
                // 如果DOM未加载完,监听事件稍后注入。这确保 targetContainer 存在。
                window.addEventListener('DOMContentLoaded', tryInjectUI, { once: true });
            }
            return; // tryInjectUI 内部会再次调用此函数来更新状态
        }

        const fetchButton = document.getElementById(FETCH_BUTTON_ID);
        const textareasTable = document.getElementById(TEXTAREAS_TABLE_ID);
        const progressDiv = document.getElementById(PROGRESS_DIV_ID);

        if (fetchButton && textareasTable && progressDiv) {
            const headersNowAvailable = monitoredRequestData.sign && monitoredRequestData.token && monitoredRequestData.uuid;
            if (headersNowAvailable) {
                fetchButton.disabled = false;
                fetchButton.style.backgroundColor = '#4CAF50'; // 绿色,表示可用
                fetchButton.style.cursor = 'pointer';
                textareasTable.style.display = ''; // 显示表格
                if (progressDiv.textContent.includes("请先在上方输入框搜索")) {
                     progressDiv.textContent = "提示: 必要信息已捕获!可点击上方按钮获取数据。";
                     progressDiv.style.color = '#333'; // 正常颜色
                }
            } else {
                fetchButton.disabled = true;
                fetchButton.style.backgroundColor = '#aaa'; // 灰色,表示禁用
                fetchButton.style.cursor = 'not-allowed';
                textareasTable.style.display = 'none'; // 隐藏表格
                progressDiv.textContent = '提示: 请先在页面顶部的输入框进行一次查询(可能需要输入验证码),本工具捕获到必要信息后,上方按钮将启用。';
                progressDiv.style.color = '#777';
            }
        }
    }

    // Monkey patch window.fetch
    const originalFetch = unsafeWindow.fetch;
    unsafeWindow.fetch = async function(...args) {
        const resource = args[0];
        const config = args[1];
        let requestUrl = (typeof resource === 'string') ? resource : resource.url;
        let responseClone; // 用于安全地读取响应体而不消耗它
        let result;

        // 先执行原始请求
        try {
            result = await originalFetch.apply(this, args);
            if (requestUrl === TARGET_API_URL) {
                 responseClone = result.clone(); // 克隆响应对象以便安全地读取body
            }
        } catch(err) {
            console.error("[MIIT Monitor (fetch)] Original fetch error:", err);
            throw err; // 重新抛出原始错误
        }


        if (requestUrl === TARGET_API_URL) {
            console.log(`[MIIT Monitor (fetch)] 捕获到页面自身对目标API的请求: ${requestUrl}`);
            const requestConfigHeaders = headersToObject(config && config.headers ? config.headers : (resource instanceof Request ? resource.headers : {}));

            let headersChangedOrNewlyFound = false;
            REQUIRED_MONITORED_HEADERS.forEach(headerName => {
                const headerValue = requestConfigHeaders[headerName.toLowerCase()];
                if (headerValue) {
                    if (monitoredRequestData[headerName] !== headerValue) {
                        console.log(`[MIIT Monitor (fetch)] 更新 ${headerName}: ${headerValue.substring(0,30)}...`);
                        monitoredRequestData[headerName] = headerValue;
                        headersChangedOrNewlyFound = true;
                    }
                }
            });

            if (headersChangedOrNewlyFound || (monitoredRequestData.sign && !uiInjected)) {
                updateUIAfterHeadersCaptured();
            }

            // 调试:可以打印克隆的响应体
            // responseClone.text().then(text => console.log('[MIIT Monitor (fetch)] Cloned Response body for target URL:', text.substring(0, 200)));
        }
        return result;
    };

    // Monkey patch XMLHttpRequest
    const originalXhrOpen = unsafeWindow.XMLHttpRequest.prototype.open;
    const originalXhrSetRequestHeader = unsafeWindow.XMLHttpRequest.prototype.setRequestHeader;
    const originalXhrSend = unsafeWindow.XMLHttpRequest.prototype.send;
    const xhrRequestDataMap = new WeakMap();

    unsafeWindow.XMLHttpRequest.prototype.open = function(method, url, ...restArgs) {
        if (String(url) === TARGET_API_URL) {
            xhrRequestDataMap.set(this, { url: url, headers: {}, method: method, processedOnSend: false });
        } else {
            if (xhrRequestDataMap.has(this)) xhrRequestDataMap.delete(this);
        }
        return originalXhrOpen.apply(this, [method, url, ...restArgs]);
    };

    unsafeWindow.XMLHttpRequest.prototype.setRequestHeader = function(header, value) {
        const requestData = xhrRequestDataMap.get(this);
        if (requestData) {
            requestData.headers[header.toLowerCase()] = value;
        }
        return originalXhrSetRequestHeader.apply(this, [header, value]);
    };

    unsafeWindow.XMLHttpRequest.prototype.send = function(...sendArgs) {
        const requestData = xhrRequestDataMap.get(this);
        if (requestData && requestData.url === TARGET_API_URL && !requestData.processedOnSend) {
            requestData.processedOnSend = true;
            console.log(`[MIIT Monitor (XHR)] 捕获到页面自身对目标API的 ${requestData.method} 请求: ${requestData.url}`);

            let headersChangedOrNewlyFound = false;
            REQUIRED_MONITORED_HEADERS.forEach(headerName => {
                const headerValue = requestData.headers[headerName.toLowerCase()];
                if (headerValue) {
                     if (monitoredRequestData[headerName] !== headerValue) {
                        console.log(`[MIIT Monitor (XHR)] 更新 ${headerName}: ${headerValue.substring(0,30)}...`);
                        monitoredRequestData[headerName] = headerValue;
                        headersChangedOrNewlyFound = true;
                    }
                }
            });

            if (headersChangedOrNewlyFound || (monitoredRequestData.sign && !uiInjected)) {
                updateUIAfterHeadersCaptured();
            }
        }
        return originalXhrSend.apply(this, sendArgs);
    };

    console.log('[MIIT Helper] Userscript v0.6 已加载。');

    window.addEventListener('DOMContentLoaded', () => {
        tryInjectUI(); // 在DOM加载后立即尝试注入初始UI
    }, { once: true });

})();