🎉 初始化项目

This commit is contained in:
2026-02-10 17:48:27 +08:00
parent f3da9c506a
commit db934ebed7
1575 changed files with 348967 additions and 0 deletions

View File

@@ -0,0 +1,280 @@
import { seedrandom, droll } from '../../../lib.js';
import { chat_metadata, main_api, getMaxContextSize, extension_prompts, getCurrentChatId } from '../../../script.js';
import { getStringHash } from '../../utils.js';
import { textgenerationwebui_banned_in_macros } from '../../textgen-settings.js';
import { inject_ids } from '../../constants.js';
import { MacroRegistry, MacroCategory, MacroValueType } from '../engine/MacroRegistry.js';
/**
* Registers SillyTavern's core built-in macros in the MacroRegistry.
*
* These macros correspond to the main {{...}} macros that are available
* in prompts (time/date/chat info, utility macros, etc.). They are
* intended to preserve the behavior of the existing regex-based macros
* in macros.js while using the new MacroRegistry/MacroEngine pipeline.
*/
export function registerCoreMacros() {
// {{space}} -> ' '
MacroRegistry.registerMacro('space', {
category: MacroCategory.UTILITY,
unnamedArgs: [
{
name: 'count',
optional: true,
defaultValue: '1',
type: MacroValueType.INTEGER,
description: 'Number of spaces to insert.',
},
],
description: 'Returns one or more spaces. One space by default, more if the count argument is specified.',
returns: 'One or more spaces.',
exampleUsage: ['{{space}}', '{{space::4}}'],
handler: ({ unnamedArgs: [count] }) => ' '.repeat(Number(count ?? 1)),
});
// {{newline}} -> '\n'
MacroRegistry.registerMacro('newline', {
category: MacroCategory.UTILITY,
unnamedArgs: [
{
name: 'count',
optional: true,
defaultValue: '1',
type: MacroValueType.INTEGER,
description: 'Number of newlines to insert.',
},
],
description: 'Inserts one or more newlines. One newline by default, more if the count argument is specified.',
returns: 'One or more \\n.',
exampleUsage: ['{{newline}}', '{{newline::2}}'],
handler: ({ unnamedArgs: [count] }) => '\n'.repeat(Number(count ?? 1)),
});
// {{noop}} -> ''
MacroRegistry.registerMacro('noop', {
category: MacroCategory.UTILITY,
description: 'Does nothing and produces an empty string.',
returns: '',
handler: () => '',
});
// {{trim}} -> macro will currently replace itself with itself. Trimming is handled in post-processing.
MacroRegistry.registerMacro('trim', {
category: MacroCategory.UTILITY,
description: 'Trims all whitespaces around the trim macro.',
returns: '',
handler: () => '{{trim}}',
});
// {{input}} -> current textarea content
MacroRegistry.registerMacro('input', {
category: MacroCategory.UTILITY,
description: 'Current text from the send textarea.',
returns: 'Current text from the send textarea.',
handler: () => (/** @type {HTMLTextAreaElement} */(document.querySelector('#send_textarea')))?.value ?? '',
});
// {{maxPrompt}} -> max context size
MacroRegistry.registerMacro('maxPrompt', {
category: MacroCategory.STATE,
description: 'Maximum prompt context size.',
returns: 'Maximum prompt context size.',
returnType: MacroValueType.INTEGER,
handler: () => String(getMaxContextSize()),
});
// String utilities
MacroRegistry.registerMacro('reverse', {
category: MacroCategory.UTILITY,
unnamedArgs: [
{
name: 'value',
type: MacroValueType.STRING,
description: 'The string to reverse.',
},
],
description: 'Reverses the characters of the argument provided.',
returns: 'Reversed string.',
exampleUsage: ['{{reverse::I am Lana}}'],
handler: ({ unnamedArgs: [value] }) => Array.from(value).reverse().join(''),
});
// Comment macro: {{// ...}} -> '' (consumes any arguments)
MacroRegistry.registerMacro('//', {
aliases: [{ alias: 'comment' }],
category: MacroCategory.UTILITY,
list: true, // We consume any arguments as if this is a list, but we'll ignore them in the handler anyway
strictArgs: false, // and we also always remove it, even if the parsing might say it's invalid
description: 'Comment macro that produces an empty string. Can be used for writing into prompt definitions, without being passed to the context.',
returns: '',
displayOverride: '{{// ...}}',
exampleUsage: ['{{// This is a comment}}'],
handler: () => '',
});
// Time and date macros
// Dice roll macro: {{roll 1d6}} or {{roll: 1d6}}
MacroRegistry.registerMacro('roll', {
category: MacroCategory.RANDOM,
unnamedArgs: [
{
name: 'formula',
sampleValue: '1d20',
description: 'Dice roll formula using droll syntax (e.g. 1d20).',
type: 'string',
},
],
description: 'Rolls dice using droll syntax (e.g. {{roll 1d20}}).',
returns: 'Dice roll result.',
returnType: MacroValueType.INTEGER,
exampleUsage: [
'{{roll::1d20}}',
'{{roll::6}}',
'{{roll::3d6+4}}',
],
handler: ({ unnamedArgs: [formula] }) => {
// If only digits were provided, treat it as `1dX`.
if (/^\d+$/.test(formula)) {
formula = `1d${formula}`;
}
const isValid = droll.validate(formula);
if (!isValid) {
console.debug(`Invalid roll formula: ${formula}`);
return '';
}
const result = droll.roll(formula);
if (result === false) return '';
return String(result.total);
},
});
// Random choice macro: {{random::a::b}} or {{random a,b}}
MacroRegistry.registerMacro('random', {
category: MacroCategory.RANDOM,
list: true,
description: 'Picks a random item from a list. Will be re-rolled every time macros are resolved.',
returns: 'Randomly selected item from the list.',
exampleUsage: ['{{random::blonde::brown::red::black::blue}}'],
handler: ({ list }) => {
// Handle old legacy cases, where we have to split the list manually
if (list.length === 1) {
list = readSingleArgsRandomList(list[0]);
}
if (list.length === 0) {
return '';
}
const rng = seedrandom('added entropy.', { entropy: true });
const randomIndex = Math.floor(rng() * list.length);
return list[randomIndex];
},
});
// Deterministic choice macro: {{pick::a::b}} or {{pick a,b}}
MacroRegistry.registerMacro('pick', {
category: MacroCategory.RANDOM,
list: true,
description: 'Picks a random item from a list, but keeps the choice stable for a given chat and macro position.',
returns: 'Stable randomly selected item from the list.',
exampleUsage: ['{{pick::blonde::brown::red::black::blue}}'],
handler: ({ list, range, env }) => {
// Handle old legacy cases, where we have to split the list manually
if (list.length === 1) {
list = readSingleArgsRandomList(list[0]);
}
if (!list.length) {
return '';
}
const chatIdHash = getChatIdHash();
// Use the full original input string for deterministic behavior
const rawContentHash = env.contentHash;
const offset = typeof range?.startOffset === 'number' ? range.startOffset : 0;
const combinedSeedString = `${chatIdHash}-${rawContentHash}-${offset}`;
const finalSeed = getStringHash(combinedSeedString);
const rng = seedrandom(String(finalSeed));
const randomIndex = Math.floor(rng() * list.length);
return list[randomIndex];
},
});
/** @param {string} listString @return {string[]} */
function readSingleArgsRandomList(listString) {
// If it contains double colons, those will have precedence over comma-seperated lists.
// This can only happen if the macro only had a single colon to introduce the list...
// like, {{random:a::b::c}}
if (listString.includes('::')) {
return listString.split('::').map((/** @type {string} */ item) => item.trim());
}
// Otherwise, we fall back and split by commas that may be present
return listString
.replace(/\\,/g, '##<23>COMMA<4D>##')
.split(',')
.map((/** @type {string} */ item) => item.trim().replace(/##<23>COMMA<4D>##/g, ','));
}
// Banned words macro: {{banned "word"}}
MacroRegistry.registerMacro('banned', {
category: MacroCategory.UTILITY,
unnamedArgs: [
{
name: 'word',
sampleValue: 'word',
description: 'Word to ban for textgenerationwebui backend.',
type: 'string',
},
],
description: 'Bans a word for textgenerationwebui backend. (Strips quotes surrounding the banned word, if present)',
returns: '',
exampleUsage: ['{{banned::delve}}'],
handler: ({ unnamedArgs: [bannedWord] }) => {
// Strip quotes via regex, which were allowed in legacy syntax
bannedWord = bannedWord.replace(/^"|"$/g, '');
if (main_api === 'textgenerationwebui') {
console.log('Found banned word in macros: ' + bannedWord);
textgenerationwebui_banned_in_macros.push(bannedWord);
}
return '';
},
});
// Outlet macro: {{outlet::key}}
MacroRegistry.registerMacro('outlet', {
category: MacroCategory.UTILITY,
unnamedArgs: [
{
name: 'key',
sampleValue: 'my-outlet-key',
description: 'Outlet key.',
type: 'string',
},
],
description: 'Returns the world info outlet prompt for a given outlet key.',
returns: 'World info outlet prompt.',
exampleUsage: ['{{outlet::character-achievements}}'],
handler: ({ unnamedArgs: [outlet] }) => {
if (!outlet) return '';
const value = extension_prompts[inject_ids.CUSTOM_WI_OUTLET(outlet)]?.value;
return value || '';
},
});
}
function getChatIdHash() {
const cachedIdHash = chat_metadata['chat_id_hash'];
if (typeof cachedIdHash === 'number') {
return cachedIdHash;
}
const chatId = chat_metadata['main_chat'] ?? getCurrentChatId();
const chatIdHash = getStringHash(chatId);
chat_metadata['chat_id_hash'] = chatIdHash;
return chatIdHash;
}