Merge branch 'staging' into woo-yeah

9a2968d1eb10e1fd6d05e79551ba3258cfadbf4f

Cohee <18619528+Cohee1207@users.noreply.github.com>

13 files changed, +138 -53Showing whitespace changes
default/config.yaml+15 -13
@@ -133,24 +133,26 @@ whitelistImportDomains:
133133## headers:
134134## User-Agent: "Googlebot/2.1 (+http://www.google.com/bot.html)"
135135requestOverrides: []
136-# -- EXTENSIONS CONFIGURATION --
136+
137+# EXTENSIONS CONFIGURATION
138+extensions:
137139 # Enable UI extensions
138140enableExtensions enabled: true
139141 # Automatically update extensions when a release version changes
140142enableExtensionsAutoUpdate autoUpdate: true
143+ models:
144+ # Enables automatic model download from HuggingFace
145+ autoDownload: true
146+ # Additional models for extensions. Expects model IDs from HuggingFace model hub in ONNX format
147+ classification: Cohee/distilbert-base-uncased-go-emotions-onnx
148+ captioning: Xenova/vit-gpt2-image-captioning
149+ embedding: Cohee/jina-embeddings-v2-base-en
150+ speechToText: Xenova/whisper-small
151+ textToSpeech: Xenova/speecht5_tts
152+
141153# Additional model tokenizers can be downloaded on demand.
142154# Disabling will fallback to another locally available tokenizer.
143155enableDownloadableTokenizers: true
144-# Extension settings
145-extras:
146- # Disables automatic model download from HuggingFace
147- disableAutoDownload: false
148- # Extra models for plugins. Expects model IDs from HuggingFace model hub in ONNX format
149- classificationModel: Cohee/distilbert-base-uncased-go-emotions-onnx
150- captioningModel: Xenova/vit-gpt2-image-captioning
151- embeddingModel: Cohee/jina-embeddings-v2-base-en
152- speechToTextModel: Xenova/whisper-small
153- textToSpeechModel: Xenova/speecht5_tts
154156# -- OPENAI CONFIGURATION --
155157# A placeholder message to use in strict prompt post-processing mode when the prompt doesn't start with a user message
156158promptPlaceholder: "[Start a new chat]"
jsconfig.json+1 -1
@@ -15,7 +15,7 @@
1515 "**/node_modules/**",
1616 "**/dist/**",
1717 "**/.git/**",
1818 "public/lib/**",
1919 "backups/**",
2020 "data/**",
2121 "cache/**",
post-install.js+40 -0
@@ -64,6 +64,46 @@ const keyMigrationMap = [
6464 newKey: 'backups.chat.throttleInterval',
6565 migrate: (value) => value,
6666 },
67+ {
68+ oldKey: 'enableExtensions',
69+ newKey: 'extensions.enabled',
70+ migrate: (value) => value,
71+ },
72+ {
73+ oldKey: 'enableExtensionsAutoUpdate',
74+ newKey: 'extensions.autoUpdate',
75+ migrate: (value) => value,
76+ },
77+ {
78+ oldKey: 'extras.disableAutoDownload',
79+ newKey: 'extensions.models.autoDownload',
80+ migrate: (value) => !value,
81+ },
82+ {
83+ oldKey: 'extras.classificationModel',
84+ newKey: 'extensions.models.classification',
85+ migrate: (value) => value,
86+ },
87+ {
88+ oldKey: 'extras.captioningModel',
89+ newKey: 'extensions.models.captioning',
90+ migrate: (value) => value,
91+ },
92+ {
93+ oldKey: 'extras.embeddingModel',
94+ newKey: 'extensions.models.embedding',
95+ migrate: (value) => value,
96+ },
97+ {
98+ oldKey: 'extras.speechToTextModel',
99+ newKey: 'extensions.models.speechToText',
100+ migrate: (value) => value,
101+ },
102+ {
103+ oldKey: 'extras.textToSpeechModel',
104+ newKey: 'extensions.models.textToSpeech',
105+ migrate: (value) => value,
106+ },
67107];
68108
69109/**
public/locales/zh-cn.json+8 -8
@@ -1191,9 +1191,9 @@
11911191 "welcome_message_part_8": "您可随时通过",
11921192 "welcome_message_part_9": "图标来更改此设置。",
11931193 "Persona Name:": "用户角色名称:",
11941194 "Temporarily disable automatic replies from this character": "暂时禁用此角色的自动回复临时禁言此角色",
11951195 "Enable automatic replies from this character": "启用此角色的自动回复解除禁言此角色",
11961196 "Trigger a message from this character": "从此角色触发消息强制触发该角色发言",
11971197 "Move up": "向上移动",
11981198 "Move down": "向下移动",
11991199 "View character card": "查看角色卡片",
@@ -1838,7 +1838,7 @@
18381838 "Enter the Git URL of the extension to install": "输入扩展程序的 Git URL 以安装",
18391839 "Disclaimer:": "免责声明:",
18401840 "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.": "使用外部的扩展程序可能存在意料外的副作用和安全隐患。在导入扩展程序前,请一定确认其来源可信。我们不为第三方扩展程序造成的任何损失负责。",
18411841 "Prompt Itemization": "将提示词分条提示词拆分",
18421842 "Show Raw Prompt": "显示原始提示词",
18431843 "Copy Prompt": "复制提示词",
18441844 "Show Prompt Differences": "显示提示词差异",
@@ -2045,8 +2045,8 @@
20452045 "Post a GitHub issue": "在 GitHub 发布问题",
20462046 "Contact the developers": "联系开发者",
20472047 "If you're connected to an API, try asking me something!": "若您已经配置好API,尝试发送些什么吧!",
20482048 "Title/Memo": "标题/备忘录(备忘)",
20492049 "Strategy": "Strategy触发策略",
20502050 "Position": "位置插入位置",
20512051 "Trigger %": "触发率 %触发概率%"
20522052}
public/scripts/st-context.js+6 -2
@@ -12,6 +12,7 @@ import {
1212 extension_prompts,
1313 Generate,
1414 generateQuietPrompt,
15+ getCharacters,
1516 getCurrentChatId,
1617 getRequestHeaders,
1718 getThumbnailUrl,
@@ -55,7 +56,7 @@ import { MacrosParser } from './macros.js';
5556import { oai_settings } from './openai.js';
5657import { callGenericPopup, Popup, POPUP_RESULT, POPUP_TYPE } from './popup.js';
5758import { power_user, registerDebugFunction } from './power-user.js';
5859import { humanizedDateTime, isMobile, shouldSendOnEnter } from './RossAscends-mods.js';
5960import { ScraperManager } from './scrapers.js';
6061import { executeSlashCommands, executeSlashCommandsWithOptions, registerSlashCommand } from './slash-commands.js';
6162import { SlashCommand } from './slash-commands/SlashCommand.js';
@@ -65,7 +66,7 @@ import { tag_map, tags } from './tags.js';
6566import { textgenerationwebui_settings } from './textgen-settings.js';
6667import { tokenizers, getTextTokens, getTokenCount, getTokenCountAsync, getTokenizerModel } from './tokenizers.js';
6768import { ToolManager } from './tool-calling.js';
6869import { timestampToMoment, uuidv4 } from './utils.js';
6970
7071export function getContext() {
7172 return {
@@ -167,6 +168,9 @@ export function getContext() {
167168 chatCompletionSettings: oai_settings,
168169 textCompletionSettings: textgenerationwebui_settings,
169170 powerUserSettings: power_user,
171+ getCharacters,
172+ uuidv4,
173+ humanizedDateTime,
170174 };
171175}
172176
src/endpoints/avatars.js+2 -1
@@ -9,6 +9,7 @@ import { sync as writeFileAtomicSync } from 'write-file-atomic';
99import { jsonParser, urlencodedParser } from '../express-common.js';
1010import { AVATAR_WIDTH, AVATAR_HEIGHT } from '../constants.js';
1111import { getImages, tryParse } from '../util.js';
12+import { getFileNameValidationFunction } from '../middleware/validateFileName.js';
1213
1314export const router = express.Router();
1415
@@ -17,7 +18,7 @@ router.post('/get', jsonParser, function (request, response) {
1718 response.send(JSON.stringify(images));
1819});
1920
2021router.post('/delete', jsonParser, getFileNameValidationFunction('avatar'), function (request, response) {
2122 if (!request.body) return response.sendStatus(400);
2223
2324 if (request.body.avatar !== sanitize(request.body.avatar)) {
src/endpoints/backgrounds.js+2 -1
@@ -7,6 +7,7 @@ import sanitize from 'sanitize-filename';
77import { jsonParser, urlencodedParser } from '../express-common.js';
88import { invalidateThumbnail } from './thumbnails.js';
99import { getImages } from '../util.js';
10+import { getFileNameValidationFunction } from '../middleware/validateFileName.js';
1011
1112export const router = express.Router();
1213
@@ -15,7 +16,7 @@ router.post('/all', jsonParser, function (request, response) {
1516 response.send(JSON.stringify(images));
1617});
1718
1819router.post('/delete', jsonParser, getFileNameValidationFunction('bg'), function (request, response) {
1920 if (!request.body) return response.sendStatus(400);
2021
2122 if (request.body.bg !== sanitize(request.body.bg)) {
src/endpoints/characters.js+10 -9
@@ -14,6 +14,7 @@ import jimp from 'jimp';
1414
1515import { AVATAR_WIDTH, AVATAR_HEIGHT } from '../constants.js';
1616import { jsonParser, urlencodedParser } from '../express-common.js';
17+import { default as validateAvatarUrlMiddleware, getFileNameValidationFunction } from '../middleware/validateFileName.js';
1718import { deepMerge, humanizedISO8601DateTime, tryParse, extractFileFromZipBuffer, MemoryLimitedMap, getConfigValue } from '../util.js';
1819import { TavernCardValidator } from '../validator/TavernCardValidator.js';
1920import { parse, write } from '../character-card-parser.js';
@@ -756,7 +757,7 @@ router.post('/create', urlencodedParser, async function (request, response) {
756757 }
757758});
758759
759760router.post('/rename', jsonParser, validateAvatarUrlMiddleware, async function (request, response) {
760761 if (!request.body.avatar_url || !request.body.new_name) {
761762 return response.sendStatus(400);
762763 }
@@ -803,7 +804,7 @@ router.post('/rename', jsonParser, async function (request, response) {
803804 }
804805});
805806
806807router.post('/edit', urlencodedParser, validateAvatarUrlMiddleware, async function (request, response) {
807808 if (!request.body) {
808809 console.error('Error: no response body detected');
809810 response.status(400).send('Error: no response body detected');
@@ -852,7 +853,7 @@ router.post('/edit', urlencodedParser, async function (request, response) {
852853 * @param {Object} response - The HTTP response object.
853854 * @returns {void}
854855 */
855856router.post('/edit-attribute', jsonParser, validateAvatarUrlMiddleware, async function (request, response) {
856857 console.log(request.body);
857858 if (!request.body) {
858859 console.error('Error: no response body detected');
@@ -898,7 +899,7 @@ router.post('/edit-attribute', jsonParser, async function (request, response) {
898899 *
899900 * @returns {void}
900901 * */
901902router.post('/merge-attributes', jsonParser, getFileNameValidationFunction('avatar'), async function (request, response) {
902903 try {
903904 const update = request.body;
904905 const avatarPath = path.join(request.user.directories.characters, update.avatar);
@@ -929,7 +930,7 @@ router.post('/merge-attributes', jsonParser, async function (request, response)
929930 }
930931});
931932
932933router.post('/delete', jsonParser, validateAvatarUrlMiddleware, async function (request, response) {
933934 if (!request.body || !request.body.avatar_url) {
934935 return response.sendStatus(400);
935936 }
@@ -992,7 +993,7 @@ router.post('/all', jsonParser, async function (request, response) {
992993 }
993994});
994995
995996router.post('/get', jsonParser, validateAvatarUrlMiddleware, async function (request, response) {
996997 try {
997998 if (!request.body) return response.sendStatus(400);
998999 const item = request.body.avatar_url;
@@ -1011,7 +1012,7 @@ router.post('/get', jsonParser, async function (request, response) {
10111012 }
10121013});
10131014
10141015router.post('/chats', jsonParser, validateAvatarUrlMiddleware, async function (request, response) {
10151016 if (!request.body) return response.sendStatus(400);
10161017
10171018 const characterDirectory = (request.body.avatar_url).replace('.png', '');
@@ -1160,7 +1161,7 @@ router.post('/import', urlencodedParser, async function (request, response) {
11601161 }
11611162});
11621163
11631164router.post('/duplicate', jsonParser, validateAvatarUrlMiddleware, async function (request, response) {
11641165 try {
11651166 if (!request.body.avatar_url) {
11661167 console.log('avatar URL not found in request body');
@@ -1207,7 +1208,7 @@ router.post('/duplicate', jsonParser, async function (request, response) {
12071208 }
12081209});
12091210
12101211router.post('/export', jsonParser, validateAvatarUrlMiddleware, async function (request, response) {
12111212 try {
12121213 if (!request.body.format || !request.body.avatar_url) {
12131214 return response.sendStatus(400);
src/endpoints/chats.js+8 -7
@@ -9,6 +9,7 @@ import { sync as writeFileAtomicSync } from 'write-file-atomic';
99import _ from 'lodash';
1010
1111import { jsonParser, urlencodedParser } from '../express-common.js';
12+import validateAvatarUrlMiddleware from '../middleware/validateFileName.js';
1213import {
1314 getConfigValue,
1415 humanizedISO8601DateTime,
@@ -294,7 +295,7 @@ function importRisuChat(userName, characterName, jsonData) {
294295
295296export const router = express.Router();
296297
297298router.post('/save', jsonParser, validateAvatarUrlMiddleware, function (request, response) {
298299 try {
299300 const directoryName = String(request.body.avatar_url).replace('.png', '');
300301 const chatData = request.body.chat;
@@ -310,7 +311,7 @@ router.post('/save', jsonParser, function (request, response) {
310311 }
311312});
312313
313314router.post('/get', jsonParser, validateAvatarUrlMiddleware, function (request, response) {
314315 try {
315316 const dirName = String(request.body.avatar_url).replace('.png', '');
316317 const directoryPath = path.join(request.user.directories.chats, dirName);
@@ -347,7 +348,7 @@ router.post('/get', jsonParser, function (request, response) {
347348});
348349
349350
350351router.post('/rename', jsonParser, validateAvatarUrlMiddleware, async function (request, response) {
351352 if (!request.body || !request.body.original_file || !request.body.renamed_file) {
352353 return response.sendStatus(400);
353354 }
@@ -372,7 +373,7 @@ router.post('/rename', jsonParser, async function (request, response) {
372373 return response.send({ ok: true, sanitizedFileName });
373374});
374375
375376router.post('/delete', jsonParser, validateAvatarUrlMiddleware, function (request, response) {
376377 const dirName = String(request.body.avatar_url).replace('.png', '');
377378 const fileName = String(request.body.chatfile);
378379 const filePath = path.join(request.user.directories.chats, dirName, sanitize(fileName));
@@ -388,7 +389,7 @@ router.post('/delete', jsonParser, function (request, response) {
388389 return response.send('ok');
389390});
390391
391392router.post('/export', jsonParser, validateAvatarUrlMiddleware, async function (request, response) {
392393 if (!request.body.file || (!request.body.avatar_url && request.body.is_group === false)) {
393394 return response.sendStatus(400);
394395 }
@@ -478,7 +479,7 @@ router.post('/group/import', urlencodedParser, function (request, response) {
478479 }
479480});
480481
481482router.post('/import', urlencodedParser, validateAvatarUrlMiddleware, function (request, response) {
482483 if (!request.body) return response.sendStatus(400);
483484
484485 const format = request.body.file_type;
@@ -626,7 +627,7 @@ router.post('/group/save', jsonParser, (request, response) => {
626627 return response.send({ ok: true });
627628});
628629
629630router.post('/search', jsonParser, validateAvatarUrlMiddleware, function (request, response) {
630631 try {
631632 const { query, avatar_url, group_id } = request.body;
632633 let chatFiles = [];
src/endpoints/settings.js+5 -4
@@ -9,9 +9,10 @@ import { SETTINGS_FILE } from '../constants.js';
99import { getConfigValue, generateTimestamp, removeOldBackups } from '../util.js';
1010import { jsonParser } from '../express-common.js';
1111import { getAllUserHandles, getUserDirectories } from '../users.js';
12+import { getFileNameValidationFunction } from '../middleware/validateFileName.js';
1213
1314const ENABLE_EXTENSIONS = !!getConfigValue('enableExtensionsextensions.enabled', true);
1415const ENABLE_EXTENSIONS_AUTO_UPDATE = !!getConfigValue('enableExtensionsAutoUpdateextensions.autoUpdate', true);
1516const ENABLE_ACCOUNTS = getConfigValue('enableUserAccounts', false);
1617
1718// 10 minutes
@@ -296,7 +297,7 @@ router.post('/get-snapshots', jsonParser, async (request, response) => {
296297 }
297298});
298299
299300router.post('/load-snapshot', jsonParser, getFileNameValidationFunction('name'), async (request, response) => {
300301 try {
301302 const userFilesPattern = getFilePrefix(request.user.profile.handle);
302303
@@ -330,7 +331,7 @@ router.post('/make-snapshot', jsonParser, async (request, response) => {
330331 }
331332});
332333
333334router.post('/restore-snapshot', jsonParser, getFileNameValidationFunction('name'), async (request, response) => {
334335 try {
335336 const userFilesPattern = getFilePrefix(request.user.profile.handle);
336337
src/endpoints/vectors.js+1 -1
@@ -164,7 +164,7 @@ function getSourceSettings(source, request) {
164164 };
165165 case 'transformers':
166166 return {
167167 model: getConfigValue('extrasextensions.embeddingModelmodels.embedding', ''),
168168 };
169169 case 'palm':
170170 return {
src/middleware/validateFileName.js+34 -0
@@ -0,0 +1,34 @@
1+import path from 'node:path';
2+
3+/**
4+ * Gets a middleware function that validates the field in the request body.
5+ * @param {string} fieldName Field name
6+ * @returns {import('express').RequestHandler} Middleware function
7+ */
8+export function getFileNameValidationFunction(fieldName) {
9+ /**
10+ * Validates the field in the request body.
11+ * @param {import('express').Request} req Request object
12+ * @param {import('express').Response} res Response object
13+ * @param {import('express').NextFunction} next Next middleware
14+ */
15+ return function validateAvatarUrlMiddleware(req, res, next) {
16+ if (req.body && fieldName in req.body && typeof req.body[fieldName] === 'string') {
17+ const forbiddenRegExp = path.sep === '/' ? /[/\x00]/ : /[/\x00\\]/;
18+ if (forbiddenRegExp.test(req.body[fieldName])) {
19+ console.error('An error occurred while validating the request body', {
20+ handle: req.user.profile.handle,
21+ path: req.originalUrl,
22+ field: fieldName,
23+ value: req.body[fieldName],
24+ });
25+ return res.sendStatus(400);
26+ }
27+ }
28+
29+ next();
30+ };
31+}
32+
33+const avatarUrlValidationFunction = getFileNameValidationFunction('avatar_url');
34+export default avatarUrlValidationFunction;
src/transformers.js+6 -6
@@ -19,31 +19,31 @@ const tasks = {
1919 'text-classification': {
2020 defaultModel: 'Cohee/distilbert-base-uncased-go-emotions-onnx',
2121 pipeline: null,
2222 configField: 'extrasextensions.classificationModelmodels.classification',
2323 quantized: true,
2424 },
2525 'image-to-text': {
2626 defaultModel: 'Xenova/vit-gpt2-image-captioning',
2727 pipeline: null,
2828 configField: 'extrasextensions.captioningModelmodels.captioning',
2929 quantized: true,
3030 },
3131 'feature-extraction': {
3232 defaultModel: 'Xenova/all-mpnet-base-v2',
3333 pipeline: null,
3434 configField: 'extrasextensions.embeddingModelmodels.embedding',
3535 quantized: true,
3636 },
3737 'automatic-speech-recognition': {
3838 defaultModel: 'Xenova/whisper-small',
3939 pipeline: null,
4040 configField: 'extrasextensions.speechToTextModelmodels.speechToText',
4141 quantized: true,
4242 },
4343 'text-to-speech': {
4444 defaultModel: 'Xenova/speecht5_tts',
4545 pipeline: null,
4646 configField: 'extrasextensions.textToSpeechModelmodels.textToSpeech',
4747 quantized: false,
4848 },
4949};
@@ -132,7 +132,7 @@ export async function getPipeline(task, forceModel = '') {
132132
133133 const cacheDir = path.join(globalThis.DATA_ROOT, '_cache');
134134 const model = forceModel || getModelForTask(task);
135135 const localOnly = !getConfigValue('extrasextensions.disableAutoDownloadmodels.autoDownload', falsetrue);
136136 console.log('Initializing transformers.js pipeline for task', task, 'with model', model);
137137 const instance = await pipeline(task, model, { cache_dir: cacheDir, quantized: tasks[task].quantized ?? true, local_files_only: localOnly });
138138 tasks[task].pipeline = instance;