Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
9b41710
Strip everything before and including </think> (handles unclosed thin…
Hao19911125 Mar 1, 2026
e38278e
Log 样式优化
Hao19911125 Mar 1, 2026
08d7094
Log样式优化
Hao19911125 Mar 1, 2026
24ca510
小白板内容曝露给ena-planner
Hao19911125 Mar 1, 2026
f5eacba
小白板内容曝露给ena-planner
Hao19911125 Mar 1, 2026
c66366f
Merge branch 'RT15548:main' into main
Hao19911125 Mar 3, 2026
ce32861
修正世界书宏读取问题
Hao19911125 Mar 4, 2026
fe9736a
修正summary触发绿灯的问题
Hao19911125 Mar 9, 2026
d7f6d1f
向量存储到ST端
Hao19911125 Mar 9, 2026
ea940be
向量存储到ST端
Hao19911125 Mar 9, 2026
554966f
向量到ST服务器
Hao19911125 Mar 9, 2026
46d92fc
向量存储到ST端
Hao19911125 Mar 9, 2026
d6c3f45
backup file名称修正
Hao19911125 Mar 9, 2026
7275692
存取向量逻辑修正
Hao19911125 Mar 9, 2026
1a2eb50
切聊天时清掉旧 summary
Hao19911125 Mar 10, 2026
ae45853
新增向量备份管理 UI(清单 + Modal)
Mar 16, 2026
0613fed
备份管理 Modal 移至父窗口,修复层级与配色问题
Mar 16, 2026
13d79e4
删除聊天时自动清理服务器向量备份
Mar 16, 2026
d9d475b
修复 serverPath 含前导斜杠导致删除失败的问题
Mar 16, 2026
5b2d65b
normalizeManifestEntry 读取时同步 strip serverPath 前导斜杠
Mar 16, 2026
072992c
重要NPC生成路径:拆分添加按钮 + 完整角色档案模板
Hao19911125 Mar 17, 2026
63461d7
高级设置模板编辑器加注授权声明
Hao19911125 Mar 17, 2026
f8cea6a
授权声明仅在重要NPC生成模板下显示
Hao19911125 Mar 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion modules/ena-planner/ena-planner.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { EnaPlannerStorage } from '../../core/server-storage.js';
import { postToIframe, isTrustedIframeEvent } from '../../core/iframe-messaging.js';
import { DEFAULT_PROMPT_BLOCKS, BUILTIN_TEMPLATES } from './ena-planner-presets.js';
import { formatOutlinePrompt } from '../story-outline/story-outline.js';
import jsyaml from '../../libs/js-yaml.mjs';

const EXT_NAME = 'ena-planner';
const OVERLAY_ID = 'xiaobaix-ena-planner-overlay';
Expand Down Expand Up @@ -551,6 +552,7 @@ function matchSelective(entry, scanText) {
const keys2 = Array.isArray(entry?.keysecondary) ? entry.keysecondary.filter(Boolean) : [];

const total = keys.length;
if (total === 0) return false;
const hit = keys.reduce((acc, kw) => acc + (keywordPresent(scanText, kw) ? 1 : 0), 0);

let ok = false;
Expand Down Expand Up @@ -838,6 +840,17 @@ function resolveGetMessageVariableMacros(text, messageVars) {
});
}

function resolveFormatMessageVariableMacros(text, messageVars) {
return text.replace(/{{\s*format_message_variable::([^}]+)\s*}}/g, (_, rawPath) => {
const path = String(rawPath || '').trim();
if (!path) return '';
const val = deepGet(messageVars, path);
if (val == null) return '';
if (typeof val === 'string') return val;
try { return jsyaml.dump(val, { lineWidth: -1, noRefs: true }); } catch { return safeStringify(val); }
});
}

function getLatestMessageVarTable() {
try {
if (window.Mvu?.getMvuData) {
Expand All @@ -858,6 +871,7 @@ async function renderTemplateAll(text, env, messageVars) {
out = await evalEjsIfPossible(out, env);
out = substituteMacrosViaST(out);
out = resolveGetMessageVariableMacros(out, messageVars);
out = resolveFormatMessageVariableMacros(out, messageVars);
return out;
}

Expand Down Expand Up @@ -1133,7 +1147,7 @@ async function buildPlannerMessages(rawUserInput) {
const vectorRaw = '';

// Build scanText for worldbook keyword activation
const scanText = [charBlockRaw, cachedSummary, recentChatRaw, vectorRaw, plotsRaw, rawUserInput].join('\n\n');
const scanText = [charBlockRaw, recentChatRaw, vectorRaw, plotsRaw, rawUserInput].join('\n\n');

const worldbookRaw = await buildWorldbookBlock(scanText);
const outlineRaw = typeof formatOutlinePrompt === 'function' ? (formatOutlinePrompt() || '') : '';
Expand Down
72 changes: 72 additions & 0 deletions modules/story-outline/story-outline-prompt.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,52 @@ const DEFAULT_JSON_TEMPLATES = {
"stance": "核心态度·具体表现。例如:'中立·唯利是图'、'友善·盲目崇拜' 或 '敌对·疯狂'",
"secret": "该角色掌握的一个关键信息、道具或秘密。必须结合'剧情大纲'生成,作为一个潜在的剧情钩子。"
}
}`,
importantNpc: `{
"name": "角色全名",
"aliases": ["别名1", "别名2", "英文名/拼音"],
"intro": "白描一句话:外貌+身份。仅用名词和动词,禁止形容词和比喻。例:'黑色长直发过腰,左眼下泪痣,着灰色风衣的赏金猎人。'",
"appearance": {
"build": "体型白描(如:比{{user}}高一个头。宽肩,窄腰。)",
"face": "面部白描(如:颧骨高,下颌线锐利。左眉尾有一道旧疤。)",
"hair_and_eyes": "发型发色、瞳色",
"marks": "显著标记——疤痕、痣、纹身等,无则写'无'",
"attire": "当前穿着"
},
"background": "角色来历与当前处境。必须交代因果链:什么过去→塑造了什么性格→为什么出现在当前场景。200字左右。",
"world_adaptation": {},
"personality_palette": {
"base_color": "底色——驱动一切行为的最底层核心性格(如:恐惧、控制欲、孤独)",
"main_colors": ["主色调1", "主色调2——日常最常表现出的性格"],
"accents": ["点缀——不常见但在特定情境下浮现的性格"],
"derivatives": [
"[主色调1]衍生一:(写具体场景+具体行为,不是定义。错误:'她很温柔';正确:'会在{{user}}加班时默默端一杯温水放在桌上,不说话,放下就走')",
"[主色调1]衍生二:(另一个场景的表现,衍生之间可以互相矛盾——这才是真实的人)",
"[主色调2]衍生一:...",
"[底色]衍生一:(底色通常不轻易暴露,写什么条件下会泄漏出来)",
"[点缀]衍生一:..."
]
},
"speaking": {
"style": "语气、语速、口癖、惯用词",
"samples": ["台词示例1——展现主色调", "台词示例2——展现底色泄漏", "台词示例3——展现对{{user}}的态度"],
"attitude_to_user": "对{{user}}的态度及其原因"
},
"understanding": [
{
"about": "某个性格特质或行为模式",
"clarification": "这个特质的真正含义是……不是……在什么情况下会……常见误读是……正确理解是……"
},
{
"about": "另一个容易被AI误读的特质",
"clarification": "解释动机而非重复描述。预判AI可能的补全方向并提前纠正。"
}
],
"game_data": {
"stance": "核心态度·具体表现(如:'中立·唯利是图'、'友善·盲目崇拜'、'敌对·疯狂')",
"secret": "角色掌握的一个关键秘密/信息/道具。必须结合剧情大纲生成,作为剧情钩子。",
"motivation": "核心驱动力与行动优先级准则"
}
}`,
stranger: `[{ "name": "角色名", "location": "当前地点", "info": "一句话简介" }]`,
worldGenStep1: `{
Expand Down Expand Up @@ -258,6 +304,31 @@ const DEFAULT_PROMPTS = {
u2: v => `${worldInfo}\n\n${history(v.historyCount)}\n\n剧情秘密大纲(*从这里提取线索赋予角色秘密*):\n${wrap('story_outline', v.storyOutline) || '<story_outline>\n(无)\n</story_outline>'}\n\n需要生成:【${v.strangerName} - ${v.strangerInfo}】\n\n输出要求:\n1. 必须是合法 JSON\n2. 使用标准 JSON 语法:所有键名和字符串都使用半角双引号 "\n3. 文本字段(intro/background/persona/game_data 等)中,如需表示引号,请使用单引号或中文引号「」或"",不要使用半角双引号 "\n4. aliases须含简称或绰号\n\n模板:${JSON_TEMPLATES.npc}`,
a2: () => `了解,开始生成JSON:`
},
importantNpc: {
u1: v => `你是TRPG重要角色档案生成器。将陌生人【${v.strangerName} - ${v.strangerInfo}】扩充为剧情核心角色的完整档案。

核心写作原则:
1. **基础信息用绝对零度白描**:只写客观事实,不用形容词/比喻/模糊词(似乎、仿佛、宛如)。用名词和动词直接呈现。
× "她有一头好看柔顺的黑色长发" → √ "黑色长直发,过腰。瞳色黑。"
× "他身材魁梧,给人压迫感" → √ "身高比{{user}}高一个头。宽肩,厚背。"

2. **性格用调色盘+衍生展开**:人的性格像调色盘,底色是最深层驱动力,主色调是日常表现,点缀是偶尔闪现的侧面。每种性格必须通过"衍生"展开为具体场景行为——不是贴标签,是写"在什么情况下会做什么"。衍生之间可以互相矛盾,这才是真实的人。
× "温柔衍生:她很温柔,对人很好。"(标签重复)
√ "温柔衍生:生气时——真正生气基本都和{{user}}有关。当有人欺负{{user}},她会一把拉住{{user}}让其靠自己,然后用冰冷目光看对方。"

3. **台词示例**:3句具体台词,分别展现主色调、底色泄漏、对{{user}}的态度。

4. **二次解释(understanding数组)**:逐条针对角色最容易被误读的性格特质,写结构化纠偏。每条包含about(哪个特质)和clarification(真正含义、不是什么、在什么情况下怎样)。至少2条。这不是重复调色盘,是解释动机和预判误读。
× "关于温柔:她很温柔,对人好。"(重复调色盘)
√ "关于乐观的双重性:和{{user}}在一起时是真实的,和其他人相处时是维持人设的假象。脆弱时只会在{{user}}面前表现。"

5. **世界观适配(world_adaptation对象)**:根据故事世界观动态生成键值对。修仙世界→灵根、境界、功法等字段;赛博世界→义体部位、型号、副作用等字段;现代世界→可留空对象。不预设固定字段,由你根据世界观判断需要什么。

基于世界观、剧情大纲和现有角色关系,输出严格JSON。`,
a1: () => `明白。我将严格遵循白描原则和调色盘衍生写法,按JSON模板输出完整角色档案,不含多余文本。`,
u2: v => `${worldInfo}\n\n${history(v.historyCount)}\n\n剧情秘密大纲(*从这里提取线索赋予角色秘密和动机*):\n${wrap('story_outline', v.storyOutline) || '<story_outline>\n(无)\n</story_outline>'}\n\n需要生成:【${v.strangerName} - ${v.strangerInfo}】\n\n输出要求:\n1. 必须是合法 JSON\n2. 使用标准 JSON 语法:所有键名和字符串都使用半角双引号 "\n3. 文本字段中如需表示引号,请使用单引号或中文引号「」或"",不要使用半角双引号 "\n4. aliases须含简称或绰号\n5. personality_palette.derivatives 至少5条,每条都是具体场景+具体行为\n6. speaking.samples 须3句具体台词\n7. understanding数组至少2条,每条须含about和clarification\n8. world_adaptation根据世界观动态生成键值对,无特殊体系则输出空对象{}\n9. 总输出约800-1500字\n\n模板:${JSON_TEMPLATES.importantNpc}`,
a2: () => `了解,开始以白描+调色盘衍生法生成重要角色档案JSON:`
},
stranger: {
u1: v => `你是TRPG数据整理助手。从剧情文本中提取{{user}}遇到的陌生人/NPC,整理为JSON数组。`,
a1: () => `明白。请提供【世界观】和【剧情经历】,我将提取角色并以JSON数组输出。`,
Expand Down Expand Up @@ -585,6 +656,7 @@ export const buildSmsMessages = v => build('sms', v);
export const buildSummaryMessages = v => build('summary', v);
export const buildInviteMessages = v => build('invite', v);
export const buildNpcGenerationMessages = v => build('npc', v);
export const buildImportantNpcGenerationMessages = v => build('importantNpc', v);
export const buildExtractStrangersMessages = v => build('stranger', v);
export const buildWorldGenStep1Messages = v => build('worldGenStep1', v);
export const buildWorldGenStep2Messages = v => build('worldGenStep2', v);
Expand Down
6 changes: 4 additions & 2 deletions modules/story-outline/story-outline.html

Large diffs are not rendered by default.

9 changes: 6 additions & 3 deletions modules/story-outline/story-outline.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import { StoryOutlineStorage } from "../../core/server-storage.js";
import { promptManager } from "../../../../../openai.js";
import {
buildSmsMessages, buildSummaryMessages, buildSmsHistoryContent, buildExistingSummaryContent,
buildNpcGenerationMessages, formatNpcToWorldbookContent, buildExtractStrangersMessages,
buildNpcGenerationMessages, buildImportantNpcGenerationMessages, formatNpcToWorldbookContent, buildExtractStrangersMessages,
buildWorldGenStep1Messages, buildWorldGenStep2Messages, buildWorldSimMessages, buildSceneSwitchMessages,
buildInviteMessages, buildLocalMapGenMessages, buildLocalMapRefreshMessages, buildLocalSceneGenMessages,
buildOverlayHtml, MOBILE_LAYOUT_STYLE, DESKTOP_LAYOUT_STYLE, getPromptConfigPayload, setPromptConfig
Expand Down Expand Up @@ -874,14 +874,17 @@ async function handleCheckStrangerWb({ requestId, strangerName }) {
postFrame({ type: 'CHECK_STRANGER_WORLDBOOK_RESULT', requestId, found: !!r, ...(r && { worldbookUid: r.uid, worldbook: r.bookName, entryName: r.entry.comment || r.entry.key?.[0] || strangerName }) });
}

async function handleGenNpc({ requestId, strangerName, strangerInfo }) {
async function handleGenNpc({ requestId, strangerName, strangerInfo, npcType = 'npc' }) {
try {
const comm = getCommSettings();
const ctx = getContext(), char = ctx.characters?.[ctx.characterId];
if (!char) return replyErr('GENERATE_NPC_RESULT', requestId, '未找到当前角色卡');
const primary = char.data?.extensions?.world;
if (!primary || !world_names?.includes(primary)) return replyErr('GENERATE_NPC_RESULT', requestId, '角色卡未绑定世界书,请先绑定世界书');
const msgs = buildNpcGenerationMessages(getCommonPromptVars({ strangerName, strangerInfo: strangerInfo || '(无描述)' }));
const vars = getCommonPromptVars({ strangerName, strangerInfo: strangerInfo || '(无描述)' });
const msgs = npcType === 'importantNpc'
? buildImportantNpcGenerationMessages(vars)
: buildNpcGenerationMessages(vars);
const npc = await callLLMJson({ messages: msgs, validate: V.npc });
if (!npc?.name) return replyErr('GENERATE_NPC_RESULT', requestId, 'NPC 生成失败:无法解析 JSON 数据');
const wd = await loadWorldInfo(primary); if (!wd) return replyErr('GENERATE_NPC_RESULT', requestId, `无法加载世界书: ${primary}`);
Expand Down
36 changes: 36 additions & 0 deletions modules/story-summary/story-summary-ui.js
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,19 @@
$('vector-io-status').textContent = '导入中...';
postMsg('VECTOR_IMPORT_PICK');
};
$('btn-backup-server').onclick = () => {
$('btn-backup-server').disabled = true;
$('server-io-status').textContent = '备份中...';
postMsg('VECTOR_BACKUP_SERVER');
};

$('btn-restore-server').onclick = () => {
$('btn-restore-server').disabled = true;
$('server-io-status').textContent = '恢复中...';
postMsg('VECTOR_RESTORE_SERVER');
};

$('btn-manage-backups').onclick = () => postMsg('VECTOR_LIST_BACKUPS');

initAnchorUI();
postMsg('REQUEST_ANCHOR_STATS');
Expand Down Expand Up @@ -1500,6 +1513,28 @@
$('vector-io-status').textContent = '导入失败: ' + (d.error || '未知错误');
}
break;
case 'VECTOR_BACKUP_RESULT':
$('btn-backup-server').disabled = false;
if (d.success) {
$('server-io-status').textContent = `☁️ 备份成功: ${(d.size / 1024 / 1024).toFixed(2)}MB (${d.chunkCount} 片段, ${d.eventCount} 事件)`;
} else {
$('server-io-status').textContent = '备份失败: ' + (d.error || '未知错误');
}
break;

case 'VECTOR_RESTORE_RESULT':
$('btn-restore-server').disabled = false;
if (d.success) {
let msg = `☁️ 恢复成功: ${d.chunkCount} 片段, ${d.eventCount} 事件`;
if (d.warnings?.length) {
msg += '\n⚠️ ' + d.warnings.join('\n⚠️ ');
}
$('server-io-status').textContent = msg;
postMsg('REQUEST_VECTOR_STATS');
} else {
$('server-io-status').textContent = '恢复失败: ' + (d.error || '未知错误');
}
break;

case 'RECALL_LOG':
setRecallLog(d.text || '');
Expand Down Expand Up @@ -1777,4 +1812,5 @@

setHtml(container, html);
}

})();
13 changes: 13 additions & 0 deletions modules/story-summary/story-summary.html
Original file line number Diff line number Diff line change
Expand Up @@ -561,6 +561,18 @@ <h2 id="editor-title">编辑</h2>
style="flex:1">导入向量数据</button>
</div>
<div class="settings-hint" id="vector-io-status"></div>
<div class="settings-btn-row" style="margin-top:6px">
<button class="btn btn-sm" id="btn-backup-server"
style="flex:1">☁️ 备份向量到服务器</button>
<button class="btn btn-sm" id="btn-restore-server"
style="flex:1">☁️ 从服务器恢复向量</button>
</div>
<div class="settings-hint" id="server-io-status"></div>
<div style="margin-top:6px">
<button class="btn btn-sm" id="btn-manage-backups" style="width:100%">
☁️ 管理服务器向量备份
</button>
</div>
</div>
</div>
</div>
Expand Down Expand Up @@ -854,6 +866,7 @@ <h2 id="confirm-title">确认操作</h2>
</div>
</div>
</div>

</body>

</html>
Loading