油猴脚本实现自动分组
This commit is contained in:
509
油猴脚本.js
Normal file
509
油猴脚本.js
Normal file
@@ -0,0 +1,509 @@
|
||||
// ==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
|
||||
// @connect ark.cn-beijing.volces.com
|
||||
// @connect *.volces.com
|
||||
// @connect volces.com
|
||||
// ==/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 PROMPT_CACHE_KEY = 'bili_ai_custom_prompt_v1';
|
||||
// ==========================================
|
||||
|
||||
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 += `<div style="margin-top:4px;">➜ ${msg}</div>`;
|
||||
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();
|
||||
}
|
||||
|
||||
function withNoCache(url) {
|
||||
const sep = url.includes('?') ? '&' : '?';
|
||||
return `${url}${sep}_ts=${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
||||
}
|
||||
|
||||
function fetchJsonNoCache(url, options = {}) {
|
||||
const headers = {
|
||||
...(options.headers || {}),
|
||||
'Cache-Control': 'no-cache',
|
||||
'Pragma': 'no-cache'
|
||||
};
|
||||
return fetch(withNoCache(url), {
|
||||
...options,
|
||||
cache: 'no-store',
|
||||
headers
|
||||
}).then(r => r.json());
|
||||
}
|
||||
|
||||
// 获取已有收藏夹列表 (过滤掉默认收藏夹)
|
||||
async function getMyFolders(biliData) {
|
||||
const url = `https://api.bilibili.com/x/v3/fav/folder/created/list-all?up_mid=${biliData.mid}`;
|
||||
const res = await fetchJsonNoCache(url, { credentials: 'include' });
|
||||
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);
|
||||
}
|
||||
|
||||
function runConnectivityCheck() {
|
||||
const checkBtn = document.getElementById('ai-check-btn');
|
||||
if (checkBtn) {
|
||||
checkBtn.disabled = true;
|
||||
checkBtn.style.background = '#bbb';
|
||||
checkBtn.innerText = '⏳ 自检中...';
|
||||
}
|
||||
|
||||
const finish = () => {
|
||||
if (checkBtn) {
|
||||
checkBtn.disabled = false;
|
||||
checkBtn.style.background = '#00a1d6';
|
||||
checkBtn.innerText = '🔍 连通性自检';
|
||||
}
|
||||
};
|
||||
|
||||
if (!API_KEY || !API_KEY.trim()) {
|
||||
logStatus('❌ 自检失败:API Key 为空');
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
logStatus('🧪 开始连通性自检:发送最小测试请求...');
|
||||
|
||||
GM_xmlhttpRequest({
|
||||
method: 'POST',
|
||||
url: API_URL,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': getAuthHeader(API_KEY)
|
||||
},
|
||||
data: JSON.stringify({
|
||||
model: MODEL_NAME,
|
||||
messages: [{ role: 'user', content: 'ping' }],
|
||||
temperature: 0,
|
||||
max_tokens: 8
|
||||
}),
|
||||
timeout: 20000,
|
||||
onload: function(response) {
|
||||
const bodyText = (response && response.responseText) ? response.responseText : '';
|
||||
if (response.status === 200) {
|
||||
logStatus('✅ 自检通过:模型接口可连通,鉴权与模型参数基本可用');
|
||||
try {
|
||||
const parsed = JSON.parse(bodyText);
|
||||
const brief = parsed && parsed.choices && parsed.choices[0] && parsed.choices[0].message
|
||||
? parsed.choices[0].message.content
|
||||
: '';
|
||||
if (brief) logStatus(`🧪 模型返回片段:${String(brief).slice(0, 40)}`);
|
||||
} catch (e) {
|
||||
// 自检只做可用性判断,解析失败不影响网络连通结论
|
||||
}
|
||||
} else {
|
||||
logStatus(`❌ 自检失败:HTTP ${response.status}`);
|
||||
logStatus(`🧾 响应片段:${String(bodyText).slice(0, 160) || '空响应体'}`);
|
||||
logStatus('🔎 若提示 model 无效,请把 MODEL_NAME 换成你在火山方舟创建的 endpoint/model 标识');
|
||||
}
|
||||
console.log('ConnectivityCheck onload:', response);
|
||||
finish();
|
||||
},
|
||||
onerror: function(err) {
|
||||
const errMsg = [
|
||||
`status=${err && typeof err.status !== 'undefined' ? err.status : 'unknown'}`,
|
||||
`readyState=${err && typeof err.readyState !== 'undefined' ? err.readyState : 'unknown'}`,
|
||||
`finalUrl=${err && err.finalUrl ? err.finalUrl : API_URL}`
|
||||
].join(' | ');
|
||||
logStatus(`❌ 自检失败(网络层):${errMsg}`);
|
||||
logStatus('🔎 网络层失败通常是代理/防火墙/扩展拦截,不是模型业务错误');
|
||||
console.error('ConnectivityCheck onerror:', err);
|
||||
finish();
|
||||
},
|
||||
ontimeout: function() {
|
||||
logStatus('❌ 自检超时(20s):网络不稳定或请求被拦截');
|
||||
finish();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function splitIntoChunks(arr, chunkSize) {
|
||||
const chunks = [];
|
||||
for (let i = 0; i < arr.length; i += chunkSize) {
|
||||
chunks.push(arr.slice(i, i + chunkSize));
|
||||
}
|
||||
return chunks;
|
||||
}
|
||||
|
||||
function buildAnalysisPrompt(videos, existingFolderNames, userRequirement) {
|
||||
const customRuleText = userRequirement
|
||||
? `\n\n【⭐⭐⭐用户特殊需求 (最高优先级)⭐⭐⭐】\n用户的特别指示是:"${userRequirement}"\n请你务必听从!\n⚠️ 致命警告:如果用户的指示中提到了要把视频放入某个分类,请你务必在上面的【已有收藏夹】列表里寻找最匹配的准确名称!如果用户打字简写了(比如用户说“音乐”,但已有的是“我的音乐”),你必须输出已有收藏夹的完整名称“我的音乐”,绝不允许凭空新建近义词分类!`
|
||||
: '';
|
||||
|
||||
return `你是一个逻辑极其严密的文件整理专家。我现在需要你帮我把一批 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(videos)}`;
|
||||
}
|
||||
|
||||
function requestAiClassification(prompt, maxRetryCount) {
|
||||
const requestData = JSON.stringify({
|
||||
model: MODEL_NAME,
|
||||
messages: [{role: 'user', content: prompt}],
|
||||
temperature: 0.1
|
||||
});
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let retried = 0;
|
||||
|
||||
const send = () => {
|
||||
GM_xmlhttpRequest({
|
||||
method: 'POST',
|
||||
url: API_URL,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': getAuthHeader(API_KEY)
|
||||
},
|
||||
data: requestData,
|
||||
timeout: 60000,
|
||||
onload: function(response) {
|
||||
if (response.status !== 200) {
|
||||
reject(new Error(`HTTP ${response.status}: ${response.responseText || ''}`));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const raw = JSON.parse(response.responseText);
|
||||
let content = raw.choices[0].message.content;
|
||||
content = content.replace(/```json/g, '').replace(/```/g, '').trim();
|
||||
const parsed = JSON.parse(content);
|
||||
resolve(parsed);
|
||||
} catch (e) {
|
||||
reject(new Error(`模型返回解析失败: ${e && e.message ? e.message : e}`));
|
||||
}
|
||||
},
|
||||
onerror: function(err) {
|
||||
const statusVal = err && typeof err.status !== 'undefined' ? err.status : 'unknown';
|
||||
if ((statusVal === 0 || statusVal === '0') && retried < maxRetryCount) {
|
||||
retried++;
|
||||
setTimeout(send, 1500 * retried);
|
||||
return;
|
||||
}
|
||||
|
||||
reject(new Error([
|
||||
`status=${statusVal}`,
|
||||
`readyState=${err && typeof err.readyState !== 'undefined' ? err.readyState : 'unknown'}`,
|
||||
`finalUrl=${err && err.finalUrl ? err.finalUrl : API_URL}`
|
||||
].join(' | ')));
|
||||
},
|
||||
ontimeout: function() {
|
||||
if (retried < maxRetryCount) {
|
||||
retried++;
|
||||
setTimeout(send, 1500 * retried);
|
||||
return;
|
||||
}
|
||||
reject(new Error('请求超时(60s)'));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
send();
|
||||
});
|
||||
}
|
||||
|
||||
async function startProcess() {
|
||||
const biliData = getBiliData();
|
||||
const btn = document.getElementById('ai-start-btn');
|
||||
const customPromptInput = document.getElementById('ai-custom-prompt');
|
||||
|
||||
if (!API_KEY || !API_KEY.trim()) {
|
||||
alert("请先在脚本配置区填写火山方舟 API Key!");
|
||||
return;
|
||||
}
|
||||
|
||||
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 fetchJsonNoCache(listUrl, { credentials: 'include' });
|
||||
|
||||
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 batchSize = 30;
|
||||
const batches = splitIntoChunks(videoDataForAI, batchSize);
|
||||
const mergedCategories = {};
|
||||
const seenVideoSet = new Set();
|
||||
|
||||
for (let i = 0; i < batches.length; i++) {
|
||||
const batchVideos = batches[i];
|
||||
const percent = Math.floor(((i + 1) / batches.length) * 100);
|
||||
logStatus(`🧩 分析进度:第 ${i + 1}/${batches.length} 批(${batchVideos.length} 条,${percent}%)`);
|
||||
|
||||
const prompt = buildAnalysisPrompt(batchVideos, existingFolderNames, userRequirement);
|
||||
let aiResult;
|
||||
try {
|
||||
aiResult = await requestAiClassification(prompt, 2);
|
||||
} catch (err) {
|
||||
logStatus(`❌ 第 ${i + 1} 批分析失败:${err && err.message ? err.message : err}`);
|
||||
logStatus('🔎 排查建议:1) 保持网络稳定 2) 点击“连通性自检”确认服务可达 3) 重新点击开始继续');
|
||||
resetButton(btn);
|
||||
return;
|
||||
}
|
||||
|
||||
const categories = aiResult && aiResult.categories ? aiResult.categories : {};
|
||||
for (const [categoryName, vids] of Object.entries(categories)) {
|
||||
if (!Array.isArray(vids) || vids.length === 0) continue;
|
||||
if (!mergedCategories[categoryName]) mergedCategories[categoryName] = [];
|
||||
|
||||
for (const v of vids) {
|
||||
if (!v || typeof v.id === 'undefined' || typeof v.type === 'undefined') continue;
|
||||
const uniqKey = `${v.id}:${v.type}`;
|
||||
if (seenVideoSet.has(uniqKey)) continue;
|
||||
seenVideoSet.add(uniqKey);
|
||||
mergedCategories[categoryName].push({ id: v.id, type: v.type });
|
||||
}
|
||||
}
|
||||
|
||||
await sleep(500);
|
||||
}
|
||||
|
||||
console.log('💡 AI 分析结果(合并后):\n', mergedCategories);
|
||||
logStatus('💡 思考完毕!开始执行移动...');
|
||||
|
||||
let processedCount = 0;
|
||||
for (const [categoryName, vids] of Object.entries(mergedCategories)) {
|
||||
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} 个视频。请刷新页面!`);
|
||||
logStatus('ℹ️ 页面数据可能有缓存延迟,如列表未即时变化可稍后手动刷新;无需强制刷新即可继续下一轮整理。');
|
||||
btn.innerText = '✅ 已完成,可继续整理';
|
||||
btn.style.background = '#4CAF50';
|
||||
btn.disabled = false;
|
||||
btn.onclick = startProcess;
|
||||
|
||||
} 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 = '🤖<br>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 = `
|
||||
<div style="background:#fb7299; color:#fff; padding:12px 15px; font-weight:bold; font-size:15px; display:flex; justify-content:space-between; align-items:center;">
|
||||
<span>🤖 AI 收藏夹整理助理</span>
|
||||
<span id="ai-close-btn" style="cursor:pointer; font-size:18px; line-height:1;">×</span>
|
||||
</div>
|
||||
<div style="background:#fff; padding:15px; border:1px solid #eee; border-top:none;">
|
||||
<p style="margin:0 0 8px 0; font-size:13px; color:#555;">有什么特定的整理要求吗?(选填)</p>
|
||||
<textarea id="ai-custom-prompt" placeholder="例如:\n- 把所有 Vue 相关的放一个文件夹\n- 把时长超过1小时的单独拎出来\n(不填则优先放入已有收藏夹,并由 AI 自由发挥补充分类)" style="width:100%; height:80px; padding:8px; box-sizing:border-box; border:1px solid #ddd; border-radius:6px; font-size:13px; resize:none; margin-bottom:12px; outline:none;"></textarea>
|
||||
<button id="ai-start-btn" style="width:100%; padding:10px; background:#fb7299; color:white; border:none; border-radius:6px; font-size:14px; font-weight:bold; cursor:pointer; transition:background 0.2s;">🚀 开始深度整理</button>
|
||||
<button id="ai-check-btn" style="width:100%; padding:10px; margin-top:8px; background:#00a1d6; color:white; border:none; border-radius:6px; font-size:14px; font-weight:bold; cursor:pointer; transition:background 0.2s;">🔍 连通性自检</button>
|
||||
<div id="ai-status-log" style="margin-top:15px; background:#f4f4f4; padding:8px; border-radius:6px; font-size:12px; color:#333; height:120px; overflow-y:auto; word-break:break-all;">
|
||||
等待指令...
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
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;
|
||||
document.getElementById('ai-check-btn').onclick = runConnectivityCheck;
|
||||
|
||||
const promptInput = document.getElementById('ai-custom-prompt');
|
||||
const cachedPrompt = localStorage.getItem(PROMPT_CACHE_KEY);
|
||||
if (cachedPrompt) {
|
||||
promptInput.value = cachedPrompt;
|
||||
}
|
||||
promptInput.addEventListener('input', () => {
|
||||
localStorage.setItem(PROMPT_CACHE_KEY, promptInput.value);
|
||||
});
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initUI);
|
||||
} else {
|
||||
initUI();
|
||||
}
|
||||
|
||||
})();
|
||||
Reference in New Issue
Block a user