mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-06-05 21:59:27 +02:00
STscript improvements (see below)
/abort command, {{pipe}} macro in named args, subcommand batch escaping with backslash, string literals and rules for /if, else clause for /if
This commit is contained in:
@ -163,12 +163,18 @@ parser.addCommand('echo', echoCallback, [], '<span class="monospace">(text)</spa
|
|||||||
parser.addCommand('gen', generateCallback, [], '<span class="monospace">(prompt)</span> – generates text using the provided prompt and passes it to the next command through the pipe.', true, true);
|
parser.addCommand('gen', generateCallback, [], '<span class="monospace">(prompt)</span> – generates text using the provided prompt and passes it to the next command through the pipe.', true, true);
|
||||||
parser.addCommand('genraw', generateRawCallback, [], '<span class="monospace">(prompt)</span> – generates text using the provided prompt and passes it to the next command through the pipe. Does not include chat history or character card.', true, true);
|
parser.addCommand('genraw', generateRawCallback, [], '<span class="monospace">(prompt)</span> – generates text using the provided prompt and passes it to the next command through the pipe. Does not include chat history or character card.', true, true);
|
||||||
parser.addCommand('addswipe', addSwipeCallback, ['swipeadd'], '<span class="monospace">(text)</span> – adds a swipe to the last chat message.', true, true);
|
parser.addCommand('addswipe', addSwipeCallback, ['swipeadd'], '<span class="monospace">(text)</span> – adds a swipe to the last chat message.', true, true);
|
||||||
|
parser.addCommand('abort', abortCallback, [], ' – aborts the slash command batch execution', true, true);
|
||||||
registerVariableCommands();
|
registerVariableCommands();
|
||||||
|
|
||||||
const NARRATOR_NAME_KEY = 'narrator_name';
|
const NARRATOR_NAME_KEY = 'narrator_name';
|
||||||
const NARRATOR_NAME_DEFAULT = 'System';
|
const NARRATOR_NAME_DEFAULT = 'System';
|
||||||
export const COMMENT_NAME_DEFAULT = 'Note';
|
export const COMMENT_NAME_DEFAULT = 'Note';
|
||||||
|
|
||||||
|
function abortCallback() {
|
||||||
|
$('#send_textarea').val('');
|
||||||
|
throw new Error('/abort command executed');
|
||||||
|
}
|
||||||
|
|
||||||
async function generateRawCallback(_, arg) {
|
async function generateRawCallback(_, arg) {
|
||||||
if (!arg) {
|
if (!arg) {
|
||||||
console.warn('WARN: No argument provided for /genraw command');
|
console.warn('WARN: No argument provided for /genraw command');
|
||||||
@ -250,7 +256,7 @@ async function addSwipeCallback(_, arg) {
|
|||||||
api: 'manual',
|
api: 'manual',
|
||||||
model: 'slash command',
|
model: 'slash command',
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
await saveChatConditional();
|
await saveChatConditional();
|
||||||
await reloadCurrentChat();
|
await reloadCurrentChat();
|
||||||
@ -981,16 +987,29 @@ function setBackgroundCallback(_, bg) {
|
|||||||
/**
|
/**
|
||||||
* Executes slash commands in the provided text
|
* Executes slash commands in the provided text
|
||||||
* @param {string} text Slash command text
|
* @param {string} text Slash command text
|
||||||
|
* @param {boolean} unescape Whether to unescape the batch separator
|
||||||
* @returns {Promise<{interrupt: boolean, newText: string, pipe: string} | boolean>}
|
* @returns {Promise<{interrupt: boolean, newText: string, pipe: string} | boolean>}
|
||||||
*/
|
*/
|
||||||
async function executeSlashCommands(text) {
|
async function executeSlashCommands(text, unescape = false) {
|
||||||
if (!text) {
|
if (!text) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Unescape the pipe character
|
||||||
|
if (unescape) {
|
||||||
|
text = text.replace(/\\\|/g, '|');
|
||||||
|
}
|
||||||
|
|
||||||
// Hack to allow multi-line slash commands
|
// Hack to allow multi-line slash commands
|
||||||
// All slash command messages should begin with a slash
|
// All slash command messages should begin with a slash
|
||||||
const lines = text.split('|').map(line => line.trim());
|
const placeholder = '\u200B'; // Use a zero-width space as a placeholder
|
||||||
|
const chars = text.split('');
|
||||||
|
for (let i = 1; i < chars.length; i++) {
|
||||||
|
if (chars[i] === '|' && chars[i - 1] !== '\\') {
|
||||||
|
chars[i] = placeholder;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const lines = chars.join('').split(placeholder).map(line => line.trim());
|
||||||
const linesToRemove = [];
|
const linesToRemove = [];
|
||||||
|
|
||||||
let interrupt = false;
|
let interrupt = false;
|
||||||
@ -1016,7 +1035,15 @@ async function executeSlashCommands(text) {
|
|||||||
console.debug('Slash command executing:', result);
|
console.debug('Slash command executing:', result);
|
||||||
let unnamedArg = result.value || pipeResult;
|
let unnamedArg = result.value || pipeResult;
|
||||||
|
|
||||||
if (typeof unnamedArg === 'string' && /{{pipe}}/i.test(unnamedArg)) {
|
if (pipeResult && typeof result.args === 'object') {
|
||||||
|
for (const [key, value] of Object.entries(result.args)) {
|
||||||
|
if (typeof value === 'string' && /{{pipe}}/i.test(value)) {
|
||||||
|
result.args[key] = value.replace(/{{pipe}}/i, pipeResult);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pipeResult && typeof unnamedArg === 'string' && /{{pipe}}/i.test(unnamedArg)) {
|
||||||
unnamedArg = unnamedArg.replace(/{{pipe}}/i, pipeResult);
|
unnamedArg = unnamedArg.replace(/{{pipe}}/i, pipeResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,6 +3,10 @@ import { extension_settings } from "./extensions.js";
|
|||||||
import { executeSlashCommands, registerSlashCommand } from "./slash-commands.js";
|
import { executeSlashCommands, registerSlashCommand } from "./slash-commands.js";
|
||||||
|
|
||||||
function getLocalVariable(name) {
|
function getLocalVariable(name) {
|
||||||
|
if (!chat_metadata.variables) {
|
||||||
|
chat_metadata.variables = {};
|
||||||
|
}
|
||||||
|
|
||||||
const localVariable = chat_metadata?.variables[name];
|
const localVariable = chat_metadata?.variables[name];
|
||||||
|
|
||||||
return localVariable || '';
|
return localVariable || '';
|
||||||
@ -125,67 +129,101 @@ function listVariablesCallback() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function ifCallback(args, command) {
|
async function ifCallback(args, command) {
|
||||||
const a = getLocalVariable(args.a) || getGlobalVariable(args.a) || Number(args.a);
|
// Resultion order: numeric literal, local variable, global variable, string literal
|
||||||
const b = getLocalVariable(args.b) || getGlobalVariable(args.b) || Number(args.b);
|
const a = isNaN(Number(args.a)) ? (getLocalVariable(args.a) || getGlobalVariable(args.a) || args.a || '') : Number(args.a);
|
||||||
|
const b = isNaN(Number(args.b)) ? (getLocalVariable(args.b) || getGlobalVariable(args.b) || args.b || '') : Number(args.b);
|
||||||
const rule = args.rule;
|
const rule = args.rule;
|
||||||
|
|
||||||
if (!a || !b || !rule) {
|
if (!rule) {
|
||||||
toastr.warning('Both operands and the rule must be specified for the /if command.', 'Invalid /if command');
|
toastr.warning('Both operands and the rule must be specified for the /if command.', 'Invalid /if command');
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
const aNumber = Number(a);
|
if ((typeof a === 'number' && isNaN(a)) || (typeof a === 'string' && a === '')) {
|
||||||
const bNumber = Number(b);
|
toastr.warning('The first operand must be a number, string or a variable name for the /if command.', 'Invalid /if command');
|
||||||
|
|
||||||
if (isNaN(aNumber) || isNaN(bNumber)) {
|
|
||||||
toastr.warning('Both operands must be numbers for the /if command.', 'Invalid /if command');
|
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
let result = false;
|
let result = false;
|
||||||
|
|
||||||
switch (rule) {
|
if (typeof a === 'string') {
|
||||||
case 'gt':
|
const aString = String(a).toLowerCase();
|
||||||
result = aNumber > bNumber;
|
const bString = String(b).toLowerCase();
|
||||||
break;
|
|
||||||
case 'gte':
|
switch (rule) {
|
||||||
result = aNumber >= bNumber;
|
case 'in':
|
||||||
break;
|
result = aString.includes(bString);
|
||||||
case 'lt':
|
break;
|
||||||
result = aNumber < bNumber;
|
case 'nin':
|
||||||
break;
|
result = !aString.includes(bString);
|
||||||
case 'lte':
|
break;
|
||||||
result = aNumber <= bNumber;
|
case 'eq':
|
||||||
break;
|
result = aString === bString;
|
||||||
case 'eq':
|
break;
|
||||||
result = aNumber === bNumber;
|
case 'neq':
|
||||||
break;
|
result = aString !== bString;
|
||||||
default:
|
break;
|
||||||
toastr.warning('Unknown rule for the /if command.', 'Invalid /if command');
|
default:
|
||||||
return '';
|
toastr.warning('Unknown rule for the /if command for type string.', 'Invalid /if command');
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
} else if (typeof a === 'number') {
|
||||||
|
const aNumber = Number(a);
|
||||||
|
const bNumber = Number(b);
|
||||||
|
|
||||||
|
switch (rule) {
|
||||||
|
case 'gt':
|
||||||
|
result = aNumber > bNumber;
|
||||||
|
break;
|
||||||
|
case 'gte':
|
||||||
|
result = aNumber >= bNumber;
|
||||||
|
break;
|
||||||
|
case 'lt':
|
||||||
|
result = aNumber < bNumber;
|
||||||
|
break;
|
||||||
|
case 'lte':
|
||||||
|
result = aNumber <= bNumber;
|
||||||
|
break;
|
||||||
|
case 'eq':
|
||||||
|
result = aNumber === bNumber;
|
||||||
|
break;
|
||||||
|
case 'neq':
|
||||||
|
result = aNumber !== bNumber;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
toastr.warning('Unknown rule for the /if command for type number.', 'Invalid /if command');
|
||||||
|
return '';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result && command) {
|
if (result && command) {
|
||||||
if (command.startsWith('"')) {
|
return await executeSubCommands(command);
|
||||||
command = command.slice(1);
|
} else if (!result && args.else && typeof args.else === 'string' && args.else !== '') {
|
||||||
}
|
return await executeSubCommands(args.else);
|
||||||
|
|
||||||
if (command.endsWith('"')) {
|
|
||||||
command = command.slice(0, -1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await executeSlashCommands(command);
|
|
||||||
|
|
||||||
if (!result || typeof result !== 'object') {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
return result?.pipe || '';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function executeSubCommands(command) {
|
||||||
|
if (command.startsWith('"')) {
|
||||||
|
command = command.slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (command.endsWith('"')) {
|
||||||
|
command = command.slice(0, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const unescape = true;
|
||||||
|
const result = await executeSlashCommands(command, unescape);
|
||||||
|
|
||||||
|
if (!result || typeof result !== 'object') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return result?.pipe || '';
|
||||||
|
}
|
||||||
|
|
||||||
export function registerVariableCommands() {
|
export function registerVariableCommands() {
|
||||||
registerSlashCommand('listvar', listVariablesCallback, [''], ' – list registered chat variables', true, true);
|
registerSlashCommand('listvar', listVariablesCallback, [''], ' – list registered chat variables', true, true);
|
||||||
registerSlashCommand('setvar', (args, value) => setLocalVariable(args.key || args.name, value), [], '<span class="monospace">key=varname (value)</span> – set a local variable value and pass it down the pipe, e.g. <tt>/setvar key=color green</tt>', true, true);
|
registerSlashCommand('setvar', (args, value) => setLocalVariable(args.key || args.name, value), [], '<span class="monospace">key=varname (value)</span> – set a local variable value and pass it down the pipe, e.g. <tt>/setvar key=color green</tt>', true, true);
|
||||||
@ -194,5 +232,5 @@ export function registerVariableCommands() {
|
|||||||
registerSlashCommand('setglobalvar', (args, value) => setGlobalVariable(args.key || args.name, value), [], '<span class="monospace">key=varname (value)</span> – set a global variable value and pass it down the pipe, e.g. <tt>/setglobalvar key=color green</tt>', true, true);
|
registerSlashCommand('setglobalvar', (args, value) => setGlobalVariable(args.key || args.name, value), [], '<span class="monospace">key=varname (value)</span> – set a global variable value and pass it down the pipe, e.g. <tt>/setglobalvar key=color green</tt>', true, true);
|
||||||
registerSlashCommand('getglobalvar', (_, value) => getGlobalVariable(value), [], '<span class="monospace">(key)</span> – get a global variable value and pass it down the pipe, e.g. <tt>/getglobalvar height</tt>', true, true);
|
registerSlashCommand('getglobalvar', (_, value) => getGlobalVariable(value), [], '<span class="monospace">(key)</span> – get a global variable value and pass it down the pipe, e.g. <tt>/getglobalvar height</tt>', true, true);
|
||||||
registerSlashCommand('addglobalvar', (args, value) => addGlobalVariable(args.key || args.name, value), [], '<span class="monospace">key=varname (increment)</span> – add a value to a global variable and pass the result down the pipe, e.g. <tt>/addglobalvar score 10</tt>', true, true);
|
registerSlashCommand('addglobalvar', (args, value) => addGlobalVariable(args.key || args.name, value), [], '<span class="monospace">key=varname (increment)</span> – add a value to a global variable and pass the result down the pipe, e.g. <tt>/addglobalvar score 10</tt>', true, true);
|
||||||
registerSlashCommand('if', ifCallback, [], '<span class="monospace">a=varname1 b=varname2 rule=comparison "(command)"</span> – compare the value of variable "a" with the value of variable "b", and if the condition yields true, then execute any valid slash command enclosed in quotes and pass the result of the command execution down the pipe. Numeric values for "a" and "b" supported. Available rules: gt => a > b, gte => a >= b, lt => a < b, lte => a <= b, eq => a == b e.g. <tt>/if a=score a=10 rule=gte "/speak You win"</tt> triggers a /speak command if the value of "score" is greater or equals 10.', true, true);
|
registerSlashCommand('if', ifCallback, [], '<span class="monospace">a=varname1 b=varname2 rule=comparison else="(alt.command)" "(command)"</span> – compare the value of variable "a" with the value of variable "b", and if the condition yields true, then execute any valid slash command enclosed in quotes and pass the result of the command execution down the pipe. Numeric values and string literals for "a" and "b" supported. Available rules: gt => a > b, gte => a >= b, lt => a < b, lte => a <= b, eq => a == b, neq => a != b, in (strings) => a includes b, nin (strings) => a not includes b, e.g. <tt>/if a=score a=10 rule=gte "/speak You win"</tt> triggers a /speak command if the value of "score" is greater or equals 10.', true, true);
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user