Add new WI position 'outlet' to expose WI entries triggered to be placed as macros literally anywhere (#4523) * feat: add outlet prompt support for world info entries * feat: add outlet name field with autocomplete for world info entries * feat: add outlet entries for world info support to WI parsing * feat: add tooltip and validation for WI outlet name entries * Trim macro outlet name Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * refactor: consolidate WI injection cleanup and fix strict equality comparison * fix: update outlet macro regex pattern to use double colon delimiter * fix: replace carriage returns with newlines in WI entry outlet tooltip * fix: add null checks and array validation for WI outlet entries handling * feat: add outletName field to world info entry definition schema (supports /setwientryfield command) * fix: add outletName field to export of character lore books * do not remove outlet name on position change, but filter out for auto complete * Match WI extensions field name Co-authored-by: Cohee <18619528+Cohee1207@users.noreply.github.com> * outlet name being used in char WI import too * docs: add outlet macro documentation to help template * be explicit about risu/novel/agnai wi not supporting outlets * Inject A/N and TC sysprompt after populating outlets --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Cohee <18619528+Cohee1207@users.noreply.github.com>

7cba4e88ab4c79257be3438138ceac327dda0aeb

Wolfsblvt <wolfsblvt@gmail.com>

Signed
8 files changed, +182 -67Ignore whitespace
public/css/world-info.css+4 -0
@@ -301,6 +301,10 @@ select.keyselect+span.select2-container .select2-selection--multiple {
301301 display: none;
302302}
303303
304+.world_entry:not(:has(select[name="position"] option[value="7"]:checked)) .world_entry_form_control:has(input[name="outletName"]) {
305+ display: none;
306+}
307+
304308.world_entry label[for="__invisible"] {
305309 visibility: hidden;
306310 pointer-events: none;
public/index.html+10 -0
@@ -6476,6 +6476,13 @@
64766476 </div>
64776477 </div>
64786478 <div name="perEntryOverridesBlock" class="flex-container wide100p alignitemscenter">
6479+ <div class="world_entry_form_control flex1" title="Set the outlet name for this WI entry.&#10;&#10;WI entries with position 'outlet' will not be added to the prompt automatically. Instead, they will be collected and can be used as a macro in the prompt.&#10;Add {{outlet::YourName}} to the prompt in any place you want to add all WI entries this specific outlet." data-i18n="[title]wi_outlet_name">
6480+ <small class="textAlignCenter">
6481+ <span data-i18n="Outlet Name">Outlet Name</span>
6482+ <div class="fa-solid fa-circle-info opacity50p"></div>
6483+ </small>
6484+ <input class="text_pole margin0" name="outletName" type="text" placeholder="Outlet Name" data-i18n="[placeholder]Outlet Name">
6485+ </div>
64796486 <div class="world_entry_form_control flex1">
64806487 <small class="textAlignCenter" data-i18n="Scan Depth">Scan Depth</small>
64816488 <input class="text_pole margin0" name="scanDepth" type="number" placeholder="Use global setting" data-i18n="[placeholder]Use global setting" max="1000">
@@ -6813,6 +6820,9 @@
68136820 <option value="4" data-role="2" data-i18n="at Depth AI">
68146821 @D 🤖
68156822 </option>
6823+ <option value="7" data-role="" data-i18n="Outlet">
6824+ ➡️ Outlet
6825+ </option>
68166826 </select>
68176827 </div>
68186828 <div class="world_entry_form_control wi-enter-footer-text flex-container flexNoGap">
public/script.js+32 -25
@@ -3575,18 +3575,6 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
35753575 creatorNotes,
35763576 } = getCharacterCardFields();
35773577
3578- if (main_api !== 'openai') {
3579- if (power_user.sysprompt.enabled) {
3580- system = power_user.prefer_character_prompt && system
3581- ? substituteParams(system, name1, name2, (power_user.sysprompt.content ?? ''))
3582- : baseChatReplace(power_user.sysprompt.content, name1, name2);
3583- system = isInstruct ? substituteParams(system, name1, name2, power_user.sysprompt.content) : system;
3584- } else {
3585- // Nullify if it's not enabled
3586- system = '';
3587- }
3588- }
3589-
35903578 // Depth prompt (character-specific A/N)
35913579 removeDepthPrompts();
35923580 const groupDepthPrompts = getGroupDepthPrompts(selected_group, Number(this_chid));
@@ -3717,13 +3705,6 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
37173705
37183706 let mesExamplesArray = parseMesExamples(mesExamples, isInstruct);
37193707
3720- //////////////////////////////////
3721- // Extension added strings
3722- // Set non-WI AN
3723- setFloatingPrompt();
3724- // Add persona description to prompt
3725- addPersonaDescriptionExtensionPrompt();
3726-
37273708 // Add WI to prompt (and also inject WI to AN value via hijack)
37283709 // Make quiet prompt available for WIAN
37293710 setExtensionPrompt(inject_ids.QUIET_PROMPT, quiet_prompt || '', extension_prompt_types.IN_PROMPT, 0, true);
@@ -3738,7 +3719,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
37383719 creatorNotes: creatorNotes,
37393720 trigger: GENERATION_TYPE_TRIGGERS.includes(type) ? type : 'normal',
37403721 };
37413722 const { worldInfoString, worldInfoBefore, worldInfoAfter, worldInfoExamples, worldInfoDepth, outletEntries } = await getWorldInfoPrompt(chatForWI, this_max_context, dryRun, globalScanData);
37423723 setExtensionPrompt(inject_ids.QUIET_PROMPT, '', extension_prompt_types.IN_PROMPT, 0, true);
37433724
37443725 // Add message example WI
@@ -3770,17 +3751,41 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
37703751 if (skipWIAN !== true) {
37713752 console.log('skipWIAN not active, adding WIAN');
37723753 // Add all depth WI entries to prompt
37733754 flushWIDepthInjectionsflushWIInjections();
37743755 if (Array.isArray(worldInfoDepth)) {
37753756 worldInfoDepth.forEach((e) => {
37763757 const joinedEntries = e.entries.join('\n');
37773758 setExtensionPrompt(inject_ids.CUSTOM_WI_DEPTH_ROLE(e.depth, e.role), joinedEntries, extension_prompt_types.IN_CHAT, e.depth, false, e.role);
37783759 });
37793760 }
3761+ if (outletEntries && typeof outletEntries === 'object' && Object.keys(outletEntries).length > 0) {
3762+ Object.entries(outletEntries).forEach(([key, value]) => {
3763+ setExtensionPrompt(inject_ids.CUSTOM_WI_OUTLET(key), value.join('\n'), extension_prompt_types.NONE, 0);
3764+ });
3765+ }
37803766 } else {
37813767 console.log('skipping WIAN');
37823768 }
37833769
3770+ // Set non-WI AN
3771+ setFloatingPrompt();
3772+
3773+ // Add persona description to prompt
3774+ addPersonaDescriptionExtensionPrompt();
3775+
3776+ // Prepare the system prompt for Text Completion APIs
3777+ if (main_api !== 'openai') {
3778+ if (power_user.sysprompt.enabled) {
3779+ system = power_user.prefer_character_prompt && system
3780+ ? substituteParams(system, name1, name2, (power_user.sysprompt.content ?? ''))
3781+ : baseChatReplace(power_user.sysprompt.content, name1, name2);
3782+ system = isInstruct ? substituteParams(system, name1, name2, power_user.sysprompt.content) : system;
3783+ } else {
3784+ // Nullify if it's not enabled
3785+ system = '';
3786+ }
3787+ }
3788+
37843789 // Collect before / after story string injections
37853790 const beforeScenarioAnchor = await getExtensionPrompt(extension_prompt_types.BEFORE_PROMPT);
37863791 const afterScenarioAnchor = await getExtensionPrompt(extension_prompt_types.IN_PROMPT);
@@ -4751,10 +4756,12 @@ async function doChatInject(messages, isContinue) {
47514756 return injectedIndices;
47524757}
47534758
47544759function flushWIDepthInjectionsflushWIInjections() {
4755- //prevent custom depth WI entries (which have unique random key names) from duplicating
4760+ const depthPrefix = inject_ids.CUSTOM_WI_DEPTH;
4761+ const outletPrefix = inject_ids.CUSTOM_WI_OUTLET('');
4762+
47564763 for (const key of Object.keys(extension_prompts)) {
47574764 if (key.startsWith(inject_idsdepthPrefix) || key.CUSTOM_WI_DEPTHstartsWith(outletPrefix)) {
47584765 delete extension_prompts[key];
47594766 }
47604767 }
@@ -4775,7 +4782,7 @@ function unblockGeneration(type) {
47754782 showSwipeButtons();
47764783 setGenerationProgress(0);
47774784 flushEphemeralStoppingStrings();
47784785 flushWIDepthInjectionsflushWIInjections();
47794786}
47804787
47814788export function getNextMessageId(type) {
public/scripts/constants.js+1 -0
@@ -51,6 +51,7 @@ export const inject_ids = {
5151 DEPTH_PROMPT_INDEX: (index) => `DEPTH_PROMPT_${index}`,
5252 CUSTOM_WI_DEPTH: 'customDepthWI',
5353 CUSTOM_WI_DEPTH_ROLE: (depth, role) => `customDepthWI_${depth}_${role}`,
54+ CUSTOM_WI_OUTLET: (key) => `customWIOutlet_${key}`,
5455};
5556
5657export const COMETAPI_IGNORE_PATTERNS = [
public/scripts/macros.js+14 -1
@@ -1,10 +1,11 @@
11import { Handlebars, moment, seedrandom, droll } from '../lib.js';
22import { chat, chat_metadata, main_api, getMaxContextSize, getCurrentChatId, substituteParams, eventSource, event_types, extension_prompts } from '../script.js';
33import { timestampToMoment, isDigitsOnly, getStringHash, escapeRegex, uuidv4 } from './utils.js';
44import { textgenerationwebui_banned_in_macros } from './textgen-settings.js';
55import { getInstructMacros } from './instruct-mode.js';
66import { getVariableMacros } from './variables.js';
77import { isMobile } from './RossAscends-mods.js';
8+import { inject_ids } from './constants.js';
89
910/**
1011 * @typedef Macro
@@ -459,6 +460,17 @@ function getTimeDiffMacro() {
459460}
460461
461462/**
463+ * Returns the outlet prompt for a given outlet key.
464+ * @param {string} key - The outlet key
465+ * @returns {string} The outlet prompt
466+ */
467+function getOutletPrompt(key) {
468+ const value = extension_prompts[inject_ids.CUSTOM_WI_OUTLET(key)]?.value;
469+ // Macros should already be parsed, but we'll do it again just in case
470+ return substituteParams(value);
471+}
472+
473+/**
462474 * Substitutes {{macro}} parameters in a string.
463475 * @param {string} content - The string to substitute parameters in.
464476 * @param {EnvObject} env - Map of macro names to the values they'll be substituted with. If the param
@@ -518,6 +530,7 @@ export function evaluateMacros(content, env, postProcessFn) {
518530 { regex: /{{datetimeformat +([^}]*)}}/gi, replace: (_, format) => moment().format(format) },
519531 { regex: /{{idle_duration}}/gi, replace: () => getTimeSinceLastMessage() },
520532 { regex: /{{time_UTC([-+]\d+)}}/gi, replace: (_, offset) => moment().utc().utcOffset(parseInt(offset, 10)).format('LT') },
533+ { regex: /{{outlet::(.+?)}}/gi, replace: (_, key) => getOutletPrompt(key.trim()) || '' },
521534 getTimeDiffMacro(),
522535 getBannedWordsMacro(),
523536 getRandomReplaceMacro(),
public/scripts/templates/macros.html+1 -0
@@ -22,6 +22,7 @@
2222 <li><tt>&lcub;&lcub;char&rcub;&rcub;</tt> – <span data-i18n="help_macros_16">the Character's name</span></li>
2323 <li><tt>&lcub;&lcub;version&rcub;&rcub;</tt> – <span data-i18n="help_macros_17">the Character's version number</span></li>
2424 <li><tt>&lcub;&lcub;charDepthPrompt&rcub;&rcub;</tt> – <span data-i18n="help_macros_charDepthPrompt">the Character's @ Depth Note</span></li>
25+ <li><tt>&lcub;&lcub;outlet::(name)&rcub;&rcub;</tt> – <span data-i18n="help_macros_outletName">the WI entry content for the outlet with the specified name</span></li>
2526 <li><tt>&lcub;&lcub;group&rcub;&rcub;</tt> – <span data-i18n="help_macros_18">a comma-separated list of group member names (including muted) or the character name in solo chats. Alias: &lcub;&lcub;charIfNotGroup&rcub;&rcub;</span></li>
2627 <li><tt>&lcub;&lcub;groupNotMuted&rcub;&rcub;</tt> – <span data-i18n="help_groupNotMuted">the same as &lcub;&lcub;group&rcub;&rcub;, but excludes muted members</span></li>
2728 <li><tt>&lcub;&lcub;model&rcub;&rcub;</tt> – <span data-i18n="help_macros_19">a text generation model name for the currently selected API. </span><b data-i18n="Can be inaccurate!">Can be inaccurate!</b></li>
public/scripts/world-info.js+119 -41
@@ -156,6 +156,7 @@ const KNOWN_DECORATORS = ['@@activate', '@@dont_activate'];
156156 * @property {Array} worldInfoDepth - Array of depth entries
157157 * @property {Array} anBefore - Array of entries before Author's Note
158158 * @property {Array} anAfter - Array of entries after Author's Note
159+ * @property {{[key: string]: string[]}} outletEntries - Array of entries to be added to an outlet
159160 */
160161
161162/**
@@ -166,6 +167,7 @@ const KNOWN_DECORATORS = ['@@activate', '@@dont_activate'];
166167 * @property {any[]} WIDepthEntries The depth entries.
167168 * @property {any[]} ANBeforeEntries The entries before Author's Note.
168169 * @property {any[]} ANAfterEntries The entries after Author's Note.
170+ * @property {{[key: string]: string[]}} outletEntries - Array of entries to be added to an outlet
169171 * @property {Set<any>} allActivatedEntries All entries.
170172 */
171173
@@ -817,6 +819,7 @@ export const world_info_position = {
817819 atDepth: 4,
818820 EMTop: 5,
819821 EMBottom: 6,
822+ outlet: 7,
820823};
821824
822825export const wi_anchor_position = {
@@ -866,6 +869,7 @@ export async function getWorldInfoPrompt(chat, maxContext, isDryRun, globalScanD
866869 worldInfoDepth: activatedWorldInfo.WIDepthEntries ?? [],
867870 anBefore: activatedWorldInfo.ANBeforeEntries ?? [],
868871 anAfter: activatedWorldInfo.ANAfterEntries ?? [],
872+ outletEntries: activatedWorldInfo.outletEntries ?? {},
869873 };
870874}
871875
@@ -3425,6 +3429,19 @@ export async function getWorldEntry(name, data, entry) {
34253429 contentInput.val(entry.content).trigger('input', { skipCount: true, noSave: true });
34263430 editTemplate.find('.editor_maximize').attr('data-for', contentInputId);
34273431
3432+ // Outlet name
3433+ const outletNameInput = editTemplate.find('input[name="outletName"]');
3434+ outletNameInput.data('uid', entry.uid);
3435+ outletNameInput.on('input', async function (_, { noSave = false } = {}) {
3436+ const uid = $(this).data('uid');
3437+ const value = $(this).val();
3438+ data.entries[uid].outletName = value;
3439+ setWIOriginalDataValue(data, uid, 'extensions.outlet_name', data.entries[uid].outletName);
3440+ !noSave && await saveWorldInfo(name, data);
3441+ });
3442+ outletNameInput.val(entry.outletName ?? '').trigger('input', { noSave: true });
3443+ setTimeout(() => createEntryInputAutocomplete(outletNameInput, getOutletNameCallback(data), { allowMultiple: true }), 1);
3444+
34283445 // Scan depth
34293446 const scanDepthInput = editTemplate.find('input[name="scanDepth"]');
34303447 scanDepthInput.data('uid', entry.uid);
@@ -3597,63 +3614,105 @@ export async function getWorldEntry(name, data, entry) {
35973614 return headerTemplate;
35983615}
35993616
3617+
36003618/**
3601- * Get the inclusion groups for the autocomplete.
3619+ * Builds a jQuery UI autocomplete callback: (control, request, response) => void
36023620 * @param {anyobject} data[opt={}] WI- dataOptional arguments
3603- * @returns {(input: any, output: any) => any} Callback function for the autocomplete
3621+ * @param {{entries: Record<string, any>}} [opt.data] - Your WI data
3622+ * @param {(entry:any)=>string|string[]|null|undefined} [opt.collectValues] - Extract values from one entry
3623+ * @param {() => Iterable<string>} [opt.includeExtras] - Optional global extras to include
3624+ * @param {(ctx:{result:string[], control:JQuery, input:any, haystack:string[]})=>string[]} [opt.postFilter] - Optional final filter step (for special rules like your "group" de-dupe logic)
36043625 */
3605-function getInclusionGroupCallback(data) {
3626+function buildAutocompleteCallback({ data, collectValues, includeExtras = () => [], postFilter } = {}) {
36063627 return function (control, input, output) {
36073628 const uid = $(control).data('uid');
3608- const thisGroups = String($(control).val()).split(/,\s*/).filter(x => x).map(x => x.toLowerCase());
3629+
3609- const groups = new Set();
3630+ // Collect unique values from all *other* entries
3610- for (const entry of Object.values(data.entries)) {
3631+ const values = new Set();
3611- // Skip the groups of this entry, because auto-complete should only suggest the ones that are already available on other entries
3632+ for (const entry of Object.values(data.entries ?? {})) {
36123633 if (entry?.uid == uid) continue;
3613- if (entry.group) {
3634+ const raw = collectValues(entry);
3614- entry.group.split(/,\s*/).filter(x => x).forEach(x => groups.add(x));
3635+ if (raw == null) continue;
3636+ const arr = Array.isArray(raw) ? raw : [raw];
3637+ for (const v of arr) {
3638+ const s = String(v).trim();
3639+ if (s) values.add(s);
36153640 }
36163641 }
36173642
3618- const haystack = Array.from(groups);
3643+ // Add optional global extras
3619- haystack.sort((a, b) => a.localeCompare(b));
3644+ for (const v of includeExtras()) {
36203645 const needles = input.termString(v).toLowerCasetrim();
3621- const hasExactMatch = haystack.findIndex(x => x.toLowerCase() == needle) !== -1;
3646+ if (s) values.add(s);
3622- const result = haystack.filter(x => x.toLowerCase().includes(needle) && (!thisGroups.includes(x) || hasExactMatch && thisGroups.filter(g => g == x).length == 1));
3647+ }
3648+
3649+ // Sort stable & locale-aware
3650+ const haystack = Array.from(values).sort((a, b) => a.localeCompare(b));
3651+
3652+ // Case-insensitive contains
3653+ const needle = String(input.term ?? '').toLowerCase();
3654+ let result = haystack.filter(x => x.toLowerCase().includes(needle));
3655+
3656+ // Optional final-pass semantics
3657+ if (postFilter) {
3658+ result = postFilter({ result, control: $(control), input, haystack });
3659+ }
36233660
36243661 output(result);
36253662 };
36263663}
36273664
3628-function getAutomationIdCallback(data) {
3665+/**
3629- return function (control, input, output) {
3666+ * Splits a string into an array of strings, separated by commas and trimmed
3630- const uid = $(control).data('uid');
3667+ * @param {string} s - The string to split
3631- const ids = new Set();
3668+ * @returns {string[]} An array of strings, separated by commas and trimmed
3632- for (const entry of Object.values(data.entries)) {
3669+ */
3633- // Skip automation id of this entry, because auto-complete should only suggest the ones that are already available on other entries
3670+const splitCsv = s => String(s ?? '').split(/,\s*/).filter(Boolean);
3634- if (entry.uid == uid) continue;
3635- if (entry.automationId) {
3636- ids.add(String(entry.automationId));
3637- }
3638- }
36393671
3640- if ('quickReplyApi' in globalThis) {
3672+/**
3641- for (const automationId of globalThis.quickReplyApi.listAutomationIds()) {
3673+ * Get the inclusion groups for the autocomplete.
3642- ids.add(String(automationId));
3674+ * @param {any} data WI data
3643- }
3675+ * @returns {(input: any, output: any) => any} Callback function for the autocomplete
3644- }
3676+ */
3677+function getInclusionGroupCallback(data) {
3678+ return buildAutocompleteCallback({
3679+ data,
3680+ collectValues: entry => entry.group ? splitCsv(entry.group) : [],
3681+ postFilter: ({ result, control, input, haystack }) => {
3682+ const thisGroups = splitCsv(String($(control).val()));
3683+ const needle = String(input.term ?? '').toLowerCase();
3684+ const hasExactMatch = haystack.some(x => x.toLowerCase() === needle);
3685+
3686+ // include suggestion if it contains the needle AND
3687+ // (not already present OR (exact match typed && appears only once))
3688+ return result.filter(x =>
3689+ !thisGroups.includes(x) ||
3690+ (hasExactMatch && thisGroups.filter(g => g === x).length === 1),
3691+ );
3692+ },
3693+ });
3694+}
36453695
3646- const haystack = Array.from(ids);
3696+function getAutomationIdCallback(data) {
3647- haystack.sort((a, b) => a.localeCompare(b));
3697+ return buildAutocompleteCallback({
3648- const needle = input.term.toLowerCase();
3698+ data,
3649- const result = haystack.filter(x => x.toLowerCase().includes(needle));
3699+ collectValues: entry => entry.automationId != null ? [String(entry.automationId)] : [],
3700+ includeExtras: () =>
3701+ ('quickReplyApi' in globalThis && globalThis.quickReplyApi?.listAutomationIds)
3702+ ? globalThis.quickReplyApi.listAutomationIds()
3703+ : [],
3704+ });
3705+}
36503706
3651- output(result);
3707+function getOutletNameCallback(data) {
3652- };
3708+ return buildAutocompleteCallback({
3709+ data,
3710+ collectValues: entry => entry.position === world_info_position.outlet && entry.outletName ? [entry.outletName] : [],
3711+ });
36533712}
36543713
36553714/**
36563715 * Create an autocomplete for thean inclusioninput groupelement.
36573716 * @param {JQuery<HTMLElement>} input - Input element to attach the autocomplete to
36583717 * @param {(control: JQuery<HTMLElement>, input: any, output: any) => any} callback - Source data callbacks
36593718 * @param {object} [options={}] - Optional arguments
@@ -3770,6 +3829,7 @@ export const newWorldInfoEntryDefinition = {
37703829 probability: { default: 100, type: 'number' },
37713830 useProbability: { default: true, type: 'boolean' },
37723831 depth: { default: DEFAULT_DEPTH, type: 'number' },
3832+ outletName: { default: '', type: 'string' },
37733833 group: { default: '', type: 'string' },
37743834 groupOverride: { default: false, type: 'boolean' },
37753835 groupWeight: { default: DEFAULT_WEIGHT, type: 'number' },
@@ -4281,7 +4341,7 @@ export async function checkWorldInfo(chat, maxContext, isDryRun, globalScanData
42814341 timedEffects.checkTimedEffects();
42824342
42834343 if (sortedEntries.length === 0) {
42844344 return { worldInfoBefore: '', worldInfoAfter: '', WIDepthEntries: [], EMEntries: [], ANBeforeEntries: [], ANAfterEntries: [], outletEntries: {}, allActivatedEntries: new Set() };
42854345 }
42864346
42874347 /** @type {number[]} Represents the delay levels for entries that are delayed until recursion */
@@ -4682,6 +4742,8 @@ export async function checkWorldInfo(chat, maxContext, isDryRun, globalScanData
46824742 const ANTopEntries = [];
46834743 const ANBottomEntries = [];
46844744 const WIDepthEntries = [];
4745+ /** @type {{[key: string]: string[]}} */
4746+ const WIOutletEntries = {};
46854747
46864748 // Appends from insertion order 999 to 1. Use unshift for this purpose
46874749 // TODO (kingbri): Change to use WI Anchor positioning instead of separate top/bottom arrays
@@ -4730,6 +4792,18 @@ export async function checkWorldInfo(chat, maxContext, isDryRun, globalScanData
47304792 }
47314793 break;
47324794 }
4795+ case world_info_position.outlet: {
4796+ if (!entry.outletName) {
4797+ console.warn(`[WI] Entry ${entry.uid} has position 'outlet' but no outlet name. Skipping.`);
4798+ break;
4799+ }
4800+ if (Array.isArray(WIOutletEntries[entry.outletName])) {
4801+ WIOutletEntries[entry.outletName].push(content);
4802+ } else {
4803+ WIOutletEntries[entry.outletName] = [content];
4804+ }
4805+ break;
4806+ }
47334807 default:
47344808 break;
47354809 }
@@ -4751,7 +4825,7 @@ export async function checkWorldInfo(chat, maxContext, isDryRun, globalScanData
47514825 console.log(`[WI] ${isDryRun ? 'Hypothetically adding' : 'Adding'} ${allActivatedEntries.size} entries to prompt`, Array.from(allActivatedEntries.values()));
47524826 console.debug(`[WI] --- DONE${isDryRun ? ' (DRY RUN)' : ''} ---`);
47534827
47544828 return { worldInfoBefore, worldInfoAfter, EMEntries, WIDepthEntries, ANBeforeEntries: ANTopEntries, ANAfterEntries: ANBottomEntries, outletEntries: WIOutletEntries, allActivatedEntries: new Set(allActivatedEntries.values()) };
47554829}
47564830
47574831/**
@@ -4971,6 +5045,7 @@ function convertAgnaiMemoryBook(inputObj) {
49715045 displayIndex: index,
49725046 probability: 100,
49735047 useProbability: true,
5048+ outletName: '',
49745049 group: '',
49755050 groupOverride: false,
49765051 groupWeight: DEFAULT_WEIGHT,
@@ -5015,6 +5090,7 @@ function convertRisuLorebook(inputObj) {
50155090 displayIndex: index,
50165091 probability: entry.activationPercent ?? 100,
50175092 useProbability: entry.activationPercent ?? true,
5093+ outletName: '',
50185094 group: '',
50195095 groupOverride: false,
50205096 groupWeight: DEFAULT_WEIGHT,
@@ -5064,6 +5140,7 @@ function convertNovelLorebook(inputObj) {
50645140 displayIndex: index,
50655141 probability: 100,
50665142 useProbability: true,
5143+ outletName: '',
50675144 group: '',
50685145 groupOverride: false,
50695146 groupWeight: DEFAULT_WEIGHT,
@@ -5114,6 +5191,7 @@ export function convertCharacterBook(characterBook) {
51145191 useProbability: entry.extensions?.useProbability ?? true,
51155192 depth: entry.extensions?.depth ?? DEFAULT_DEPTH,
51165193 selectiveLogic: entry.extensions?.selectiveLogic ?? world_info_logic.AND_ANY,
5194+ outletName: entry.extensions?.outlet_name ?? '',
51175195 group: entry.extensions?.group ?? '',
51185196 groupOverride: entry.extensions?.group_override ?? false,
51195197 groupWeight: entry.extensions?.group_weight ?? DEFAULT_WEIGHT,
src/endpoints/characters.js+1 -0
@@ -689,6 +689,7 @@ function convertWorldInfoToCharacterBook(name, entries) {
689689 useProbability: entry.useProbability ?? false,
690690 depth: entry.depth ?? 4,
691691 selectiveLogic: entry.selectiveLogic ?? 0,
692+ outlet_name: entry.outletName ?? '',
692693 group: entry.group ?? '',
693694 group_override: entry.groupOverride ?? false,
694695 group_weight: entry.groupWeight ?? null,