2171 lines
99 KiB
JavaScript
2171 lines
99 KiB
JavaScript
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|||
|
|
// 导入
|
|||
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|||
|
|
|
|||
|
|
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 };
|