调整-局部剧情-流式开关-寻找NPC按钮-编辑模板形式。

This commit is contained in:
RT15548
2025-12-24 03:09:05 +08:00
committed by GitHub
parent 19352d9f56
commit 29bc41fe19
3 changed files with 493 additions and 117 deletions

View File

@@ -27,6 +27,7 @@ import { getContext } from "../../../../../st-context.js";
import { streamingGeneration } from "../streaming-generation.js";
import { EXT_ID, extensionFolderPath } from "../../core/constants.js";
import { createModuleEvents, event_types } from "../../core/event-manager.js";
import { StoryOutlinePromptStorage, StoryOutlineSettingsStorage } from "../../core/server-storage.js";
import { promptManager } from "../../../../../openai.js";
import {
buildSmsMessages, buildSummaryMessages, buildSmsHistoryContent, buildExistingSummaryContent,
@@ -220,7 +221,7 @@ function getOutlineStore() {
if (!chat_metadata) return null;
const ext = chat_metadata.extensions ||= {}, lwb = ext[EXT_ID] ||= {};
return lwb.storyOutline ||= {
mapData: null, stage: 0, deviationScore: 0, simulationProgress: 0, simulationTarget: 5, playerLocation: '家',
mapData: null, stage: 0, deviationScore: 0, simulationTarget: 5, playerLocation: '家',
outlineData: { meta: null, world: null, outdoor: null, indoor: null, sceneSetup: null, strangers: null, contacts: null },
dataChecked: { meta: true, world: true, outdoor: true, indoor: true, sceneSetup: true, strangers: false, contacts: false, characterContactSms: false }
};
@@ -229,7 +230,7 @@ function getOutlineStore() {
/** 全局/通讯设置读写 */
const getGlobalSettings = () => getStore(STORAGE_KEYS.global, { apiUrl: '', apiKey: '', model: '', mode: 'assist' });
const saveGlobalSettings = s => setStore(STORAGE_KEYS.global, s);
const getCommSettings = () => ({ historyCount: 50, npcPosition: 0, npcOrder: 100, ...getStore(STORAGE_KEYS.comm, {}) });
const getCommSettings = () => ({ historyCount: 50, npcPosition: 0, npcOrder: 100, stream: false, ...getStore(STORAGE_KEYS.comm, {}) });
const saveCommSettings = s => setStore(STORAGE_KEYS.comm, s);
/** 获取角色卡信息 */
@@ -252,10 +253,44 @@ function getCharSmsHistory() {
// ==================== 5. LLM调用 ====================
const STREAM_DONE_EVT = 'xiaobaix_streaming_completed';
let streamLlmQueue = Promise.resolve();
function createStreamingWaiter(sessionId, timeoutMs = 180000) {
let done = false;
let timer = null;
let handler = null;
const cleanup = () => {
if (done) return;
done = true;
try { if (timer) clearTimeout(timer); } catch {}
try { eventSource.removeListener?.(STREAM_DONE_EVT, handler); } catch {}
};
const promise = new Promise((resolve, reject) => {
handler = (payload) => {
if (!payload || payload.sessionId !== sessionId) return;
cleanup();
resolve(String(payload.finalText ?? ''));
};
timer = setTimeout(() => {
cleanup();
reject(new Error('Streaming timeout'));
}, timeoutMs);
try { eventSource.on?.(STREAM_DONE_EVT, handler); } catch (e) {
cleanup();
reject(e);
}
});
return { promise, cleanup };
}
/** 调用LLM */
async function callLLM(promptOrMsgs, useRaw = false) {
const { apiUrl, apiKey, model } = getGlobalSettings();
const useStream = !!getCommSettings()?.stream;
const normalize = r => {
if (r == null) return '';
@@ -271,47 +306,81 @@ async function callLLM(promptOrMsgs, useRaw = false) {
return String(r);
};
// 构建基础选项
const opts = { nonstream: 'true', lock: 'on' };
if (apiUrl?.trim()) Object.assign(opts, { api: 'openai', apiurl: apiUrl.trim(), ...(apiKey && { apipassword: apiKey }), ...(model && { model }) });
const baseOpts = { lock: 'on' };
if (!useStream) baseOpts.nonstream = 'true';
if (apiUrl?.trim()) Object.assign(baseOpts, { api: 'openai', apiurl: apiUrl.trim(), ...(apiKey && { apipassword: apiKey }), ...(model && { model }) });
if (useRaw) {
const messages = Array.isArray(promptOrMsgs)
? promptOrMsgs
: [{ role: 'user', content: String(promptOrMsgs || '').trim() }];
if (!useStream) {
const opts = { ...baseOpts };
// 直接把消息转成 top 参数格式,不做预处理
// {$worldInfo} 和 {$historyN} 由 xbgenrawCommand 内部处理
const roleMap = { user: 'user', assistant: 'assistant', system: 'sys' };
const topParts = messages
.filter(m => m?.role && typeof m.content === 'string' && m.content.trim())
.map(m => {
const role = roleMap[m.role] || m.role;
return `${role}={${m.content}}`;
});
const topParam = topParts.join(';');
if (useRaw) {
const messages = Array.isArray(promptOrMsgs)
? promptOrMsgs
: [{ role: 'user', content: String(promptOrMsgs || '').trim() }];
opts.top = topParam;
// 不设置 addon让 xbgenrawCommand 自己处理 {$worldInfo} 占位符替换
const roleMap = { user: 'user', assistant: 'assistant', system: 'sys' };
const topParts = messages
.filter(m => m?.role && typeof m.content === 'string' && m.content.trim())
.map(m => {
const role = roleMap[m.role] || m.role;
return `${role}={${m.content}}`;
});
const topParam = topParts.join(';');
opts.top = topParam;
const raw = await streamingGeneration.xbgenrawCommand(opts, '');
const text = normalize(raw).trim();
const raw = await streamingGeneration.xbgenrawCommand(opts, '');
const text = normalize(raw).trim();
if (isDebug()) {
try {
console.groupCollapsed('[StoryOutline] callLLM(useRaw via xbgenrawCommand)');
console.log('opts.top.length', topParam.length);
console.log('raw', raw);
console.log('normalized.length', text.length);
console.groupEnd();
} catch { }
if (isDebug()) {
try {
console.groupCollapsed('[StoryOutline] callLLM(useRaw via xbgenrawCommand)');
console.log('opts.top.length', topParam.length);
console.log('raw', raw);
console.log('normalized.length', text.length);
console.groupEnd();
} catch { }
}
return text;
}
return text;
opts.as = 'user';
opts.position = 'history';
return normalize(await streamingGeneration.xbgenCommand(opts, promptOrMsgs)).trim();
}
opts.as = 'user';
opts.position = 'history';
return normalize(await streamingGeneration.xbgenCommand(opts, promptOrMsgs)).trim();
const runStreaming = async () => {
const sessionId = 'xb10';
const waiter = createStreamingWaiter(sessionId);
const opts = { ...baseOpts, id: sessionId };
try {
if (useRaw) {
const messages = Array.isArray(promptOrMsgs)
? promptOrMsgs
: [{ role: 'user', content: String(promptOrMsgs || '').trim() }];
const roleMap = { user: 'user', assistant: 'assistant', system: 'sys' };
const topParts = messages
.filter(m => m?.role && typeof m.content === 'string' && m.content.trim())
.map(m => {
const role = roleMap[m.role] || m.role;
return `${role}={${m.content}}`;
});
opts.top = topParts.join(';');
await streamingGeneration.xbgenrawCommand(opts, '');
return (await waiter.promise).trim();
}
opts.as = 'user';
opts.position = 'history';
await streamingGeneration.xbgenCommand(opts, promptOrMsgs);
return (await waiter.promise).trim();
} finally {
waiter.cleanup();
}
};
streamLlmQueue = streamLlmQueue.then(runStreaming, runStreaming);
return streamLlmQueue;
}
/** 调用LLM并解析JSON */
@@ -444,11 +513,17 @@ function formatOutlinePrompt() {
if (c?.strangers && d.strangers?.length) { charC += "* 陌路人:\n"; d.strangers.forEach(p => charC += ` - ${p.name}${p.location ? ` @ ${p.location}` : ''}: ${p.info || ''}\n`); }
if (charC) { has = true; text += `### 周边人物 (Characters)\n${charC}\n`; }
// 当前剧情
if (c?.sceneSetup && d.sceneSetup) {
const ss = d.sceneSetup.sideStory || d.sceneSetup.side_story || d.sceneSetup;
if (ss && (ss.surface || ss.inner)) { has = true; text += "### 当前剧情 (Current Scene)\n"; if (ss.surface) text += `* 表象: ${ss.surface}\n`; if (ss.inner) text += `* 里层 (潜台词): ${ss.inner}\n`; text += "\n"; }
}
// 当前剧情
if (c?.sceneSetup && d.sceneSetup) {
const ss = d.sceneSetup.sideStory || d.sceneSetup.side_story || d.sceneSetup;
if (ss && (ss.Facade || ss.Undercurrent)) {
has = true;
text += "### 当前剧情 (Current Scene)\n";
if (ss.Facade) text += `* 表现: ${ss.Facade}\n`;
if (ss.Undercurrent) text += `* 暗流: ${ss.Undercurrent}\n`;
text += "\n";
}
}
// 角色卡短信
if (c?.characterContactSms) {
@@ -539,7 +614,7 @@ function sendSettings() {
const store = getOutlineStore(), { name: charName, desc: charDesc } = getCharInfo();
postFrame({
type: "LOAD_SETTINGS", globalSettings: getGlobalSettings(), commSettings: getCommSettings(),
stage: store?.stage ?? 0, deviationScore: store?.deviationScore ?? 0, simulationProgress: store?.simulationProgress ?? 0,
stage: store?.stage ?? 0, deviationScore: store?.deviationScore ?? 0,
simulationTarget: store?.simulationTarget ?? 5, playerLocation: store?.playerLocation ?? '家',
dataChecked: store?.dataChecked || {}, outlineData: store?.outlineData || {}, promptConfig: getPromptConfigPayload?.(),
characterCardName: charName, characterCardDescription: charDesc,
@@ -549,6 +624,18 @@ function sendSettings() {
const loadAndSend = () => { const s = getOutlineStore(); if (s?.mapData) postFrame({ type: "LOAD_MAP_DATA", mapData: s.mapData }); sendSettings(); };
function sendSimStateOnly() {
const store = getOutlineStore();
postFrame({
type: "LOAD_SETTINGS",
commSettings: getCommSettings(),
stage: store?.stage ?? 0,
deviationScore: store?.deviationScore ?? 0,
simulationTarget: store?.simulationTarget ?? 5,
playerLocation: store?.playerLocation ?? '家',
});
}
// ==================== 9. 请求处理器 ====================
const reply = (type, reqId, data) => postFrame({ type, requestId: reqId, ...data });
@@ -578,19 +665,26 @@ function mergeSimData(orig, upd) {
return r;
}
/** 检查自动推演 */
async function checkAutoSim(reqId) {
const store = getOutlineStore();
if (!store || (store.simulationProgress || 0) < (store.simulationTarget ?? 5)) return;
const data = { meta: store.outlineData?.meta || {}, world: store.outlineData?.world || null, maps: { outdoor: store.outlineData?.outdoor || null, indoor: store.outlineData?.indoor || null } };
await handleSimWorld({ requestId: `wsim_auto_${Date.now()}`, currentData: JSON.stringify(data), isAuto: true });
function tickSimCountdown(store) {
if (!store) return;
const prevRaw = Number(store.simulationTarget);
const prev = Number.isFinite(prevRaw) ? prevRaw : 5;
const next = prev - 1;
store.simulationTarget = next;
store.updatedAt = Date.now();
saveMetadataDebounced?.();
sendSimStateOnly();
if (prev > 0 && next <= 0) {
try { processCommands?.('/echo 该进行世界推演啦!'); } catch {}
}
}
// 验证器
const V = {
sum: o => o?.summary, npc: o => o?.name && o?.aliases, arr: o => Array.isArray(o),
scene: o => !!o?.review?.deviation && !!(o?.local_map || o?.scene_setup?.local_map),
lscene: o => !!o?.side_story, inv: o => typeof o?.invite === 'boolean' && o?.reply,
lscene: o => !!(o?.side_story?.Incident && o?.side_story?.Facade && o?.side_story?.Undercurrent),
inv: o => typeof o?.invite === 'boolean' && o?.reply,
sms: o => typeof o?.reply === 'string' && o.reply.length > 0,
wg1: d => !!d && typeof d === 'object', // 只要是对象就行,后续会 normalize
wg2: d => !!(d?.world && (d?.maps || d?.world?.maps)?.outdoor),
@@ -783,10 +877,9 @@ async function handleSceneSwitch({ requestId, prevLocationName, prevLocationInfo
const data = await callLLMJson({ messages: msgs, validate: V.scene });
if (!data || !V.scene(data)) return replyErr('SCENE_SWITCH_RESULT', requestId, '场景生成失败:无法解析 JSON 数据');
const delta = data.review?.deviation?.score_delta || 0, old = store?.deviationScore || 0, newS = Math.min(100, Math.max(0, old + delta));
if (store) { store.deviationScore = newS; if (targetLocationType !== 'home') store.simulationProgress = (store.simulationProgress || 0) + 1; saveMetadataDebounced?.(); }
if (store) { store.deviationScore = newS; tickSimCountdown(store); }
const lm = data.local_map || data.scene_setup?.local_map || null;
reply('SCENE_SWITCH_RESULT', requestId, { success: true, sceneData: { review: data.review, localMap: lm, strangers: [], scoreDelta: delta, newScore: newS } });
checkAutoSim(requestId);
} catch (e) { replyErr('SCENE_SWITCH_RESULT', requestId, `场景切换失败: ${e.message}`); }
}
@@ -816,6 +909,7 @@ async function handleGenLocalMap({ requestId, outdoorDescription }) {
const msgs = buildLocalMapGenMessages({ storyOutline: formatOutlinePrompt(), outdoorDescription: outdoorDescription || '', historyCount: getCommSettings().historyCount || 50 });
const data = await callLLMJson({ messages: msgs, validate: V.lm });
if (!data?.inside) return replyErr('GENERATE_LOCAL_MAP_RESULT', requestId, '局部地图生成失败:无法解析 JSON 数据');
tickSimCountdown(getOutlineStore());
reply('GENERATE_LOCAL_MAP_RESULT', requestId, { success: true, localMapData: data.inside });
} catch (e) { replyErr('GENERATE_LOCAL_MAP_RESULT', requestId, `局部地图生成失败: ${e.message}`); }
}
@@ -826,6 +920,7 @@ async function handleRefreshLocalMap({ requestId, locationName, currentLocalMap,
const msgs = buildLocalMapRefreshMessages({ storyOutline: formatOutlinePrompt(), locationName: locationName || store?.playerLocation || '未知地点', locationInfo: currentLocalMap?.description || '', currentLocalMap: currentLocalMap || null, outdoorDescription: outdoorDescription || '', historyCount: comm.historyCount || 50, playerLocation: store?.playerLocation });
const data = await callLLMJson({ messages: msgs, validate: V.lm });
if (!data?.inside) return replyErr('REFRESH_LOCAL_MAP_RESULT', requestId, '局部地图刷新失败:无法解析 JSON 数据');
tickSimCountdown(store);
reply('REFRESH_LOCAL_MAP_RESULT', requestId, { success: true, localMapData: data.inside });
} catch (e) { replyErr('REFRESH_LOCAL_MAP_RESULT', requestId, `局部地图刷新失败: ${e.message}`); }
}
@@ -836,11 +931,11 @@ async function handleGenLocalScene({ requestId, locationName, locationInfo }) {
const msgs = buildLocalSceneGenMessages({ storyOutline: formatOutlinePrompt(), locationName: locationName || store?.playerLocation || '未知地点', locationInfo: locationInfo || '', stage: store?.stage || 0, currentAtmosphere: getAtmosphere(store), historyCount: comm.historyCount || 50, mode, playerLocation: store?.playerLocation });
const data = await callLLMJson({ messages: msgs, validate: V.lscene });
if (!data || !V.lscene(data)) return replyErr('GENERATE_LOCAL_SCENE_RESULT', requestId, '局部剧情生成失败:无法解析 JSON 数据');
if (store) { store.simulationProgress = (store.simulationProgress || 0) + 1; saveMetadataDebounced?.(); }
const ssf = data.side_story || null, intro = ssf?.Introduce || ssf?.introduce || '';
const ss = ssf ? (() => { const { Introduce, introduce: i2, story, ...rest } = ssf; return rest; })() : null;
reply('GENERATE_LOCAL_SCENE_RESULT', requestId, { success: true, sceneSetup: { sideStory: ss, review: data.review || null }, introduce: intro, loc: locationName });
checkAutoSim(requestId);
tickSimCountdown(store);
const ssf = data.side_story || null;
const intro = (ssf?.Incident || '').trim();
const ss = ssf ? { Facade: ssf.Facade || '', Undercurrent: ssf.Undercurrent || '' } : null;
reply('GENERATE_LOCAL_SCENE_RESULT', requestId, { success: true, sceneSetup: { sideStory: ss, review: data.review || null }, introduce: intro, loc: locationName });
} catch (e) { replyErr('GENERATE_LOCAL_SCENE_RESULT', requestId, `局部剧情生成失败: ${e.message}`); }
}
@@ -898,7 +993,7 @@ async function handleGenWorld({ requestId, playerRequests }) {
const msgs = buildWorldGenStep2Messages({ historyCount: comm.historyCount || 50, playerRequests, mode: 'assist' });
const wd = await callLLMJson({ messages: msgs, validate: V.wga });
if (!wd?.maps?.outdoor || !Array.isArray(wd.maps.outdoor.nodes)) return replyErr('GENERATE_WORLD_RESULT', requestId, '生成失败:返回数据缺少地图节点');
if (store) { Object.assign(store, { stage: 0, deviationScore: 0, simulationProgress: 0, simulationTarget: randRange(3, 7) }); store.outlineData = { ...wd }; saveMetadataDebounced?.(); }
if (store) { Object.assign(store, { stage: 0, deviationScore: 0, simulationTarget: randRange(3, 7) }); store.outlineData = { ...wd }; saveMetadataDebounced?.(); sendSimStateOnly(); }
return reply('GENERATE_WORLD_RESULT', requestId, { success: true, worldData: wd });
}
@@ -925,7 +1020,7 @@ async function handleGenWorld({ requestId, playerRequests }) {
const final = { meta: s1d.meta, world: s2d.world, maps: s2d.maps, playerLocation: s2d.playerLocation };
step1Cache = null;
if (store) { Object.assign(store, { stage: 0, deviationScore: 0, simulationProgress: 0, simulationTarget: randRange(3, 7) }); store.outlineData = final; saveMetadataDebounced?.(); }
if (store) { Object.assign(store, { stage: 0, deviationScore: 0, simulationTarget: randRange(3, 7) }); store.outlineData = final; saveMetadataDebounced?.(); sendSimStateOnly(); }
reply('GENERATE_WORLD_RESULT', requestId, { success: true, worldData: final });
} catch (e) { replyErr('GENERATE_WORLD_RESULT', requestId, `生成失败: ${e.message}`); }
}
@@ -946,7 +1041,7 @@ async function handleRetryStep2({ requestId }) {
const final = { meta: s1d.meta, world: s2d.world, maps: s2d.maps, playerLocation: s2d.playerLocation };
step1Cache = null;
if (store) { Object.assign(store, { stage: 0, deviationScore: 0, simulationProgress: 0, simulationTarget: randRange(3, 7) }); store.outlineData = final; saveMetadataDebounced?.(); }
if (store) { Object.assign(store, { stage: 0, deviationScore: 0, simulationTarget: randRange(3, 7) }); store.outlineData = final; saveMetadataDebounced?.(); sendSimStateOnly(); }
reply('GENERATE_WORLD_RESULT', requestId, { success: true, worldData: final });
} catch (e) { replyErr('GENERATE_WORLD_RESULT', requestId, `Step 2 重试失败: ${e.message}`); }
}
@@ -958,7 +1053,7 @@ async function handleSimWorld({ requestId, currentData, isAuto }) {
const data = await callLLMJson({ messages: msgs, validate: V.w });
if (!data || !V.w(data)) return replyErr('SIMULATE_WORLD_RESULT', requestId, mode === 'assist' ? '世界推演失败:无法解析 JSON 数据(需包含 world 或 maps 字段)' : '世界推演失败:无法解析 JSON 数据');
const orig = safe(() => JSON.parse(currentData)) || {}, merged = mergeSimData(orig, data);
if (store) { store.stage = (store.stage || 0) + 1; store.simulationProgress = 0; store.simulationTarget = randRange(3, 7); saveMetadataDebounced?.(); }
if (store) { store.stage = (store.stage || 0) + 1; store.simulationTarget = randRange(3, 7); saveMetadataDebounced?.(); sendSimStateOnly(); }
reply('SIMULATE_WORLD_RESULT', requestId, { success: true, simData: merged, isAuto: !!isAuto });
} catch (e) { replyErr('SIMULATE_WORLD_RESULT', requestId, `推演失败: ${e.message}`); }
}
@@ -968,18 +1063,25 @@ function handleSaveSettings(d) {
if (d.commSettings) saveCommSettings(d.commSettings);
const store = getOutlineStore();
if (store) {
['stage', 'deviationScore', 'simulationProgress', 'simulationTarget', 'playerLocation'].forEach(k => { if (d[k] !== undefined) store[k] = d[k]; });
['stage', 'deviationScore', 'simulationTarget', 'playerLocation'].forEach(k => { if (d[k] !== undefined) store[k] = d[k]; });
if (d.dataChecked) store.dataChecked = d.dataChecked;
if (d.allData) store.outlineData = d.allData;
store.updatedAt = Date.now();
saveMetadataDebounced?.();
}
injectOutline();
try {
StoryOutlineSettingsStorage?.set?.('settings', {
globalSettings: getGlobalSettings(),
commSettings: getCommSettings(),
});
} catch {}
}
function handleSavePrompts(d) {
if (!d?.promptConfig) return;
setPromptConfig?.(d.promptConfig, true);
const payload = setPromptConfig?.(d.promptConfig, true);
try { StoryOutlinePromptStorage?.set?.('promptConfig', payload || d.promptConfig); } catch {}
postFrame({ type: "PROMPT_CONFIG_UPDATED", promptConfig: getPromptConfigPayload?.() });
}
@@ -1194,8 +1296,28 @@ document.addEventListener('xiaobaixEnabledChanged', e => {
// ==================== 初始化 ====================
async function initPromptConfigFromServer() {
try {
const cfg = await StoryOutlinePromptStorage?.get?.('promptConfig', null);
if (!cfg) return;
setPromptConfig?.(cfg, true);
postFrame({ type: "PROMPT_CONFIG_UPDATED", promptConfig: getPromptConfigPayload?.() });
} catch { }
}
async function initSettingsFromServer() {
try {
const s = await StoryOutlineSettingsStorage?.get?.('settings', null);
if (!s || typeof s !== 'object') return;
if (s.globalSettings) saveGlobalSettings(s.globalSettings);
if (s.commSettings) saveCommSettings(s.commSettings);
} catch { }
}
jQuery(() => {
if (!getSettings().storyOutline?.enabled) return;
initSettingsFromServer();
initPromptConfigFromServer();
registerEvents();
setTimeout(injectOutline, 200);
window.registerModuleCleanup?.('storyOutline', cleanup);