// ==UserScript==
// @name B站 AI 收藏夹自动细化整理 (V8.2 火山方舟版)
// @namespace http://tampermonkey.net/
// @version 8.2.0
// @description 使用火山方舟大模型接口,强力约束优先匹配已有收藏夹,听从对话框指令
// @author 某不知名的根号三 & Gemini
// @match https://space.bilibili.com/*
// @grant GM_xmlhttpRequest
// ==/UserScript==
(function() {
'use strict';
// ================= 配置区 =================
// 火山方舟 Chat Completions 接口
const API_URL = 'https://ark.cn-beijing.volces.com/api/v3/chat/completions';
// 你的火山方舟 API Key(支持填写原始 Key 或 Bearer 前缀格式)
const API_KEY = 'ffcead97-0c80-4ff7-8b71-ee255a512f07';
// 火山方舟模型名称(示例:deepseek-v3-1-terminus)
const MODEL_NAME = 'deepseek-v3-1-terminus';
// ==========================================
const sleep = ms => new Promise(r => setTimeout(r, ms));
function getAuthHeader(apiKey) {
const key = (apiKey || '').trim();
if (!key) return '';
return /^Bearer\s+/i.test(key) ? key : `Bearer ${key}`;
}
// 状态日志打印小工具
function logStatus(msg) {
console.log(msg);
const logDiv = document.getElementById('ai-status-log');
if (logDiv) {
logDiv.innerHTML += `
➜ ${msg}
`;
logDiv.scrollTop = logDiv.scrollHeight;
}
}
function getBiliData() {
const midMatch = document.cookie.match(/DedeUserID=([^;]+)/);
const csrfMatch = document.cookie.match(/bili_jct=([^;]+)/);
return { mid: midMatch ? midMatch[1] : '', csrf: csrfMatch ? csrfMatch[1] : '' };
}
function getSourceMediaId() {
const params = new URLSearchParams(window.location.search);
return params.get('fid') || params.get('media_id') || params.get('id');
}
function buildFormData(obj) {
return new URLSearchParams(obj).toString();
}
// 获取已有收藏夹列表 (过滤掉默认收藏夹)
async function getMyFolders(biliData) {
const url = `https://api.bilibili.com/x/v3/fav/folder/created/list-all?up_mid=${biliData.mid}`;
const res = await fetch(url, { credentials: 'include' }).then(r => r.json());
if (res.code === 0 && res.data && res.data.list) {
const folderMap = {};
res.data.list.forEach(f => {
if (f.title !== '默认收藏夹') folderMap[f.title] = f.id;
});
return folderMap;
}
return {};
}
async function createFolder(title, biliData) {
logStatus(`📁 正在新建收藏夹:【${title}】`);
const url = 'https://api.bilibili.com/x/v3/fav/folder/add';
const data = buildFormData({ title: title, privacy: 1, csrf: biliData.csrf });
const res = await fetch(url, {
method: 'POST', credentials: 'include',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: data
}).then(r => r.json());
if (res.code === 0) return res.data.id;
throw new Error(`新建失败: ${res.message}`);
}
async function moveVideos(sourceMediaId, tarMediaId, resourcesStr, biliData) {
const url = 'https://api.bilibili.com/x/v3/fav/resource/move';
const data = buildFormData({
src_media_id: sourceMediaId, tar_media_id: tarMediaId, mid: biliData.mid, resources: resourcesStr, csrf: biliData.csrf
});
const res = await fetch(url, {
method: 'POST', credentials: 'include',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: data
}).then(r => r.json());
if (res.code !== 0) console.error("移动失败:", res.message);
}
async function startProcess() {
const biliData = getBiliData();
const btn = document.getElementById('ai-start-btn');
const customPromptInput = document.getElementById('ai-custom-prompt');
if (!biliData.mid || !biliData.csrf) return alert("请确保你在 B 站已登录!");
const sourceMediaId = getSourceMediaId();
if (!sourceMediaId) return alert("未能识别当前页面的收藏夹 ID!请确保你在某个具体的收藏夹页面内。");
const userRequirement = customPromptInput.value.trim();
btn.innerText = '🔄 整理中,请看下方日志...';
btn.disabled = true;
btn.style.background = '#ccc';
document.getElementById('ai-status-log').innerHTML = '';
try {
// 1. 获取现有的“家底”
logStatus(`正在获取现有的收藏夹列表...`);
const existingFoldersMap = await getMyFolders(biliData);
const existingFolderNames = Object.keys(existingFoldersMap);
logStatus(`📦 发现 ${existingFolderNames.length} 个已有收藏夹`);
// 2. 全量抓取视频
logStatus(`开始全量抓取当前收藏夹视频...`);
let allVideos = [];
let pn = 1;
const ps = 20;
while (true) {
logStatus(`正在读取第 ${pn} 页...`);
const listUrl = `https://api.bilibili.com/x/v3/fav/resource/list?media_id=${sourceMediaId}&pn=${pn}&ps=${ps}&platform=web`;
const listRes = await fetch(listUrl, { credentials: 'include' }).then(r => r.json());
if (listRes.code !== 0) {
logStatus(`❌ 读取出错: ${listRes.message}`);
break;
}
const videos = (listRes.data && listRes.data.medias) ? listRes.data.medias : [];
if (videos.length === 0) break;
allVideos.push(...videos);
if (videos.length < ps) break;
pn++;
await sleep(300);
}
if (allVideos.length === 0) {
logStatus("⚠️ 当前收藏夹是空的!");
resetButton(btn);
return;
}
logStatus(`✅ 读取完毕!共获取到 ${allVideos.length} 个视频。`);
logStatus(`🧠 正在呼叫 ${MODEL_NAME} 进行匹配与思考,请耐心等待...`);
const videoDataForAI = allVideos.map(v => ({
id: v.id, type: v.type, title: v.title, intro: v.intro ? v.intro.substring(0, 30) : ''
}));
// ====== 强化版:动态注入存量数据和用户需求 ======
const customRuleText = userRequirement ? `\n\n【⭐⭐⭐用户特殊需求 (最高优先级)⭐⭐⭐】\n用户的特别指示是:"${userRequirement}"\n请你务必听从!\n⚠️ 致命警告:如果用户的指示中提到了要把视频放入某个分类,请你务必在上面的【已有收藏夹】列表里寻找最匹配的准确名称!如果用户打字简写了(比如用户说“音乐”,但已有的是“我的音乐”),你必须输出已有收藏夹的完整名称“我的音乐”,绝不允许凭空新建近义词分类!` : '';
const combinedPrompt = `你是一个逻辑极其严密的文件整理专家。我现在需要你帮我把一批 B 站视频分类。
非常重要:用户目前已经建好了以下这些收藏夹:
[ ${existingFolderNames.length > 0 ? existingFolderNames.join(', ') : '暂无'} ]
请你严格按照以下 3 个步骤执行:
【步骤 1:存量强制匹配】
通读所有视频。只要视频内容沾边,就必须一字不差地使用上述【已有收藏夹】的名称作为分类键名。
【步骤 2:谨慎新建】
只有当某几个视频确实与所有“已有收藏夹”都毫不相干时,你才可以创建一个新的涵盖面广的“大类”。绝不为单一视频建新分类,孤立视频请塞入最贴近的已有分类。
【步骤 3:绝无遗漏】
确保列表中的**每一个视频**都被分配到了具体的分类中,绝对不可以遗漏任何一个 ID!${customRuleText}
请严格输出合法的纯 JSON 格式数据。包含 "thoughts" 和 "categories" 两个字段。
示例:
{
"thoughts": "分析发现,视频A符合已有的'游戏实况'分类。用户要求把XX放入YY,我发现已有收藏夹中叫'YY合集',因此我将它们放入'YY合集'中...",
"categories": {
"已有收藏夹准确名字1": [{"id": 111, "type": 2}],
"新创建的大类名字": [{"id": 222, "type": 2}]
}
}
以下是待处理的所有视频:
${JSON.stringify(videoDataForAI)}`;
GM_xmlhttpRequest({
method: "POST",
url: API_URL,
headers: {
"Content-Type": "application/json",
"Authorization": getAuthHeader(API_KEY)
},
data: JSON.stringify({
model: MODEL_NAME,
messages: [{role: "user", content: combinedPrompt}],
temperature: 0.1 // 保持极低的温度,确保严谨匹配,不乱造词
}),
onload: async function(response) {
if(response.status !== 200) {
logStatus(`❌ API报错:${response.status}`);
console.error(response.responseText);
resetButton(btn);
return;
}
let content = JSON.parse(response.responseText).choices[0].message.content;
content = content.replace(/```json/g, '').replace(/```/g, '').trim();
const aiResult = JSON.parse(content);
console.log("💡 AI 思考过程:\n", aiResult.thoughts);
logStatus(`💡 思考完毕!(按F12可看AI脑洞)`);
let processedCount = 0;
for (const [categoryName, vids] of Object.entries(aiResult.categories)) {
if(!vids || vids.length === 0) continue;
let targetFolderId = existingFoldersMap[categoryName];
if (!targetFolderId) {
targetFolderId = await createFolder(categoryName, biliData);
existingFoldersMap[categoryName] = targetFolderId;
await sleep(1000);
}
logStatus(`🚚 正将 ${vids.length} 个视频移入【${categoryName}】...`);
const resourcesStr = vids.map(v => `${v.id}:${v.type}`).join(',');
await moveVideos(sourceMediaId, targetFolderId, resourcesStr, biliData);
processedCount += vids.length;
await sleep(500);
}
logStatus(`🎉 整理完成!共处理了 ${processedCount} 个视频。请刷新页面!`);
btn.innerText = '✅ 整理完成,点我重置';
btn.style.background = '#4CAF50';
btn.disabled = false;
btn.onclick = () => window.location.reload();
},
onerror: function(err) {
logStatus(`❌ 请求大模型失败,请检查网络或 API Key`);
console.error(err);
resetButton(btn);
}
});
} catch (error) {
logStatus(`❌ 发生未知错误,请看 F12 控制台`);
console.error(error);
resetButton(btn);
}
}
function resetButton(btn) {
btn.innerText = '🚀 开始深度整理';
btn.style.background = '#fb7299';
btn.disabled = false;
btn.onclick = startProcess;
}
// ================= UI 构建区 =================
function initUI() {
if (document.getElementById('ai-sort-wrapper')) return;
const floatBtn = document.createElement('div');
floatBtn.id = 'ai-float-btn';
floatBtn.innerHTML = '🤖
AI整理';
floatBtn.style.cssText = 'position:fixed; bottom:30px; left:30px; z-index:9999; background:#fb7299; color:white; width:50px; height:50px; border-radius:25px; display:flex; align-items:center; justify-content:center; text-align:center; font-size:12px; font-weight:bold; cursor:pointer; box-shadow: 0 4px 10px rgba(251, 114, 153, 0.4); transition:0.3s;';
const panel = document.createElement('div');
panel.id = 'ai-sort-wrapper';
panel.style.cssText = 'position:fixed; bottom:30px; left:30px; z-index:10000; width:320px; display:none; flex-direction:column; box-shadow: 0 5px 20px rgba(0,0,0,0.2); border-radius:10px; overflow:hidden; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;';
panel.innerHTML = `
🤖 AI 收藏夹整理助理
×
有什么特定的整理要求吗?(选填)
等待指令...
`;
document.body.appendChild(floatBtn);
document.body.appendChild(panel);
floatBtn.onclick = () => {
floatBtn.style.display = 'none';
panel.style.display = 'flex';
};
document.getElementById('ai-close-btn').onclick = () => {
panel.style.display = 'none';
floatBtn.style.display = 'flex';
};
document.getElementById('ai-start-btn').onclick = startProcess;
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initUI);
} else {
initUI();
}
})();