mirror of
https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology.git
synced 2026-05-15 22:30:38 +08:00
Integrate ENA planner task profiles
This commit is contained in:
@@ -2,10 +2,22 @@ import { extension_settings } from '../../../../extensions.js';
|
||||
import { getRequestHeaders, saveSettingsDebounced, substituteParamsExtended } from '../../../../../script.js';
|
||||
import { EnaPlannerStorage, migrateFromLWBIfNeeded } from './ena-planner-storage.js';
|
||||
import { DEFAULT_PROMPT_BLOCKS, BUILTIN_TEMPLATES } from './ena-planner-presets.js';
|
||||
import {
|
||||
createBuiltinPromptBlock,
|
||||
createCustomPromptBlock,
|
||||
createProfileId,
|
||||
ensureTaskProfiles,
|
||||
getActiveTaskProfile,
|
||||
setActiveTaskProfileId,
|
||||
upsertTaskProfile,
|
||||
} from '../prompting/prompt-profiles.js';
|
||||
import { debugLog } from '../runtime/debug-logging.js';
|
||||
import jsyaml from '../vendor/js-yaml.mjs';
|
||||
|
||||
const EXT_NAME = 'ena-planner';
|
||||
const BME_MODULE_NAME = 'st_bme';
|
||||
const PLANNER_TASK_TYPE = 'planner';
|
||||
const LEGACY_PLANNER_TASK_PROFILE_MIGRATION_VERSION = 1;
|
||||
const VECTOR_RECALL_TIMEOUT_MS = 30000;
|
||||
const PLANNER_REQUEST_TIMEOUT_MS = 90000;
|
||||
|
||||
@@ -114,6 +126,316 @@ function notifyNativeChange(kind, payload) {
|
||||
}
|
||||
}
|
||||
|
||||
function getBmeSettings() {
|
||||
const settings = extension_settings?.[BME_MODULE_NAME];
|
||||
return settings && typeof settings === 'object' ? settings : {};
|
||||
}
|
||||
|
||||
function hasPlannerTaskProfileMigration(settings = getBmeSettings()) {
|
||||
return Number(settings?.enaPlannerTaskProfileMigrationVersion || 0) >= LEGACY_PLANNER_TASK_PROFILE_MIGRATION_VERSION;
|
||||
}
|
||||
|
||||
function getPlannerTaskProfile() {
|
||||
return getActiveTaskProfile(getBmeSettings(), PLANNER_TASK_TYPE);
|
||||
}
|
||||
|
||||
function sortPlannerProfileBlocks(blocks = []) {
|
||||
return [...(Array.isArray(blocks) ? blocks : [])]
|
||||
.map((block, index) => ({ ...block, _orderIndex: index }))
|
||||
.sort((left, right) => {
|
||||
const leftOrder = Number.isFinite(Number(left?.order))
|
||||
? Number(left.order)
|
||||
: left._orderIndex;
|
||||
const rightOrder = Number.isFinite(Number(right?.order))
|
||||
? Number(right.order)
|
||||
: right._orderIndex;
|
||||
return leftOrder - rightOrder;
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeLegacyPlannerPromptBlocks(blocks = []) {
|
||||
return (Array.isArray(blocks) ? blocks : [])
|
||||
.filter((block) => block && typeof block === 'object')
|
||||
.map((block, index) => ({
|
||||
id: String(block?.id || `ena-legacy-block-${index + 1}`),
|
||||
name: String(block?.name || `提示词块 ${index + 1}`),
|
||||
role: ['system', 'user', 'assistant'].includes(String(block?.role || '').trim())
|
||||
? String(block.role).trim()
|
||||
: 'system',
|
||||
content: String(block?.content || ''),
|
||||
order: Number.isFinite(Number(block?.order)) ? Number(block.order) : index,
|
||||
}))
|
||||
.filter((block) => String(block.content || '').trim());
|
||||
}
|
||||
|
||||
function buildPlannerProfileBlocksFromLegacy(promptBlocks = []) {
|
||||
const normalizedBlocks = normalizeLegacyPlannerPromptBlocks(promptBlocks);
|
||||
const systemBlocks = normalizedBlocks.filter((block) => block.role === 'system');
|
||||
const userBlocks = normalizedBlocks.filter((block) => block.role === 'user');
|
||||
const assistantBlocks = normalizedBlocks.filter((block) => block.role === 'assistant');
|
||||
const builtins = [
|
||||
'plannerCharacterCard',
|
||||
'plannerWorldbook',
|
||||
'plannerRecentChat',
|
||||
'plannerMemory',
|
||||
'plannerPreviousPlots',
|
||||
];
|
||||
const result = [];
|
||||
let order = 0;
|
||||
|
||||
const pushCustom = (block) => {
|
||||
result.push(createCustomPromptBlock(PLANNER_TASK_TYPE, {
|
||||
name: block.name,
|
||||
role: block.role,
|
||||
content: block.content,
|
||||
injectionMode: 'relative',
|
||||
order: order++,
|
||||
}));
|
||||
};
|
||||
|
||||
systemBlocks.forEach(pushCustom);
|
||||
builtins.forEach((sourceKey) => {
|
||||
result.push(createBuiltinPromptBlock(PLANNER_TASK_TYPE, sourceKey, {
|
||||
injectionMode: 'relative',
|
||||
order: order++,
|
||||
}));
|
||||
});
|
||||
userBlocks.forEach(pushCustom);
|
||||
result.push(createBuiltinPromptBlock(PLANNER_TASK_TYPE, 'plannerUserInput', {
|
||||
injectionMode: 'relative',
|
||||
order: order++,
|
||||
}));
|
||||
assistantBlocks.forEach(pushCustom);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function normalizePlannerGenerationNumber(value) {
|
||||
if (value == null || value === '') return null;
|
||||
const num = Number(value);
|
||||
return Number.isFinite(num) ? num : null;
|
||||
}
|
||||
|
||||
function buildPlannerGenerationFromLegacyConfig(plannerConfig = {}) {
|
||||
const api = plannerConfig?.api && typeof plannerConfig.api === 'object'
|
||||
? plannerConfig.api
|
||||
: {};
|
||||
return {
|
||||
stream:
|
||||
typeof api.stream === 'boolean'
|
||||
? api.stream
|
||||
: api.stream === 'true'
|
||||
? true
|
||||
: api.stream === 'false'
|
||||
? false
|
||||
: true,
|
||||
temperature: normalizePlannerGenerationNumber(api.temperature),
|
||||
top_p: normalizePlannerGenerationNumber(api.top_p),
|
||||
top_k: normalizePlannerGenerationNumber(api.top_k),
|
||||
frequency_penalty: normalizePlannerGenerationNumber(api.frequency_penalty),
|
||||
presence_penalty: normalizePlannerGenerationNumber(api.presence_penalty),
|
||||
max_completion_tokens: normalizePlannerGenerationNumber(api.max_tokens),
|
||||
};
|
||||
}
|
||||
|
||||
function buildComparablePlannerGenerationSnapshot(generation = {}) {
|
||||
return {
|
||||
stream:
|
||||
generation?.stream === true
|
||||
? true
|
||||
: generation?.stream === false
|
||||
? false
|
||||
: null,
|
||||
temperature: normalizePlannerGenerationNumber(generation?.temperature),
|
||||
top_p: normalizePlannerGenerationNumber(generation?.top_p),
|
||||
top_k: normalizePlannerGenerationNumber(generation?.top_k),
|
||||
frequency_penalty: normalizePlannerGenerationNumber(generation?.frequency_penalty),
|
||||
presence_penalty: normalizePlannerGenerationNumber(generation?.presence_penalty),
|
||||
max_completion_tokens: normalizePlannerGenerationNumber(generation?.max_completion_tokens),
|
||||
};
|
||||
}
|
||||
|
||||
function arePlannerGenerationSettingsEquivalent(left = {}, right = {}) {
|
||||
return JSON.stringify(buildComparablePlannerGenerationSnapshot(left)) === JSON.stringify(buildComparablePlannerGenerationSnapshot(right));
|
||||
}
|
||||
|
||||
function normalizePlannerProfileBlockComparisonPayload(blocks = []) {
|
||||
return sortPlannerProfileBlocks(blocks).map((block) => ({
|
||||
role: String(block?.role || ''),
|
||||
type: String(block?.type || 'custom'),
|
||||
sourceKey: String(block?.sourceKey || ''),
|
||||
content: String(block?.content || '').trim(),
|
||||
enabled: block?.enabled !== false,
|
||||
}));
|
||||
}
|
||||
|
||||
function arePlannerProfileBlocksEquivalent(left = [], right = []) {
|
||||
return JSON.stringify(normalizePlannerProfileBlockComparisonPayload(left)) === JSON.stringify(normalizePlannerProfileBlockComparisonPayload(right));
|
||||
}
|
||||
|
||||
function buildPlannerMigrationProfileName(baseName = '', fallbackName = 'ENA 当前配置', usedNames = new Set()) {
|
||||
const base = String(baseName || '').trim() || fallbackName;
|
||||
let nextName = base;
|
||||
let suffix = 2;
|
||||
while (usedNames.has(nextName)) {
|
||||
nextName = `${base} ${suffix}`;
|
||||
suffix += 1;
|
||||
}
|
||||
usedNames.add(nextName);
|
||||
return nextName;
|
||||
}
|
||||
|
||||
function createLegacyPlannerTaskProfile(name, promptBlocks, plannerConfig, options = {}) {
|
||||
return {
|
||||
id: createProfileId(PLANNER_TASK_TYPE),
|
||||
name,
|
||||
taskType: PLANNER_TASK_TYPE,
|
||||
builtin: false,
|
||||
enabled: true,
|
||||
promptMode: 'block-based',
|
||||
updatedAt: nowISO(),
|
||||
blocks: buildPlannerProfileBlocksFromLegacy(promptBlocks),
|
||||
generation: buildPlannerGenerationFromLegacyConfig(plannerConfig),
|
||||
metadata: {
|
||||
migratedFromLegacy: true,
|
||||
enaLegacyTemplateName: String(options.templateName || ''),
|
||||
enaLegacySource: String(options.source || 'legacy-ena'),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function migrateLegacyPlannerTaskProfilesIfNeeded() {
|
||||
const settings = getBmeSettings();
|
||||
if (hasPlannerTaskProfileMigration(settings)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const plannerConfig = ensureSettings({ defaultEnabled: false });
|
||||
let nextTaskProfiles = ensureTaskProfiles(settings);
|
||||
const plannerBucket = nextTaskProfiles?.[PLANNER_TASK_TYPE] || {
|
||||
activeProfileId: 'default',
|
||||
profiles: [],
|
||||
};
|
||||
const hasExistingCustomProfiles = Array.isArray(plannerBucket.profiles)
|
||||
&& plannerBucket.profiles.some((profile) => String(profile?.id || '') !== 'default');
|
||||
|
||||
if (hasExistingCustomProfiles) {
|
||||
extension_settings[BME_MODULE_NAME] = {
|
||||
...settings,
|
||||
taskProfiles: nextTaskProfiles,
|
||||
enaPlannerTaskProfileMigrationVersion: LEGACY_PLANNER_TASK_PROFILE_MIGRATION_VERSION,
|
||||
};
|
||||
saveSettingsDebounced?.();
|
||||
return false;
|
||||
}
|
||||
|
||||
const defaultPlannerProfile = getActiveTaskProfile({}, PLANNER_TASK_TYPE);
|
||||
const defaultPlannerBlocks = Array.isArray(defaultPlannerProfile?.blocks)
|
||||
? defaultPlannerProfile.blocks
|
||||
: [];
|
||||
const defaultPlannerGeneration = defaultPlannerProfile?.generation || {};
|
||||
const currentBlocks = Array.isArray(plannerConfig.promptBlocks)
|
||||
? plannerConfig.promptBlocks
|
||||
: getDefaultSettings().promptBlocks;
|
||||
const promptTemplates = plannerConfig?.promptTemplates && typeof plannerConfig.promptTemplates === 'object'
|
||||
? plannerConfig.promptTemplates
|
||||
: {};
|
||||
const activeTemplateName = String(plannerConfig.activePromptTemplate || '').trim();
|
||||
const usedNames = new Set(
|
||||
(Array.isArray(plannerBucket.profiles) ? plannerBucket.profiles : [])
|
||||
.map((profile) => String(profile?.name || '').trim())
|
||||
.filter(Boolean),
|
||||
);
|
||||
const seenSignatures = new Set();
|
||||
const profileSpecs = [];
|
||||
let activeProfileName = '';
|
||||
|
||||
const appendProfileSpec = (name, promptBlocks, options = {}) => {
|
||||
const migratedBlocks = buildPlannerProfileBlocksFromLegacy(promptBlocks);
|
||||
const migratedGeneration = buildPlannerGenerationFromLegacyConfig(plannerConfig);
|
||||
if (
|
||||
arePlannerProfileBlocksEquivalent(migratedBlocks, defaultPlannerBlocks)
|
||||
&& arePlannerGenerationSettingsEquivalent(migratedGeneration, defaultPlannerGeneration)
|
||||
&& options.allowDefaultDuplicate !== true
|
||||
) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const signature = JSON.stringify({
|
||||
blocks: normalizePlannerProfileBlockComparisonPayload(migratedBlocks),
|
||||
generation: buildComparablePlannerGenerationSnapshot(migratedGeneration),
|
||||
});
|
||||
if (seenSignatures.has(signature)) {
|
||||
return '';
|
||||
}
|
||||
seenSignatures.add(signature);
|
||||
|
||||
const uniqueName = buildPlannerMigrationProfileName(name, options.fallbackName, usedNames);
|
||||
profileSpecs.push({
|
||||
name: uniqueName,
|
||||
promptBlocks,
|
||||
templateName: options.templateName || '',
|
||||
source: options.source || 'legacy-ena',
|
||||
active: options.active === true,
|
||||
});
|
||||
return uniqueName;
|
||||
};
|
||||
|
||||
for (const [templateName, templateBlocks] of Object.entries(promptTemplates)) {
|
||||
if (!Array.isArray(templateBlocks)) continue;
|
||||
const appendedName = appendProfileSpec(templateName, templateBlocks, {
|
||||
fallbackName: 'ENA 模板',
|
||||
templateName,
|
||||
source: 'legacy-template',
|
||||
});
|
||||
if (
|
||||
appendedName
|
||||
&& activeTemplateName === templateName
|
||||
&& arePlannerProfileBlocksEquivalent(templateBlocks, currentBlocks)
|
||||
) {
|
||||
activeProfileName = appendedName;
|
||||
}
|
||||
}
|
||||
|
||||
if (!activeProfileName) {
|
||||
activeProfileName = appendProfileSpec(
|
||||
activeTemplateName ? `${activeTemplateName}(当前)` : 'ENA 当前配置',
|
||||
currentBlocks,
|
||||
{
|
||||
fallbackName: 'ENA 当前配置',
|
||||
source: 'legacy-working-copy',
|
||||
active: true,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
let activeProfileId = '';
|
||||
for (const spec of profileSpecs) {
|
||||
const profile = createLegacyPlannerTaskProfile(spec.name, spec.promptBlocks, plannerConfig, {
|
||||
templateName: spec.templateName,
|
||||
source: spec.source,
|
||||
});
|
||||
nextTaskProfiles = upsertTaskProfile(nextTaskProfiles, PLANNER_TASK_TYPE, profile, {
|
||||
setActive: false,
|
||||
});
|
||||
if (spec.name === activeProfileName || (spec.active && !activeProfileId)) {
|
||||
activeProfileId = profile.id;
|
||||
}
|
||||
}
|
||||
|
||||
if (activeProfileId) {
|
||||
nextTaskProfiles = setActiveTaskProfileId(nextTaskProfiles, PLANNER_TASK_TYPE, activeProfileId);
|
||||
}
|
||||
|
||||
extension_settings[BME_MODULE_NAME] = {
|
||||
...settings,
|
||||
taskProfiles: nextTaskProfiles,
|
||||
enaPlannerTaskProfileMigrationVersion: LEGACY_PLANNER_TASK_PROFILE_MIGRATION_VERSION,
|
||||
};
|
||||
saveSettingsDebounced?.();
|
||||
return profileSpecs.length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* -------------------------
|
||||
* Helpers
|
||||
@@ -172,6 +494,7 @@ async function loadConfig() {
|
||||
const hasSavedConfig = !!(loaded && typeof loaded === 'object');
|
||||
config = hasSavedConfig ? loaded : getDefaultSettings({ enabled: false });
|
||||
ensureSettings({ defaultEnabled: hasSavedConfig ? true : false });
|
||||
migrateLegacyPlannerTaskProfilesIfNeeded();
|
||||
state.logs = Array.isArray(await EnaPlannerStorage.get('logs', [])) ? await EnaPlannerStorage.get('logs', []) : [];
|
||||
|
||||
if (extension_settings?.[EXT_NAME]) {
|
||||
@@ -954,27 +1277,22 @@ async function callPlanner(messages, options = {}) {
|
||||
if (!s.api.baseUrl) throw new Error('未配置 API URL');
|
||||
if (!s.api.apiKey) throw new Error('未配置 API KEY');
|
||||
if (!s.api.model) throw new Error('未选择模型');
|
||||
const generation = resolvePlannerGenerationSettings();
|
||||
|
||||
const url = buildUrl('/chat/completions');
|
||||
|
||||
const body = {
|
||||
model: s.api.model,
|
||||
messages,
|
||||
stream: !!s.api.stream
|
||||
stream: generation.stream === true
|
||||
};
|
||||
|
||||
const t = Number(s.api.temperature);
|
||||
if (!Number.isNaN(t)) body.temperature = t;
|
||||
const tp = Number(s.api.top_p);
|
||||
if (!Number.isNaN(tp)) body.top_p = tp;
|
||||
const tk = Number(s.api.top_k);
|
||||
if (!Number.isNaN(tk) && tk > 0) body.top_k = tk;
|
||||
const pp = s.api.presence_penalty === '' ? null : Number(s.api.presence_penalty);
|
||||
if (pp != null && !Number.isNaN(pp)) body.presence_penalty = pp;
|
||||
const fp = s.api.frequency_penalty === '' ? null : Number(s.api.frequency_penalty);
|
||||
if (fp != null && !Number.isNaN(fp)) body.frequency_penalty = fp;
|
||||
const mt = s.api.max_tokens === '' ? null : Number(s.api.max_tokens);
|
||||
if (mt != null && !Number.isNaN(mt) && mt > 0) body.max_tokens = mt;
|
||||
if (generation.temperature != null) body.temperature = generation.temperature;
|
||||
if (generation.top_p != null) body.top_p = generation.top_p;
|
||||
if (generation.top_k != null && generation.top_k > 0) body.top_k = generation.top_k;
|
||||
if (generation.presence_penalty != null) body.presence_penalty = generation.presence_penalty;
|
||||
if (generation.frequency_penalty != null) body.frequency_penalty = generation.frequency_penalty;
|
||||
if (generation.max_tokens != null && generation.max_tokens > 0) body.max_tokens = generation.max_tokens;
|
||||
|
||||
const controller = new AbortController();
|
||||
const plannerRequestTimeoutMs = getPlannerRequestTimeoutMs();
|
||||
@@ -996,7 +1314,7 @@ async function callPlanner(messages, options = {}) {
|
||||
throw new Error(`规划请求失败: ${res.status} ${text}`.slice(0, 500));
|
||||
}
|
||||
|
||||
if (!s.api.stream) {
|
||||
if (!generation.stream) {
|
||||
const data = await res.json();
|
||||
const text = String(data?.choices?.[0]?.message?.content ?? data?.choices?.[0]?.text ?? '');
|
||||
if (text) options?.onDelta?.(text, text);
|
||||
@@ -1219,9 +1537,89 @@ async function clearPlannerLogs() {
|
||||
* Build planner messages
|
||||
* --------------------------
|
||||
*/
|
||||
function getPromptBlocksByRole(role) {
|
||||
function resolvePlannerGenerationSettings() {
|
||||
const s = ensureSettings();
|
||||
return (s.promptBlocks || []).filter(b => b?.role === role && String(b?.content ?? '').trim());
|
||||
const profile = getPlannerTaskProfile();
|
||||
const generation = profile?.generation && typeof profile.generation === 'object'
|
||||
? profile.generation
|
||||
: {};
|
||||
|
||||
const pickNumber = (profileValue, fallbackValue) => {
|
||||
const normalizedProfileValue = normalizePlannerGenerationNumber(profileValue);
|
||||
if (normalizedProfileValue != null) return normalizedProfileValue;
|
||||
return normalizePlannerGenerationNumber(fallbackValue);
|
||||
};
|
||||
|
||||
const stream =
|
||||
generation?.stream === true
|
||||
? true
|
||||
: generation?.stream === false
|
||||
? false
|
||||
: Boolean(s.api.stream);
|
||||
|
||||
return {
|
||||
profile,
|
||||
stream,
|
||||
temperature: pickNumber(generation?.temperature, s.api.temperature),
|
||||
top_p: pickNumber(generation?.top_p, s.api.top_p),
|
||||
top_k: pickNumber(generation?.top_k, s.api.top_k),
|
||||
presence_penalty: pickNumber(generation?.presence_penalty, s.api.presence_penalty),
|
||||
frequency_penalty: pickNumber(generation?.frequency_penalty, s.api.frequency_penalty),
|
||||
max_tokens: pickNumber(generation?.max_completion_tokens, s.api.max_tokens),
|
||||
};
|
||||
}
|
||||
|
||||
function getPlannerPromptBlocksForRuntime() {
|
||||
const profile = getPlannerTaskProfile();
|
||||
const blocks = sortPlannerProfileBlocks(profile?.blocks || []).filter(
|
||||
(block) => block?.enabled !== false,
|
||||
);
|
||||
if (blocks.length > 0) {
|
||||
return {
|
||||
source: 'task-profile',
|
||||
profile,
|
||||
blocks,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
source: 'legacy-config',
|
||||
profile: null,
|
||||
blocks: normalizeLegacyPlannerPromptBlocks(ensureSettings().promptBlocks || []).map(
|
||||
(block, index) => ({
|
||||
id: block.id,
|
||||
name: block.name,
|
||||
role: block.role,
|
||||
type: 'custom',
|
||||
sourceKey: '',
|
||||
content: block.content,
|
||||
order: Number.isFinite(Number(block?.order)) ? Number(block.order) : index,
|
||||
enabled: true,
|
||||
}),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
function resolvePlannerBuiltinBlockContent(block = {}, context = {}) {
|
||||
const sourceKey = String(block?.sourceKey || '').trim();
|
||||
switch (sourceKey) {
|
||||
case 'plannerCharacterCard':
|
||||
return String(context.charBlock || '');
|
||||
case 'plannerWorldbook':
|
||||
return String(context.worldbook || '');
|
||||
case 'plannerRecentChat':
|
||||
return String(context.recentChat || '');
|
||||
case 'plannerMemory':
|
||||
return String(context.bmeMemory || '').trim()
|
||||
? `<bme_memory>\n${String(context.bmeMemory || '').trim()}\n</bme_memory>`
|
||||
: '';
|
||||
case 'plannerPreviousPlots':
|
||||
return String(context.plots || '');
|
||||
case 'plannerUserInput':
|
||||
return String(context.userMsgContent || '');
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
async function buildPlannerMessages(rawUserInput) {
|
||||
@@ -1231,10 +1629,7 @@ async function buildPlannerMessages(rawUserInput) {
|
||||
const charObj = getCurrentCharSafe();
|
||||
const env = await prepareEjsEnv();
|
||||
const messageVars = getLatestMessageVarTable();
|
||||
|
||||
const enaSystemBlocks = getPromptBlocksByRole('system');
|
||||
const enaAssistantBlocks = getPromptBlocksByRole('assistant');
|
||||
const enaUserBlocks = getPromptBlocksByRole('user');
|
||||
const plannerPromptConfig = getPlannerPromptBlocksForRuntime();
|
||||
|
||||
const charBlockRaw = formatCharCardBlock(charObj);
|
||||
|
||||
@@ -1292,51 +1687,47 @@ async function buildPlannerMessages(rawUserInput) {
|
||||
const bmeMemory = memoryBlock || '';
|
||||
const worldbook = await renderTemplateAll(worldbookRaw, env, messageVars);
|
||||
const userInput = await renderTemplateAll(rawUserInput, env, messageVars);
|
||||
const userMsgContent = `以下是玩家的最新指令哦~:\n[${userInput}]`;
|
||||
|
||||
const plannerBlockContext = {
|
||||
charBlock,
|
||||
worldbook,
|
||||
recentChat,
|
||||
bmeMemory,
|
||||
plots,
|
||||
userInput,
|
||||
userMsgContent,
|
||||
};
|
||||
|
||||
const messages = [];
|
||||
|
||||
// 1) Ena system prompts
|
||||
for (const b of enaSystemBlocks) {
|
||||
const content = await renderTemplateAll(b.content, env, messageVars);
|
||||
messages.push({ role: 'system', content });
|
||||
}
|
||||
|
||||
// 2) Character card
|
||||
if (String(charBlock).trim()) messages.push({ role: 'system', content: charBlock });
|
||||
|
||||
// 3) Worldbook
|
||||
if (String(worldbook).trim()) messages.push({ role: 'system', content: worldbook });
|
||||
|
||||
// 4) Chat history (last 2 AI responses — floors N-1 & N-3)
|
||||
if (String(recentChat).trim()) messages.push({ role: 'system', content: recentChat });
|
||||
|
||||
// 4.5) BME memory — after chat context, before plots
|
||||
if (bmeMemory.trim()) {
|
||||
messages.push({ role: 'system', content: `<bme_memory>\n${bmeMemory}\n</bme_memory>` });
|
||||
}
|
||||
|
||||
// 5) Previous plots
|
||||
if (String(plots).trim()) messages.push({ role: 'system', content: plots });
|
||||
|
||||
// 6) User input (with friendly framing)
|
||||
const userMsgContent = `以下是玩家的最新指令哦~:\n[${userInput}]`;
|
||||
messages.push({ role: 'user', content: userMsgContent });
|
||||
|
||||
// Extra user blocks before user message
|
||||
for (const b of enaUserBlocks) {
|
||||
const content = await renderTemplateAll(b.content, env, messageVars);
|
||||
messages.splice(Math.max(0, messages.length - 1), 0, { role: 'user', content: `【extra-user-block】\n${content}` });
|
||||
}
|
||||
|
||||
// 7) Assistant blocks
|
||||
for (const b of enaAssistantBlocks) {
|
||||
const content = await renderTemplateAll(b.content, env, messageVars);
|
||||
messages.push({ role: 'assistant', content });
|
||||
for (const block of plannerPromptConfig.blocks) {
|
||||
if (!block || block.enabled === false) continue;
|
||||
let content = '';
|
||||
if (String(block.type || 'custom') === 'builtin') {
|
||||
if (String(block.content || '').trim()) {
|
||||
content = await renderTemplateAll(block.content, env, messageVars);
|
||||
} else {
|
||||
content = resolvePlannerBuiltinBlockContent(block, plannerBlockContext);
|
||||
}
|
||||
} else {
|
||||
content = await renderTemplateAll(block.content, env, messageVars);
|
||||
}
|
||||
if (!String(content || '').trim()) continue;
|
||||
messages.push({
|
||||
role: ['system', 'user', 'assistant'].includes(String(block.role || '').trim())
|
||||
? String(block.role).trim()
|
||||
: 'system',
|
||||
content,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
messages,
|
||||
meta: {
|
||||
promptSource: plannerPromptConfig.source,
|
||||
profileId: plannerPromptConfig.profile?.id || '',
|
||||
profileName: plannerPromptConfig.profile?.name || '',
|
||||
charBlockRaw,
|
||||
worldbookRaw,
|
||||
recentChatRaw,
|
||||
@@ -1363,6 +1754,11 @@ async function runPlanningOnce(rawUserInput, silent = false, options = {}) {
|
||||
try {
|
||||
const { messages, meta } = await buildPlannerMessages(rawUserInput);
|
||||
log.requestMessages = messages;
|
||||
if (meta && typeof meta === 'object') {
|
||||
log.promptSource = String(meta.promptSource || '');
|
||||
log.profileId = String(meta.profileId || '');
|
||||
log.profileName = String(meta.profileName || '');
|
||||
}
|
||||
|
||||
const rawReply = await callPlanner(messages, options);
|
||||
log.rawReply = rawReply;
|
||||
@@ -1423,7 +1819,7 @@ async function doInterceptAndPlanThenSend() {
|
||||
const { filtered, plannerRecall } = await runPlanningOnce(raw, false, {
|
||||
onDelta(_piece, full) {
|
||||
if (!state.isPlanning) return;
|
||||
if (!ensureSettings().api.stream) return;
|
||||
if (!resolvePlannerGenerationSettings().stream) return;
|
||||
const preview = filterPlannerPreview(full);
|
||||
ta.value = `${raw}\n\n${preview}`.trim();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// ST-BME: 任务预设与兼容迁移层
|
||||
|
||||
import { DEFAULT_PROMPT_BLOCKS as DEFAULT_PLANNER_PROMPT_BLOCKS } from "../ena-planner/ena-planner-presets.js";
|
||||
import { DEFAULT_TASK_PROFILE_TEMPLATES } from "./default-task-profile-templates.js";
|
||||
|
||||
const TASK_TYPES = [
|
||||
@@ -10,6 +11,7 @@ const TASK_TYPES = [
|
||||
"summary_rollup",
|
||||
"reflection",
|
||||
"consolidation",
|
||||
"planner",
|
||||
];
|
||||
|
||||
const TASK_TYPE_META = {
|
||||
@@ -41,6 +43,10 @@ const TASK_TYPE_META = {
|
||||
label: "整合",
|
||||
description: "分析新旧记忆的冲突、去重与进化。",
|
||||
},
|
||||
planner: {
|
||||
label: "规划",
|
||||
description: "为下一轮回复生成剧情规划与写作提示。",
|
||||
},
|
||||
};
|
||||
|
||||
const BUILTIN_BLOCK_DEFINITIONS = [
|
||||
@@ -170,6 +176,48 @@ const BUILTIN_BLOCK_DEFINITIONS = [
|
||||
role: "system",
|
||||
description: "注入当前活跃的故事时间线标签与来源。extract 任务使用,帮助 LLM 定位本批对话在剧情时间轴上的位置。",
|
||||
},
|
||||
{
|
||||
sourceKey: "plannerCharacterCard",
|
||||
name: "规划:角色卡",
|
||||
role: "system",
|
||||
description: "注入 ENA Planner 使用的角色卡整合块(description / personality / scenario)。",
|
||||
taskTypes: ["planner"],
|
||||
},
|
||||
{
|
||||
sourceKey: "plannerWorldbook",
|
||||
name: "规划:世界书",
|
||||
role: "system",
|
||||
description: "注入 ENA Planner 自己解析出的世界书块,保持当前规划链路的激活与排序语义。",
|
||||
taskTypes: ["planner"],
|
||||
},
|
||||
{
|
||||
sourceKey: "plannerRecentChat",
|
||||
name: "规划:最近聊天",
|
||||
role: "system",
|
||||
description: "注入最近若干条 AI 回复片段,并沿用 ENA 的清洗规则去掉 think/排除标签。",
|
||||
taskTypes: ["planner"],
|
||||
},
|
||||
{
|
||||
sourceKey: "plannerMemory",
|
||||
name: "规划:BME 记忆",
|
||||
role: "system",
|
||||
description: "注入供 ENA 规划使用的 BME 召回记忆块。",
|
||||
taskTypes: ["planner"],
|
||||
},
|
||||
{
|
||||
sourceKey: "plannerPreviousPlots",
|
||||
name: "规划:历史 plot",
|
||||
role: "system",
|
||||
description: "注入最近的 <plot> 历史规划块,帮助保持剧情推进连续性。",
|
||||
taskTypes: ["planner"],
|
||||
},
|
||||
{
|
||||
sourceKey: "plannerUserInput",
|
||||
name: "规划:玩家输入",
|
||||
role: "user",
|
||||
description: "注入当前玩家输入,并保留 ENA 当前使用的用户消息包裹格式。",
|
||||
taskTypes: ["planner"],
|
||||
},
|
||||
];
|
||||
|
||||
const DEFAULT_TASK_PROFILE_VERSION = 3;
|
||||
@@ -671,6 +719,173 @@ const DEFAULT_TRAILING_BLOCK_BLUEPRINTS = [
|
||||
},
|
||||
];
|
||||
|
||||
function getPlannerPromptBlockContentByRole(role = "system") {
|
||||
return String(
|
||||
(Array.isArray(DEFAULT_PLANNER_PROMPT_BLOCKS) ? DEFAULT_PLANNER_PROMPT_BLOCKS : []).find(
|
||||
(block) => String(block?.role || "").trim() === String(role || "").trim(),
|
||||
)?.content || "",
|
||||
);
|
||||
}
|
||||
|
||||
function buildPlannerDefaultTaskProfileTemplate() {
|
||||
return {
|
||||
id: "default",
|
||||
name: "默认预设",
|
||||
taskType: "planner",
|
||||
version: 4,
|
||||
builtin: true,
|
||||
enabled: true,
|
||||
description: TASK_TYPE_META.planner?.description || "",
|
||||
promptMode: "block-based",
|
||||
updatedAt: "2026-04-23T16:30:00.000Z",
|
||||
blocks: [
|
||||
{
|
||||
id: "planner-default-system",
|
||||
name: "Ena Planner System",
|
||||
type: "custom",
|
||||
enabled: true,
|
||||
role: "system",
|
||||
sourceKey: "",
|
||||
sourceField: "",
|
||||
content: getPlannerPromptBlockContentByRole("system"),
|
||||
injectionMode: "relative",
|
||||
order: 0,
|
||||
},
|
||||
{
|
||||
id: "planner-default-character-card",
|
||||
name: "角色卡",
|
||||
type: "builtin",
|
||||
enabled: true,
|
||||
role: "system",
|
||||
sourceKey: "plannerCharacterCard",
|
||||
sourceField: "",
|
||||
content: "",
|
||||
injectionMode: "relative",
|
||||
order: 1,
|
||||
},
|
||||
{
|
||||
id: "planner-default-worldbook",
|
||||
name: "世界书",
|
||||
type: "builtin",
|
||||
enabled: true,
|
||||
role: "system",
|
||||
sourceKey: "plannerWorldbook",
|
||||
sourceField: "",
|
||||
content: "",
|
||||
injectionMode: "relative",
|
||||
order: 2,
|
||||
},
|
||||
{
|
||||
id: "planner-default-recent-chat",
|
||||
name: "最近聊天",
|
||||
type: "builtin",
|
||||
enabled: true,
|
||||
role: "system",
|
||||
sourceKey: "plannerRecentChat",
|
||||
sourceField: "",
|
||||
content: "",
|
||||
injectionMode: "relative",
|
||||
order: 3,
|
||||
},
|
||||
{
|
||||
id: "planner-default-memory",
|
||||
name: "BME 记忆",
|
||||
type: "builtin",
|
||||
enabled: true,
|
||||
role: "system",
|
||||
sourceKey: "plannerMemory",
|
||||
sourceField: "",
|
||||
content: "",
|
||||
injectionMode: "relative",
|
||||
order: 4,
|
||||
},
|
||||
{
|
||||
id: "planner-default-previous-plots",
|
||||
name: "历史 plot",
|
||||
type: "builtin",
|
||||
enabled: true,
|
||||
role: "system",
|
||||
sourceKey: "plannerPreviousPlots",
|
||||
sourceField: "",
|
||||
content: "",
|
||||
injectionMode: "relative",
|
||||
order: 5,
|
||||
},
|
||||
{
|
||||
id: "planner-default-user-input",
|
||||
name: "玩家输入",
|
||||
type: "builtin",
|
||||
enabled: true,
|
||||
role: "user",
|
||||
sourceKey: "plannerUserInput",
|
||||
sourceField: "",
|
||||
content: "",
|
||||
injectionMode: "relative",
|
||||
order: 6,
|
||||
},
|
||||
{
|
||||
id: "planner-default-assistant-seed",
|
||||
name: "Assistant Seed",
|
||||
type: "custom",
|
||||
enabled: true,
|
||||
role: "assistant",
|
||||
sourceKey: "",
|
||||
sourceField: "",
|
||||
content: getPlannerPromptBlockContentByRole("assistant"),
|
||||
injectionMode: "relative",
|
||||
order: 7,
|
||||
},
|
||||
],
|
||||
generation: {
|
||||
llm_preset: "",
|
||||
max_context_tokens: null,
|
||||
max_completion_tokens: null,
|
||||
reply_count: null,
|
||||
stream: true,
|
||||
temperature: 1,
|
||||
top_p: 1,
|
||||
top_k: 0,
|
||||
top_a: null,
|
||||
min_p: null,
|
||||
seed: null,
|
||||
frequency_penalty: null,
|
||||
presence_penalty: null,
|
||||
repetition_penalty: null,
|
||||
squash_system_messages: null,
|
||||
reasoning_effort: null,
|
||||
request_thoughts: null,
|
||||
enable_function_calling: null,
|
||||
enable_web_search: null,
|
||||
character_name_prefix: null,
|
||||
wrap_user_messages_in_quotes: null,
|
||||
},
|
||||
regex: {
|
||||
enabled: true,
|
||||
inheritStRegex: true,
|
||||
sources: {
|
||||
global: true,
|
||||
preset: true,
|
||||
character: true,
|
||||
},
|
||||
stages: {
|
||||
"input.userMessage": true,
|
||||
"input.recentMessages": true,
|
||||
"input.candidateText": true,
|
||||
"input.finalPrompt": false,
|
||||
"output.rawResponse": false,
|
||||
"output.beforeParse": false,
|
||||
input: true,
|
||||
output: false,
|
||||
},
|
||||
localRules: [],
|
||||
},
|
||||
metadata: {
|
||||
migratedFromLegacy: false,
|
||||
legacyPromptField: "",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function applyRuntimeDefaultTemplateOverrides(taskType, template = null) {
|
||||
if (!template || typeof template !== "object") {
|
||||
return template;
|
||||
@@ -705,6 +920,9 @@ function applyRuntimeDefaultTemplateOverrides(taskType, template = null) {
|
||||
}
|
||||
|
||||
function getDefaultTaskProfileTemplate(taskType) {
|
||||
if (String(taskType || "") === "planner") {
|
||||
return buildPlannerDefaultTaskProfileTemplate();
|
||||
}
|
||||
const template = DEFAULT_TASK_PROFILE_TEMPLATES?.[taskType];
|
||||
if (!template || typeof template !== "object") {
|
||||
return null;
|
||||
@@ -1590,6 +1808,11 @@ export function createCustomPromptBlock(taskType, overrides = {}) {
|
||||
|
||||
export function createBuiltinPromptBlock(taskType, sourceKey = "", overrides = {}) {
|
||||
const definition =
|
||||
BUILTIN_BLOCK_DEFINITIONS.find(
|
||||
(item) =>
|
||||
item.sourceKey === sourceKey &&
|
||||
(!Array.isArray(item.taskTypes) || item.taskTypes.includes(taskType)),
|
||||
) ||
|
||||
BUILTIN_BLOCK_DEFINITIONS.find((item) => item.sourceKey === sourceKey) ||
|
||||
BUILTIN_BLOCK_DEFINITIONS[0];
|
||||
return normalizePromptBlock(taskType, {
|
||||
@@ -1975,8 +2198,18 @@ export function getTaskTypes() {
|
||||
return [...TASK_TYPES];
|
||||
}
|
||||
|
||||
export function getBuiltinBlockDefinitions() {
|
||||
return BUILTIN_BLOCK_DEFINITIONS.map((definition) => ({ ...definition }));
|
||||
export function getBuiltinBlockDefinitions(taskType = "") {
|
||||
const normalizedTaskType = String(taskType || "").trim();
|
||||
return BUILTIN_BLOCK_DEFINITIONS
|
||||
.filter(
|
||||
(definition) =>
|
||||
normalizedTaskType === "planner"
|
||||
? Array.isArray(definition.taskTypes) && definition.taskTypes.includes("planner")
|
||||
: !Array.isArray(definition.taskTypes) ||
|
||||
!normalizedTaskType ||
|
||||
definition.taskTypes.includes(normalizedTaskType),
|
||||
)
|
||||
.map((definition) => ({ ...definition }));
|
||||
}
|
||||
|
||||
export function cloneTaskProfile(profile = {}, options = {}) {
|
||||
|
||||
@@ -23,6 +23,7 @@ assert.equal(migrated.taskProfilesVersion, 3);
|
||||
assert.ok(migrated.taskProfiles);
|
||||
assert.ok(migrated.taskProfiles.extract);
|
||||
assert.ok(migrated.taskProfiles.recall);
|
||||
assert.ok(migrated.taskProfiles.planner);
|
||||
|
||||
const extractProfile = getActiveTaskProfile(
|
||||
{
|
||||
@@ -153,6 +154,22 @@ assert.deepEqual(
|
||||
],
|
||||
);
|
||||
assert.ok(defaults.summary_rollup.profiles.length > 0);
|
||||
assert.ok(defaults.planner.profiles.length > 0);
|
||||
assert.deepEqual(
|
||||
defaults.planner.profiles[0].blocks.map((block) => block.sourceKey || block.id),
|
||||
[
|
||||
"planner-default-system",
|
||||
"plannerCharacterCard",
|
||||
"plannerWorldbook",
|
||||
"plannerRecentChat",
|
||||
"plannerMemory",
|
||||
"plannerPreviousPlots",
|
||||
"plannerUserInput",
|
||||
"planner-default-assistant-seed",
|
||||
],
|
||||
);
|
||||
assert.equal(defaults.planner.profiles[0].generation.stream, true);
|
||||
assert.equal(defaults.planner.profiles[0].generation.temperature, 1);
|
||||
|
||||
const upgradedLegacyDefault = getActiveTaskProfile(
|
||||
{
|
||||
|
||||
@@ -117,6 +117,30 @@ function getSharedLlmPresetState() {
|
||||
return sanitizeLlmPresetSettings(settings || {});
|
||||
}
|
||||
|
||||
function openPlannerTaskPresetWorkspace() {
|
||||
const taskTabBtn = document.querySelector('.bme-tab-btn[data-tab="task"]');
|
||||
taskTabBtn?.click();
|
||||
|
||||
const activatePlannerTaskType = () => {
|
||||
const plannerBtn = document.querySelector(
|
||||
'[data-task-action="switch-task-type"][data-task-type="planner"]',
|
||||
);
|
||||
plannerBtn?.click();
|
||||
return Boolean(plannerBtn);
|
||||
};
|
||||
|
||||
if (activatePlannerTaskType()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
activatePlannerTaskType();
|
||||
});
|
||||
});
|
||||
return Boolean(taskTabBtn);
|
||||
}
|
||||
|
||||
function buildPlannerLlmSnapshot(source = {}) {
|
||||
return {
|
||||
llmApiUrl: String(source?.llmApiUrl || '').trim(),
|
||||
@@ -503,13 +527,6 @@ function collectPatch() {
|
||||
customPrefix: getVal('bme-planner-prefix-custom').trim(),
|
||||
apiKey: getVal('bme-planner-api-key'),
|
||||
model: getVal('bme-planner-model').trim(),
|
||||
stream: toBool(getVal('bme-planner-stream'), false),
|
||||
temperature: toNum(getVal('bme-planner-temp'), 1),
|
||||
top_p: toNum(getVal('bme-planner-top-p'), 1),
|
||||
top_k: Math.floor(toNum(getVal('bme-planner-top-k'), 0)),
|
||||
presence_penalty: getVal('bme-planner-pp').trim(),
|
||||
frequency_penalty: getVal('bme-planner-fp').trim(),
|
||||
max_tokens: getVal('bme-planner-mt').trim(),
|
||||
},
|
||||
includeGlobalWorldbooks: toBool(getVal('bme-planner-include-global-wb'), false),
|
||||
excludeWorldbookPosition4: toBool(getVal('bme-planner-wb-pos4'), true),
|
||||
@@ -519,9 +536,6 @@ function collectPatch() {
|
||||
chatExcludeTags: csvToArr(getVal('bme-planner-exclude-tags')),
|
||||
logsPersist: toBool(getVal('bme-planner-logs-persist'), true),
|
||||
logsMax: Math.max(1, Math.min(200, Math.floor(toNum(getVal('bme-planner-logs-max'), 20)))),
|
||||
promptBlocks: cfgCache?.promptBlocks || [],
|
||||
promptTemplates: cfgCache?.promptTemplates || {},
|
||||
activePromptTemplate: $('bme-planner-tpl-select')?.value || '',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -682,6 +696,15 @@ function bindOnce(section) {
|
||||
scheduleSave();
|
||||
});
|
||||
|
||||
$('bme-planner-open-task-presets')?.addEventListener('click', () => {
|
||||
const opened = openPlannerTaskPresetWorkspace();
|
||||
if (!opened) {
|
||||
setLocalStatus('bme-planner-api-status', '未找到任务预设工作区,请手动切到“任务 -> 规划”', 'error');
|
||||
return;
|
||||
}
|
||||
setLocalStatus('bme-planner-api-status', '已切换到“任务 -> 规划”预设编辑器', 'success');
|
||||
});
|
||||
|
||||
/* Prompts + templates */
|
||||
$('bme-planner-keep-tags')?.addEventListener('change', onKeepTagsBlur);
|
||||
|
||||
|
||||
161
ui/panel.html
161
ui/panel.html
@@ -3031,170 +3031,23 @@
|
||||
<div class="bme-config-card">
|
||||
<div class="bme-config-card-head">
|
||||
<div>
|
||||
<div class="bme-config-card-title">规划 LLM · 生成参数</div>
|
||||
<div class="bme-config-card-title">生成参数 · Prompt 编排</div>
|
||||
<div class="bme-config-card-subtitle">
|
||||
流式输出用于实时预览,数值留空表示不覆盖渠道默认。
|
||||
这两部分现在统一收敛到任务预设里的“规划”任务,避免 ENA 面板和任务面板各维护一套配置。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bme-config-row">
|
||||
<label for="bme-planner-stream">流式输出</label>
|
||||
<select id="bme-planner-stream" class="bme-config-input">
|
||||
<option value="true">开启</option>
|
||||
<option value="false">关闭</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="bme-planner-param-grid">
|
||||
<div class="bme-config-row">
|
||||
<label for="bme-planner-temp">Temperature</label>
|
||||
<input
|
||||
id="bme-planner-temp"
|
||||
class="bme-config-input"
|
||||
type="number"
|
||||
step="0.1"
|
||||
min="0"
|
||||
max="2"
|
||||
/>
|
||||
</div>
|
||||
<div class="bme-config-row">
|
||||
<label for="bme-planner-top-p">Top P</label>
|
||||
<input
|
||||
id="bme-planner-top-p"
|
||||
class="bme-config-input"
|
||||
type="number"
|
||||
step="0.05"
|
||||
min="0"
|
||||
max="1"
|
||||
/>
|
||||
</div>
|
||||
<div class="bme-config-row">
|
||||
<label for="bme-planner-top-k">Top K</label>
|
||||
<input
|
||||
id="bme-planner-top-k"
|
||||
class="bme-config-input"
|
||||
type="number"
|
||||
step="1"
|
||||
min="0"
|
||||
/>
|
||||
</div>
|
||||
<div class="bme-config-row">
|
||||
<label for="bme-planner-pp">Presence penalty</label>
|
||||
<input
|
||||
id="bme-planner-pp"
|
||||
class="bme-config-input"
|
||||
type="text"
|
||||
placeholder="-2 ~ 2"
|
||||
/>
|
||||
</div>
|
||||
<div class="bme-config-row">
|
||||
<label for="bme-planner-fp">Frequency penalty</label>
|
||||
<input
|
||||
id="bme-planner-fp"
|
||||
class="bme-config-input"
|
||||
type="text"
|
||||
placeholder="-2 ~ 2"
|
||||
/>
|
||||
</div>
|
||||
<div class="bme-config-row">
|
||||
<label for="bme-planner-mt">最大 Token 数</label>
|
||||
<input
|
||||
id="bme-planner-mt"
|
||||
class="bme-config-input"
|
||||
type="text"
|
||||
placeholder="留空则不限制"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bme-config-card">
|
||||
<div class="bme-config-card-head">
|
||||
<div>
|
||||
<div class="bme-config-card-title">提示词 · 模板</div>
|
||||
<div class="bme-config-card-subtitle">
|
||||
模板保存的是当前提示词块列表;切换模板会覆盖当前编辑中的块。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bme-config-row">
|
||||
<label for="bme-planner-tpl-select">活动模板</label>
|
||||
<select id="bme-planner-tpl-select" class="bme-config-input">
|
||||
<option value="">-- 选择模板 --</option>
|
||||
</select>
|
||||
<div class="bme-config-help">
|
||||
现在每个规划预设都可以同时携带自己的生成参数和 Prompt block。ENA 这里仍然只负责连接配置、上下文来源、输出过滤和调试日志。
|
||||
</div>
|
||||
<div class="bme-config-actions">
|
||||
<button
|
||||
class="bme-config-secondary-btn"
|
||||
id="bme-planner-tpl-save"
|
||||
id="bme-planner-open-task-presets"
|
||||
type="button"
|
||||
>
|
||||
<i class="fa-solid fa-floppy-disk"></i>
|
||||
<span>覆盖保存</span>
|
||||
</button>
|
||||
<button
|
||||
class="bme-config-secondary-btn"
|
||||
id="bme-planner-tpl-saveas"
|
||||
type="button"
|
||||
>
|
||||
<i class="fa-solid fa-file-circle-plus"></i>
|
||||
<span>另存为</span>
|
||||
</button>
|
||||
<button
|
||||
class="bme-config-secondary-btn bme-config-danger-btn"
|
||||
id="bme-planner-tpl-delete"
|
||||
type="button"
|
||||
>
|
||||
<i class="fa-solid fa-trash-can"></i>
|
||||
<span>删除</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="bme-planner-undo-bar" id="bme-planner-tpl-undo" hidden>
|
||||
<span
|
||||
>模板 <strong id="bme-planner-tpl-undo-name"></strong> 已删除</span
|
||||
>
|
||||
<button
|
||||
class="bme-config-secondary-btn"
|
||||
id="bme-planner-tpl-undo-btn"
|
||||
type="button"
|
||||
>
|
||||
撤销
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bme-config-card">
|
||||
<div class="bme-config-card-head">
|
||||
<div>
|
||||
<div class="bme-config-card-title">提示词 · 块编排</div>
|
||||
<div class="bme-config-card-subtitle">
|
||||
每个块会作为一条独立消息发送给规划 LLM。系统会在块之后自动追加:角色卡、世界书、BME 结构化记忆、聊天历史和历史 plot。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="bme-planner-prompt-list" class="bme-planner-prompt-list"></div>
|
||||
<div
|
||||
class="bme-planner-prompt-empty"
|
||||
id="bme-planner-prompt-empty"
|
||||
hidden
|
||||
>
|
||||
暂无提示词块
|
||||
</div>
|
||||
<div class="bme-config-actions">
|
||||
<button
|
||||
class="bme-config-secondary-btn"
|
||||
id="bme-planner-add-prompt"
|
||||
type="button"
|
||||
>
|
||||
<i class="fa-solid fa-plus"></i>
|
||||
<span>添加块</span>
|
||||
</button>
|
||||
<button
|
||||
class="bme-config-secondary-btn bme-config-danger-btn"
|
||||
id="bme-planner-reset-prompt"
|
||||
type="button"
|
||||
>
|
||||
<i class="fa-solid fa-rotate-left"></i>
|
||||
<span>恢复默认</span>
|
||||
<i class="fa-solid fa-list-check"></i>
|
||||
<span>打开任务预设 · 规划</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
19
ui/panel.js
19
ui/panel.js
@@ -173,6 +173,7 @@ const GRAPH_WRITE_ACTION_IDS = [
|
||||
const TASK_PROFILE_GENERATION_GROUPS = [
|
||||
{
|
||||
title: "API 配置",
|
||||
excludeTaskTypes: ["planner"],
|
||||
fields: [
|
||||
{
|
||||
key: "llm_preset",
|
||||
@@ -8412,6 +8413,8 @@ function _getTaskProfileWorkspaceState(settings = _getSettings?.() || {}) {
|
||||
currentGlobalRegexRuleId = globalRegexRules[0]?.id || "";
|
||||
}
|
||||
|
||||
const builtinBlockDefinitions = getBuiltinBlockDefinitions(currentTaskProfileTaskType);
|
||||
|
||||
return {
|
||||
settings,
|
||||
taskProfiles,
|
||||
@@ -8431,7 +8434,7 @@ function _getTaskProfileWorkspaceState(settings = _getSettings?.() || {}) {
|
||||
regexRules.find((rule) => rule.id === currentTaskProfileRuleId) || null,
|
||||
selectedGlobalRegexRule:
|
||||
globalRegexRules.find((rule) => rule.id === currentGlobalRegexRuleId) || null,
|
||||
builtinBlockDefinitions: getBuiltinBlockDefinitions(),
|
||||
builtinBlockDefinitions,
|
||||
runtimeDebug,
|
||||
};
|
||||
}
|
||||
@@ -9626,11 +9629,11 @@ async function _handleTaskProfileWorkspaceClick(event) {
|
||||
document.getElementById("bme-task-profile-import-all")?.click();
|
||||
return;
|
||||
case "restore-all-profiles": {
|
||||
const taskTypes = getTaskTypeOptions().map((t) => t.id);
|
||||
const confirmed = window.confirm(
|
||||
"这会将全部 6 个任务的默认预设恢复为出厂状态。已保存的自定义预设不受影响,通用正则规则也不受影响。是否继续?",
|
||||
`这会将全部 ${taskTypes.length} 个任务的默认预设恢复为出厂状态。已保存的自定义预设不受影响,通用正则规则也不受影响。是否继续?`,
|
||||
);
|
||||
if (!confirmed) return;
|
||||
const taskTypes = getTaskTypeOptions().map((t) => t.id);
|
||||
let restored = state.taskProfiles;
|
||||
const extraPatch = {};
|
||||
for (const tt of taskTypes) {
|
||||
@@ -9727,6 +9730,7 @@ function _renderTaskProfileWorkspace(state) {
|
||||
state.taskTypeOptions.find((item) => item.id === state.taskType) ||
|
||||
state.taskTypeOptions[0];
|
||||
const profileUpdatedAt = _formatTaskProfileTime(state.profile.updatedAt);
|
||||
const totalTaskTypes = Array.isArray(state.taskTypeOptions) ? state.taskTypeOptions.length : 0;
|
||||
|
||||
return `
|
||||
<div class="bme-task-shell">
|
||||
@@ -9757,10 +9761,10 @@ function _renderTaskProfileWorkspace(state) {
|
||||
</div>
|
||||
</div>
|
||||
<div class="bme-task-action-bar-right">
|
||||
<button class="bme-config-secondary-btn bme-bulk-profile-btn bme-task-btn-danger" data-task-action="restore-all-profiles" type="button" title="恢复全部 6 个任务的默认预设">
|
||||
<button class="bme-config-secondary-btn bme-bulk-profile-btn bme-task-btn-danger" data-task-action="restore-all-profiles" type="button" title="恢复全部 ${_escAttr(String(totalTaskTypes || 0))} 个任务的默认预设">
|
||||
<i class="fa-solid fa-arrows-rotate"></i><span>恢复全部</span>
|
||||
</button>
|
||||
<button class="bme-config-secondary-btn bme-bulk-profile-btn" data-task-action="export-all-profiles" type="button" title="导出全部 6 个任务预设">
|
||||
<button class="bme-config-secondary-btn bme-bulk-profile-btn" data-task-action="export-all-profiles" type="button" title="导出全部 ${_escAttr(String(totalTaskTypes || 0))} 个任务预设">
|
||||
<i class="fa-solid fa-file-export"></i><span>导出全部</span>
|
||||
</button>
|
||||
<button class="bme-config-secondary-btn bme-bulk-profile-btn" data-task-action="import-all-profiles" type="button" title="导入全部预设(覆盖当前)">
|
||||
@@ -9881,9 +9885,12 @@ function _renderTaskPromptTab(state) {
|
||||
|
||||
function _renderTaskGenerationTab(state) {
|
||||
const inputGroups = TASK_PROFILE_INPUT_GROUPS[state.taskType] || [];
|
||||
const generationGroups = TASK_PROFILE_GENERATION_GROUPS.filter(
|
||||
(group) => !Array.isArray(group.excludeTaskTypes) || !group.excludeTaskTypes.includes(state.taskType),
|
||||
);
|
||||
return `
|
||||
<div class="bme-task-tab-body">
|
||||
${TASK_PROFILE_GENERATION_GROUPS.map(
|
||||
${generationGroups.map(
|
||||
(group) => `
|
||||
<div class="bme-config-card">
|
||||
<div class="bme-config-card-head">
|
||||
|
||||
Reference in New Issue
Block a user