mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-06-05 21:59:27 +02:00
External extension import UI + backend
This commit is contained in:
@@ -2509,6 +2509,7 @@
|
||||
<input id="extensions_connect" class="menu_button" type="submit" value="Connect">
|
||||
</div>
|
||||
<input id="extensions_details" class="alignitemsflexstart menu_button" type="button" value="Manage extensions">
|
||||
<div id="third_party_extension_button" title="Import Extension From Git Repo" class="menu_button fa-solid fa-cloud-arrow-down faSmallFontSquareFix"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="extensions_settings" class="flex1 wide50p">
|
||||
@@ -2853,7 +2854,6 @@
|
||||
<div id="rm_button_create" title="Create New Character" class="menu_button fa-solid fa-user-plus "></div>
|
||||
<div id="character_import_button" title="Import Character from File" class="menu_button fa-solid fa-file-arrow-up faSmallFontSquareFix"></div>
|
||||
<div id="external_import_button" title="Import content from external URL" class="menu_button fa-solid fa-cloud-arrow-down faSmallFontSquareFix"></div>
|
||||
<div id="thrid_party_extension_button" title="Import extension from git repo" class="menu_button fa-solid fa-cloud-arrow-down faSmallFontSquareFix"></div>
|
||||
<div id="rm_button_group_chats" title="Create New Chat Group" class="menu_button fa-solid fa-users-gear "></div>
|
||||
<input id="character_search_bar" class="text_pole width100p" type="search" placeholder="Search..." maxlength="50" />
|
||||
<select id="character_sort_order" title="Characters sorting order">
|
||||
|
@@ -8494,23 +8494,21 @@ $(document).ready(function () {
|
||||
}
|
||||
});
|
||||
|
||||
$('#thrid_party_extension_button').on('click', async () => {
|
||||
const html = `<h3>Enter the URL of the content to import</h3>
|
||||
Supported sources:<br>
|
||||
<ul class="justifyLeft">
|
||||
<li>Chub characters (direct link or id)<br>Example: <tt>Anonymous/example-character</tt></li>
|
||||
<li>Chub lorebooks (direct link or id)<br>Example: <tt>lorebooks/bartleby/example-lorebook</tt></li>
|
||||
<li>More coming soon...</li>
|
||||
<ul>`
|
||||
$('#third_party_extension_button').on('click', async () => {
|
||||
const html = `<h3>Enter the Git URL of the extension to import</h3>
|
||||
<br>
|
||||
<p><b>Disclaimer:</b> Please be aware that using external extensions can have unintended side effects and may pose security risks. Always make sure you trust the source before importing an extension. We are not responsible for any damage caused by third-party extensions.</p>
|
||||
<br>
|
||||
<p>Example: <tt> https://github.com/author/extension-name </tt></p>`
|
||||
const input = await callPopup(html, 'input');
|
||||
|
||||
if (!input) {
|
||||
console.debug('Custom content import cancelled');
|
||||
console.debug('Extension import cancelled');
|
||||
return;
|
||||
}
|
||||
|
||||
const url = input.trim();
|
||||
console.debug('Custom content import started', url);
|
||||
console.debug('Extension import started', url);
|
||||
|
||||
const request = await fetch('/get_extension', {
|
||||
method: 'POST',
|
||||
@@ -8519,11 +8517,14 @@ $(document).ready(function () {
|
||||
});
|
||||
|
||||
if (!request.ok) {
|
||||
toastr.info(request.statusText, 'Custom content import failed');
|
||||
console.error('Custom content import failed', request.status, request.statusText);
|
||||
toastr.info(request.statusText, 'Extension import failed');
|
||||
console.error('Extension import failed', request.status, request.statusText);
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await request.json();
|
||||
toastr.success(`Extension "${response.display_name}" by ${response.author} (version ${response.version}) has been imported successfully!`, 'Extension import successful');
|
||||
console.debug(`Extension "${response.display_name}" has been imported successfully at ${response.extensionPath}`);
|
||||
});
|
||||
|
||||
const $dropzone = $(document.body);
|
||||
|
118
server.js
118
server.js
@@ -2711,10 +2711,27 @@ app.post('/poe_suggest', jsonParser, async function (request, response) {
|
||||
|
||||
});
|
||||
|
||||
/**
|
||||
* Discover the extension folders
|
||||
* If the folder is called third-party, search for subfolders instead
|
||||
*/
|
||||
app.get('/discover_extensions', jsonParser, function (_, response) {
|
||||
|
||||
// get all folders in the extensions folder, except third-party
|
||||
const extensions = fs
|
||||
.readdirSync(directories.extensions)
|
||||
.filter(f => fs.statSync(path.join(directories.extensions, f)).isDirectory());
|
||||
.filter(f => fs.statSync(path.join(directories.extensions, f)).isDirectory())
|
||||
.filter(f => f !== 'third-party');
|
||||
|
||||
// get all folders in the third-party folder
|
||||
const thirdPartyExtensions = fs
|
||||
.readdirSync(path.join(directories.extensions, 'third-party'))
|
||||
.filter(f => fs.statSync(path.join(directories.extensions, 'third-party', f)).isDirectory());
|
||||
|
||||
// add the third-party extensions to the extensions array
|
||||
extensions.push(...thirdPartyExtensions.map(f => `third-party/${f}`));
|
||||
console.log(extensions);
|
||||
|
||||
|
||||
return response.send(extensions);
|
||||
});
|
||||
@@ -4346,74 +4363,63 @@ async function getImageBuffers(zipFilePath) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is used ensure the list of extensions is up to date.
|
||||
* @param {object} extensionList
|
||||
* @returns
|
||||
*/
|
||||
function getExtensions(extensionList) {
|
||||
const extensions = [];
|
||||
|
||||
// if the extension folder does not exist, git clone the extension, also check if the extension is up to date with the git repo, but don't pull if it is not up to date
|
||||
for (const extension of extensionList) {
|
||||
const extensionPath = path.join(directories.extensions, extension.name);
|
||||
if (!fs.existsSync(extensionPath)) {
|
||||
console.log(`Extension ${extension.name} does not exist. Cloning...`);
|
||||
try {
|
||||
git.clone(extension.url, extensionPath);
|
||||
} catch (error) {
|
||||
console.error(`Failed to clone extension ${extension.name}`);
|
||||
console.error(error);
|
||||
}
|
||||
} else {
|
||||
console.log(`Extension ${extension.name} exists
|
||||
Checking if extension is up to date...`);
|
||||
try {
|
||||
git.fetch(extensionPath);
|
||||
const status = git.status(extensionPath);
|
||||
if (status.behind > 0) {
|
||||
console.log(`Extension ${extension.name} is not up to date. Pulling...`);
|
||||
git.pull(extensionPath);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to check if extension ${extension.name} is up to date`);
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
extensions.push(extension.name);
|
||||
}
|
||||
|
||||
}
|
||||
const simpleGit = require('simple-git');
|
||||
|
||||
/**
|
||||
* This function is used to git clone a single extension into the thrid-party-extensions folder
|
||||
*/
|
||||
* This function extracts the extension information from the manifest file.
|
||||
* @param {string} extensionPath - The path of the extension folder
|
||||
* @returns {Object} - Returns the manifest data as an object
|
||||
*/
|
||||
async function getManifest(extensionPath) {
|
||||
const manifestPath = path.join(extensionPath, 'manifest.json');
|
||||
|
||||
// Check if manifest.json exists
|
||||
if (!fs.existsSync(manifestPath)) {
|
||||
throw new Error(`Manifest file not found at ${manifestPath}`);
|
||||
}
|
||||
|
||||
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
||||
return manifest;
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP POST handler function to clone a git repository from a provided URL, read the extension manifest,
|
||||
* and return extension information and path.
|
||||
*
|
||||
* @param {Object} request - HTTP Request object, expects a JSON body with a 'url' property.
|
||||
* @param {Object} response - HTTP Response object used to respond to the HTTP request.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
app.post('/get_extension', jsonParser, async (request, response) => {
|
||||
if (!request.body.url) {
|
||||
return response.sendStatus(400);
|
||||
return response.status(400).send('Bad Request: URL is required in the request body.');
|
||||
}
|
||||
|
||||
try {
|
||||
const url = request.body.url;
|
||||
let result;
|
||||
const git = simpleGit();
|
||||
|
||||
// git clone and then get the resulting folder path of the extension
|
||||
const extensionPath = git.clone(url, directories.extensions + '/third-party');
|
||||
|
||||
// load the info from the manifest.json in the extension folder
|
||||
const manifest = JSON.parse(fs.readFileSync(extensionPath + '/manifest.json', 'utf8'));
|
||||
// pull version, author, display_name
|
||||
const version = manifest.version;
|
||||
const author = manifest.author;
|
||||
const display_name = manifest.display_name;
|
||||
console.log(`Extension ${display_name} has been cloned`);
|
||||
// return the version, author, display_name, and the path to the extension folder
|
||||
// get the name of the repo from the url and create the extensions folder path
|
||||
const extensionPath = path.join(directories.extensions, 'third-party', path.basename(url, '.git'));
|
||||
|
||||
// Check if a folder already exists at the specified location
|
||||
if (fs.existsSync(extensionPath)) {
|
||||
return response.status(409).send(`Directory already exists at ${extensionPath}`);
|
||||
}
|
||||
|
||||
await git.clone(url, extensionPath);
|
||||
console.log(`Extension has been cloned at ${extensionPath}`);
|
||||
|
||||
// Load the info from the manifest.json in the extension folder
|
||||
const { version, author, display_name } = await getManifest(extensionPath);
|
||||
|
||||
// Return the version, author, display_name, and the path to the extension folder
|
||||
return response.send({ version, author, display_name, extensionPath });
|
||||
|
||||
} catch (error) {
|
||||
console.log('Importing custom content failed', error);
|
||||
return response.sendStatus(500);
|
||||
return response.status(500).send(`Server Error: ${error.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
Reference in New Issue
Block a user