// ═══════════════════════════════════════════════════════════════════════════ // 导入 // ═══════════════════════════════════════════════════════════════════════════ 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 = /<>([\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: '
' }), 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: `${t.name}` }); 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 = $('
'); 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 = $('手动触发'); 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('
暂无全局任务
'); } else { globalTasks.forEach(task => { globalList.append(createCloudTaskItem(task)); }); } const characterList = modalTemplate.find('.cloud-character-tasks'); if (characterTasks.length === 0) { characterList.html('
暂无角色任务
'); } 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 = `<>${jsCode}<>`; 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 };