mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-03-10 00:50:11 +01:00
Merge remote-tracking branch 'fork/mistral' into mistral
This commit is contained in:
commit
5e086321d0
@ -12,7 +12,7 @@ module.exports = {
|
||||
overrides: [
|
||||
{
|
||||
// Server-side files (plus this configuration file)
|
||||
files: ['src/**/*.js', './*.js'],
|
||||
files: ['src/**/*.js', './*.js', 'plugins/**/*.js'],
|
||||
env: {
|
||||
node: true,
|
||||
},
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -42,3 +42,4 @@ access.log
|
||||
/vectors/
|
||||
/cache/
|
||||
public/css/user.css
|
||||
/plugins/
|
||||
|
@ -58,3 +58,5 @@ openai:
|
||||
deepl:
|
||||
# Available options: default, more, less, prefer_more, prefer_less
|
||||
formality: default
|
||||
# -- SERVER PLUGIN CONFIGURATION --
|
||||
enableServerPlugins: false
|
||||
|
0
plugins/.gitkeep
Normal file
0
plugins/.gitkeep
Normal file
@ -1,5 +1,9 @@
|
||||
/*will apply to anything 1000px or less. this catches ipads, horizontal phones, and vertical phones)*/
|
||||
@media screen and (max-width: 1000px) {
|
||||
#send_form.compact #leftSendForm, #send_form.compact #rightSendForm {
|
||||
flex-wrap: nowrap;
|
||||
width: unset;
|
||||
}
|
||||
|
||||
.bg_button {
|
||||
font-size: 15px;
|
||||
|
@ -2797,9 +2797,12 @@
|
||||
<div class="flex-container flexnowrap alignitemscenter">
|
||||
<select id="themes" class="margin0">
|
||||
</select>
|
||||
<div id="ui-preset-save-button" title="Save changes to a new theme file" data-i18n="[title]Save changes to a new theme file" class="menu_button margin0">
|
||||
<div id="ui-preset-update-button" title="Update a theme file" data-i18n="[title]Update a theme file" class="menu_button margin0">
|
||||
<i class="fa-solid fa-save"></i>
|
||||
</div>
|
||||
<div id="ui-preset-save-button" title="Save as a new theme" data-i18n="[title]Save as a new theme" class="menu_button margin0">
|
||||
<i class="fa-solid fa-paste"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div name="themeElements" data-newbie-hidden class="flex-container flexFlowColumn flexNoGap">
|
||||
@ -2993,6 +2996,10 @@
|
||||
<input id="messageTokensEnabled" type="checkbox" />
|
||||
<span data-i18n="Show Message Token Count">Message Token Count</span>
|
||||
</label>
|
||||
<label for="compact_input_area" class="checkbox_label">
|
||||
<input id="compact_input_area" type="checkbox" />
|
||||
<span data-i18n="Compact Input Area (Mobile)">Compact Input Area <i class="fa-solid fa-mobile-screen-button"></i></span>
|
||||
</label>
|
||||
<label data-newbie-hidden for="hotswapEnabled" class="checkbox_label">
|
||||
<input id="hotswapEnabled" type="checkbox" />
|
||||
<span data-i18n="Characters Hotswap">Characters Hotswap</span>
|
||||
@ -3644,6 +3651,7 @@
|
||||
<label id="rm_group_automode_label" class="checkbox_label whitespacenowrap">
|
||||
<input id="rm_group_automode" type="checkbox" />
|
||||
<span data-i18n="Auto Mode">Auto Mode</span>
|
||||
<input id="rm_group_automode_delay" class="text_pole textarea_compact widthUnset" type="number" min="1" max="999" step="1" value="5" title="Auto Mode delay" />
|
||||
</label>
|
||||
<label id="rm_group_hidemutedsprites_label" class="checkbox_label whitespacenowrap">
|
||||
<input id="rm_group_hidemutedsprites" type="checkbox" />
|
||||
|
@ -322,6 +322,11 @@ async function onChatEvent() {
|
||||
}
|
||||
|
||||
async function forceSummarizeChat() {
|
||||
if (extension_settings.memory.source === summary_sources.extras) {
|
||||
toastr.warning('Force summarization is not supported for Extras API');
|
||||
return;
|
||||
}
|
||||
|
||||
const context = getContext();
|
||||
|
||||
const skipWIAN = extension_settings.memory.SkipWIAN;
|
||||
@ -664,7 +669,7 @@ jQuery(function () {
|
||||
|
||||
<textarea id="memory_contents" class="text_pole textarea_compact" rows="6" placeholder="Summary will be generated here..."></textarea>
|
||||
<div class="memory_contents_controls">
|
||||
<div id="memory_force_summarize" class="menu_button menu_button_icon">
|
||||
<div id="memory_force_summarize" data-source="main" class="menu_button menu_button_icon">
|
||||
<i class="fa-solid fa-database"></i>
|
||||
<span>Summarize now</span>
|
||||
</div>
|
||||
|
@ -1711,7 +1711,7 @@ async function getPrompt(generationType, message, trigger, quietPrompt) {
|
||||
prompt = message || getRawLastMessage();
|
||||
break;
|
||||
case generationMode.FREE:
|
||||
prompt = trigger.trim();
|
||||
prompt = generateFreeModePrompt(trigger.trim());
|
||||
break;
|
||||
case generationMode.FACE_MULTIMODAL:
|
||||
case generationMode.CHARACTER_MULTIMODAL:
|
||||
@ -1730,6 +1730,36 @@ async function getPrompt(generationType, message, trigger, quietPrompt) {
|
||||
return prompt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a free prompt with a character-specific prompt prefix support.
|
||||
* @param {string} trigger - The prompt to use for the image generation.
|
||||
* @returns {string}
|
||||
*/
|
||||
function generateFreeModePrompt(trigger) {
|
||||
return trigger
|
||||
.replace(/(?:^char(\s|,)|\{\{charPrefix\}\})/gi, (_, suffix) => {
|
||||
const getLastCharacterKey = () => {
|
||||
if (typeof this_chid !== 'undefined') {
|
||||
return getCharaFilename(this_chid);
|
||||
}
|
||||
const context = getContext();
|
||||
for (let i = context.chat.length - 1; i >= 0; i--) {
|
||||
const message = context.chat[i];
|
||||
if (message.is_user || message.is_system) {
|
||||
continue;
|
||||
} else if (typeof message.original_avatar === 'string') {
|
||||
return message.original_avatar.replace(/\.[^/.]+$/, '');
|
||||
}
|
||||
}
|
||||
throw new Error('No usable messages found.');
|
||||
};
|
||||
|
||||
const key = getLastCharacterKey();
|
||||
const value = (extension_settings.sd.character_prompts[key] || '').trim();
|
||||
return value ? value + (suffix || '') : '';
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a prompt using multimodal captioning.
|
||||
* @param {number} generationType - The type of image generation to perform.
|
||||
|
@ -44,7 +44,7 @@ class OpenAITtsProvider {
|
||||
</div>
|
||||
<div>
|
||||
<label for="openai-tts-speed">Speed: <span id="openai-tts-speed-output"></span></label>
|
||||
<input type="range" id="openai-tts-speed" value="1" min="0.25" max="4" step="0.25">
|
||||
<input type="range" id="openai-tts-speed" value="1" min="0.25" max="4" step="0.05">
|
||||
</div>`;
|
||||
return html;
|
||||
}
|
||||
|
@ -110,10 +110,18 @@ export const group_generation_mode = {
|
||||
APPEND: 1,
|
||||
};
|
||||
|
||||
const DEFAULT_AUTO_MODE_DELAY = 5;
|
||||
|
||||
export const groupCandidatesFilter = new FilterHelper(debounce(printGroupCandidates, 100));
|
||||
setInterval(groupChatAutoModeWorker, 5000);
|
||||
let autoModeWorker = null;
|
||||
const saveGroupDebounced = debounce(async (group, reload) => await _save(group, reload), 500);
|
||||
|
||||
function setAutoModeWorker() {
|
||||
clearInterval(autoModeWorker);
|
||||
const autoModeDelay = groups.find(x => x.id === selected_group)?.auto_mode_delay ?? DEFAULT_AUTO_MODE_DELAY;
|
||||
autoModeWorker = setInterval(groupChatAutoModeWorker, autoModeDelay * 1000);
|
||||
}
|
||||
|
||||
async function _save(group, reload = true) {
|
||||
await fetch('/api/groups/edit', {
|
||||
method: 'POST',
|
||||
@ -1035,6 +1043,15 @@ async function onGroupGenerationModeInput(e) {
|
||||
}
|
||||
}
|
||||
|
||||
async function onGroupAutoModeDelayInput(e) {
|
||||
if (openGroupId) {
|
||||
let _thisGroup = groups.find((x) => x.id == openGroupId);
|
||||
_thisGroup.auto_mode_delay = Number(e.target.value);
|
||||
await editGroup(openGroupId, false, false);
|
||||
setAutoModeWorker();
|
||||
}
|
||||
}
|
||||
|
||||
async function onGroupNameInput() {
|
||||
if (openGroupId) {
|
||||
let _thisGroup = groups.find((x) => x.id == openGroupId);
|
||||
@ -1231,6 +1248,7 @@ function select_group_chats(groupId, skipAnimation) {
|
||||
$('#rm_group_submit').prop('disabled', !groupHasMembers);
|
||||
$('#rm_group_allow_self_responses').prop('checked', group && group.allow_self_responses);
|
||||
$('#rm_group_hidemutedsprites').prop('checked', group && group.hideMutedSprites);
|
||||
$('#rm_group_automode_delay').val(group?.auto_mode_delay ?? DEFAULT_AUTO_MODE_DELAY);
|
||||
|
||||
// bottom buttons
|
||||
if (openGroupId) {
|
||||
@ -1249,6 +1267,7 @@ function select_group_chats(groupId, skipAnimation) {
|
||||
}
|
||||
|
||||
updateFavButtonState(group?.fav ?? false);
|
||||
setAutoModeWorker();
|
||||
|
||||
// top bar
|
||||
if (group) {
|
||||
@ -1441,6 +1460,7 @@ async function createGroup() {
|
||||
let allowSelfResponses = !!$('#rm_group_allow_self_responses').prop('checked');
|
||||
let activationStrategy = Number($('#rm_group_activation_strategy').find(':selected').val()) ?? group_activation_strategy.NATURAL;
|
||||
let generationMode = Number($('#rm_group_generation_mode').find(':selected').val()) ?? group_generation_mode.SWAP;
|
||||
let autoModeDelay = Number($('#rm_group_automode_delay').val()) ?? DEFAULT_AUTO_MODE_DELAY;
|
||||
const members = newGroupMembers;
|
||||
const memberNames = characters.filter(x => members.includes(x.avatar)).map(x => x.name).join(', ');
|
||||
|
||||
@ -1469,6 +1489,7 @@ async function createGroup() {
|
||||
fav: fav_grp_checked,
|
||||
chat_id: chatName,
|
||||
chats: chats,
|
||||
auto_mode_delay: autoModeDelay,
|
||||
}),
|
||||
});
|
||||
|
||||
@ -1741,6 +1762,7 @@ jQuery(() => {
|
||||
$('#rm_group_allow_self_responses').on('input', onGroupSelfResponsesClick);
|
||||
$('#rm_group_activation_strategy').on('change', onGroupActivationStrategyInput);
|
||||
$('#rm_group_generation_mode').on('change', onGroupGenerationModeInput);
|
||||
$('#rm_group_automode_delay').on('input', onGroupAutoModeDelayInput);
|
||||
$('#group_avatar_button').on('input', uploadGroupAvatar);
|
||||
$('#rm_group_restore_avatar').on('click', restoreGroupAvatar);
|
||||
$(document).on('click', '.group_member .right_menu_button', onGroupActionClick);
|
||||
|
@ -232,6 +232,7 @@ let power_user = {
|
||||
aux_field: 'character_version',
|
||||
restore_user_input: true,
|
||||
reduced_motion: false,
|
||||
compact_input_area: true,
|
||||
};
|
||||
|
||||
let themes = [];
|
||||
@ -273,6 +274,7 @@ const storage_keys = {
|
||||
enableZenSliders: 'enableZenSliders',
|
||||
enableLabMode: 'enableLabMode',
|
||||
reduced_motion: 'reduced_motion',
|
||||
compact_input_area: 'compact_input_area',
|
||||
};
|
||||
|
||||
const contextControls = [
|
||||
@ -450,6 +452,13 @@ function switchReducedMotion() {
|
||||
$('#reduced_motion').prop('checked', power_user.reduced_motion);
|
||||
}
|
||||
|
||||
function switchCompactInputArea() {
|
||||
const value = localStorage.getItem(storage_keys.compact_input_area);
|
||||
power_user.compact_input_area = value === null ? true : value == 'true';
|
||||
$('#send_form').toggleClass('compact', power_user.compact_input_area);
|
||||
$('#compact_input_area').prop('checked', power_user.compact_input_area);
|
||||
}
|
||||
|
||||
var originalSliderValues = [];
|
||||
|
||||
async function switchLabMode() {
|
||||
@ -1246,6 +1255,15 @@ async function applyTheme(name) {
|
||||
action: async () => {
|
||||
localStorage.setItem(storage_keys.reduced_motion, String(power_user.reduced_motion));
|
||||
$('#reduced_motion').prop('checked', power_user.reduced_motion);
|
||||
switchReducedMotion();
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'compact_input_area',
|
||||
action: async () => {
|
||||
localStorage.setItem(storage_keys.compact_input_area, String(power_user.compact_input_area));
|
||||
$('#compact_input_area').prop('checked', power_user.compact_input_area);
|
||||
switchCompactInputArea();
|
||||
},
|
||||
},
|
||||
];
|
||||
@ -1504,6 +1522,7 @@ function loadPowerUserSettings(settings, data) {
|
||||
|
||||
$(`#character_sort_order option[data-order="${power_user.sort_order}"][data-field="${power_user.sort_field}"]`).prop('selected', true);
|
||||
switchReducedMotion();
|
||||
switchCompactInputArea();
|
||||
reloadMarkdownProcessor(power_user.render_formulas);
|
||||
loadInstructMode(data);
|
||||
loadContextSettings();
|
||||
@ -1893,11 +1912,26 @@ function sortEntitiesList(entities) {
|
||||
});
|
||||
}
|
||||
|
||||
async function saveTheme() {
|
||||
const name = await callPopup('Enter a theme preset name:', 'input');
|
||||
/**
|
||||
* Updates the current UI theme file.
|
||||
*/
|
||||
async function updateTheme() {
|
||||
await saveTheme(power_user.theme);
|
||||
toastr.success('Theme saved.');
|
||||
}
|
||||
|
||||
if (!name) {
|
||||
return;
|
||||
/**
|
||||
* Saves the current theme to the server.
|
||||
* @param {string|undefined} name Theme name. If undefined, a popup will be shown to enter a name.
|
||||
* @returns {Promise<void>} A promise that resolves when the theme is saved.
|
||||
*/
|
||||
async function saveTheme(name = undefined) {
|
||||
if (typeof name !== 'string') {
|
||||
name = await callPopup('Enter a theme preset name:', 'input', power_user.theme);
|
||||
|
||||
if (!name) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const theme = {
|
||||
@ -1932,6 +1966,8 @@ async function saveTheme() {
|
||||
hotswap_enabled: power_user.hotswap_enabled,
|
||||
custom_css: power_user.custom_css,
|
||||
bogus_folders: power_user.bogus_folders,
|
||||
reduced_motion: power_user.reduced_motion,
|
||||
compact_input_area: power_user.compact_input_area,
|
||||
};
|
||||
|
||||
const response = await fetch('/savetheme', {
|
||||
@ -2804,7 +2840,8 @@ $(document).ready(() => {
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$('#ui-preset-save-button').on('click', saveTheme);
|
||||
$('#ui-preset-save-button').on('click', () => saveTheme());
|
||||
$('#ui-preset-update-button').on('click', () => updateTheme());
|
||||
$('#movingui-preset-save-button').on('click', saveMovingUI);
|
||||
|
||||
$('#never_resize_avatars').on('input', function () {
|
||||
@ -3151,6 +3188,13 @@ $(document).ready(() => {
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$('#compact_input_area').on('input', function () {
|
||||
power_user.compact_input_area = !!$(this).prop('checked');
|
||||
localStorage.setItem(storage_keys.compact_input_area, String(power_user.compact_input_area));
|
||||
switchCompactInputArea();
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$(document).on('click', '#debug_table [data-debug-function]', function () {
|
||||
const functionId = $(this).data('debug-function');
|
||||
const functionRecord = debug_functions.find(f => f.functionId === functionId);
|
||||
|
16
server.js
16
server.js
@ -93,9 +93,9 @@ const cliArguments = yargs(hideBin(process.argv))
|
||||
}).parseSync();
|
||||
|
||||
// change all relative paths
|
||||
const directory = process['pkg'] ? path.dirname(process.execPath) : __dirname;
|
||||
const serverDirectory = process['pkg'] ? path.dirname(process.execPath) : __dirname;
|
||||
console.log(process['pkg'] ? 'Running from binary' : 'Running from source');
|
||||
process.chdir(directory);
|
||||
process.chdir(serverDirectory);
|
||||
|
||||
const app = express();
|
||||
app.use(compression());
|
||||
@ -621,6 +621,8 @@ const setupTasks = async function () {
|
||||
exitProcess();
|
||||
});
|
||||
|
||||
await loadPlugins();
|
||||
|
||||
console.log('Launching...');
|
||||
|
||||
if (autorun) open(autorunUrl.toString());
|
||||
@ -632,6 +634,16 @@ const setupTasks = async function () {
|
||||
}
|
||||
};
|
||||
|
||||
async function loadPlugins() {
|
||||
try {
|
||||
const pluginDirectory = path.join(serverDirectory, 'plugins');
|
||||
const loader = require('./src/plugin-loader');
|
||||
await loader.loadPlugins(app, pluginDirectory);
|
||||
} catch {
|
||||
console.log('Plugin loading failed.');
|
||||
}
|
||||
}
|
||||
|
||||
if (listen && !getConfigValue('whitelistMode', true) && !getConfigValue('basicAuthMode', false)) {
|
||||
if (getConfigValue('securityOverride', false)) {
|
||||
console.warn(color.red('Security has been overridden. If it\'s not a trusted network, change the settings.'));
|
||||
|
@ -411,7 +411,7 @@ async function sendMistralAIRequest(request, response) {
|
||||
const lastMsg = messages[messages.length - 1];
|
||||
if (messages.length > 0 && lastMsg && (lastMsg.role === 'system' || lastMsg.role === 'assistant')) {
|
||||
lastMsg.role = 'user';
|
||||
if(lastMsg.role === 'assistant') {
|
||||
if (lastMsg.role === 'assistant') {
|
||||
lastMsg.content = lastMsg.name + ': ' + lastMsg.content;
|
||||
} else if (lastMsg.role === 'system') {
|
||||
lastMsg.content = '[INST] ' + lastMsg.content + ' [/INST]';
|
||||
|
@ -73,6 +73,7 @@ router.post('/create', jsonParser, (request, response) => {
|
||||
fav: request.body.fav,
|
||||
chat_id: request.body.chat_id ?? id,
|
||||
chats: request.body.chats ?? [id],
|
||||
auto_mode_delay: request.body.auto_mode_delay ?? 5,
|
||||
};
|
||||
const pathToFile = path.join(DIRECTORIES.groups, `${id}.json`);
|
||||
const fileData = JSON.stringify(groupMetadata);
|
||||
|
@ -1,21 +1,41 @@
|
||||
const fetch = require('node-fetch').default;
|
||||
const { SECRET_KEYS, readSecret } = require('./endpoints/secrets');
|
||||
|
||||
const SOURCES = {
|
||||
'mistral': {
|
||||
secretKey: SECRET_KEYS.MISTRAL,
|
||||
url: 'api.mistral.ai',
|
||||
model: 'mistral-embed',
|
||||
},
|
||||
'openai': {
|
||||
secretKey: SECRET_KEYS.OPENAI,
|
||||
url: 'api.openai.com',
|
||||
model: 'text-embedding-ada-002',
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the vector for the given text from an OpenAI compatible endpoint.
|
||||
* @param {string} text - The text to get the vector for
|
||||
* @param {string} source - The source of the vector
|
||||
* @returns {Promise<number[]>} - The vector for the text
|
||||
*/
|
||||
async function getOpenAIVector(text, source) {
|
||||
const isMistral = source === 'mistral';
|
||||
const key = readSecret(isMistral ? SECRET_KEYS.MISTRALAI : SECRET_KEYS.OPENAI);
|
||||
const config = SOURCES[source];
|
||||
|
||||
if (!key) {
|
||||
console.log('No OpenAI key found');
|
||||
throw new Error('No OpenAI key found');
|
||||
if (!config) {
|
||||
console.log('Unknown source', source);
|
||||
throw new Error('Unknown source');
|
||||
}
|
||||
|
||||
const url = isMistral ? 'api.mistral.ai' : 'api.openai.com';
|
||||
const key = readSecret(config.secretKey);
|
||||
|
||||
if (!key) {
|
||||
console.log('No API key found');
|
||||
throw new Error('No API key found');
|
||||
}
|
||||
|
||||
const url = config.url;
|
||||
const response = await fetch(`https://${url}/v1/embeddings`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
@ -24,22 +44,22 @@ async function getOpenAIVector(text, source) {
|
||||
},
|
||||
body: JSON.stringify({
|
||||
input: text,
|
||||
model: isMistral ? 'mistral-embed' : 'text-embedding-ada-002',
|
||||
model: config.model,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
console.log('OpenAI request failed', response.statusText, text);
|
||||
throw new Error('OpenAI request failed');
|
||||
console.log('API request failed', response.statusText, text);
|
||||
throw new Error('API request failed');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const vector = data?.data[0]?.embedding;
|
||||
|
||||
if (!Array.isArray(vector)) {
|
||||
console.log('OpenAI response was not an array');
|
||||
throw new Error('OpenAI response was not an array');
|
||||
console.log('API response was not an array');
|
||||
throw new Error('API response was not an array');
|
||||
}
|
||||
|
||||
return vector;
|
||||
|
162
src/plugin-loader.js
Normal file
162
src/plugin-loader.js
Normal file
@ -0,0 +1,162 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { getConfigValue } = require('./util');
|
||||
const enableServerPlugins = getConfigValue('enableServerPlugins', false);
|
||||
|
||||
/**
|
||||
* Determine if a file is a CommonJS module.
|
||||
* @param {string} file Path to file
|
||||
* @returns {boolean} True if file is a CommonJS module
|
||||
*/
|
||||
const isCommonJS = (file) => path.extname(file) === '.js';
|
||||
|
||||
/**
|
||||
* Determine if a file is an ECMAScript module.
|
||||
* @param {string} file Path to file
|
||||
* @returns {boolean} True if file is an ECMAScript module
|
||||
*/
|
||||
const isESModule = (file) => path.extname(file) === '.mjs';
|
||||
|
||||
/**
|
||||
* Load and initialize server plugins from a directory if they are enabled.
|
||||
* @param {import('express').Express} app Express app
|
||||
* @param {string} pluginsPath Path to plugins directory
|
||||
* @returns {Promise<any>} Promise that resolves when all plugins are loaded
|
||||
*/
|
||||
async function loadPlugins(app, pluginsPath) {
|
||||
// Server plugins are disabled.
|
||||
if (!enableServerPlugins) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Plugins directory does not exist.
|
||||
if (!fs.existsSync(pluginsPath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const files = fs.readdirSync(pluginsPath);
|
||||
|
||||
// No plugins to load.
|
||||
if (files.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const file of files) {
|
||||
const pluginFilePath = path.join(pluginsPath, file);
|
||||
|
||||
if (fs.statSync(pluginFilePath).isDirectory()) {
|
||||
await loadFromDirectory(app, pluginFilePath);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Not a JavaScript file.
|
||||
if (!isCommonJS(file) && !isESModule(file)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
await loadFromFile(app, pluginFilePath);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadFromDirectory(app, pluginDirectoryPath) {
|
||||
const files = fs.readdirSync(pluginDirectoryPath);
|
||||
|
||||
// No plugins to load.
|
||||
if (files.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Plugin is an npm package.
|
||||
const packageJsonFilePath = path.join(pluginDirectoryPath, 'package.json');
|
||||
if (fs.existsSync(packageJsonFilePath)) {
|
||||
if (await loadFromPackage(app, packageJsonFilePath)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Plugin is a CommonJS module.
|
||||
const cjsFilePath = path.join(pluginDirectoryPath, 'index.js');
|
||||
if (fs.existsSync(cjsFilePath)) {
|
||||
if (await loadFromFile(app, cjsFilePath)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Plugin is an ECMAScript module.
|
||||
const esmFilePath = path.join(pluginDirectoryPath, 'index.mjs');
|
||||
if (fs.existsSync(esmFilePath)) {
|
||||
if (await loadFromFile(app, esmFilePath)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads and initializes a plugin from an npm package.
|
||||
* @param {import('express').Express} app Express app
|
||||
* @param {string} packageJsonPath Path to package.json file
|
||||
* @returns {Promise<boolean>} Promise that resolves to true if plugin was loaded successfully
|
||||
*/
|
||||
async function loadFromPackage(app, packageJsonPath) {
|
||||
try {
|
||||
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
||||
if (packageJson.main) {
|
||||
const pluginFilePath = path.join(path.dirname(packageJsonPath), packageJson.main);
|
||||
return await loadFromFile(app, pluginFilePath);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to load plugin from ${packageJsonPath}: ${error}`);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads and initializes a plugin from a file.
|
||||
* @param {import('express').Express} app Express app
|
||||
* @param {string} pluginFilePath Path to plugin directory
|
||||
* @returns {Promise<boolean>} Promise that resolves to true if plugin was loaded successfully
|
||||
*/
|
||||
async function loadFromFile(app, pluginFilePath) {
|
||||
try {
|
||||
const plugin = await getPluginModule(pluginFilePath);
|
||||
console.log(`Initializing plugin from ${pluginFilePath}`);
|
||||
return await initPlugin(app, plugin);
|
||||
} catch (error) {
|
||||
console.error(`Failed to load plugin from ${pluginFilePath}: ${error}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes a plugin module.
|
||||
* @param {import('express').Express} app Express app
|
||||
* @param {any} plugin Plugin module
|
||||
* @returns {Promise<boolean>} Promise that resolves to true if plugin was initialized successfully
|
||||
*/
|
||||
async function initPlugin(app, plugin) {
|
||||
if (typeof plugin.init === 'function') {
|
||||
await plugin.init(app);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a module from a file depending on the module type.
|
||||
* @param {string} pluginFilePath Path to plugin file
|
||||
* @returns {Promise<any>} Promise that resolves to plugin module
|
||||
*/
|
||||
async function getPluginModule(pluginFilePath) {
|
||||
if (isCommonJS(pluginFilePath)) {
|
||||
return require(pluginFilePath);
|
||||
}
|
||||
if (isESModule(pluginFilePath)) {
|
||||
return await import(pluginFilePath);
|
||||
}
|
||||
throw new Error(`Unsupported module type in ${pluginFilePath}`);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
loadPlugins,
|
||||
};
|
Loading…
x
Reference in New Issue
Block a user