mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-01-31 19:45:35 +01:00
Merge remote-tracking branch 'upstream/staging' into staging
This commit is contained in:
commit
18e6a82306
@ -9581,6 +9581,10 @@ jQuery(async function () {
|
|||||||
valueBeforeManualInput = $(this).val();
|
valueBeforeManualInput = $(this).val();
|
||||||
console.log(valueBeforeManualInput);
|
console.log(valueBeforeManualInput);
|
||||||
})
|
})
|
||||||
|
.on('change', function (e) {
|
||||||
|
e.target.focus();
|
||||||
|
e.target.dispatchEvent(new Event('keyup'));
|
||||||
|
})
|
||||||
.on('keydown', function (e) {
|
.on('keydown', function (e) {
|
||||||
const masterSelector = '#' + $(this).data('for');
|
const masterSelector = '#' + $(this).data('for');
|
||||||
const masterElement = $(masterSelector);
|
const masterElement = $(masterSelector);
|
||||||
|
16
server.js
16
server.js
@ -608,8 +608,13 @@ const setupTasks = async function () {
|
|||||||
await loadTokenizers();
|
await loadTokenizers();
|
||||||
await statsEndpoint.init();
|
await statsEndpoint.init();
|
||||||
|
|
||||||
const exitProcess = () => {
|
const cleanupPlugins = await loadPlugins();
|
||||||
|
|
||||||
|
const exitProcess = async () => {
|
||||||
statsEndpoint.onExit();
|
statsEndpoint.onExit();
|
||||||
|
if (typeof cleanupPlugins === 'function') {
|
||||||
|
await cleanupPlugins();
|
||||||
|
}
|
||||||
process.exit();
|
process.exit();
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -621,7 +626,6 @@ const setupTasks = async function () {
|
|||||||
exitProcess();
|
exitProcess();
|
||||||
});
|
});
|
||||||
|
|
||||||
await loadPlugins();
|
|
||||||
|
|
||||||
console.log('Launching...');
|
console.log('Launching...');
|
||||||
|
|
||||||
@ -634,13 +638,19 @@ const setupTasks = async function () {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads server plugins from a directory.
|
||||||
|
* @returns {Promise<Function>} Function to be run on server exit
|
||||||
|
*/
|
||||||
async function loadPlugins() {
|
async function loadPlugins() {
|
||||||
try {
|
try {
|
||||||
const pluginDirectory = path.join(serverDirectory, 'plugins');
|
const pluginDirectory = path.join(serverDirectory, 'plugins');
|
||||||
const loader = require('./src/plugin-loader');
|
const loader = require('./src/plugin-loader');
|
||||||
await loader.loadPlugins(app, pluginDirectory);
|
const cleanupPlugins = await loader.loadPlugins(app, pluginDirectory);
|
||||||
|
return cleanupPlugins;
|
||||||
} catch {
|
} catch {
|
||||||
console.log('Plugin loading failed.');
|
console.log('Plugin loading failed.');
|
||||||
|
return () => {};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -106,6 +106,10 @@ router.post('/deepl', jsonParser, async (request, response) => {
|
|||||||
return response.sendStatus(400);
|
return response.sendStatus(400);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (request.body.lang === 'zh-CN' || request.body.lang === 'zh-TW') {
|
||||||
|
request.body.lang = 'ZH';
|
||||||
|
}
|
||||||
|
|
||||||
const text = request.body.text;
|
const text = request.body.text;
|
||||||
const lang = request.body.lang;
|
const lang = request.body.lang;
|
||||||
const formality = getConfigValue('deepl.formality', 'default');
|
const formality = getConfigValue('deepl.formality', 'default');
|
||||||
@ -221,7 +225,7 @@ router.post('/deeplx', jsonParser, async (request, response) => {
|
|||||||
|
|
||||||
const text = request.body.text;
|
const text = request.body.text;
|
||||||
let lang = request.body.lang;
|
let lang = request.body.lang;
|
||||||
if (request.body.lang === 'zh-CN') {
|
if (request.body.lang === 'zh-CN' || request.body.lang === 'zh-TW') {
|
||||||
lang = 'ZH';
|
lang = 'ZH';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
const url = require('url');
|
||||||
|
const express = require('express');
|
||||||
const { getConfigValue } = require('./util');
|
const { getConfigValue } = require('./util');
|
||||||
const enableServerPlugins = getConfigValue('enableServerPlugins', false);
|
const enableServerPlugins = getConfigValue('enableServerPlugins', false);
|
||||||
|
|
||||||
@ -21,31 +23,35 @@ const isESModule = (file) => path.extname(file) === '.mjs';
|
|||||||
* Load and initialize server plugins from a directory if they are enabled.
|
* Load and initialize server plugins from a directory if they are enabled.
|
||||||
* @param {import('express').Express} app Express app
|
* @param {import('express').Express} app Express app
|
||||||
* @param {string} pluginsPath Path to plugins directory
|
* @param {string} pluginsPath Path to plugins directory
|
||||||
* @returns {Promise<any>} Promise that resolves when all plugins are loaded
|
* @returns {Promise<Function>} Promise that resolves when all plugins are loaded. Resolves to a "cleanup" function to
|
||||||
|
* be called before the server shuts down.
|
||||||
*/
|
*/
|
||||||
async function loadPlugins(app, pluginsPath) {
|
async function loadPlugins(app, pluginsPath) {
|
||||||
|
const exitHooks = [];
|
||||||
|
const emptyFn = () => {};
|
||||||
|
|
||||||
// Server plugins are disabled.
|
// Server plugins are disabled.
|
||||||
if (!enableServerPlugins) {
|
if (!enableServerPlugins) {
|
||||||
return;
|
return emptyFn;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Plugins directory does not exist.
|
// Plugins directory does not exist.
|
||||||
if (!fs.existsSync(pluginsPath)) {
|
if (!fs.existsSync(pluginsPath)) {
|
||||||
return;
|
return emptyFn;
|
||||||
}
|
}
|
||||||
|
|
||||||
const files = fs.readdirSync(pluginsPath);
|
const files = fs.readdirSync(pluginsPath);
|
||||||
|
|
||||||
// No plugins to load.
|
// No plugins to load.
|
||||||
if (files.length === 0) {
|
if (files.length === 0) {
|
||||||
return;
|
return emptyFn;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
const pluginFilePath = path.join(pluginsPath, file);
|
const pluginFilePath = path.join(pluginsPath, file);
|
||||||
|
|
||||||
if (fs.statSync(pluginFilePath).isDirectory()) {
|
if (fs.statSync(pluginFilePath).isDirectory()) {
|
||||||
await loadFromDirectory(app, pluginFilePath);
|
await loadFromDirectory(app, pluginFilePath, exitHooks);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -54,11 +60,14 @@ async function loadPlugins(app, pluginsPath) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
await loadFromFile(app, pluginFilePath);
|
await loadFromFile(app, pluginFilePath, exitHooks);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Call all plugin "exit" functions at once and wait for them to finish
|
||||||
|
return () => Promise.all(exitHooks.map(exitFn => exitFn()));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadFromDirectory(app, pluginDirectoryPath) {
|
async function loadFromDirectory(app, pluginDirectoryPath, exitHooks) {
|
||||||
const files = fs.readdirSync(pluginDirectoryPath);
|
const files = fs.readdirSync(pluginDirectoryPath);
|
||||||
|
|
||||||
// No plugins to load.
|
// No plugins to load.
|
||||||
@ -69,7 +78,7 @@ async function loadFromDirectory(app, pluginDirectoryPath) {
|
|||||||
// Plugin is an npm package.
|
// Plugin is an npm package.
|
||||||
const packageJsonFilePath = path.join(pluginDirectoryPath, 'package.json');
|
const packageJsonFilePath = path.join(pluginDirectoryPath, 'package.json');
|
||||||
if (fs.existsSync(packageJsonFilePath)) {
|
if (fs.existsSync(packageJsonFilePath)) {
|
||||||
if (await loadFromPackage(app, packageJsonFilePath)) {
|
if (await loadFromPackage(app, packageJsonFilePath, exitHooks)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -77,7 +86,7 @@ async function loadFromDirectory(app, pluginDirectoryPath) {
|
|||||||
// Plugin is a CommonJS module.
|
// Plugin is a CommonJS module.
|
||||||
const cjsFilePath = path.join(pluginDirectoryPath, 'index.js');
|
const cjsFilePath = path.join(pluginDirectoryPath, 'index.js');
|
||||||
if (fs.existsSync(cjsFilePath)) {
|
if (fs.existsSync(cjsFilePath)) {
|
||||||
if (await loadFromFile(app, cjsFilePath)) {
|
if (await loadFromFile(app, cjsFilePath, exitHooks)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -85,7 +94,7 @@ async function loadFromDirectory(app, pluginDirectoryPath) {
|
|||||||
// Plugin is an ECMAScript module.
|
// Plugin is an ECMAScript module.
|
||||||
const esmFilePath = path.join(pluginDirectoryPath, 'index.mjs');
|
const esmFilePath = path.join(pluginDirectoryPath, 'index.mjs');
|
||||||
if (fs.existsSync(esmFilePath)) {
|
if (fs.existsSync(esmFilePath)) {
|
||||||
if (await loadFromFile(app, esmFilePath)) {
|
if (await loadFromFile(app, esmFilePath, exitHooks)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -95,14 +104,16 @@ async function loadFromDirectory(app, pluginDirectoryPath) {
|
|||||||
* Loads and initializes a plugin from an npm package.
|
* Loads and initializes a plugin from an npm package.
|
||||||
* @param {import('express').Express} app Express app
|
* @param {import('express').Express} app Express app
|
||||||
* @param {string} packageJsonPath Path to package.json file
|
* @param {string} packageJsonPath Path to package.json file
|
||||||
|
* @param {Array<Function>} exitHooks Array of functions to be run on plugin exit. Will be pushed to if the plugin has
|
||||||
|
* an "exit" function.
|
||||||
* @returns {Promise<boolean>} Promise that resolves to true if plugin was loaded successfully
|
* @returns {Promise<boolean>} Promise that resolves to true if plugin was loaded successfully
|
||||||
*/
|
*/
|
||||||
async function loadFromPackage(app, packageJsonPath) {
|
async function loadFromPackage(app, packageJsonPath, exitHooks) {
|
||||||
try {
|
try {
|
||||||
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
||||||
if (packageJson.main) {
|
if (packageJson.main) {
|
||||||
const pluginFilePath = path.join(path.dirname(packageJsonPath), packageJson.main);
|
const pluginFilePath = path.join(path.dirname(packageJsonPath), packageJson.main);
|
||||||
return await loadFromFile(app, pluginFilePath);
|
return await loadFromFile(app, pluginFilePath, exitHooks);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to load plugin from ${packageJsonPath}: ${error}`);
|
console.error(`Failed to load plugin from ${packageJsonPath}: ${error}`);
|
||||||
@ -114,13 +125,16 @@ async function loadFromPackage(app, packageJsonPath) {
|
|||||||
* Loads and initializes a plugin from a file.
|
* Loads and initializes a plugin from a file.
|
||||||
* @param {import('express').Express} app Express app
|
* @param {import('express').Express} app Express app
|
||||||
* @param {string} pluginFilePath Path to plugin directory
|
* @param {string} pluginFilePath Path to plugin directory
|
||||||
|
* @param {Array.<Function>} exitHooks Array of functions to be run on plugin exit. Will be pushed to if the plugin has
|
||||||
|
* an "exit" function.
|
||||||
* @returns {Promise<boolean>} Promise that resolves to true if plugin was loaded successfully
|
* @returns {Promise<boolean>} Promise that resolves to true if plugin was loaded successfully
|
||||||
*/
|
*/
|
||||||
async function loadFromFile(app, pluginFilePath) {
|
async function loadFromFile(app, pluginFilePath, exitHooks) {
|
||||||
try {
|
try {
|
||||||
const plugin = await getPluginModule(pluginFilePath);
|
const fileUrl = url.pathToFileURL(pluginFilePath).toString();
|
||||||
|
const plugin = await import(fileUrl);
|
||||||
console.log(`Initializing plugin from ${pluginFilePath}`);
|
console.log(`Initializing plugin from ${pluginFilePath}`);
|
||||||
return await initPlugin(app, plugin);
|
return await initPlugin(app, plugin, exitHooks);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to load plugin from ${pluginFilePath}: ${error}`);
|
console.error(`Failed to load plugin from ${pluginFilePath}: ${error}`);
|
||||||
return false;
|
return false;
|
||||||
@ -128,33 +142,65 @@ async function loadFromFile(app, pluginFilePath) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes a plugin module.
|
* Check whether a plugin ID is valid (only lowercase alphanumeric, hyphens, and underscores).
|
||||||
* @param {import('express').Express} app Express app
|
* @param {string} id The plugin ID to check
|
||||||
* @param {any} plugin Plugin module
|
* @returns {boolean} True if the plugin ID is valid.
|
||||||
* @returns {Promise<boolean>} Promise that resolves to true if plugin was initialized successfully
|
|
||||||
*/
|
*/
|
||||||
async function initPlugin(app, plugin) {
|
function isValidPluginID(id) {
|
||||||
if (typeof plugin.init === 'function') {
|
return /^[a-z0-9_-]+$/.test(id);
|
||||||
await plugin.init(app);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads a module from a file depending on the module type.
|
* Initializes a plugin module.
|
||||||
* @param {string} pluginFilePath Path to plugin file
|
* @param {import('express').Express} app Express app
|
||||||
* @returns {Promise<any>} Promise that resolves to plugin module
|
* @param {any} plugin Plugin module
|
||||||
|
* @param {Array.<Function>} exitHooks Array of functions to be run on plugin exit. Will be pushed to if the plugin has
|
||||||
|
* an "exit" function.
|
||||||
|
* @returns {Promise<boolean>} Promise that resolves to true if plugin was initialized successfully
|
||||||
*/
|
*/
|
||||||
async function getPluginModule(pluginFilePath) {
|
async function initPlugin(app, plugin, exitHooks) {
|
||||||
if (isCommonJS(pluginFilePath)) {
|
const info = plugin.info || plugin.default?.info;
|
||||||
return require(pluginFilePath);
|
if (typeof info !== 'object') {
|
||||||
|
console.error('Failed to load plugin module; plugin info not found');
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
if (isESModule(pluginFilePath)) {
|
|
||||||
return await import(pluginFilePath);
|
// We don't currently use "name" or "description" but it would be nice to have a UI for listing server plugins, so
|
||||||
|
// require them now just to be safe
|
||||||
|
for (const field of ['id', 'name', 'description']) {
|
||||||
|
if (typeof info[field] !== 'string') {
|
||||||
|
console.error(`Failed to load plugin module; plugin info missing field '${field}'`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
throw new Error(`Unsupported module type in ${pluginFilePath}`);
|
|
||||||
|
if (typeof plugin.init !== 'function') {
|
||||||
|
console.error('Failed to load plugin module; no init function');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { id } = info;
|
||||||
|
|
||||||
|
if (!isValidPluginID(id)) {
|
||||||
|
console.error(`Failed to load plugin module; invalid plugin ID '${id}'`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow the plugin to register API routes under /api/plugins/[plugin ID] via a router
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
await plugin.init(router);
|
||||||
|
|
||||||
|
// Add API routes to the app if the plugin registered any
|
||||||
|
if (router.stack.length > 0) {
|
||||||
|
app.use(`/api/plugins/${id}`, router);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof plugin.exit === 'function') {
|
||||||
|
exitHooks.push(plugin.exit);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user