🎉 初始化项目

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,136 @@
import { MacroRegistry, MacroCategory, MacroValueType } from '../engine/MacroRegistry.js';
import { chat, chat_metadata } from '../../../script.js';
/**
* Registers macros that inspect the current chat log and swipe state
* (message texts, indices, swipes, and context boundaries).
*/
export function registerChatMacros() {
MacroRegistry.registerMacro('lastMessage', {
category: MacroCategory.CHAT,
description: 'Last message in the chat.',
returns: 'Last message in the chat.',
handler: () => String(getLastMessage() ?? ''),
});
MacroRegistry.registerMacro('lastMessageId', {
category: MacroCategory.CHAT,
description: 'Index of the last message in the chat.',
returns: 'Index of the last message in the chat.',
returnType: MacroValueType.INTEGER,
handler: () => String(getLastMessageId() ?? ''),
});
MacroRegistry.registerMacro('lastUserMessage', {
category: MacroCategory.CHAT,
description: 'Last user message in the chat.',
returns: 'Last user message in the chat.',
handler: () => String(getLastUserMessage() ?? ''),
});
MacroRegistry.registerMacro('lastCharMessage', {
category: MacroCategory.CHAT,
description: 'Last character/bot message in the chat.',
returns: 'Last character/bot message in the chat.',
handler: () => String(getLastCharMessage() ?? ''),
});
MacroRegistry.registerMacro('firstIncludedMessageId', {
category: MacroCategory.CHAT,
description: 'Index of the first message included in the current context.',
returns: 'Index of the first message included in the context.',
returnType: MacroValueType.INTEGER,
handler: () => String(getFirstIncludedMessageId() ?? ''),
});
MacroRegistry.registerMacro('firstDisplayedMessageId', {
category: MacroCategory.CHAT,
description: 'Index of the first displayed message in the chat.',
returns: 'Index of the first displayed message in the chat.',
returnType: MacroValueType.INTEGER,
handler: () => String(getFirstDisplayedMessageId() ?? ''),
});
MacroRegistry.registerMacro('lastSwipeId', {
category: MacroCategory.CHAT,
description: '1-based index of the last swipe for the last message.',
returns: '1-based index of the last swipe.',
returnType: MacroValueType.INTEGER,
handler: () => String(getLastSwipeId() ?? ''),
});
MacroRegistry.registerMacro('currentSwipeId', {
category: MacroCategory.CHAT,
description: '1-based index of the current swipe.',
returns: '1-based index of the current swipe.',
returnType: MacroValueType.INTEGER,
handler: () => String(getCurrentSwipeId() ?? ''),
});
}
function getLastMessageId({ exclude_swipe_in_propress = true, filter = null } = {}) {
if (!Array.isArray(chat) || chat.length === 0) {
return null;
}
for (let i = chat.length - 1; i >= 0; i--) {
const message = chat[i];
if (exclude_swipe_in_propress && message.swipes && message.swipe_id >= message.swipes.length) {
continue;
}
if (!filter || filter(message)) {
return i;
}
}
return null;
}
function getLastMessage() {
const mid = getLastMessageId();
return typeof mid === 'number' ? (chat[mid]?.mes ?? '') : '';
}
function getLastUserMessage() {
const mid = getLastMessageId({ filter: m => m.is_user && !m.is_system });
return typeof mid === 'number' ? (chat[mid]?.mes ?? '') : '';
}
function getLastCharMessage() {
const mid = getLastMessageId({ filter: m => !m.is_user && !m.is_system });
return typeof mid === 'number' ? (chat[mid]?.mes ?? '') : '';
}
function getFirstIncludedMessageId() {
const value = chat_metadata['lastInContextMessageId'];
return typeof value === 'number' ? value : null;
}
function getFirstDisplayedMessageId() {
const mesElement = document.querySelector('#chat .mes');
const mesId = Number(mesElement?.getAttribute('mesid'));
if (!Number.isNaN(mesId) && mesId >= 0) {
return mesId;
}
return null;
}
function getLastSwipeId() {
const mid = getLastMessageId({ exclude_swipe_in_propress: false });
if (typeof mid !== 'number') {
return null;
}
const swipes = chat[mid]?.swipes;
return Array.isArray(swipes) ? swipes.length : null;
}
function getCurrentSwipeId() {
const mid = getLastMessageId({ exclude_swipe_in_propress: false });
if (typeof mid !== 'number') {
return null;
}
const swipeId = chat[mid]?.swipe_id;
return typeof swipeId === 'number' ? swipeId + 1 : null;
}

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;
}

View File

@@ -0,0 +1,181 @@
import { MacroRegistry, MacroCategory, MacroValueType } from '../engine/MacroRegistry.js';
import { isMobile } from '../../RossAscends-mods.js';
import { parseMesExamples, main_api } from '../../../script.js';
import { power_user } from '../../power-user.js';
import { formatInstructModeExamples } from '../../instruct-mode.js';
/** @typedef {import('../engine/MacroEnv.types.js').MacroEnv} MacroEnv */
/**
* Registers macros that mostly act as simple accessors to MacroEnv fields
* (names, character card fields, system metadata, extras) or basic
* environment flags.
*/
export function registerEnvMacros() {
// Names and participant macros (from MacroEnv.names)
MacroRegistry.registerMacro('user', {
category: MacroCategory.NAMES,
description: 'Your current Persona username.',
returns: 'Persona username.',
handler: ({ env }) => env.names.user,
});
MacroRegistry.registerMacro('char', {
category: MacroCategory.NAMES,
description: 'The character\'s name.',
returns: 'Character name.',
handler: ({ env }) => env.names.char,
});
MacroRegistry.registerMacro('group', {
aliases: [{ alias: 'charIfNotGroup', visible: false }],
category: MacroCategory.NAMES,
description: 'Comma-separated list of group member names (including muted) or the character name in solo chats.',
returns: 'List of group member names.',
handler: ({ env }) => env.names.group ?? '',
});
MacroRegistry.registerMacro('groupNotMuted', {
category: MacroCategory.NAMES,
description: 'Comma-separated list of group member names excluding muted members.',
returns: 'List of group member names excluding muted members.',
handler: ({ env }) => env.names.groupNotMuted ?? '',
});
MacroRegistry.registerMacro('notChar', {
category: MacroCategory.NAMES,
description: 'Comma-separated list of all participants except the current speaker.',
returns: 'List of all participants except the current speaker.',
handler: ({ env }) => env.names.notChar ?? '',
});
// Character card field macros (from MacroEnv.character)
MacroRegistry.registerMacro('charPrompt', {
category: MacroCategory.CHARACTER,
description: 'The character\'s Main Prompt override.',
returns: 'Character Main Prompt override.',
handler: ({ env }) => env.character.charPrompt ?? '',
});
MacroRegistry.registerMacro('charInstruction', {
category: MacroCategory.CHARACTER,
description: 'The character\'s Post-History Instructions override.',
returns: 'Character Post-History Instructions override.',
handler: ({ env }) => env.character.charInstruction ?? '',
});
MacroRegistry.registerMacro('charDescription', {
aliases: [{ alias: 'description' }],
category: MacroCategory.CHARACTER,
description: 'The character\'s description.',
returns: 'Character description.',
handler: ({ env }) => env.character.description ?? '',
});
MacroRegistry.registerMacro('charPersonality', {
aliases: [{ alias: 'personality' }],
category: MacroCategory.CHARACTER,
description: 'The character\'s personality.',
returns: 'Character personality.',
handler: ({ env }) => env.character.personality ?? '',
});
MacroRegistry.registerMacro('charScenario', {
aliases: [{ alias: 'scenario' }],
category: MacroCategory.CHARACTER,
description: 'The character\'s scenario.',
returns: 'Character scenario.',
handler: ({ env }) => env.character.scenario ?? '',
});
MacroRegistry.registerMacro('persona', {
category: MacroCategory.CHARACTER,
description: 'Your current Persona description.',
returns: 'Persona description.',
handler: ({ env }) => env.character.persona ?? '',
});
MacroRegistry.registerMacro('mesExamplesRaw', {
category: MacroCategory.CHARACTER,
description: 'Unformatted dialogue examples from the character card.',
returns: 'Unformatted dialogue examples.',
handler: ({ env }) => env.character.mesExamplesRaw ?? '',
});
MacroRegistry.registerMacro('mesExamples', {
category: MacroCategory.CHARACTER,
description: 'The character\'s dialogue examples, formatted for instruct mode when enabled.',
returns: 'Formatted dialogue examples.',
handler: ({ env }) => {
const raw = env.character.mesExamplesRaw ?? '';
if (!raw) return '';
const isInstruct = !!power_user?.instruct?.enabled && main_api !== 'openai';
const parsed = parseMesExamples(raw, isInstruct);
if (!Array.isArray(parsed) || parsed.length === 0) {
return '';
}
if (!isInstruct) {
return parsed.join('');
}
const formatted = formatInstructModeExamples(parsed, env.names.user, env.names.char);
return Array.isArray(formatted) ? formatted.join('') : '';
},
});
MacroRegistry.registerMacro('charDepthPrompt', {
category: MacroCategory.CHARACTER,
description: 'The character\'s @ Depth Note.',
returns: 'Character @ Depth Note.',
handler: ({ env }) => env.character.charDepthPrompt ?? '',
});
MacroRegistry.registerMacro('charCreatorNotes', {
aliases: [{ alias: 'creatorNotes' }],
category: MacroCategory.CHARACTER,
description: 'Creator notes from the character card.',
returns: 'Creator notes.',
handler: ({ env }) => env.character.creatorNotes ?? '',
});
// Character version macros (legacy variants and documented {{charVersion}})
MacroRegistry.registerMacro('charVersion', {
aliases: [
{ alias: 'version', visible: false }, // Legacy alias
{ alias: 'char_version', visible: false }, // Legacy underscore variant
],
category: MacroCategory.CHARACTER,
description: 'The character\'s version number.',
returns: 'Character version number.',
handler: ({ env }) => env.character.version ?? '',
});
// System / env extras macros (from MacroEnv.system / MacroEnv.extra)
MacroRegistry.registerMacro('model', {
category: MacroCategory.STATE,
description: 'Model name for the currently selected API (Chat Completion or Chat Completion).',
returns: 'Model name.',
handler: ({ env }) => env.system.model,
});
MacroRegistry.registerMacro('original', {
category: MacroCategory.CHARACTER,
description: 'Original message content for {{original}} substitution in in character prompt overrides.',
returns: 'Original message content.',
handler: ({ env }) => {
const value = env.functions.original();
return value;
},
});
// Device / environment macros
MacroRegistry.registerMacro('isMobile', {
category: MacroCategory.STATE,
description: '"true" if currently running in a mobile environment, "false" otherwise.',
returns: 'Whether the environment is mobile.',
returnType: MacroValueType.BOOLEAN,
handler: () => String(isMobile()),
});
}

View File

@@ -0,0 +1,76 @@
import { MacroRegistry, MacroCategory } from '../engine/MacroRegistry.js';
import { power_user } from '../../power-user.js';
/**
* Registers instruct-mode related {{...}} macros (instruct* and system
* prompt/context macros) in the MacroRegistry.
*/
export function registerInstructMacros() {
/**
* Helper to register macros that just expose a value from power_user.instruct.
* The first name is the primary, subsequent names become visible aliases.
* @param {string[]} names - First is primary, rest are aliases.
* @param {() => string} getValue
* @param {() => boolean} isEnabled
* @param {string} description
* @param {string} [category=MacroCategory.PROMPTS]
*/
function registerSimple(names, getValue, isEnabled, description, category = MacroCategory.PROMPTS) {
const [primary, ...aliasNames] = names;
const aliases = aliasNames.map(alias => ({ alias }));
MacroRegistry.registerMacro(primary, {
category,
description,
aliases: aliases.length > 0 ? aliases : undefined,
handler: () => (isEnabled() ? (getValue() ?? '') : ''),
});
}
const instEnabled = () => !!power_user.instruct.enabled;
const sysEnabled = () => !!power_user.sysprompt.enabled;
// Instruct template macros
registerSimple(['instructStoryStringPrefix'], () => power_user.instruct.story_string_prefix, instEnabled, 'Instruct story string prefix.');
registerSimple(['instructStoryStringSuffix'], () => power_user.instruct.story_string_suffix, instEnabled, 'Instruct story string suffix.');
registerSimple(['instructUserPrefix', 'instructInput'], () => power_user.instruct.input_sequence, instEnabled, 'Instruct input / user prefix sequence.');
registerSimple(['instructUserSuffix'], () => power_user.instruct.input_suffix, instEnabled, 'Instruct input / user suffix sequence.');
registerSimple(['instructAssistantPrefix', 'instructOutput'], () => power_user.instruct.output_sequence, instEnabled, 'Instruct output / assistant prefix sequence.');
registerSimple(['instructAssistantSuffix', 'instructSeparator'], () => power_user.instruct.output_suffix, instEnabled, 'Instruct output / assistant suffix sequence.');
registerSimple(['instructSystemPrefix'], () => power_user.instruct.system_sequence, instEnabled, 'Instruct system prefix sequence.');
registerSimple(['instructSystemSuffix'], () => power_user.instruct.system_suffix, instEnabled, 'Instruct system suffix sequence.');
registerSimple(['instructFirstAssistantPrefix', 'instructFirstOutputPrefix'], () => power_user.instruct.first_output_sequence || power_user.instruct.output_sequence, instEnabled, 'Instruct first assistant / output prefix sequence');
registerSimple(['instructLastAssistantPrefix', 'instructLastOutputPrefix'], () => power_user.instruct.last_output_sequence || power_user.instruct.output_sequence, instEnabled, 'Instruct last assistant / output prefix sequence.');
registerSimple(['instructStop'], () => power_user.instruct.stop_sequence, instEnabled, 'Instruct stop sequence.');
registerSimple(['instructUserFiller'], () => power_user.instruct.user_alignment_message, instEnabled, 'Instruct user alignment filler.');
registerSimple(['instructSystemInstructionPrefix'], () => power_user.instruct.last_system_sequence, instEnabled, 'Instruct system instruction prefix sequence.');
registerSimple(['instructFirstUserPrefix', 'instructFirstInput'], () => power_user.instruct.first_input_sequence || power_user.instruct.input_sequence, instEnabled, 'Instruct first user / input prefix sequence.');
registerSimple(['instructLastUserPrefix', 'instructLastInput'], () => power_user.instruct.last_input_sequence || power_user.instruct.input_sequence, instEnabled, 'Instruct last user / input prefix sequence.');
// System prompt macros
registerSimple(['defaultSystemPrompt', 'instructSystem', 'instructSystemPrompt'], () => power_user.sysprompt.content, sysEnabled, 'Default system prompt.');
MacroRegistry.registerMacro('systemPrompt', {
category: MacroCategory.PROMPTS,
description: 'Active system prompt text (optionally overridden by character prompt)',
handler: ({ env }) => {
const isEnabled = !!power_user.sysprompt.enabled;
if (!isEnabled) return '';
if (power_user.prefer_character_prompt && env.character.charPrompt) {
return env.character.charPrompt;
}
return power_user.sysprompt.content ?? '';
},
});
// Context template macros
registerSimple(['exampleSeparator', 'chatSeparator'], () => power_user.context.example_separator, () => true, 'Separator used between example chat blocks in text completion prompts.');
registerSimple(['chatStart'], () => power_user.context.chat_start, () => true, 'Chat start marker used in text completion prompts.');
}

View File

@@ -0,0 +1,40 @@
import { MacroRegistry, MacroCategory } from '../engine/MacroRegistry.js';
import { eventSource, event_types } from '../../events.js';
let lastGenerationTypeValue = '';
let lastGenerationTypeTrackingInitialized = false;
function ensureLastGenerationTypeTracking() {
if (lastGenerationTypeTrackingInitialized) {
return;
}
lastGenerationTypeTrackingInitialized = true;
try {
eventSource?.on?.(event_types.GENERATION_STARTED, (type, _params, isDryRun) => {
if (isDryRun) return;
lastGenerationTypeValue = type || 'normal';
});
eventSource?.on?.(event_types.CHAT_CHANGED, () => {
lastGenerationTypeValue = '';
});
} catch {
// In non-runtime environments (tests), eventSource may be undefined or not fully initialized.
}
}
/**
* Registers macros that depend on runtime application state or event tracking
* rather than static environment fields.
*/
export function registerStateMacros() {
ensureLastGenerationTypeTracking();
MacroRegistry.registerMacro('lastGenerationType', {
category: MacroCategory.STATE,
description: 'Type of the last queued generation request (e.g. "normal", "impersonate", "regenerate", "quiet", "swipe", "continue"). Empty if none yet or chat was switched.',
returns: 'Type of the last queued generation request.',
handler: () => lastGenerationTypeValue,
});
}

View File

@@ -0,0 +1,151 @@
import { moment } from '../../../lib.js';
import { chat } from '../../../script.js';
import { timestampToMoment } from '../../utils.js';
import { MacroRegistry, MacroCategory, MacroValueType } from '../engine/MacroRegistry.js';
/**
* Registers time/date related macros and utilities.
*/
export function registerTimeMacros() {
// Time and date macros
MacroRegistry.registerMacro('time', {
category: MacroCategory.TIME,
// Optional single list argument: UTC offset, e.g. {{time::UTC+2}}
unnamedArgs: [
{
name: 'offset',
optional: true,
defaultValue: 'null',
type: MacroValueType.STRING,
sampleValue: 'UTC+2',
description: 'UTC offset in the format UTC±(offset).',
},
],
description: 'Current local time, or UTC offset when called as {{time::UTC±(offset)}}',
returns: 'A time string in the format HH:mm.',
displayOverride: '{{time::[UTC±(offset)]}}',
exampleUsage: ['{{time}}', '{{time::UTC+2}}', '{{time::UTC-7}}'],
handler: ({ unnamedArgs: [offsetSpec] }) => {
if (!offsetSpec) return moment().format('LT');
const match = /^UTC([+-]\d+)$/.exec(offsetSpec);
if (!match) return moment().format('LT');
const offset = Number.parseInt(match[1], 10);
if (Number.isNaN(offset)) return moment().format('LT');
return moment().utc().utcOffset(offset).format('LT');
},
});
MacroRegistry.registerMacro('date', {
category: MacroCategory.TIME,
description: 'Current local date as a string in the local short format.',
returns: 'Current local date in local short format.',
handler: () => moment().format('LL'),
});
MacroRegistry.registerMacro('weekday', {
category: MacroCategory.TIME,
description: 'Current weekday name.',
returns: 'Current weekday name.',
handler: () => moment().format('dddd'),
});
MacroRegistry.registerMacro('isotime', {
category: MacroCategory.TIME,
description: 'Current time in HH:mm format.',
returns: 'Current time in HH:mm format.',
handler: () => moment().format('HH:mm'),
});
MacroRegistry.registerMacro('isodate', {
category: MacroCategory.TIME,
description: 'Current date in YYYY-MM-DD format.',
returns: 'Current date in YYYY-MM-DD format.',
handler: () => moment().format('YYYY-MM-DD'),
});
MacroRegistry.registerMacro('datetimeformat', {
category: MacroCategory.TIME,
unnamedArgs: [
{
name: 'format',
sampleValue: 'YYYY-MM-DD HH:mm:ss',
description: 'Moment.js format string.',
type: 'string',
},
],
description: 'Formats the current date/time using the given moment.js format string.',
returns: 'Formatted date/time string.',
exampleUsage: ['{{datetimeformat::YYYY-MM-DD HH:mm:ss}}', '{{datetimeformat::LLLL}}'],
handler: ({ unnamedArgs: [format] }) => moment().format(format),
});
MacroRegistry.registerMacro('idleDuration', {
aliases: [{ alias: 'idle_duration', visible: false }],
category: MacroCategory.TIME,
description: 'Human-readable duration since the last user message.',
returns: 'Human-readable duration since the last user message.',
handler: () => getTimeSinceLastMessage(),
});
// Time difference between two values
MacroRegistry.registerMacro('timeDiff', {
category: MacroCategory.TIME,
unnamedArgs: [
{
name: 'left',
sampleValue: '2023-01-01 12:00:00',
description: 'Left time value.',
type: 'string',
},
{
name: 'right',
sampleValue: '2023-01-01 15:00:00',
description: 'Right time value.',
type: 'string',
},
],
description: 'Human-readable difference between two times. Order of times does not matter, it will return the absolute difference.',
returns: 'Human-readable difference between two times.',
displayOverride: '{{timeDiff::left::right}}', // Shorten this, otherwise it's too long. Full dates don't really help for understanding the macro.
exampleUsage: ['{{ timeDiff :: 2023-01-01 12:00:00 :: 2023-01-01 15:00:00 }}'],
handler: ({ unnamedArgs: [left, right] }) => {
const diff = moment.duration(moment(left).diff(moment(right)));
return diff.humanize(true);
},
});
}
function getTimeSinceLastMessage() {
const now = moment();
if (Array.isArray(chat) && chat.length > 0) {
let lastMessage;
let takeNext = false;
for (let i = chat.length - 1; i >= 0; i--) {
const message = chat[i];
if (message.is_system) {
continue;
}
if (message.is_user && takeNext) {
lastMessage = message;
break;
}
takeNext = true;
}
if (lastMessage?.send_date) {
const lastMessageDate = timestampToMoment(lastMessage.send_date);
const duration = moment.duration(now.diff(lastMessageDate));
return duration.humanize();
}
}
return 'just now';
}

View File

@@ -0,0 +1,224 @@
import { MacroRegistry, MacroCategory, MacroValueType } from '../engine/MacroRegistry.js';
/**
* Registers variable-related {{...}} macros that operate on local and global
* variables (e.g. {{setvar}}, {{getvar}}, {{incvar}}, etc.).
*/
export function registerVariableMacros() {
const ctx = SillyTavern.getContext();
// {{setvar::name::value}} -> '' (side-effect on local variable)
MacroRegistry.registerMacro('setvar', {
category: MacroCategory.VARIABLE,
unnamedArgs: [
{
name: 'name',
type: MacroValueType.STRING,
description: 'The name of the local variable to set.',
},
{
name: 'value',
type: [MacroValueType.STRING, MacroValueType.NUMBER],
description: 'The value to set the local variable to.',
},
],
description: 'Sets a local variable to the given value.',
returns: '',
exampleUsage: ['{{setvar::myvar::foo}}', '{{setvar::myintvar::3}}'],
handler: ({ unnamedArgs: [name, value] }) => {
ctx.variables.local.set(name, value);
return '';
},
});
// {{addvar::name::value}} -> '' (side-effect via addLocalVariable)
MacroRegistry.registerMacro('addvar', {
category: MacroCategory.VARIABLE,
unnamedArgs: [
{
name: 'name',
type: MacroValueType.STRING,
description: 'The name of the local variable to add to.',
},
{
name: 'value',
type: [MacroValueType.STRING, MacroValueType.NUMBER],
description: 'The value to add to the local variable.',
},
],
description: 'Adds a value to an existing local variable (numeric or string append). If the variable does not exist, it will be created.',
returns: '',
exampleUsage: ['{{addvar::mystrvar::foo}}', '{{addvar::myintvar::3}}'],
handler: ({ unnamedArgs: [name, value] }) => {
ctx.variables.local.add(name, value);
return '';
},
});
// {{incvar::name}} -> returns new value
MacroRegistry.registerMacro('incvar', {
category: MacroCategory.VARIABLE,
unnamedArgs: [
{
name: 'name',
type: MacroValueType.STRING,
description: 'The name of the local variable to increment.',
},
],
description: 'Increments a local variable by 1 and returns the new value. If the variable does not exist, it will be created.',
returns: 'The new value of the local variable.',
returnType: MacroValueType.NUMBER,
exampleUsage: ['{{incvar::myintvar}}'],
handler: ({ unnamedArgs: [name], normalize }) => {
const result = ctx.variables.local.inc(name);
return normalize(result);
},
});
// {{decvar::name}} -> returns new value
MacroRegistry.registerMacro('decvar', {
category: MacroCategory.VARIABLE,
unnamedArgs: [
{
name: 'name',
type: MacroValueType.STRING,
description: 'The name of the local variable to decrement.',
},
],
description: 'Decrements a local variable by 1 and returns the new value. If the variable does not exist, it will be created.',
returns: 'The new value of the local variable.',
returnType: MacroValueType.NUMBER,
exampleUsage: ['{{decvar::myintvar}}'],
handler: ({ unnamedArgs: [name], normalize }) => {
const result = ctx.variables.local.dec(name);
return normalize(result);
},
});
// {{getvar::name}} -> returns current value
MacroRegistry.registerMacro('getvar', {
category: MacroCategory.VARIABLE,
unnamedArgs: [
{
name: 'name',
type: MacroValueType.STRING,
description: 'The name of the local variable to get.',
},
],
description: 'Gets the value of a local variable.',
returns: 'The value of the local variable.',
returnType: [MacroValueType.STRING, MacroValueType.NUMBER],
exampleUsage: ['{{getvar::myvar}}', '{{getvar::myintvar}}'],
handler: ({ unnamedArgs: [name], normalize }) => {
const result = ctx.variables.local.get(name);
return normalize(result);
},
});
// {{setglobalvar::name::value}} -> ''
MacroRegistry.registerMacro('setglobalvar', {
category: MacroCategory.VARIABLE,
unnamedArgs: [
{
name: 'name',
type: MacroValueType.STRING,
description: 'The name of the global variable to set.',
},
{
name: 'value',
type: [MacroValueType.STRING, MacroValueType.NUMBER],
description: 'The value to set the global variable to.',
},
],
description: 'Sets a global variable to the given value.',
returns: '',
exampleUsage: ['{{setglobalvar::myvar::foo}}', '{{setglobalvar::myintvar::3}}'],
handler: ({ unnamedArgs: [name, value] }) => {
ctx.variables.global.set(name, value);
return '';
},
});
// {{addglobalvar::name::value}} -> ''
MacroRegistry.registerMacro('addglobalvar', {
category: MacroCategory.VARIABLE,
unnamedArgs: [
{
name: 'name',
type: MacroValueType.STRING,
description: 'The name of the global variable to add to.',
},
{
name: 'value',
type: [MacroValueType.STRING, MacroValueType.NUMBER],
description: 'The value to add to the global variable.',
},
],
description: 'Adds a value to an existing global variable (numeric or string append). If the variable does not exist, it will be created.',
returns: '',
exampleUsage: ['{{addglobalvar::mystrvar::foo}}', '{{addglobalvar::myintvar::3}}'],
handler: ({ unnamedArgs: [name, value] }) => {
ctx.variables.global.add(name, value);
return '';
},
});
// {{incglobalvar::name}} -> returns new value
MacroRegistry.registerMacro('incglobalvar', {
category: MacroCategory.VARIABLE,
unnamedArgs: [
{
name: 'name',
type: MacroValueType.STRING,
description: 'The name of the global variable to increment.',
},
],
description: 'Increments a global variable by 1 and returns the new value. If the variable does not exist, it will be created.',
returns: 'The new value of the global variable.',
returnType: MacroValueType.NUMBER,
handler: ({ unnamedArgs: [name], normalize }) => {
const result = ctx.variables.global.inc(name);
return normalize(result);
},
});
// {{decglobalvar::name}} -> returns new value
MacroRegistry.registerMacro('decglobalvar', {
category: MacroCategory.VARIABLE,
unnamedArgs: [
{
name: 'name',
type: MacroValueType.STRING,
description: 'The name of the global variable to decrement.',
},
],
description: 'Decrements a global variable by 1 and returns the new value. If the variable does not exist, it will be created.',
returns: 'The new value of the global variable.',
returnType: MacroValueType.NUMBER,
exampleUsage: ['{{decglobalvar::myintvar}}'],
handler: ({ unnamedArgs: [name], normalize }) => {
const result = ctx.variables.global.dec(name);
return normalize(result);
},
});
// {{getglobalvar::name}} -> returns current value
MacroRegistry.registerMacro('getglobalvar', {
category: MacroCategory.VARIABLE,
unnamedArgs: [
{
name: 'name',
type: MacroValueType.STRING,
description: 'The name of the global variable to get.',
},
],
description: 'Gets the value of a global variable.',
returns: 'The value of the global variable.',
returnType: [MacroValueType.STRING, MacroValueType.NUMBER],
exampleUsage: ['{{getglobalvar::myvar}}', '{{getglobalvar::myintvar}}'],
handler: ({ unnamedArgs: [name], normalize }) => {
const result = ctx.variables.global.get(name);
return normalize(result);
},
});
}