// ==UserScript== // @name 豆包批量删除聊天记录 // @namespace https://docs.scriptcat.org/ // @version 0.5.1 // @description 豆包批量删除聊天记录 // @author You // @match https://www.doubao.com/* // @grant none // @noframes // ==/UserScript== // ---------- 类型声明(编辑器智能提示) ---------- /** * @typedef {Object} RequestData * @property {string|null} body * @property {Object} query * @property {Object} headers * @property {any} raw */ /** * @typedef {Object} ResponseData * @property {string} body * @property {Object} headers * @property {any} raw */ /** * @typedef {Object} InterceptorContext * @property {string} url * @property {string} method * @property {RequestData} request * @property {ResponseData} [response] * @property {function(function(): void): Promise} execute * @property {any} [key] 可自由添加 */ /** * @typedef {Object} InterceptorHooks * @property {function(InterceptorContext): void|Promise} [before] * @property {function(InterceptorContext): void|Promise} [after] */ /** * 全局请求拦截器 * @type {function(string|RegExp, InterceptorHooks): number} */ // eslint-disable-next-line no-unused-vars /* global requestInterceptor */ // ------------------------------------------------------ (function (global) { let __originalXHR__, __originalFetch__; const __requestInterceptors__ = new Map(); let interceptorIdCounter = 0; function matchUrl(targetUrl, matcher) { return typeof matcher === 'string' ? targetUrl.includes(matcher) : matcher instanceof RegExp ? matcher.test(targetUrl) : false; } function createExecuteWrapper(resolveHook) { let executePromise = null; return async function execute(handler) { if (executePromise) await executePromise; const origXHR = __originalXHR__, origFetch = __originalFetch__; const tempXHR = global.XMLHttpRequest, tempFetch = global.fetch; global.XMLHttpRequest = origXHR; global.fetch = origFetch; try { executePromise = new Promise((resolveInner) => { const p = handler(resolveHook); Promise.resolve(p).then(resolveInner); }); await executePromise; } finally { executePromise = null; global.XMLHttpRequest = tempXHR; global.fetch = tempFetch; } }; } async function awaitHookExecution(hook, context) { if (!hook) return; try { const ret = hook(context); if (ret && typeof ret.then === 'function') await ret; } catch (err) { console.error('[RequestInterceptor] 钩子执行异常:', err); } } function createContext(url, method, headers, raw, bodyText) { let query; try { query = Object.fromEntries(new URL(url, location.origin).searchParams); } catch { query = {}; } return { url, method, request: { body: bodyText ?? null, query, headers: { ...headers }, raw }, execute: createExecuteWrapper(() => { }) }; } function attachResponseToContext(context, resHeaders, bodyText, rawRes) { context.response = { body: bodyText, headers: { ...resHeaders }, raw: rawRes }; } function hijackXHR() { if (__originalXHR__) return; __originalXHR__ = global.XMLHttpRequest; const proto = __originalXHR__.prototype; const origOpen = proto.open, origSend = proto.send; proto.open = function (method, url, ...rest) { this.__reqMeta = { method, url, reqHeaders: new Map() }; const origSetHeader = this.setRequestHeader; this.setRequestHeader = function (k, v) { this.__reqMeta.reqHeaders.set(k, v); return origSetHeader.call(this, k, v); }; return origOpen.call(this, method, url, ...rest); }; proto.send = async function (...args) { const meta = this.__reqMeta; const matchedInterceptors = Array.from(__requestInterceptors__.values()) .filter(item => matchUrl(meta.url, item.matcher)); const headersObj = Object.fromEntries(meta.reqHeaders); const bodyText = typeof args[0] === 'string' ? args[0] : null; const contexts = matchedInterceptors.map(inter => ({ interceptor: inter, context: createContext(meta.url, meta.method, headersObj, this, bodyText) })); // 执行 before for (const { interceptor, context } of contexts) { await awaitHookExecution(interceptor.before, context); } // 保存上下文 this.__interceptorContexts = contexts; origSend.call(this, ...args); this.addEventListener('loadend', async () => { try { const saved = this.__interceptorContexts; if (!saved) return; const resHeadersStr = this.getAllResponseHeaders(); const resHeadersObj = {}; resHeadersStr.split('\r\n').forEach(line => { const p = line.split(': '); if (p.length === 2) resHeadersObj[p[0]] = p[1]; }); const resBody = this.responseText; for (const { interceptor, context } of saved) { attachResponseToContext(context, resHeadersObj, resBody, this); await awaitHookExecution(interceptor.after, context); // 处理 once 自动移除 if (interceptor.options.once) { try { requestInterceptor.remove(interceptor.id); } catch (e) { } } } } catch (err) { console.error('[RequestInterceptor] XHR after 钩子异常:', err); } }); }; } function hijackFetch() { if (__originalFetch__) return; __originalFetch__ = global.fetch; const origFetch = __originalFetch__; global.fetch = async function (input, init) { let urlStr, request; if (typeof input === 'string') { urlStr = input; request = new Request(input, init); } else if (input instanceof Request) { urlStr = input.url; request = input; } else if (input instanceof URL) { urlStr = input.toString(); request = new Request(input, init); } else { urlStr = String(input); request = new Request(urlStr, init); } const matchedInterceptors = Array.from(__requestInterceptors__.values()) .filter(item => matchUrl(urlStr, item.matcher)); const headersObj = Object.fromEntries(request.headers); const bodyText = typeof init?.body === 'string' ? init.body : null; const contexts = matchedInterceptors.map(inter => ({ interceptor: inter, context: createContext(urlStr, request.method, headersObj, request, bodyText) })); // 执行 before for (const { interceptor, context } of contexts) { await awaitHookExecution(interceptor.before, context); } let response; try { response = await origFetch.call(global, input, init); } catch (err) { // 网络错误,仍然移除 once 拦截器 for (const { interceptor } of contexts) { if (interceptor.options.once) { try { requestInterceptor.remove(interceptor.id); } catch (e) { } } } console.error(err); throw err; } // 响应成功,执行 after try { const cloneRes = response.clone(); const resBody = await cloneRes.text(); const resHeadersObj = Object.fromEntries(response.headers); for (const { interceptor, context } of contexts) { attachResponseToContext(context, resHeadersObj, resBody, response); await awaitHookExecution(interceptor.after, context); if (interceptor.options.once) { try { requestInterceptor.remove(interceptor.id); } catch (e) { } } } } catch (err) { console.error('[RequestInterceptor] fetch after 钩子异常:', err); } return response; }; } function requestInterceptor(urlMatcher, hooks, options = {}) { if (!__originalXHR__) hijackXHR(); if (!__originalFetch__) hijackFetch(); const id = interceptorIdCounter++; __requestInterceptors__.set(id, { id, matcher: urlMatcher, before: hooks.before, after: hooks.after, options }); return id; } requestInterceptor.remove = id => __requestInterceptors__.delete(id); requestInterceptor.clearAll = () => { if (__originalXHR__) { const proto = __originalXHR__.prototype; delete proto.open; delete proto.send; __originalXHR__ = null; } if (__originalFetch__) { global.fetch = __originalFetch__; __originalFetch__ = null; } __requestInterceptors__.clear(); interceptorIdCounter = 0; }; global.requestInterceptor = requestInterceptor; })(window); (function () { 'use strict'; console.log('[豆包脚本] 请求拦截器已启动'); const btn = document.createElement("button"); btn.id = "doubao-delete-btn"; btn.title = "批量删除豆包聊天记录"; // 设置按钮内部图标(这里使用一个垃圾桶 SVG 图标,可换成任意内容) btn.innerHTML = ` `; // 按钮样式(固定定位在右上角) Object.assign(btn.style, { position: "fixed", top: "86px", right: "16px", zIndex: "999999", width: "44px", height: "44px", borderRadius: "50%", border: "none", backgroundColor: "#ff4d4f", color: "white", cursor: "pointer", display: "flex", alignItems: "center", justifyContent: "center", boxShadow: "0 2px 8px rgba(0,0,0,0.2)", transition: "background-color 0.2s", }); // 鼠标悬停效果 btn.addEventListener("mouseenter", () => { btn.style.backgroundColor = "#ff7875"; }); btn.addEventListener("mouseleave", () => { btn.style.backgroundColor = "#ff4d4f"; }); let id = null; // 示例 1:拦截接口 A,before 添加属性,after 读取 requestInterceptor("/im/chain/recent_conv", { after: (ctx) => { id = JSON.parse(ctx.request.body).sequence_id; btn.addEventListener("click", () => { fetch("https://www.doubao.com/im/chain/recent_conv?version_code=20800&language=zh&device_platform=web&aid=497858&real_aid=497858&pkg_type=release_version&device_id=7651059433204729395&pc_version=3.22.5&web_id=7651059466390341129&tea_uuid=7651059466390341129®ion=CN&sys_region=CN&samantha_web=1&web_platform=browser&use-olympus-account=1&web_tab_id=13a488c4-144b-433d-a394-cbdd26f1300e", { "headers": { "accept": "application/json, text/plain, */*", "accept-language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6", "agw-js-conv": "str", "content-type": "application/json; encoding=utf-8", "priority": "u=1, i", "sec-ch-ua": "\"Microsoft Edge\";v=\"149\", \"Chromium\";v=\"149\", \"Not)A;Brand\";v=\"24\"", "sec-ch-ua-mobile": "?0", "sec-ch-ua-platform": "\"Windows\"", "sec-fetch-dest": "empty", "sec-fetch-mode": "cors", "sec-fetch-site": "same-origin" }, "referrer": "https://www.doubao.com/", "body": JSON.stringify({ "cmd": 3200, "uplink_body": { "pull_recent_conv_chain_uplink_body": { "limit": 20, "message_count_per_conv": 10, "api_version": 1, "conv_version": 0, "direction": 3, "option": { "not_need_message": true, "need_complete_conversation": true, "need_coco_conversation": true, "need_coco_bot": true, "need_pc_pin_chain": true, "pc_pin_query_type": 0 } } }, "sequence_id": id, "channel": 2, "version": "1" }), "method": "POST", "mode": "cors", "credentials": "include" }).then(rs => rs.json()) .then(rs => { const cellIds = rs.downlink_body.pull_recent_conv_chain_downlink_body.cells.map(e => e.id); id = rs.sequence_id; fetch("https://www.doubao.com/im/conversation/batch_del_user_conv?version_code=20800&language=zh&device_platform=web&aid=497858&real_aid=497858&pkg_type=release_version&device_id=7651059433204729395&pc_version=3.22.5&web_id=7651059466390341129&tea_uuid=7651059466390341129®ion=CN&sys_region=CN&samantha_web=1&web_platform=browser&use-olympus-account=1&web_tab_id=13a488c4-144b-433d-a394-cbdd26f1300e", { "headers": { "accept": "application/json, text/plain, */*", "accept-language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6", "agw-js-conv": "str", "content-type": "application/json; encoding=utf-8", "priority": "u=1, i", "sec-ch-ua": "\"Microsoft Edge\";v=\"149\", \"Chromium\";v=\"149\", \"Not)A;Brand\";v=\"24\"", "sec-ch-ua-mobile": "?0", "sec-ch-ua-platform": "\"Windows\"", "sec-fetch-dest": "empty", "sec-fetch-mode": "cors", "sec-fetch-site": "same-origin" }, "referrer": "https://www.doubao.com", "body": JSON.stringify({ "cmd": 4171, "uplink_body": { "batch_delete_user_conversation_uplink_body": { "conversation_id": cellIds, "delete_all": false, "conversation_type": 3 } }, "sequence_id": id, "channel": 2, "version": "1" }), "method": "POST", "mode": "cors", "credentials": "include" }).then(rs => rs.json()) .then(rs => { id = rs.sequence_id; }); }); }); } }); document.body.appendChild(btn); // // 示例 2:拦截同一接口,两个拦截器 context 互不影响 // requestInterceptor("/api/v0/client/settings", { // before: (ctx) => { // console.log('B before', ctx.url); // ctx.otherData = 123; // }, // after: (ctx) => { // console.log('B after', ctx.otherData); // 拿到 123 // console.log('B after 尝试读取 A 的数据:', ctx.myData); // undefined // } // }); })();