Files
LittleWhiteBox/modules/scheduled-tasks/scheduled-tasks.js

2171 lines
99 KiB
JavaScript
Raw Normal View History

2026-01-17 16:34:39 +08:00
// ═══════════════════════════════════════════════════════════════════════════
// 导入
// ═══════════════════════════════════════════════════════════════════════════
import { extension_settings, getContext, writeExtensionField } from "../../../../../extensions.js";
import { saveSettingsDebounced, characters, this_chid, chat, callPopup } from "../../../../../../script.js";
import { getPresetManager } from "../../../../../preset-manager.js";
import { oai_settings } from "../../../../../openai.js";
import { SlashCommandParser } from "../../../../../slash-commands/SlashCommandParser.js";
import { SlashCommand } from "../../../../../slash-commands/SlashCommand.js";
import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from "../../../../../slash-commands/SlashCommandArgument.js";
import { callGenericPopup, POPUP_TYPE } from "../../../../../popup.js";
import { accountStorage } from "../../../../../util/AccountStorage.js";
import { download, getFileText, uuidv4, debounce, getSortableDelay } from "../../../../../utils.js";
import { executeSlashCommand } from "../../core/slash-command.js";
import { EXT_ID } from "../../core/constants.js";
import { createModuleEvents, event_types } from "../../core/event-manager.js";
import { xbLog, CacheRegistry } from "../../core/debug-core.js";
import { TasksStorage } from "../../core/server-storage.js";
// ═══════════════════════════════════════════════════════════════════════════
// 常量和默认值
// ═══════════════════════════════════════════════════════════════════════════
const TASKS_MODULE_NAME = "xiaobaix-tasks";
const defaultSettings = { enabled: true, globalTasks: [], processedMessages: [], character_allowed_tasks: [] };
const CONFIG = { MAX_PROCESSED: 20, MAX_COOLDOWN: 10, CLEANUP_INTERVAL: 30000, TASK_COOLDOWN: 50 };
const events = createModuleEvents('scheduledTasks');
// ═══════════════════════════════════════════════════════════════════════════
// 数据迁移
// ═══════════════════════════════════════════════════════════════════════════
async function migrateToServerStorage() {
const FLAG = 'LWB_tasks_migrated_server_v1';
if (localStorage.getItem(FLAG)) return;
let count = 0;
const settings = getSettings();
for (const task of (settings.globalTasks || [])) {
if (!task) continue;
if (!task.id) task.id = uuidv4();
if (task.commands) {
await TasksStorage.set(task.id, task.commands);
delete task.commands;
count++;
}
}
if (count > 0) saveSettingsDebounced();
await new Promise((resolve) => {
const req = indexedDB.open('LittleWhiteBox_TaskScripts');
req.onerror = () => resolve();
req.onsuccess = async (e) => {
const db = e.target.result;
if (!db.objectStoreNames.contains('scripts')) {
db.close();
resolve();
return;
}
try {
const tx = db.transaction('scripts', 'readonly');
const store = tx.objectStore('scripts');
const keys = await new Promise(r => {
const req = store.getAllKeys();
req.onsuccess = () => r(req.result || []);
req.onerror = () => r([]);
});
const vals = await new Promise(r => {
const req = store.getAll();
req.onsuccess = () => r(req.result || []);
req.onerror = () => r([]);
});
for (let i = 0; i < keys.length; i++) {
if (keys[i] && vals[i]) {
await TasksStorage.set(keys[i], vals[i]);
count++;
}
}
} catch (err) {
console.warn('[Tasks] IndexedDB 迁移出错:', err);
}
db.close();
indexedDB.deleteDatabase('LittleWhiteBox_TaskScripts');
resolve();
};
});
if (count > 0) {
await TasksStorage.saveNow();
console.log(`[Tasks] 已迁移 ${count} 个脚本到服务器`);
}
localStorage.setItem(FLAG, 'true');
}
// ═══════════════════════════════════════════════════════════════════════════
// 状态
// ═══════════════════════════════════════════════════════════════════════════
let state = {
currentEditingTask: null, currentEditingIndex: -1, currentEditingId: null, currentEditingScope: 'global',
lastChatId: null, chatJustChanged: false,
isNewChat: false, lastTurnCount: 0, executingCount: 0, isCommandGenerated: false,
taskLastExecutionTime: new Map(), cleanupTimer: null, lastTasksHash: '', taskBarVisible: true,
processedMessagesSet: new Set(),
taskBarSignature: '',
floorCounts: { all: 0, user: 0, llm: 0 },
dynamicCallbacks: new Map(),
qrObserver: null,
isUpdatingTaskBar: false,
lastPresetName: ''
};
// ═══════════════════════════════════════════════════════════════════════════
// 工具函数
// ═══════════════════════════════════════════════════════════════════════════
const isAnyTaskExecuting = () => (state.executingCount || 0) > 0;
const isGloballyEnabled = () => (window.isXiaobaixEnabled !== undefined ? window.isXiaobaixEnabled : true) && getSettings().enabled;
const clampInt = (v, min, max, d = 0) => (Number.isFinite(+v) ? Math.max(min, Math.min(max, +v)) : d);
const nowMs = () => Date.now();
const normalizeTiming = (t) => (String(t || '').toLowerCase() === 'initialization' ? 'character_init' : t);
const mapTiming = (task) => ({ ...task, triggerTiming: normalizeTiming(task.triggerTiming) });
const allTasksMeta = () => [
...getSettings().globalTasks.map(mapTiming),
...getCharacterTasks().map(mapTiming),
...getPresetTasks().map(mapTiming)
];
const allTasks = allTasksMeta;
async function allTasksFull() {
const globalMeta = getSettings().globalTasks || [];
const globalTasks = await Promise.all(globalMeta.map(async (task) => ({
...task,
commands: await TasksStorage.get(task.id)
})));
return [
...globalTasks.map(mapTiming),
...getCharacterTasks().map(mapTiming),
...getPresetTasks().map(mapTiming)
];
}
// ═══════════════════════════════════════════════════════════════════════════
// 设置管理
// ═══════════════════════════════════════════════════════════════════════════
function getSettings() {
const ext = extension_settings[EXT_ID] || (extension_settings[EXT_ID] = {});
if (!ext.tasks) ext.tasks = structuredClone(defaultSettings);
const t = ext.tasks;
if (typeof t.enabled !== 'boolean') t.enabled = defaultSettings.enabled;
if (!Array.isArray(t.globalTasks)) t.globalTasks = [];
if (!Array.isArray(t.processedMessages)) t.processedMessages = [];
if (!Array.isArray(t.character_allowed_tasks)) t.character_allowed_tasks = [];
return t;
}
function hydrateProcessedSetFromSettings() {
try {
state.processedMessagesSet = new Set(getSettings().processedMessages || []);
} catch {}
}
function scheduleCleanup() {
if (state.cleanupTimer) return;
state.cleanupTimer = setInterval(() => {
const n = nowMs();
for (const [taskName, lastTime] of state.taskLastExecutionTime.entries()) {
if (n - lastTime > CONFIG.TASK_COOLDOWN * 2) state.taskLastExecutionTime.delete(taskName);
}
if (state.taskLastExecutionTime.size > CONFIG.MAX_COOLDOWN) {
const entries = [...state.taskLastExecutionTime.entries()].sort((a, b) => b[1] - a[1]).slice(0, CONFIG.MAX_COOLDOWN);
state.taskLastExecutionTime.clear();
entries.forEach(([k, v]) => state.taskLastExecutionTime.set(k, v));
}
const settings = getSettings();
if (settings.processedMessages.length > CONFIG.MAX_PROCESSED) {
settings.processedMessages = settings.processedMessages.slice(-CONFIG.MAX_PROCESSED);
state.processedMessagesSet = new Set(settings.processedMessages);
saveSettingsDebounced();
}
}, CONFIG.CLEANUP_INTERVAL);
}
const isTaskInCooldown = (name, t = nowMs()) => {
const last = state.taskLastExecutionTime.get(name);
return last && (t - last) < CONFIG.TASK_COOLDOWN;
};
const setTaskCooldown = (name) => state.taskLastExecutionTime.set(name, nowMs());
const isMessageProcessed = (key) => state.processedMessagesSet.has(key);
function markMessageAsProcessed(key) {
if (state.processedMessagesSet.has(key)) return;
state.processedMessagesSet.add(key);
const settings = getSettings();
settings.processedMessages.push(key);
if (settings.processedMessages.length > CONFIG.MAX_PROCESSED) {
settings.processedMessages = settings.processedMessages.slice(-Math.floor(CONFIG.MAX_PROCESSED / 2));
state.processedMessagesSet = new Set(settings.processedMessages);
}
saveSettingsDebounced();
}
// ═══════════════════════════════════════════════════════════════════════════
// 角色任务
// ═══════════════════════════════════════════════════════════════════════════
function getCharacterTasks() {
if (!this_chid || !characters[this_chid]) return [];
const c = characters[this_chid];
if (!c.data) c.data = {};
if (!c.data.extensions) c.data.extensions = {};
if (!c.data.extensions[TASKS_MODULE_NAME]) c.data.extensions[TASKS_MODULE_NAME] = { tasks: [] };
const list = c.data.extensions[TASKS_MODULE_NAME].tasks;
if (!Array.isArray(list)) c.data.extensions[TASKS_MODULE_NAME].tasks = [];
return c.data.extensions[TASKS_MODULE_NAME].tasks;
}
async function saveCharacterTasks(tasks) {
if (!this_chid || !characters[this_chid]) return;
await writeExtensionField(Number(this_chid), TASKS_MODULE_NAME, { tasks });
try {
if (!characters[this_chid].data) characters[this_chid].data = {};
if (!characters[this_chid].data.extensions) characters[this_chid].data.extensions = {};
if (!characters[this_chid].data.extensions[TASKS_MODULE_NAME]) characters[this_chid].data.extensions[TASKS_MODULE_NAME] = { tasks: [] };
characters[this_chid].data.extensions[TASKS_MODULE_NAME].tasks = tasks;
} catch {}
const settings = getSettings();
const avatar = characters[this_chid].avatar;
if (avatar && !settings.character_allowed_tasks?.includes(avatar)) {
settings.character_allowed_tasks ??= [];
settings.character_allowed_tasks.push(avatar);
saveSettingsDebounced();
}
}
// ═══════════════════════════════════════════════════════════════════════════
// 预设任务
// ═══════════════════════════════════════════════════════════════════════════
const PRESET_TASK_FIELD = 'scheduledTasks';
const PRESET_PROMPT_ORDER_CHARACTER_ID = 100000;
const presetTasksState = { name: '', tasks: [] };
const PresetTasksStore = (() => {
const isPlainObject = (value) => !!value && typeof value === 'object' && !Array.isArray(value);
const deepClone = (value) => {
if (value === undefined) return undefined;
if (typeof structuredClone === 'function') {
try { return structuredClone(value); } catch {}
}
try { return JSON.parse(JSON.stringify(value)); } catch { return value; }
};
const getPresetManagerSafe = () => {
try { return getPresetManager('openai'); } catch { return null; }
};
const getPresetSnapshot = (manager, name) => {
if (!manager || !name) return { source: null, clone: null };
let source = null;
try {
if (typeof manager.getCompletionPresetByName === 'function') {
source = manager.getCompletionPresetByName(name) || null;
}
} catch {}
if (!source) {
try { source = manager.getPresetSettings?.(name) || null; } catch { source = null; }
}
if (!source) return { source: null, clone: null };
return { source, clone: deepClone(source) };
};
const syncTarget = (target, source) => {
if (!target || !source) return;
Object.keys(target).forEach((key) => {
if (!Object.prototype.hasOwnProperty.call(source, key)) delete target[key];
});
Object.assign(target, source);
};
const ensurePromptOrderEntry = (preset, create = false) => {
if (!preset) return null;
if (!Array.isArray(preset.prompt_order)) {
if (!create) return null;
preset.prompt_order = [];
}
let entry = preset.prompt_order.find(item => Number(item?.character_id) === PRESET_PROMPT_ORDER_CHARACTER_ID);
if (!entry && create) {
entry = { character_id: PRESET_PROMPT_ORDER_CHARACTER_ID, order: [] };
preset.prompt_order.push(entry);
}
return entry || null;
};
const currentName = () => {
try { return getPresetManagerSafe()?.getSelectedPresetName?.() || ''; } catch { return ''; }
};
const read = (name) => {
if (!name) return [];
const manager = getPresetManagerSafe();
if (!manager) return [];
const { clone } = getPresetSnapshot(manager, name);
if (!clone) return [];
const entry = ensurePromptOrderEntry(clone, false);
if (!entry || !isPlainObject(entry.xiaobai_ext)) return [];
const tasks = entry.xiaobai_ext[PRESET_TASK_FIELD];
return Array.isArray(tasks) ? deepClone(tasks) : [];
};
const write = async (name, tasks) => {
if (!name) return;
const manager = getPresetManagerSafe();
if (!manager) return;
const { source, clone } = getPresetSnapshot(manager, name);
if (!clone) return;
const shouldCreate = Array.isArray(tasks) && tasks.length > 0;
const entry = ensurePromptOrderEntry(clone, shouldCreate);
if (entry) {
entry.xiaobai_ext = isPlainObject(entry.xiaobai_ext) ? entry.xiaobai_ext : {};
if (shouldCreate) {
entry.xiaobai_ext[PRESET_TASK_FIELD] = deepClone(tasks);
} else {
if (entry.xiaobai_ext) delete entry.xiaobai_ext[PRESET_TASK_FIELD];
if (entry.xiaobai_ext && Object.keys(entry.xiaobai_ext).length === 0) delete entry.xiaobai_ext;
}
}
await manager.savePreset(name, clone, { skipUpdate: true });
syncTarget(source, clone);
const activeName = manager.getSelectedPresetName?.();
if (activeName && activeName === name && Object.prototype.hasOwnProperty.call(clone, 'prompt_order')) {
try { oai_settings.prompt_order = structuredClone(clone.prompt_order); } catch { oai_settings.prompt_order = clone.prompt_order; }
}
};
return { currentName, read, write };
})();
const ensurePresetTaskIds = (tasks) => {
let mutated = false;
tasks?.forEach(task => {
if (task && !task.id) {
task.id = uuidv4();
mutated = true;
}
});
return mutated;
};
function resetPresetTasksCache() {
presetTasksState.name = '';
presetTasksState.tasks = [];
}
function getPresetTasks() {
const name = PresetTasksStore.currentName();
if (!name) {
resetPresetTasksCache();
return presetTasksState.tasks;
}
if (presetTasksState.name !== name || !presetTasksState.tasks.length) {
const loaded = PresetTasksStore.read(name) || [];
ensurePresetTaskIds(loaded);
presetTasksState.name = name;
presetTasksState.tasks = Array.isArray(loaded) ? loaded : [];
}
return presetTasksState.tasks;
}
async function savePresetTasks(tasks) {
const name = PresetTasksStore.currentName();
if (!name) return;
const list = Array.isArray(tasks) ? tasks : [];
ensurePresetTaskIds(list);
presetTasksState.name = name;
presetTasksState.tasks = list;
await PresetTasksStore.write(name, list);
state.lastTasksHash = '';
updatePresetTaskHint();
}
// ═══════════════════════════════════════════════════════════════════════════
// 任务列表操作
// ═══════════════════════════════════════════════════════════════════════════
const getTaskListByScope = (scope) => {
if (scope === 'character') return getCharacterTasks();
if (scope === 'preset') return getPresetTasks();
return getSettings().globalTasks;
};
async function persistTaskListByScope(scope, tasks) {
if (scope === 'character') return await saveCharacterTasks(tasks);
if (scope === 'preset') return await savePresetTasks(tasks);
const metaOnly = [];
for (const task of tasks) {
if (!task) continue;
if (!task.id) task.id = uuidv4();
if (Object.prototype.hasOwnProperty.call(task, 'commands')) {
await TasksStorage.set(task.id, String(task.commands ?? ''));
}
const meta = { ...task };
delete meta.commands;
metaOnly.push(meta);
}
getSettings().globalTasks = metaOnly;
saveSettingsDebounced();
}
async function removeTaskByScope(scope, taskId, fallbackIndex = -1) {
const list = getTaskListByScope(scope);
const idx = taskId ? list.findIndex(t => t?.id === taskId) : fallbackIndex;
if (idx < 0 || idx >= list.length) return;
const task = list[idx];
if (scope === 'global' && task?.id) {
await TasksStorage.delete(task.id);
}
list.splice(idx, 1);
await persistTaskListByScope(scope, [...list]);
}
// ═══════════════════════════════════════════════════════════════════════════
// 任务运行管理
// ═══════════════════════════════════════════════════════════════════════════
const __taskRunMap = new Map();
CacheRegistry.register('scheduledTasks', {
name: '循环任务状态',
getSize: () => {
try {
const a = state.processedMessagesSet?.size || 0;
const b = state.taskLastExecutionTime?.size || 0;
const c = state.dynamicCallbacks?.size || 0;
const d = __taskRunMap.size || 0;
const e = TasksStorage.getCacheSize() || 0;
return a + b + c + d + e;
} catch { return 0; }
},
getBytes: () => {
try {
let total = 0;
const addStr = (v) => { total += String(v ?? '').length * 2; };
const addSet = (s) => { if (!s?.forEach) return; s.forEach(v => addStr(v)); };
const addMap = (m, addValue = null) => {
if (!m?.forEach) return;
m.forEach((v, k) => { addStr(k); if (typeof addValue === 'function') addValue(v); });
};
addSet(state.processedMessagesSet);
addMap(state.taskLastExecutionTime, (v) => addStr(v));
addMap(state.dynamicCallbacks, (entry) => {
addStr(entry?.options?.timing);
addStr(entry?.options?.floorType);
addStr(entry?.options?.interval);
try { addStr(entry?.callback?.toString?.()); } catch {}
});
addMap(__taskRunMap, (entry) => {
addStr(entry?.signature);
total += (entry?.timers?.size || 0) * 8;
total += (entry?.intervals?.size || 0) * 8;
});
total += TasksStorage.getCacheBytes();
return total;
} catch { return 0; }
},
clear: () => {
try {
state.processedMessagesSet?.clear?.();
state.taskLastExecutionTime?.clear?.();
TasksStorage.clearCache();
const s = getSettings();
if (s?.processedMessages) s.processedMessages = [];
saveSettingsDebounced();
} catch {}
try {
for (const [id, entry] of state.dynamicCallbacks.entries()) {
try { entry?.abortController?.abort?.(); } catch {}
state.dynamicCallbacks.delete(id);
}
} catch {}
},
getDetail: () => {
try {
return {
processedMessages: state.processedMessagesSet?.size || 0,
cooldown: state.taskLastExecutionTime?.size || 0,
dynamicCallbacks: state.dynamicCallbacks?.size || 0,
runningSingleInstances: __taskRunMap.size || 0,
scriptCache: TasksStorage.getCacheSize() || 0,
};
} catch { return {}; }
},
});
async function __runTaskSingleInstance(taskName, jsRunner, signature = null) {
const existing = __taskRunMap.get(taskName);
if (existing) {
try { existing.abort?.abort?.(); } catch {}
try { await Promise.resolve(existing.completion).catch(() => {}); } catch {}
__taskRunMap.delete(taskName);
}
const abort = new AbortController();
const timers = new Set();
const intervals = new Set();
const entry = { abort, timers, intervals, signature, completion: null };
__taskRunMap.set(taskName, entry);
const addListener = (target, type, handler, opts = {}) => {
if (!target?.addEventListener) return;
target.addEventListener(type, handler, { ...opts, signal: abort.signal });
};
const setTimeoutSafe = (fn, t, ...a) => {
const id = setTimeout(() => {
timers.delete(id);
try { fn(...a); } catch (e) { console.error(e); }
}, t);
timers.add(id);
return id;
};
const clearTimeoutSafe = (id) => { clearTimeout(id); timers.delete(id); };
const setIntervalSafe = (fn, t, ...a) => {
const id = setInterval(fn, t, ...a);
intervals.add(id);
return id;
};
const clearIntervalSafe = (id) => { clearInterval(id); intervals.delete(id); };
entry.completion = (async () => {
try {
await jsRunner({ addListener, setTimeoutSafe, clearTimeoutSafe, setIntervalSafe, clearIntervalSafe, abortSignal: abort.signal });
} finally {
try { abort.abort(); } catch {}
try {
timers.forEach((id) => clearTimeout(id));
intervals.forEach((id) => clearInterval(id));
} catch {}
try { window?.dispatchEvent?.(new CustomEvent('xiaobaix-task-cleaned', { detail: { taskName, signature } })); } catch {}
__taskRunMap.delete(taskName);
}
})();
return entry.completion;
}
// ═══════════════════════════════════════════════════════════════════════════
// 命令执行
// ═══════════════════════════════════════════════════════════════════════════
async function executeCommands(commands, taskName) {
if (!String(commands || '').trim()) return null;
state.isCommandGenerated = true;
state.executingCount = Math.max(0, (state.executingCount || 0) + 1);
try {
return await processTaskCommands(commands, taskName);
} finally {
setTimeout(() => {
state.executingCount = Math.max(0, (state.executingCount || 0) - 1);
if (!isAnyTaskExecuting()) state.isCommandGenerated = false;
}, 500);
}
}
async function processTaskCommands(commands, taskName) {
const jsTagRegex = /<<taskjs>>([\s\S]*?)<<\/taskjs>>/g;
let lastIndex = 0, result = null, match;
while ((match = jsTagRegex.exec(commands)) !== null) {
const beforeJs = commands.slice(lastIndex, match.index).trim();
if (beforeJs) result = await executeSlashCommand(beforeJs);
const jsCode = match[1].trim();
if (jsCode) {
try { await executeTaskJS(jsCode, taskName || 'AnonymousTask'); }
catch (error) {
console.error(`[任务JS执行错误] ${error.message}`);
try { xbLog.error('scheduledTasks', `taskjs error task=${String(taskName || 'AnonymousTask')}`, error); } catch {}
}
}
lastIndex = match.index + match[0].length;
}
if (lastIndex === 0) {
result = await executeSlashCommand(commands);
} else {
const remaining = commands.slice(lastIndex).trim();
if (remaining) result = await executeSlashCommand(remaining);
}
return result;
}
function __hashStringForKey(str) {
try {
let hash = 5381;
for (let i = 0; i < str.length; i++) {
hash = ((hash << 5) + hash) ^ str.charCodeAt(i);
}
return (hash >>> 0).toString(36);
} catch { return Math.random().toString(36).slice(2); }
}
async function executeTaskJS(jsCode, taskName = 'AnonymousTask') {
const STscript = async (command) => {
if (!command) return { error: "命令为空" };
if (!command.startsWith('/')) command = '/' + command;
return await executeSlashCommand(command);
};
const codeSig = __hashStringForKey(String(jsCode || ''));
const stableKey = (String(taskName || '').trim()) || `js-${codeSig}`;
const isLightTask = stableKey.startsWith('[x]');
const taskContext = {
taskName: String(taskName || 'AnonymousTask'),
stableKey,
codeSig,
log: (msg, extra) => { try { xbLog.info('scheduledTasks', { task: stableKey, msg: String(msg ?? ''), extra }); } catch {} },
warn: (msg, extra) => { try { xbLog.warn('scheduledTasks', { task: stableKey, msg: String(msg ?? ''), extra }); } catch {} },
error: (msg, err, extra) => { try { xbLog.error('scheduledTasks', { task: stableKey, msg: String(msg ?? ''), extra }, err || null); } catch {} }
};
const old = __taskRunMap.get(stableKey);
if (old) {
try { old.abort?.abort?.(); } catch {}
if (!isLightTask) {
try { await Promise.resolve(old.completion).catch(() => {}); } catch {}
}
__taskRunMap.delete(stableKey);
}
const callbackPrefix = `${stableKey}_fl_`;
for (const [id, entry] of state.dynamicCallbacks.entries()) {
if (id.startsWith(callbackPrefix)) {
try { entry?.abortController?.abort(); } catch {}
state.dynamicCallbacks.delete(id);
}
}
const jsRunner = async (utils) => {
const { addListener, setTimeoutSafe, clearTimeoutSafe, setIntervalSafe, clearIntervalSafe, abortSignal } = utils;
const originalWindowFns = {
setTimeout: window.setTimeout,
clearTimeout: window.clearTimeout,
setInterval: window.setInterval,
clearInterval: window.clearInterval,
};
const originals = {
setTimeout: originalWindowFns.setTimeout.bind(window),
clearTimeout: originalWindowFns.clearTimeout.bind(window),
setInterval: originalWindowFns.setInterval.bind(window),
clearInterval: originalWindowFns.clearInterval.bind(window),
addEventListener: EventTarget.prototype.addEventListener,
removeEventListener: EventTarget.prototype.removeEventListener,
appendChild: Node.prototype.appendChild,
insertBefore: Node.prototype.insertBefore,
replaceChild: Node.prototype.replaceChild,
};
const timeouts = new Set();
const intervals = new Set();
const listeners = new Set();
const createdNodes = new Set();
const waiters = new Set();
const notifyActivityChange = () => {
if (waiters.size === 0) return;
for (const cb of Array.from(waiters)) { try { cb(); } catch {} }
};
const normalizeListenerOptions = (options) => (typeof options === 'boolean' ? options : !!options?.capture);
window.setTimeout = function(fn, t, ...args) {
const id = originals.setTimeout(function(...inner) {
try { fn?.(...inner); } finally { timeouts.delete(id); notifyActivityChange(); }
}, t, ...args);
timeouts.add(id);
notifyActivityChange();
return id;
};
window.clearTimeout = function(id) { originals.clearTimeout(id); timeouts.delete(id); notifyActivityChange(); };
window.setInterval = function(fn, t, ...args) { const id = originals.setInterval(fn, t, ...args); intervals.add(id); notifyActivityChange(); return id; };
window.clearInterval = function(id) { originals.clearInterval(id); intervals.delete(id); notifyActivityChange(); };
const addListenerEntry = (entry) => { listeners.add(entry); notifyActivityChange(); };
const removeListenerEntry = (target, type, listener, options) => {
let removed = false;
for (const entry of listeners) {
if (entry.target === target && entry.type === type && entry.listener === listener && entry.capture === normalizeListenerOptions(options)) {
listeners.delete(entry);
removed = true;
break;
}
}
if (removed) notifyActivityChange();
};
EventTarget.prototype.addEventListener = function(type, listener, options) {
addListenerEntry({ target: this, type, listener, capture: normalizeListenerOptions(options) });
return originals.addEventListener.call(this, type, listener, options);
};
EventTarget.prototype.removeEventListener = function(type, listener, options) {
removeListenerEntry(this, type, listener, options);
return originals.removeEventListener.call(this, type, listener, options);
};
const trackNode = (node) => { try { if (node && node.nodeType === 1) createdNodes.add(node); } catch {} };
Node.prototype.appendChild = function(child) { trackNode(child); return originals.appendChild.call(this, child); };
Node.prototype.insertBefore = function(newNode, refNode) { trackNode(newNode); return originals.insertBefore.call(this, newNode, refNode); };
Node.prototype.replaceChild = function(newNode, oldNode) { trackNode(newNode); return originals.replaceChild.call(this, newNode, oldNode); };
const restoreGlobals = () => {
window.setTimeout = originalWindowFns.setTimeout;
window.clearTimeout = originalWindowFns.clearTimeout;
window.setInterval = originalWindowFns.setInterval;
window.clearInterval = originalWindowFns.clearInterval;
EventTarget.prototype.addEventListener = originals.addEventListener;
EventTarget.prototype.removeEventListener = originals.removeEventListener;
Node.prototype.appendChild = originals.appendChild;
Node.prototype.insertBefore = originals.insertBefore;
Node.prototype.replaceChild = originals.replaceChild;
};
const hardCleanup = () => {
try { timeouts.forEach(id => originals.clearTimeout(id)); } catch {}
try { intervals.forEach(id => originals.clearInterval(id)); } catch {}
try {
for (const entry of listeners) {
const { target, type, listener, capture } = entry;
originals.removeEventListener.call(target, type, listener, capture);
}
} catch {}
try {
createdNodes.forEach(node => {
if (!node?.parentNode) return;
if (node.id?.startsWith('xiaobaix_') || node.tagName === 'SCRIPT' || node.tagName === 'STYLE') {
try { node.parentNode.removeChild(node); } catch {}
}
});
} catch {}
listeners.clear();
waiters.clear();
};
const addFloorListener = (callback, options = {}) => {
if (typeof callback !== 'function') throw new Error('callback 必须是函数');
const callbackId = `${stableKey}_fl_${uuidv4()}`;
const entryAbort = new AbortController();
try { abortSignal.addEventListener('abort', () => { try { entryAbort.abort(); } catch {} state.dynamicCallbacks.delete(callbackId); }); } catch {}
state.dynamicCallbacks.set(callbackId, {
callback,
options: {
interval: Number.isFinite(parseInt(options.interval)) ? parseInt(options.interval) : 0,
timing: options.timing || 'after_ai',
floorType: options.floorType || 'all'
},
abortController: entryAbort
});
return () => { try { entryAbort.abort(); } catch {} state.dynamicCallbacks.delete(callbackId); };
};
const runInScope = async (code) => {
// eslint-disable-next-line no-new-func -- intentional: user-defined task expression
const fn = new Function(
'taskContext', 'ctx', 'STscript', 'addFloorListener',
'addListener', 'setTimeoutSafe', 'clearTimeoutSafe', 'setIntervalSafe', 'clearIntervalSafe', 'abortSignal',
`return (async () => { ${code} })();`
);
return await fn(taskContext, taskContext, STscript, addFloorListener, addListener, setTimeoutSafe, clearTimeoutSafe, setIntervalSafe, clearIntervalSafe, abortSignal);
};
const hasActiveResources = () => (timeouts.size > 0 || intervals.size > 0 || listeners.size > 0);
const waitForAsyncSettled = () => new Promise((resolve) => {
if (abortSignal?.aborted) return resolve();
if (!hasActiveResources()) return resolve();
let finished = false;
const finalize = () => { if (finished) return; finished = true; waiters.delete(checkStatus); try { abortSignal?.removeEventListener?.('abort', finalize); } catch {} resolve(); };
const checkStatus = () => { if (finished) return; if (abortSignal?.aborted) return finalize(); if (!hasActiveResources()) finalize(); };
waiters.add(checkStatus);
try { abortSignal?.addEventListener?.('abort', finalize, { once: true }); } catch {}
checkStatus();
});
try {
await runInScope(jsCode);
await waitForAsyncSettled();
} finally {
try { hardCleanup(); } finally { restoreGlobals(); }
}
};
if (isLightTask) {
__runTaskSingleInstance(stableKey, jsRunner, codeSig);
return;
}
await __runTaskSingleInstance(stableKey, jsRunner, codeSig);
}
function handleTaskMessage(event) {
if (!event.data || event.data.source !== 'xiaobaix-iframe' || event.data.type !== 'executeTaskJS') return;
try {
const script = document.createElement('script');
script.textContent = event.data.code;
event.source.document.head.appendChild(script);
event.source.document.head.removeChild(script);
} catch (error) { console.error('执行任务JS失败:', error); }
}
// ═══════════════════════════════════════════════════════════════════════════
// 楼层计数
// ═══════════════════════════════════════════════════════════════════════════
function getFloorCounts() {
return state.floorCounts || { all: 0, user: 0, llm: 0 };
}
function pickFloorByType(floorType, counts) {
switch (floorType) {
case 'user': return Math.max(0, counts.user - 1);
case 'llm': return Math.max(0, counts.llm - 1);
default: return Math.max(0, counts.all - 1);
}
}
function calculateTurnCount() {
if (!Array.isArray(chat) || chat.length === 0) return 0;
const userMessages = chat.filter(msg => msg.is_user && !msg.is_system).length;
const aiMessages = chat.filter(msg => !msg.is_user && !msg.is_system).length;
return Math.min(userMessages, aiMessages);
}
function recountFloors() {
let user = 0, llm = 0, all = 0;
if (Array.isArray(chat)) {
for (const m of chat) {
all++;
if (m.is_system) continue;
if (m.is_user) user++; else llm++;
}
}
state.floorCounts = { all, user, llm };
}
// ═══════════════════════════════════════════════════════════════════════════
// 任务触发
// ═══════════════════════════════════════════════════════════════════════════
function shouldSkipByContext(taskTriggerTiming, triggerContext) {
if (taskTriggerTiming === 'character_init') return triggerContext !== 'chat_created';
if (taskTriggerTiming === 'plugin_init') return triggerContext !== 'plugin_initialized';
if (taskTriggerTiming === 'chat_changed') return triggerContext !== 'chat_changed';
if (taskTriggerTiming === 'only_this_floor' || taskTriggerTiming === 'any_message') {
return triggerContext !== 'before_user' && triggerContext !== 'after_ai';
}
return taskTriggerTiming !== triggerContext;
}
function matchInterval(task, counts, triggerContext) {
const currentFloor = pickFloorByType(task.floorType || 'all', counts);
if (currentFloor <= 0) return false;
if (task.triggerTiming === 'only_this_floor') return currentFloor === task.interval;
if (task.triggerTiming === 'any_message') return currentFloor % task.interval === 0;
return currentFloor % task.interval === 0;
}
async function checkAndExecuteTasks(triggerContext = 'after_ai', overrideChatChanged = null, overrideNewChat = null) {
if (!isGloballyEnabled() || isAnyTaskExecuting()) return;
const tasks = await allTasksFull();
const n = nowMs();
const counts = getFloorCounts();
const dynamicTaskList = [];
if (state.dynamicCallbacks?.size > 0) {
for (const [callbackId, entry] of state.dynamicCallbacks.entries()) {
const { callback, options, abortController } = entry || {};
if (!callback) { state.dynamicCallbacks.delete(callbackId); continue; }
if (abortController?.signal?.aborted) { state.dynamicCallbacks.delete(callbackId); continue; }
const interval = Number.isFinite(parseInt(options?.interval)) ? parseInt(options.interval) : 0;
dynamicTaskList.push({
name: callbackId,
disabled: false,
interval,
floorType: options?.floorType || 'all',
triggerTiming: options?.timing || 'after_ai',
__dynamic: true,
__callback: callback
});
}
}
const combined = [...tasks, ...dynamicTaskList];
if (combined.length === 0) return;
const tasksToExecute = combined.filter(task => {
if (task.disabled) return false;
if (isTaskInCooldown(task.name, n)) return false;
const tt = task.triggerTiming || 'after_ai';
if (tt === 'chat_changed') {
if (shouldSkipByContext(tt, triggerContext)) return false;
return true;
}
if (tt === 'character_init') return triggerContext === 'chat_created';
if (tt === 'plugin_init') return triggerContext === 'plugin_initialized';
if ((overrideChatChanged ?? state.chatJustChanged) || (overrideNewChat ?? state.isNewChat)) return false;
if (task.interval <= 0) return false;
if (shouldSkipByContext(tt, triggerContext)) return false;
return matchInterval(task, counts, triggerContext);
});
if (tasksToExecute.length === 0) return;
state.executingCount = Math.max(0, (state.executingCount || 0) + 1);
try {
for (const task of tasksToExecute) {
state.taskLastExecutionTime.set(task.name, n);
if (task.__dynamic) {
try {
const currentFloor = pickFloorByType(task.floorType || 'all', counts);
await Promise.resolve().then(() => task.__callback({
timing: triggerContext,
floors: counts,
currentFloor,
interval: task.interval,
floorType: task.floorType || 'all'
}));
} catch (e) { console.error('[动态回调错误]', task.name, e); }
} else {
await executeCommands(task.commands, task.name);
}
}
} finally {
state.executingCount = Math.max(0, (state.executingCount || 0) - 1);
}
if (triggerContext === 'after_ai') state.lastTurnCount = calculateTurnCount();
}
// ═══════════════════════════════════════════════════════════════════════════
// 事件处理
// ═══════════════════════════════════════════════════════════════════════════
async function onMessageReceived(messageId) {
if (typeof messageId !== 'number' || messageId < 0 || !chat[messageId]) return;
const message = chat[messageId];
if (message.is_user || message.is_system || message.mes === '...' ||
state.isCommandGenerated || isAnyTaskExecuting() ||
(message.swipe_id !== undefined && message.swipe_id > 0)) return;
if (!isGloballyEnabled()) return;
const messageKey = `${getContext().chatId}_${messageId}_${message.send_date || nowMs()}`;
if (isMessageProcessed(messageKey)) return;
markMessageAsProcessed(messageKey);
try { state.floorCounts.all = Math.max(0, (state.floorCounts.all || 0) + 1); state.floorCounts.llm = Math.max(0, (state.floorCounts.llm || 0) + 1); } catch {}
await checkAndExecuteTasks('after_ai');
state.chatJustChanged = state.isNewChat = false;
}
async function onGenerationEnded(chatLen) {
const len = Number(chatLen);
if (!Number.isFinite(len) || len <= 0) return;
await onMessageReceived(len - 1);
}
async function onUserMessage() {
if (!isGloballyEnabled()) return;
const messageKey = `${getContext().chatId}_user_${chat.length}`;
if (isMessageProcessed(messageKey)) return;
markMessageAsProcessed(messageKey);
try { state.floorCounts.all = Math.max(0, (state.floorCounts.all || 0) + 1); state.floorCounts.user = Math.max(0, (state.floorCounts.user || 0) + 1); } catch {}
await checkAndExecuteTasks('before_user');
state.chatJustChanged = state.isNewChat = false;
}
function onMessageDeleted() {
const settings = getSettings();
const chatId = getContext().chatId;
settings.processedMessages = settings.processedMessages.filter(key => !key.startsWith(`${chatId}_`));
state.processedMessagesSet = new Set(settings.processedMessages);
state.executingCount = 0;
state.isCommandGenerated = false;
recountFloors();
saveSettingsDebounced();
}
async function onChatChanged(chatId) {
Object.assign(state, {
chatJustChanged: true,
isNewChat: state.lastChatId !== chatId && chat.length <= 1,
lastChatId: chatId,
lastTurnCount: 0,
executingCount: 0,
isCommandGenerated: false
});
state.taskLastExecutionTime.clear();
TasksStorage.clearCache();
requestAnimationFrame(() => {
state.processedMessagesSet.clear();
const settings = getSettings();
settings.processedMessages = [];
checkEmbeddedTasks();
refreshUI();
checkAndExecuteTasks('chat_changed', false, false);
requestAnimationFrame(() => requestAnimationFrame(() => { try { updateTaskBar(); } catch {} }));
});
recountFloors();
setTimeout(() => { state.chatJustChanged = state.isNewChat = false; }, 2000);
}
async function onChatCreated() {
Object.assign(state, { isNewChat: true, chatJustChanged: true });
recountFloors();
await checkAndExecuteTasks('chat_created', false, false);
}
function onPresetChanged(event) {
const apiId = event?.apiId;
if (apiId && apiId !== 'openai') return;
resetPresetTasksCache();
state.lastTasksHash = '';
refreshUI();
}
function onMainApiChanged() {
resetPresetTasksCache();
state.lastTasksHash = '';
refreshUI();
}
// ═══════════════════════════════════════════════════════════════════════════
// UI 列表
// ═══════════════════════════════════════════════════════════════════════════
function getTasksHash() {
const globalTasks = getSettings().globalTasks;
const characterTasks = getCharacterTasks();
const presetTasks = getPresetTasks();
const presetName = PresetTasksStore.currentName();
const all = [...globalTasks, ...characterTasks, ...presetTasks];
return `${presetName || ''}|${all.map(t => `${t.id}_${t.disabled}_${t.name}_${t.interval}_${t.floorType}_${t.triggerTiming || 'after_ai'}`).join('|')}`;
}
function createTaskItemSimple(task, index, scope = 'global') {
if (!task.id) task.id = `task_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
const taskType = scope || 'global';
const floorTypeText = { user: '用户楼层', llm: 'LLM楼层' }[task.floorType] || '全部楼层';
const triggerTimingText = {
before_user: '用户前',
any_message: '任意对话',
initialization: '角色卡初始化',
character_init: '角色卡初始化',
plugin_init: '插件初始化',
only_this_floor: '仅该楼层',
chat_changed: '切换聊天后'
}[task.triggerTiming] || 'AI后';
let displayName;
if (task.interval === 0) {
displayName = `${task.name} (手动触发)`;
} else if (task.triggerTiming === 'initialization' || task.triggerTiming === 'character_init') {
displayName = `${task.name} (角色卡初始化)`;
} else if (task.triggerTiming === 'plugin_init') {
displayName = `${task.name} (插件初始化)`;
} else if (task.triggerTiming === 'chat_changed') {
displayName = `${task.name} (切换聊天后)`;
} else if (task.triggerTiming === 'only_this_floor') {
displayName = `${task.name} (仅第${task.interval}${floorTypeText})`;
} else {
displayName = `${task.name} (每${task.interval}${floorTypeText}·${triggerTimingText})`;
}
const taskElement = $('#task_item_template').children().first().clone();
taskElement.attr({ id: task.id, 'data-index': index, 'data-type': taskType });
taskElement.find('.task_name').attr('title', task.name).text(displayName);
taskElement.find('.disable_task').attr('id', `task_disable_${task.id}`).prop('checked', task.disabled);
taskElement.find('label.checkbox').attr('for', `task_disable_${task.id}`);
return taskElement;
}
function initSortable($list, onUpdate) {
const inst = (() => { try { return $list.sortable('instance'); } catch { return undefined; } })();
if (inst) return;
$list.sortable({
delay: getSortableDelay?.() || 0,
handle: '.drag-handle.menu-handle',
items: '> .task-item',
update: onUpdate
});
}
function updateTaskCounts(globalCount, characterCount, presetCount) {
const globalEl = document.getElementById('global_task_count');
const characterEl = document.getElementById('character_task_count');
const presetEl = document.getElementById('preset_task_count');
if (globalEl) globalEl.textContent = globalCount > 0 ? `(${globalCount})` : '';
if (characterEl) characterEl.textContent = characterCount > 0 ? `(${characterCount})` : '';
if (presetEl) presetEl.textContent = presetCount > 0 ? `(${presetCount})` : '';
}
function refreshTaskLists() {
updatePresetTaskHint();
const currentHash = getTasksHash();
if (currentHash === state.lastTasksHash) {
updateTaskBar();
return;
}
state.lastTasksHash = currentHash;
const $globalList = $('#global_tasks_list');
const $charList = $('#character_tasks_list');
const $presetList = $('#preset_tasks_list');
const globalTasks = getSettings().globalTasks;
const characterTasks = getCharacterTasks();
const presetTasks = getPresetTasks();
updateTaskCounts(globalTasks.length, characterTasks.length, presetTasks.length);
const globalFragment = document.createDocumentFragment();
globalTasks.forEach((task, i) => { globalFragment.appendChild(createTaskItemSimple(task, i, 'global')[0]); });
$globalList.empty().append(globalFragment);
const charFragment = document.createDocumentFragment();
characterTasks.forEach((task, i) => { charFragment.appendChild(createTaskItemSimple(task, i, 'character')[0]); });
$charList.empty().append(charFragment);
if ($presetList.length) {
const presetFragment = document.createDocumentFragment();
presetTasks.forEach((task, i) => { presetFragment.appendChild(createTaskItemSimple(task, i, 'preset')[0]); });
$presetList.empty().append(presetFragment);
}
initSortable($globalList, async function () {
const newOrderIds = $globalList.sortable('toArray');
const current = getSettings().globalTasks;
const idToTask = new Map(current.map(t => [t.id, t]));
const reordered = newOrderIds.map(id => idToTask.get(id)).filter(Boolean);
const leftovers = current.filter(t => !newOrderIds.includes(t.id));
await persistTaskListByScope('global', [...reordered, ...leftovers]);
refreshTaskLists();
});
initSortable($charList, async function () {
const newOrderIds = $charList.sortable('toArray');
const current = getCharacterTasks();
const idToTask = new Map(current.map(t => [t.id, t]));
const reordered = newOrderIds.map(id => idToTask.get(id)).filter(Boolean);
const leftovers = current.filter(t => !newOrderIds.includes(t.id));
await saveCharacterTasks([...reordered, ...leftovers]);
refreshTaskLists();
});
if ($presetList.length) {
initSortable($presetList, async function () {
const newOrderIds = $presetList.sortable('toArray');
const current = getPresetTasks();
const idToTask = new Map(current.map(t => [t.id, t]));
const reordered = newOrderIds.map(id => idToTask.get(id)).filter(Boolean);
const leftovers = current.filter(t => !newOrderIds.includes(t.id));
await savePresetTasks([...reordered, ...leftovers]);
refreshTaskLists();
});
}
updateTaskBar();
}
function updatePresetTaskHint() {
const hint = document.getElementById('preset_tasks_hint');
if (!hint) return;
const presetName = PresetTasksStore.currentName();
state.lastPresetName = presetName || '';
if (!presetName) {
hint.textContent = '未选择';
hint.classList.add('no-preset');
hint.title = '请在OpenAI设置中选择预设';
} else {
hint.textContent = `${presetName}`;
hint.classList.remove('no-preset');
hint.title = `当前OpenAI预设${presetName}`;
}
}
// ═══════════════════════════════════════════════════════════════════════════
// 任务栏
// ═══════════════════════════════════════════════════════════════════════════
const cache = { bar: null, btns: null, sig: '', ts: 0 };
const getActivatedTasks = () => isGloballyEnabled() ? allTasks().filter(t => t.buttonActivated && !t.disabled) : [];
const getBar = () => {
if (cache.bar?.isConnected) return cache.bar;
cache.bar = document.getElementById('qr--bar') || document.getElementById('qr-bar');
if (!cache.bar && !(window.quickReplyApi?.settings?.isEnabled || extension_settings?.quickReplyV2?.isEnabled)) {
const parent = document.getElementById('send_form') || document.body;
cache.bar = parent.insertBefore(
Object.assign(document.createElement('div'), {
id: 'qr-bar',
className: 'flex-container flexGap5',
innerHTML: '<div class="qr--buttons" style="display:flex;flex-wrap:wrap;justify-content:center"></div>'
}),
parent.firstChild
);
}
cache.btns = cache.bar?.querySelector('.qr--buttons');
return cache.bar;
};
function createTaskBar() {
const tasks = getActivatedTasks();
const sig = state.taskBarVisible ? tasks.map(t => t.name).join() : '';
if (sig === cache.sig && Date.now() - cache.ts < 100) return;
const bar = getBar();
if (!bar) return;
bar.style.display = state.taskBarVisible ? '' : 'none';
if (!state.taskBarVisible) return;
const btns = cache.btns || bar;
const exist = new Map([...btns.querySelectorAll('.xiaobaix-task-button')].map(el => [el.dataset.taskName, el]));
const names = new Set(tasks.map(t => t.name));
exist.forEach((el, name) => !names.has(name) && el.remove());
const frag = document.createDocumentFragment();
tasks.forEach(t => {
if (!exist.has(t.name)) {
const btn = Object.assign(document.createElement('button'), {
className: 'menu_button menu_button_icon xiaobaix-task-button interactable',
innerHTML: `<span>${t.name}</span>`
});
btn.dataset.taskName = t.name;
frag.appendChild(btn);
}
});
frag.childNodes.length && btns.appendChild(frag);
cache.sig = sig;
cache.ts = Date.now();
}
const updateTaskBar = debounce(createTaskBar, 100);
function toggleTaskBarVisibility() {
state.taskBarVisible = !state.taskBarVisible;
const bar = getBar();
bar && (bar.style.display = state.taskBarVisible ? '' : 'none');
createTaskBar();
const btn = document.getElementById('toggle_task_bar');
const txt = btn?.querySelector('small');
if (txt) {
txt.style.cssText = state.taskBarVisible ? 'opacity:1;text-decoration:none' : 'opacity:.5;text-decoration:line-through';
btn.title = state.taskBarVisible ? '隐藏任务栏' : '显示任务栏';
}
}
document.addEventListener('click', async e => {
const btn = e.target.closest('.xiaobaix-task-button');
if (!btn) return;
if (!isGloballyEnabled()) return;
window.xbqte(btn.dataset.taskName).catch(console.error);
});
new MutationObserver(updateTaskBar).observe(document.body, { childList: true, subtree: true });
// ═══════════════════════════════════════════════════════════════════════════
// 任务编辑器
// ═══════════════════════════════════════════════════════════════════════════
async function showTaskEditor(task = null, isEdit = false, scope = 'global') {
const initialScope = scope || 'global';
const sourceList = getTaskListByScope(initialScope);
if (task && scope === 'global' && task.id) {
task = { ...task, commands: await TasksStorage.get(task.id) };
}
state.currentEditingTask = task;
state.currentEditingScope = initialScope;
state.currentEditingIndex = isEdit ? sourceList.indexOf(task) : -1;
state.currentEditingId = task?.id || null;
const editorTemplate = $('#task_editor_template').clone().removeAttr('id').show();
editorTemplate.find('.task_name_edit').val(task?.name || '');
editorTemplate.find('.task_commands_edit').val(task?.commands || '');
editorTemplate.find('.task_interval_edit').val(task?.interval ?? 3);
editorTemplate.find('.task_floor_type_edit').val(task?.floorType || 'all');
editorTemplate.find('.task_trigger_timing_edit').val(task?.triggerTiming || 'after_ai');
editorTemplate.find('.task_type_edit').val(initialScope);
editorTemplate.find('.task_enabled_edit').prop('checked', !task?.disabled);
editorTemplate.find('.task_button_activated_edit').prop('checked', task?.buttonActivated || false);
function updateWarningDisplay() {
const interval = parseInt(editorTemplate.find('.task_interval_edit').val()) || 0;
const triggerTiming = editorTemplate.find('.task_trigger_timing_edit').val();
const floorType = editorTemplate.find('.task_floor_type_edit').val();
let warningElement = editorTemplate.find('.trigger-timing-warning');
if (warningElement.length === 0) {
warningElement = $('<div class="trigger-timing-warning" style="color:#ff6b6b;font-size:.8em;margin-top:4px;"></div>');
editorTemplate.find('.task_trigger_timing_edit').parent().append(warningElement);
}
const shouldShowWarning = interval > 0 && floorType === 'all' && (triggerTiming === 'after_ai' || triggerTiming === 'before_user');
if (shouldShowWarning) {
warningElement.html('⚠️ 警告:选择"全部楼层"配合"AI消息后"或"用户消息前"可能因楼层编号不匹配而不触发').show();
} else {
warningElement.hide();
}
}
function updateControlStates() {
const interval = parseInt(editorTemplate.find('.task_interval_edit').val()) || 0;
const triggerTiming = editorTemplate.find('.task_trigger_timing_edit').val();
const intervalControl = editorTemplate.find('.task_interval_edit');
const floorTypeControl = editorTemplate.find('.task_floor_type_edit');
const triggerTimingControl = editorTemplate.find('.task_trigger_timing_edit');
if (interval === 0) {
floorTypeControl.prop('disabled', true).css('opacity', '0.5');
triggerTimingControl.prop('disabled', true).css('opacity', '0.5');
let manualTriggerHint = editorTemplate.find('.manual-trigger-hint');
if (manualTriggerHint.length === 0) {
manualTriggerHint = $('<small class="manual-trigger-hint" style="color:#888;">手动触发</small>');
triggerTimingControl.parent().append(manualTriggerHint);
}
manualTriggerHint.show();
} else {
floorTypeControl.prop('disabled', false).css('opacity', '1');
triggerTimingControl.prop('disabled', false).css('opacity', '1');
editorTemplate.find('.manual-trigger-hint').hide();
if (triggerTiming === 'initialization' || triggerTiming === 'plugin_init' || triggerTiming === 'chat_changed') {
intervalControl.prop('disabled', true).css('opacity', '0.5');
floorTypeControl.prop('disabled', true).css('opacity', '0.5');
} else {
intervalControl.prop('disabled', false).css('opacity', '1');
floorTypeControl.prop('disabled', false).css('opacity', '1');
}
}
updateWarningDisplay();
}
editorTemplate.find('.task_interval_edit').on('input', updateControlStates);
editorTemplate.find('.task_trigger_timing_edit').on('change', updateControlStates);
editorTemplate.find('.task_floor_type_edit').on('change', updateControlStates);
updateControlStates();
callGenericPopup(editorTemplate, POPUP_TYPE.CONFIRM, '', { okButton: '保存' }).then(async (result) => {
if (result) {
const desiredName = String(editorTemplate.find('.task_name_edit').val() || '').trim();
const existingNames = new Set(allTasks().map(t => (t?.name || '').trim().toLowerCase()));
let uniqueName = desiredName;
if (desiredName && (!isEdit || (isEdit && task?.name?.toLowerCase() !== desiredName.toLowerCase()))) {
if (existingNames.has(desiredName.toLowerCase())) {
let idx = 1;
while (existingNames.has(`${desiredName}${idx}`.toLowerCase())) idx++;
uniqueName = `${desiredName}${idx}`;
}
}
const base = task ? structuredClone(task) : {};
const newTask = {
...base,
id: base.id || `task_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`,
name: uniqueName,
commands: String(editorTemplate.find('.task_commands_edit').val() || '').trim(),
interval: parseInt(String(editorTemplate.find('.task_interval_edit').val() || '0'), 10) || 0,
floorType: editorTemplate.find('.task_floor_type_edit').val() || 'all',
triggerTiming: editorTemplate.find('.task_trigger_timing_edit').val() || 'after_ai',
disabled: !editorTemplate.find('.task_enabled_edit').prop('checked'),
buttonActivated: editorTemplate.find('.task_button_activated_edit').prop('checked'),
createdAt: base.createdAt || new Date().toISOString(),
};
const targetScope = String(editorTemplate.find('.task_type_edit').val() || initialScope);
await saveTaskFromEditor(newTask, targetScope);
}
});
}
async function saveTaskFromEditor(task, scope) {
const targetScope = scope === 'character' || scope === 'preset' ? scope : 'global';
const isManual = (task?.interval === 0);
if (!task.name || (!isManual && !task.commands)) return;
const isEditingExistingTask = state.currentEditingIndex >= 0 || !!state.currentEditingId;
const previousScope = state.currentEditingScope || 'global';
const taskTypeChanged = isEditingExistingTask && previousScope !== targetScope;
if (targetScope === 'preset' && !PresetTasksStore.currentName()) {
toastr?.warning?.('请先选择一个OpenAI预设。');
return;
}
if (taskTypeChanged) {
await removeTaskByScope(previousScope, state.currentEditingId, state.currentEditingIndex);
state.lastTasksHash = '';
state.currentEditingIndex = -1;
state.currentEditingId = null;
}
const list = getTaskListByScope(targetScope);
let idx = state.currentEditingId ? list.findIndex(t => t?.id === state.currentEditingId) : state.currentEditingIndex;
if (idx >= 0 && idx < list.length) {
list[idx] = task;
} else {
list.push(task);
}
await persistTaskListByScope(targetScope, [...list]);
state.currentEditingScope = targetScope;
state.lastTasksHash = '';
refreshUI();
}
async function editTask(index, scope) {
const list = getTaskListByScope(scope);
const task = list[index];
if (task) showTaskEditor(task, true, scope);
}
async function deleteTask(index, scope) {
const list = getTaskListByScope(scope);
const task = list[index];
if (!task) return;
try {
const styleId = 'temp-dialog-style';
if (!document.getElementById(styleId)) {
const style = document.createElement('style');
style.id = styleId;
style.textContent = '#dialogue_popup_ok, #dialogue_popup_cancel { width: auto !important; }';
document.head.appendChild(style);
}
const result = await callPopup(`确定要删除任务 "${task.name}" 吗?`, 'confirm');
document.getElementById(styleId)?.remove();
if (result) {
await removeTaskByScope(scope, task.id, index);
refreshUI();
}
} catch (error) {
console.error('删除任务时出错:', error);
document.getElementById('temp-dialog-style')?.remove();
}
}
const getAllTaskNames = () => allTasks().filter(t => !t.disabled).map(t => t.name);
// ═══════════════════════════════════════════════════════════════════════════
// 嵌入式任务
// ═══════════════════════════════════════════════════════════════════════════
async function checkEmbeddedTasks() {
if (!this_chid) return;
const avatar = characters[this_chid]?.avatar;
const tasks = characters[this_chid]?.data?.extensions?.[TASKS_MODULE_NAME]?.tasks;
if (Array.isArray(tasks) && tasks.length > 0 && avatar) {
const settings = getSettings();
settings.character_allowed_tasks ??= [];
if (!settings.character_allowed_tasks.includes(avatar)) {
const checkKey = `AlertTasks_${avatar}`;
if (!accountStorage.getItem(checkKey)) {
accountStorage.setItem(checkKey, 'true');
let result;
try {
const templateFilePath = `scripts/extensions/third-party/LittleWhiteBox/modules/scheduled-tasks/embedded-tasks.html`;
const templateContent = await fetch(templateFilePath).then(r => r.text());
const templateElement = $(templateContent);
const taskListContainer = templateElement.find('#embedded-tasks-list');
tasks.forEach(task => {
const taskPreview = $('#task_preview_template').children().first().clone();
taskPreview.find('.task-preview-name').text(task.name);
taskPreview.find('.task-preview-interval').text(`(每${task.interval}回合)`);
taskPreview.find('.task-preview-commands').text(task.commands);
taskListContainer.append(taskPreview);
});
result = await callGenericPopup(templateElement, POPUP_TYPE.CONFIRM, '', { okButton: '允许' });
} catch {
result = await callGenericPopup(`此角色包含 ${tasks.length} 个定时任务。是否允许使用?`, POPUP_TYPE.CONFIRM, '', { okButton: '允许' });
}
if (result) {
settings.character_allowed_tasks.push(avatar);
saveSettingsDebounced();
}
}
}
}
refreshTaskLists();
}
// ═══════════════════════════════════════════════════════════════════════════
// 云端任务
// ═══════════════════════════════════════════════════════════════════════════
const CLOUD_TASKS_API = 'https://task.whitelittlebox.qzz.io/';
async function fetchCloudTasks() {
try {
const response = await fetch(CLOUD_TASKS_API, {
method: 'GET',
headers: { 'Accept': 'application/json', 'X-Plugin-Key': 'xbaix', 'Cache-Control': 'no-cache', 'Pragma': 'no-cache' },
cache: 'no-store'
});
if (!response.ok) throw new Error(`HTTP错误: ${response.status}`);
const data = await response.json();
return data.items || [];
} catch (error) {
console.error('获取云端任务失败:', error);
throw error;
}
}
async function downloadAndImportCloudTask(taskUrl, taskType) {
try {
const response = await fetch(taskUrl);
if (!response.ok) throw new Error(`下载失败: ${response.status}`);
const taskData = await response.json();
const jsonString = JSON.stringify(taskData);
const blob = new Blob([jsonString], { type: 'application/json' });
const file = new File([blob], 'cloud_task.json', { type: 'application/json' });
await importGlobalTasks(file);
} catch (error) {
console.error('下载并导入云端任务失败:', error);
await callGenericPopup(`导入失败: ${error.message}`, POPUP_TYPE.TEXT, '', { okButton: '确定' });
}
}
async function showCloudTasksModal() {
const modalTemplate = $('#cloud_tasks_modal_template').children().first().clone();
const loadingEl = modalTemplate.find('.cloud-tasks-loading');
const contentEl = modalTemplate.find('.cloud-tasks-content');
const errorEl = modalTemplate.find('.cloud-tasks-error');
callGenericPopup(modalTemplate, POPUP_TYPE.TEXT, '', { okButton: '关闭' });
try {
const cloudTasks = await fetchCloudTasks();
if (!cloudTasks || cloudTasks.length === 0) throw new Error('云端没有可用的任务');
const globalTasks = cloudTasks.filter(t => t.type === 'global');
const characterTasks = cloudTasks.filter(t => t.type === 'character');
const globalList = modalTemplate.find('.cloud-global-tasks');
if (globalTasks.length === 0) {
globalList.html('<div style="color: #888; padding: 10px;">暂无全局任务</div>');
} else {
globalTasks.forEach(task => { globalList.append(createCloudTaskItem(task)); });
}
const characterList = modalTemplate.find('.cloud-character-tasks');
if (characterTasks.length === 0) {
characterList.html('<div style="color: #888; padding: 10px;">暂无角色任务</div>');
} else {
characterTasks.forEach(task => { characterList.append(createCloudTaskItem(task)); });
}
loadingEl.hide();
contentEl.show();
} catch (error) {
loadingEl.hide();
errorEl.text(`加载失败: ${error.message}`).show();
}
}
function createCloudTaskItem(taskInfo) {
const item = $('#cloud_task_item_template').children().first().clone();
item.find('.cloud-task-name').text(taskInfo.name || '未命名任务');
item.find('.cloud-task-intro').text(taskInfo.简介 || taskInfo.intro || '无简介');
item.find('.cloud-task-download').on('click', async function () {
$(this).prop('disabled', true).find('i').removeClass('fa-download').addClass('fa-spinner fa-spin');
try {
await downloadAndImportCloudTask(taskInfo.url, taskInfo.type);
$(this).find('i').removeClass('fa-spinner fa-spin').addClass('fa-check');
$(this).find('small').text('已导入');
setTimeout(() => {
$(this).find('i').removeClass('fa-check').addClass('fa-download');
$(this).find('small').text('导入');
$(this).prop('disabled', false);
}, 2000);
} catch (error) {
$(this).find('i').removeClass('fa-spinner fa-spin').addClass('fa-download');
$(this).prop('disabled', false);
}
});
return item;
}
// ═══════════════════════════════════════════════════════════════════════════
// 导入导出
// ═══════════════════════════════════════════════════════════════════════════
async function exportSingleTask(index, scope) {
const list = getTaskListByScope(scope);
if (index < 0 || index >= list.length) return;
let task = list[index];
if (scope === 'global' && task.id) {
task = { ...task, commands: await TasksStorage.get(task.id) };
}
const fileName = `${scope}_task_${task?.name || 'unnamed'}_${new Date().toISOString().split('T')[0]}.json`;
const fileData = JSON.stringify({ type: scope, exportDate: new Date().toISOString(), tasks: [task] }, null, 4);
download(fileData, fileName, 'application/json');
}
async function importGlobalTasks(file) {
if (!file) return;
try {
const fileText = await getFileText(file);
const raw = JSON.parse(fileText);
let incomingTasks = [];
let fileType = 'global';
if (Array.isArray(raw)) {
incomingTasks = raw;
fileType = 'global';
} else if (raw && Array.isArray(raw.tasks)) {
incomingTasks = raw.tasks;
if (raw.type === 'character' || raw.type === 'global' || raw.type === 'preset') fileType = raw.type;
} else if (raw && typeof raw === 'object' && raw.name && (raw.commands || raw.interval !== undefined)) {
incomingTasks = [raw];
if (raw.type === 'character' || raw.type === 'global' || raw.type === 'preset') fileType = raw.type;
} else {
throw new Error('无效的任务文件格式');
}
const VALID_FLOOR = ['all', 'user', 'llm'];
const VALID_TIMING = ['after_ai', 'before_user', 'any_message', 'initialization', 'character_init', 'plugin_init', 'only_this_floor', 'chat_changed'];
const deepClone = (o) => JSON.parse(JSON.stringify(o || {}));
const tasksToImport = incomingTasks
.filter(t => (t?.name || '').trim() && (String(t?.commands || '').trim() || t.interval === 0))
.map(src => ({
id: uuidv4(),
name: String(src.name || '').trim(),
commands: String(src.commands || '').trim(),
interval: clampInt(src.interval, 0, 99999, 0),
floorType: VALID_FLOOR.includes(src.floorType) ? src.floorType : 'all',
triggerTiming: VALID_TIMING.includes(src.triggerTiming) ? src.triggerTiming : 'after_ai',
disabled: !!src.disabled,
buttonActivated: !!src.buttonActivated,
createdAt: src.createdAt || new Date().toISOString(),
importedAt: new Date().toISOString(),
x: (src.x && typeof src.x === 'object') ? deepClone(src.x) : {}
}));
if (!tasksToImport.length) throw new Error('没有可导入的任务');
if (fileType === 'character') {
if (!this_chid || !characters[this_chid]) {
toastr?.warning?.('角色任务请先在角色聊天界面导入。');
return;
}
const current = getCharacterTasks();
await saveCharacterTasks([...current, ...tasksToImport]);
} else if (fileType === 'preset') {
const presetName = PresetTasksStore.currentName();
if (!presetName) {
toastr?.warning?.('请先选择一个OpenAI预设后再导入预设任务。');
return;
}
const current = getPresetTasks();
await savePresetTasks([...current, ...tasksToImport]);
} else {
const currentMeta = getSettings().globalTasks;
const merged = [...currentMeta, ...tasksToImport];
await persistTaskListByScope('global', merged);
}
refreshTaskLists();
toastr?.success?.(`已导入 ${tasksToImport.length} 个任务`);
} catch (error) {
console.error('任务导入失败:', error);
toastr?.error?.(`导入失败:${error.message}`);
}
}
// ═══════════════════════════════════════════════════════════════════════════
// 调试工具
// ═══════════════════════════════════════════════════════════════════════════
function clearProcessedMessages() {
getSettings().processedMessages = [];
state.processedMessagesSet.clear();
saveSettingsDebounced();
}
function clearTaskCooldown(taskName = null) {
taskName ? state.taskLastExecutionTime.delete(taskName) : state.taskLastExecutionTime.clear();
}
function getTaskCooldownStatus() {
const status = {};
for (const [taskName, lastTime] of state.taskLastExecutionTime.entries()) {
const remaining = Math.max(0, CONFIG.TASK_COOLDOWN - (nowMs() - lastTime));
status[taskName] = { lastExecutionTime: lastTime, remainingCooldown: remaining, isInCooldown: remaining > 0 };
}
return status;
}
function getMemoryUsage() {
return {
processedMessages: getSettings().processedMessages.length,
taskCooldowns: state.taskLastExecutionTime.size,
globalTasks: getSettings().globalTasks.length,
characterTasks: getCharacterTasks().length,
scriptCache: TasksStorage.getCacheSize(),
maxProcessedMessages: CONFIG.MAX_PROCESSED,
maxCooldownEntries: CONFIG.MAX_COOLDOWN
};
}
// ═══════════════════════════════════════════════════════════════════════════
// UI 刷新和清理
// ═══════════════════════════════════════════════════════════════════════════
function refreshUI() {
refreshTaskLists();
updateTaskBar();
}
function onMessageSwiped() {
state.executingCount = 0;
state.isCommandGenerated = false;
}
function onCharacterDeleted({ character }) {
const avatar = character?.avatar;
const settings = getSettings();
if (avatar && settings.character_allowed_tasks?.includes(avatar)) {
const index = settings.character_allowed_tasks.indexOf(avatar);
if (index !== -1) {
settings.character_allowed_tasks.splice(index, 1);
saveSettingsDebounced();
}
}
}
function cleanup() {
if (state.cleanupTimer) {
clearInterval(state.cleanupTimer);
state.cleanupTimer = null;
}
state.taskLastExecutionTime.clear();
TasksStorage.clearCache();
try {
if (state.dynamicCallbacks && state.dynamicCallbacks.size > 0) {
for (const entry of state.dynamicCallbacks.values()) {
try { entry?.abortController?.abort(); } catch {}
}
state.dynamicCallbacks.clear();
}
} catch {}
events.cleanup();
window.removeEventListener('message', handleTaskMessage);
$(window).off('beforeunload', cleanup);
try {
const $qrButtons = $('#qr--bar .qr--buttons, #qr--bar, #qr-bar');
$qrButtons.off('click.xb');
$qrButtons.find('.xiaobaix-task-button').remove();
} catch {}
try { state.qrObserver?.disconnect(); } catch {}
state.qrObserver = null;
resetPresetTasksCache();
delete window.__XB_TASKS_INITIALIZED__;
}
// ═══════════════════════════════════════════════════════════════════════════
// 公共 API
// ═══════════════════════════════════════════════════════════════════════════
(function () {
if (window.__XB_TASKS_FACADE__) return;
const norm = s => String(s ?? '').normalize('NFKC').replace(/[\u200B-\u200D\uFEFF]/g, '').trim().toLowerCase();
function list(scope = 'all') {
const g = getSettings().globalTasks || [];
const c = getCharacterTasks() || [];
const p = getPresetTasks() || [];
const map = t => ({
id: t.id, name: t.name, interval: t.interval,
floorType: t.floorType, timing: t.triggerTiming, disabled: !!t.disabled
});
if (scope === 'global') return g.map(map);
if (scope === 'character') return c.map(map);
if (scope === 'preset') return p.map(map);
return { global: g.map(map), character: c.map(map), preset: p.map(map) };
}
function find(name, scope = 'all') {
const n = norm(name);
if (scope !== 'character' && scope !== 'preset') {
const g = getSettings().globalTasks || [];
const gi = g.findIndex(t => norm(t?.name) === n);
if (gi !== -1) return { scope: 'global', list: g, index: gi, task: g[gi] };
}
if (scope !== 'global' && scope !== 'preset') {
const c = getCharacterTasks() || [];
const ci = c.findIndex(t => norm(t?.name) === n);
if (ci !== -1) return { scope: 'character', list: c, index: ci, task: c[ci] };
}
if (scope !== 'global' && scope !== 'character') {
const p = getPresetTasks() || [];
const pi = p.findIndex(t => norm(t?.name) === n);
if (pi !== -1) return { scope: 'preset', list: p, index: pi, task: p[pi] };
}
return null;
}
async function setCommands(name, commands, opts = {}) {
const { mode = 'replace', scope = 'all' } = opts;
const hit = find(name, scope);
if (!hit) throw new Error(`找不到任务: ${name}`);
let old = hit.task.commands || '';
if (hit.scope === 'global' && hit.task.id) {
old = await TasksStorage.get(hit.task.id);
}
const body = String(commands ?? '');
let newCommands;
if (mode === 'append') newCommands = old ? (old + '\n' + body) : body;
else if (mode === 'prepend') newCommands = old ? (body + '\n' + old) : body;
else newCommands = body;
hit.task.commands = newCommands;
await persistTaskListByScope(hit.scope, hit.list);
refreshTaskLists();
return { ok: true, scope: hit.scope, name: hit.task.name };
}
async function setJS(name, jsCode, opts = {}) {
const commands = `<<taskjs>>${jsCode}<</taskjs>>`;
return await setCommands(name, commands, opts);
}
async function setProps(name, props, scope = 'all') {
const hit = find(name, scope);
if (!hit) throw new Error(`找不到任务: ${name}`);
Object.assign(hit.task, props || {});
await persistTaskListByScope(hit.scope, hit.list);
refreshTaskLists();
return { ok: true, scope: hit.scope, name: hit.task.name };
}
async function exec(name) {
const hit = find(name, 'all');
if (!hit) throw new Error(`找不到任务: ${name}`);
let commands = hit.task.commands || '';
if (hit.scope === 'global' && hit.task.id) {
commands = await TasksStorage.get(hit.task.id);
}
return await executeCommands(commands, hit.task.name);
}
async function dump(scope = 'all') {
const g = await Promise.all((getSettings().globalTasks || []).map(async t => ({
...structuredClone(t),
commands: await TasksStorage.get(t.id)
})));
const c = structuredClone(getCharacterTasks() || []);
const p = structuredClone(getPresetTasks() || []);
if (scope === 'global') return g;
if (scope === 'character') return c;
if (scope === 'preset') return p;
return { global: g, character: c, preset: p };
}
window.XBTasks = {
list, dump, find, setCommands, setJS, setProps, exec,
get global() { return getSettings().globalTasks; },
get character() { return getCharacterTasks(); },
get preset() { return getPresetTasks(); },
};
try { if (window.top && window.top !== window) window.top.XBTasks = window.XBTasks; } catch {}
window.__XB_TASKS_FACADE__ = true;
})();
window.xbqte = async (name) => {
try {
if (!name?.trim()) throw new Error('请提供任务名称');
const tasks = await allTasksFull();
const task = tasks.find(t => t.name.toLowerCase() === name.toLowerCase());
if (!task) throw new Error(`找不到名为 "${name}" 的任务`);
if (task.disabled) throw new Error(`任务 "${name}" 已被禁用`);
if (isTaskInCooldown(task.name)) {
const cd = getTaskCooldownStatus()[task.name];
throw new Error(`任务 "${name}" 仍在冷却中,剩余 ${cd.remainingCooldown}ms`);
}
setTaskCooldown(task.name);
const result = await executeCommands(task.commands, task.name);
return result || `已执行任务: ${task.name}`;
} catch (error) {
console.error(`执行任务失败: ${error.message}`);
throw error;
}
};
window.setScheduledTaskInterval = async (name, interval) => {
if (!name?.trim()) throw new Error('请提供任务名称');
const intervalNum = parseInt(interval);
if (isNaN(intervalNum) || intervalNum < 0 || intervalNum > 99999) {
throw new Error('间隔必须是 0-99999 之间的数字');
}
const settings = getSettings();
const gi = settings.globalTasks.findIndex(t => t.name.toLowerCase() === name.toLowerCase());
if (gi !== -1) {
settings.globalTasks[gi].interval = intervalNum;
saveSettingsDebounced();
refreshTaskLists();
return `已设置全局任务 "${name}" 的间隔为 ${intervalNum === 0 ? '手动激活' : `${intervalNum}楼层`}`;
}
const cts = getCharacterTasks();
const ci = cts.findIndex(t => t.name.toLowerCase() === name.toLowerCase());
if (ci !== -1) {
cts[ci].interval = intervalNum;
await saveCharacterTasks(cts);
refreshTaskLists();
return `已设置角色任务 "${name}" 的间隔为 ${intervalNum === 0 ? '手动激活' : `${intervalNum}楼层`}`;
}
throw new Error(`找不到名为 "${name}" 的任务`);
};
Object.assign(window, {
clearTasksProcessedMessages: clearProcessedMessages,
clearTaskCooldown,
getTaskCooldownStatus,
getTasksMemoryUsage: getMemoryUsage
});
// ═══════════════════════════════════════════════════════════════════════════
// 斜杠命令
// ═══════════════════════════════════════════════════════════════════════════
function registerSlashCommands() {
try {
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'xbqte',
callback: async (args, value) => {
if (!value) return '请提供任务名称。用法: /xbqte 任务名称';
try { return await window.xbqte(value); } catch (error) { return `错误: ${error.message}`; }
},
unnamedArgumentList: [SlashCommandArgument.fromProps({
description: '要执行的任务名称',
typeList: [ARGUMENT_TYPE.STRING],
isRequired: true,
enumProvider: getAllTaskNames
})],
helpString: '执行指定名称的定时任务。例如: /xbqte 我的任务名称'
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'xbset',
callback: async (namedArgs, taskName) => {
const name = String(taskName || '').trim();
if (!name) throw new Error('请提供任务名称');
const settings = getSettings();
let task = null, isCharacter = false, taskIndex = -1;
taskIndex = settings.globalTasks.findIndex(t => t.name.toLowerCase() === name.toLowerCase());
if (taskIndex !== -1) {
task = settings.globalTasks[taskIndex];
} else {
const cts = getCharacterTasks();
taskIndex = cts.findIndex(t => t.name.toLowerCase() === name.toLowerCase());
if (taskIndex !== -1) {
task = cts[taskIndex];
isCharacter = true;
}
}
if (!task) throw new Error(`找不到任务 "${name}"`);
const changed = [];
if (namedArgs.status !== undefined) {
const val = String(namedArgs.status).toLowerCase();
if (val === 'on' || val === 'true') { task.disabled = false; changed.push('状态=启用'); }
else if (val === 'off' || val === 'false') { task.disabled = true; changed.push('状态=禁用'); }
else throw new Error('status 仅支持 on/off');
}
if (namedArgs.interval !== undefined) {
const num = parseInt(namedArgs.interval);
if (isNaN(num) || num < 0 || num > 99999) throw new Error('interval 必须为 0-99999');
task.interval = num;
changed.push(`间隔=${num}`);
}
if (namedArgs.timing !== undefined) {
const val = String(namedArgs.timing).toLowerCase();
const valid = ['after_ai', 'before_user', 'any_message', 'initialization', 'character_init', 'plugin_init', 'only_this_floor', 'chat_changed'];
if (!valid.includes(val)) throw new Error(`timing 必须为: ${valid.join(', ')}`);
task.triggerTiming = val;
changed.push(`时机=${val}`);
}
if (namedArgs.floorType !== undefined) {
const val = String(namedArgs.floorType).toLowerCase();
if (!['all', 'user', 'llm'].includes(val)) throw new Error('floorType 必须为: all, user, llm');
task.floorType = val;
changed.push(`楼层=${val}`);
}
if (changed.length === 0) throw new Error('未提供要修改的参数');
if (isCharacter) await saveCharacterTasks(getCharacterTasks());
else saveSettingsDebounced();
refreshTaskLists();
return `已更新任务 "${name}": ${changed.join(', ')}`;
},
namedArgumentList: [
SlashCommandNamedArgument.fromProps({ name: 'status', description: '启用/禁用', typeList: [ARGUMENT_TYPE.STRING], enumList: ['on', 'off'] }),
SlashCommandNamedArgument.fromProps({ name: 'interval', description: '楼层间隔(0=手动)', typeList: [ARGUMENT_TYPE.NUMBER] }),
SlashCommandNamedArgument.fromProps({ name: 'timing', description: '触发时机', typeList: [ARGUMENT_TYPE.STRING], enumList: ['after_ai', 'before_user', 'any_message', 'initialization', 'character_init', 'plugin_init', 'only_this_floor', 'chat_changed'] }),
SlashCommandNamedArgument.fromProps({ name: 'floorType', description: '楼层类型', typeList: [ARGUMENT_TYPE.STRING], enumList: ['all', 'user', 'llm'] }),
],
unnamedArgumentList: [SlashCommandArgument.fromProps({ description: '任务名称', typeList: [ARGUMENT_TYPE.STRING], isRequired: true, enumProvider: getAllTaskNames })],
helpString: `设置任务属性。用法: /xbset status=on/off interval=数字 timing=时机 floorType=类型 任务名`
}));
} catch (error) {
console.error("注册斜杠命令时出错:", error);
}
}
// ═══════════════════════════════════════════════════════════════════════════
// 初始化
// ═══════════════════════════════════════════════════════════════════════════
async function initTasks() {
if (window.__XB_TASKS_INITIALIZED__) {
console.log('[小白X任务] 已经初始化,跳过重复注册');
return;
}
window.__XB_TASKS_INITIALIZED__ = true;
await migrateToServerStorage();
hydrateProcessedSetFromSettings();
scheduleCleanup();
if (!extension_settings[EXT_ID].tasks) {
extension_settings[EXT_ID].tasks = structuredClone(defaultSettings);
}
if (window.registerModuleCleanup) {
window.registerModuleCleanup('scheduledTasks', cleanup);
}
// eslint-disable-next-line no-restricted-syntax -- legacy task bridge; keep behavior unchanged.
window.addEventListener('message', handleTaskMessage);
$('#scheduled_tasks_enabled').on('input', e => {
const enabled = $(e.target).prop('checked');
getSettings().enabled = enabled;
saveSettingsDebounced();
try { createTaskBar(); } catch {}
});
$('#add_global_task').on('click', () => showTaskEditor(null, false, 'global'));
$('#add_character_task').on('click', () => showTaskEditor(null, false, 'character'));
$('#add_preset_task').on('click', () => showTaskEditor(null, false, 'preset'));
$('#toggle_task_bar').on('click', toggleTaskBarVisibility);
$('#import_global_tasks').on('click', () => $('#import_tasks_file').trigger('click'));
$('#cloud_tasks_button').on('click', () => showCloudTasksModal());
$('#import_tasks_file').on('change', function (e) {
const file = e.target.files[0];
if (file) { importGlobalTasks(file); $(this).val(''); }
});
$('#global_tasks_list')
.on('input', '.disable_task', function () {
const $item = $(this).closest('.task-item');
const idx = parseInt($item.attr('data-index'), 10);
const list = getSettings().globalTasks;
if (list[idx]) {
list[idx].disabled = $(this).prop('checked');
saveSettingsDebounced();
state.lastTasksHash = '';
refreshTaskLists();
}
})
.on('click', '.edit_task', function () { editTask(parseInt($(this).closest('.task-item').attr('data-index')), 'global'); })
.on('click', '.export_task', function () { exportSingleTask(parseInt($(this).closest('.task-item').attr('data-index')), 'global'); })
.on('click', '.delete_task', function () { deleteTask(parseInt($(this).closest('.task-item').attr('data-index')), 'global'); });
$('#character_tasks_list')
.on('input', '.disable_task', function () {
const $item = $(this).closest('.task-item');
const idx = parseInt($item.attr('data-index'), 10);
const tasks = getCharacterTasks();
if (tasks[idx]) {
tasks[idx].disabled = $(this).prop('checked');
saveCharacterTasks(tasks);
state.lastTasksHash = '';
refreshTaskLists();
}
})
.on('click', '.edit_task', function () { editTask(parseInt($(this).closest('.task-item').attr('data-index')), 'character'); })
.on('click', '.export_task', function () { exportSingleTask(parseInt($(this).closest('.task-item').attr('data-index')), 'character'); })
.on('click', '.delete_task', function () { deleteTask(parseInt($(this).closest('.task-item').attr('data-index')), 'character'); });
$('#preset_tasks_list')
.on('input', '.disable_task', async function () {
const $item = $(this).closest('.task-item');
const idx = parseInt($item.attr('data-index'), 10);
const tasks = getPresetTasks();
if (tasks[idx]) {
tasks[idx].disabled = $(this).prop('checked');
await savePresetTasks([...tasks]);
state.lastTasksHash = '';
refreshTaskLists();
}
})
.on('click', '.edit_task', function () { editTask(parseInt($(this).closest('.task-item').attr('data-index')), 'preset'); })
.on('click', '.export_task', function () { exportSingleTask(parseInt($(this).closest('.task-item').attr('data-index')), 'preset'); })
.on('click', '.delete_task', function () { deleteTask(parseInt($(this).closest('.task-item').attr('data-index')), 'preset'); });
$('#scheduled_tasks_enabled').prop('checked', getSettings().enabled);
refreshTaskLists();
if (event_types.GENERATION_ENDED) {
events.on(event_types.GENERATION_ENDED, onGenerationEnded);
} else {
events.on(event_types.CHARACTER_MESSAGE_RENDERED, onMessageReceived);
}
events.on(event_types.USER_MESSAGE_RENDERED, onUserMessage);
events.on(event_types.CHAT_CHANGED, onChatChanged);
events.on(event_types.CHAT_CREATED, onChatCreated);
events.on(event_types.MESSAGE_DELETED, onMessageDeleted);
events.on(event_types.MESSAGE_SWIPED, onMessageSwiped);
events.on(event_types.CHARACTER_DELETED, onCharacterDeleted);
events.on(event_types.PRESET_CHANGED, onPresetChanged);
events.on(event_types.OAI_PRESET_CHANGED_AFTER, onPresetChanged);
events.on(event_types.MAIN_API_CHANGED, onMainApiChanged);
$(window).on('beforeunload', cleanup);
registerSlashCommands();
setTimeout(() => checkEmbeddedTasks(), 1000);
setTimeout(() => {
try { checkAndExecuteTasks('plugin_initialized', false, false); } catch (e) { console.debug(e); }
}, 0);
}
export { initTasks };