Files
LittleWhiteBox/modules/scheduled-tasks/scheduled-tasks.js
2026-01-17 16:34:39 +08:00

2171 lines
99 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ═══════════════════════════════════════════════════════════════════════════
// 导入
// ═══════════════════════════════════════════════════════════════════════════
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 };