mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-06-05 21:59:27 +02:00
Merge branch 'staging' into immutable-config
This commit is contained in:
13
default/!DO-NOT-EDIT-THESE-FILES.txt
Normal file
13
default/!DO-NOT-EDIT-THESE-FILES.txt
Normal file
@@ -0,0 +1,13 @@
|
||||
These are master copies of the default content files and are managed by SillyTavern.
|
||||
|
||||
Editing any of these files would not only have no effect, but will also cause merge conflicts during update pulls.
|
||||
|
||||
You should edit their respective copies instead, for example:
|
||||
|
||||
1. /default/config.yaml => /config.yaml
|
||||
2. /default/public/css/user.css => /public/css/user.css
|
||||
etc.
|
||||
|
||||
Any questions? You're always welcome at our official documentation website:
|
||||
|
||||
https://docs.sillytavern.app/
|
@@ -71,8 +71,6 @@ autheliaAuth: false
|
||||
# the username and passwords for basic auth are the same as those
|
||||
# for the individual accounts
|
||||
perUserBasicAuth: false
|
||||
# Minimum log level to display in the terminal (DEBUG = 0, INFO = 1, WARN = 2, ERROR = 3)
|
||||
minLogLevel: 0
|
||||
|
||||
# User session timeout *in seconds* (defaults to 24 hours).
|
||||
## Set to a positive number to expire session after a certain time of inactivity
|
||||
@@ -83,6 +81,13 @@ sessionTimeout: -1
|
||||
disableCsrfProtection: false
|
||||
# Disable startup security checks - NOT RECOMMENDED
|
||||
securityOverride: false
|
||||
# -- LOGGING CONFIGURATION --
|
||||
logging:
|
||||
# Enable access logging to access.log file
|
||||
# Records new connections with timestamp, IP address and user agent
|
||||
enableAccessLog: true
|
||||
# Minimum log level to display in the terminal (DEBUG = 0, INFO = 1, WARN = 2, ERROR = 3)
|
||||
minLogLevel: 0
|
||||
# -- RATE LIMITING CONFIGURATION --
|
||||
rateLimiting:
|
||||
# Use X-Real-IP header instead of socket IP for rate limiting
|
||||
|
@@ -8,7 +8,7 @@ import path from 'node:path';
|
||||
import process from 'node:process';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { default as git } from 'simple-git';
|
||||
import { default as git, CheckRepoActions } from 'simple-git';
|
||||
import { color } from './src/util.js';
|
||||
|
||||
const __dirname = import.meta.dirname ?? path.dirname(fileURLToPath(import.meta.url));
|
||||
@@ -49,7 +49,7 @@ async function updatePlugins() {
|
||||
const pluginPath = path.join(pluginsPath, directory);
|
||||
const pluginRepo = git(pluginPath);
|
||||
|
||||
const isRepo = await pluginRepo.checkIsRepo();
|
||||
const isRepo = await pluginRepo.checkIsRepo(CheckRepoActions.IS_REPO_ROOT);
|
||||
if (!isRepo) {
|
||||
console.log(`Directory ${color.yellow(directory)} is not a Git repository`);
|
||||
continue;
|
||||
|
@@ -104,6 +104,11 @@ const keyMigrationMap = [
|
||||
newKey: 'extensions.models.textToSpeech',
|
||||
migrate: (value) => value,
|
||||
},
|
||||
{
|
||||
oldKey: 'minLogLevel',
|
||||
newKey: 'logging.minLogLevel',
|
||||
migrate: (value) => value,
|
||||
},
|
||||
// uncommend one release after 1.12.13
|
||||
/*
|
||||
{
|
||||
|
8
public/global.d.ts
vendored
8
public/global.d.ts
vendored
@@ -40,4 +40,12 @@ declare global {
|
||||
searchInputCssClass?: string;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates a text to a target language using a translation provider.
|
||||
* @param text Text to translate
|
||||
* @param lang Target language
|
||||
* @param provider Translation provider
|
||||
*/
|
||||
async function translate(text: string, lang: string, provider: string = null): Promise<string>;
|
||||
}
|
||||
|
@@ -2000,7 +2000,7 @@
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="range-block" data-source="deepseek,openrouter">
|
||||
<div class="range-block" data-source="deepseek,openrouter,custom">
|
||||
<label for="openai_show_thoughts" class="checkbox_label widthFreeExpand">
|
||||
<input id="openai_show_thoughts" type="checkbox" />
|
||||
<span>
|
||||
@@ -3186,21 +3186,33 @@
|
||||
</div>
|
||||
<h4 data-i18n="Groq Model">Groq Model</h4>
|
||||
<select id="model_groq_select">
|
||||
<optgroup label="Production Models">
|
||||
<option value="gemma2-9b-it">gemma2-9b-it</option>
|
||||
<option value="llama-3.3-70b-versatile">llama-3.3-70b-versatile</option>
|
||||
<option value="llama-3.1-8b-instant">llama-3.1-8b-instant</option>
|
||||
<option value="llama3-70b-8192">llama3-70b-8192</option>
|
||||
<option value="llama3-8b-8192">llama3-8b-8192</option>
|
||||
<option value="mixtral-8x7b-32768">mixtral-8x7b-32768</option>
|
||||
<optgroup label="Alibaba Cloud">
|
||||
<option value="qwen-2.5-32b">qwen-2.5-32b</option>
|
||||
<option value="qwen-2.5-coder-32b">qwen-2.5-coder-32b</option>
|
||||
</optgroup>
|
||||
<optgroup label="Preview Models">
|
||||
<optgroup label="DeepSeek / Alibaba Cloud">
|
||||
<option value="deepseek-r1-distill-qwen-32b">deepseek-r1-distill-qwen-32b</option>
|
||||
</optgroup>
|
||||
<optgroup label="DeepSeek / Meta">
|
||||
<option value="deepseek-r1-distill-llama-70b">deepseek-r1-distill-llama-70b</option>
|
||||
<option value="llama-3.3-70b-specdec">llama-3.3-70b-specdec</option>
|
||||
<option value="llama-3.2-1b-preview">llama-3.2-1b-preview</option>
|
||||
<option value="llama-3.2-3b-preview">llama-3.2-3b-preview</option>
|
||||
<option value="llama-3.2-11b-vision-preview">llama-3.2-11b-vision-preview</option>
|
||||
<option value="llama-3.2-90b-vision-preview">llama-3.2-90b-vision-preview</option>
|
||||
</optgroup>
|
||||
<optgroup label="Google">
|
||||
<option value="gemma2-9b-it">gemma2-9b-it</option>
|
||||
</optgroup>
|
||||
<optgroup label="Meta">
|
||||
<option value="llama-3.1-8b-instant">llama-3.1-8b-instant </option>
|
||||
<option value="llama-3.2-11b-vision-preview">llama-3.2-11b-vision-preview </option>
|
||||
<option value="llama-3.2-1b-preview">llama-3.2-1b-preview </option>
|
||||
<option value="llama-3.2-3b-preview">llama-3.2-3b-preview </option>
|
||||
<option value="llama-3.2-90b-vision-preview">llama-3.2-90b-vision-preview </option>
|
||||
<option value="llama-3.3-70b-specdec">llama-3.3-70b-specdec </option>
|
||||
<option value="llama-3.3-70b-versatile">llama-3.3-70b-versatile </option>
|
||||
<option value="llama-guard-3-8b">llama-guard-3-8b </option>
|
||||
<option value="llama3-70b-8192">llama3-70b-8192 </option>
|
||||
<option value="llama3-8b-8192">llama3-8b-8192 </option>
|
||||
</optgroup>
|
||||
<optgroup label="Mistral AI">
|
||||
<option value="mixtral-8x7b-32768">mixtral-8x7b-32768</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
</div>
|
||||
@@ -3253,6 +3265,10 @@
|
||||
<option value="sonar">sonar</option>
|
||||
<option value="sonar-pro">sonar-pro</option>
|
||||
<option value="sonar-reasoning">sonar-reasoning</option>
|
||||
<option value="sonar-reasoning-pro">sonar-reasoning-pro</option>
|
||||
</optgroup>
|
||||
<optgroup label="Offline Models">
|
||||
<option value="r1-1776">r1-1776</option>
|
||||
</optgroup>
|
||||
<optgroup label="Deprecated Models">
|
||||
<!-- These are scheduled for deprecation after 2/22/2025 -->
|
||||
|
@@ -24,10 +24,22 @@ if (typeof Array.prototype.indexOf === 'function') {
|
||||
|
||||
|
||||
/* Polyfill EventEmitter. */
|
||||
var EventEmitter = function () {
|
||||
/**
|
||||
* Creates an event emitter.
|
||||
* @param {string[]} autoFireAfterEmit Auto-fire event names
|
||||
*/
|
||||
var EventEmitter = function (autoFireAfterEmit = []) {
|
||||
this.events = {};
|
||||
this.autoFireLastArgs = new Map();
|
||||
this.autoFireAfterEmit = new Set(autoFireAfterEmit);
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds a listener to an event.
|
||||
* @param {string} event Event name
|
||||
* @param {function} listener Event listener
|
||||
* @returns
|
||||
*/
|
||||
EventEmitter.prototype.on = function (event, listener) {
|
||||
// Unknown event used by external libraries?
|
||||
if (event === undefined) {
|
||||
@@ -40,6 +52,10 @@ EventEmitter.prototype.on = function (event, listener) {
|
||||
}
|
||||
|
||||
this.events[event].push(listener);
|
||||
|
||||
if (this.autoFireAfterEmit.has(event) && this.autoFireLastArgs.has(event)) {
|
||||
listener.apply(this, this.autoFireLastArgs.get(event));
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -60,6 +76,10 @@ EventEmitter.prototype.makeLast = function (event, listener) {
|
||||
}
|
||||
|
||||
events.push(listener);
|
||||
|
||||
if (this.autoFireAfterEmit.has(event) && this.autoFireLastArgs.has(event)) {
|
||||
listener.apply(this, this.autoFireLastArgs.get(event));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -80,8 +100,17 @@ EventEmitter.prototype.makeFirst = function (event, listener) {
|
||||
}
|
||||
|
||||
events.unshift(listener);
|
||||
|
||||
if (this.autoFireAfterEmit.has(event) && this.autoFireLastArgs.has(event)) {
|
||||
listener.apply(this, this.autoFireLastArgs.get(event));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a listener from an event.
|
||||
* @param {string} event Event name
|
||||
* @param {function} listener Event listener
|
||||
*/
|
||||
EventEmitter.prototype.removeListener = function (event, listener) {
|
||||
var idx;
|
||||
|
||||
@@ -94,6 +123,10 @@ EventEmitter.prototype.removeListener = function (event, listener) {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Emits an event with optional arguments.
|
||||
* @param {string} event Event name
|
||||
*/
|
||||
EventEmitter.prototype.emit = async function (event) {
|
||||
let args = [].slice.call(arguments, 1);
|
||||
if (localStorage.getItem('eventTracing') === 'true') {
|
||||
@@ -118,6 +151,10 @@ EventEmitter.prototype.emit = async function (event) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.autoFireAfterEmit.has(event)) {
|
||||
this.autoFireLastArgs.set(event, args);
|
||||
}
|
||||
};
|
||||
|
||||
EventEmitter.prototype.emitAndWait = function (event) {
|
||||
@@ -144,10 +181,14 @@ EventEmitter.prototype.emitAndWait = function (event) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.autoFireAfterEmit.has(event)) {
|
||||
this.autoFireLastArgs.set(event, args);
|
||||
}
|
||||
};
|
||||
|
||||
EventEmitter.prototype.once = function (event, listener) {
|
||||
this.on(event, function g () {
|
||||
this.on(event, function g() {
|
||||
this.removeListener(event, g);
|
||||
listener.apply(this, arguments);
|
||||
});
|
||||
|
@@ -1602,7 +1602,6 @@
|
||||
"Character Expressions": "Expressions de personnages",
|
||||
"Translate text to English before classification": "Traduire le texte en anglais avant de le classer",
|
||||
"Show default images (emojis) if sprite missing": "Afficher les images par défaut (emojis) si le sprite est manquant",
|
||||
"Image Type - talkinghead (extras)": "Type d'image - talkinghead (extras)",
|
||||
"Classifier API": "API de classification",
|
||||
"Select the API for classifying expressions.": "Sélectionnez l'API pour classer les expressions.",
|
||||
"Main API": "API principale",
|
||||
|
@@ -1467,7 +1467,6 @@
|
||||
"menu within": "내의 메뉴",
|
||||
"Translate text to English before classification": "분류 전에 텍스트를 영어로 번역합니다.",
|
||||
"Show default images (emojis) if sprite missing": "해당하는 스프라이트가 없으면 기본 이미지 (이모지들)을 표시합니다.",
|
||||
"Image Type - talkinghead (extras)": "이미지 유형 - 토킹 헤드 (부가 사항)",
|
||||
"Classifier API": "분류를 위한 API",
|
||||
"Select the API for classifying expressions.": "감정 이미지들을 분류할 API를 선택하세요.",
|
||||
"Local": "로컬",
|
||||
|
@@ -1349,7 +1349,6 @@
|
||||
"Character Expressions": "角色表情",
|
||||
"Translate text to English before classification": "分类之前将文本翻译成英文",
|
||||
"Show default images (emojis) if sprite missing": "如果表情包缺失,则显示默认图像(表情符号)",
|
||||
"Image Type - talkinghead (extras)": "图像类型 - 说话头像(附加内容)",
|
||||
"Classifier API": "分类器 API",
|
||||
"Select the API for classifying expressions.": "选择用于对表达式进行分类的API。",
|
||||
"Main API": "主要 API",
|
||||
|
@@ -1653,7 +1653,6 @@
|
||||
"HuggingFace Token": "HuggingFace 符元",
|
||||
"Image Captioning": "圖片註解",
|
||||
"Generate Caption": "產生圖片註解",
|
||||
"Image Type - talkinghead (extras)": "圖片類型 - talkinghead(額外選項)",
|
||||
"Injection Position": "插入位置",
|
||||
"Injection position. Relative (to other prompts in prompt manager) or In-chat @ Depth.": "插入位置(與提示詞管理器中的其他提示相比)或聊天中的深度位置。",
|
||||
"Injection Template": "插入範本",
|
||||
|
@@ -512,7 +512,7 @@ export const event_types = {
|
||||
TOOL_CALLS_RENDERED: 'tool_calls_rendered',
|
||||
};
|
||||
|
||||
export const eventSource = new EventEmitter();
|
||||
export const eventSource = new EventEmitter([event_types.APP_READY]);
|
||||
|
||||
eventSource.on(event_types.CHAT_CHANGED, processChatSlashCommands);
|
||||
|
||||
|
@@ -1487,7 +1487,7 @@ jQuery(function () {
|
||||
...chat.filter(x => x?.extra?.type !== system_message_types.ASSISTANT_NOTE),
|
||||
];
|
||||
|
||||
download(JSON.stringify(chatToSave, null, 4), `Assistant - ${humanizedDateTime()}.json`, 'application/json');
|
||||
download(chatToSave.map((m) => JSON.stringify(m)).join('\n'), `Assistant - ${humanizedDateTime()}.jsonl`, 'application/json');
|
||||
});
|
||||
|
||||
// Do not change. #attachFile is added by extension.
|
||||
|
@@ -154,8 +154,18 @@ export const extension_settings = {
|
||||
refine_mode: false,
|
||||
},
|
||||
expressions: {
|
||||
/** @type {number} see `EXPRESSION_API` */
|
||||
api: undefined,
|
||||
/** @type {string[]} */
|
||||
custom: [],
|
||||
showDefault: false,
|
||||
translate: false,
|
||||
/** @type {string} */
|
||||
fallback_expression: undefined,
|
||||
/** @type {string} */
|
||||
llmPrompt: undefined,
|
||||
allowMultiple: true,
|
||||
rerollIfSame: false,
|
||||
},
|
||||
connectionManager: {
|
||||
selectedProfile: '',
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,5 @@
|
||||
<div id="{{item}}" class="expression_list_item">
|
||||
{{#each images}}
|
||||
<div class="expression_list_item interactable" data-expression="{{../expression}}" data-expression-type="{{this.type}}" data-filename="{{this.fileName}}">
|
||||
<div class="expression_list_buttons">
|
||||
<div class="menu_button expression_list_upload" title="Upload image">
|
||||
<i class="fa-solid fa-upload"></i>
|
||||
@@ -7,11 +8,14 @@
|
||||
<i class="fa-solid fa-trash"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="expression_list_title {{textClass}}">
|
||||
<span>{{item}}</span>
|
||||
{{#if isCustom}}
|
||||
<div class="expression_list_title">
|
||||
<span>{{../expression}}</span>
|
||||
{{#if ../isCustom}}
|
||||
<small class="expression_list_custom">(custom)</small>
|
||||
{{/if}}
|
||||
</div>
|
||||
<img class="expression_list_image" src="{{imageSrc}}" />
|
||||
<div class="expression_list_image_container" title="{{this.title}}">
|
||||
<img class="expression_list_image" src="{{this.imageSrc}}" alt="{{this.title}}" data-epression="{{../expression}}" />
|
||||
</div>
|
||||
</div>
|
||||
{{/each}}
|
||||
|
@@ -6,17 +6,17 @@
|
||||
</div>
|
||||
|
||||
<div class="inline-drawer-content">
|
||||
<label class="checkbox_label" for="expression_translate" title="Use the selected API from Chat Translation extension settings.">
|
||||
<label class="checkbox_label" for="expression_translate" title="Use the selected API from Chat Translation extension settings." data-i18n="[title]Use the selected API from Chat Translation extension settings.">
|
||||
<input id="expression_translate" type="checkbox">
|
||||
<span data-i18n="Translate text to English before classification">Translate text to English before classification</span>
|
||||
</label>
|
||||
<label class="checkbox_label" for="expressions_show_default">
|
||||
<input id="expressions_show_default" type="checkbox">
|
||||
<span data-i18n="Show default images (emojis) if sprite missing">Show default images (emojis) if sprite missing</span>
|
||||
<label class="checkbox_label" for="expressions_allow_multiple" title="A single expression can have multiple sprites. Whenever the expression is chosen, a random sprite for this expression will be selected." data-i18n="[title]A single expression can have multiple sprites. Whenever the expression is chosen, a random sprite for this expression will be selected.">
|
||||
<input id="expressions_allow_multiple" type="checkbox">
|
||||
<span data-i18n="Allow multiple sprites per expression">Allow multiple sprites per expression</span>
|
||||
</label>
|
||||
<label id="image_type_block" class="checkbox_label" for="image_type_toggle">
|
||||
<input id="image_type_toggle" type="checkbox">
|
||||
<span data-i18n="Image Type - talkinghead (extras)">Image Type - talkinghead (extras)</span>
|
||||
<label class="checkbox_label" for="expressions_reroll_if_same" title="If the same expression is used again, re-roll the sprite. This only applies to expressions that have multiple available sprites assigned." data-i18n="[title]If the same expression is used again, re-roll the sprite. This only applies to expressions that have multiple available sprites assigned.">
|
||||
<input id="expressions_reroll_if_same" type="checkbox">
|
||||
<span data-i18n="Re-roll if same expression is used again">Re-roll if same sprite is used again</span>
|
||||
</label>
|
||||
<div class="expression_api_block m-b-1 m-t-1">
|
||||
<label for="expression_api" data-i18n="Classifier API">Classifier API</label>
|
||||
@@ -75,8 +75,20 @@
|
||||
<span data-i18n="Remove all image overrides">Remove all image overrides</span>
|
||||
</div>
|
||||
</div>
|
||||
<p class="hint"><b data-i18n="Hint:">Hint:</b> <i><span data-i18n="Create new folder in the _space">Create new folder in the </span><b>/characters/</b> <span data-i18n="folder of your user data directory and name it as the name of the character.">folder of your user data directory and name it as the name of the character.</span>
|
||||
<span data-i18n="Put images with expressions there. File names should follow the pattern:">Put images with expressions there. File names should follow the pattern: </span><tt data-i18n="expression_label_pattern">[expression_label].[image_format]</tt></i></p>
|
||||
<p class="hint">
|
||||
<b data-i18n="Hint:">Hint:</b>
|
||||
<i>
|
||||
<span data-i18n="Create new folder in the _space">Create new folder in the </span><b>/characters/</b> <span data-i18n="folder of your user data directory and name it as the name of the character.">folder of your user data directory and name it as the name of the character.</span>
|
||||
<span data-i18n="Put images with expressions there. File names should follow the pattern:">Put images with expressions there. File names should follow the pattern: </span><tt data-i18n="expression_label_pattern">[expression_label].[image_format]</tt>
|
||||
</i>
|
||||
</p>
|
||||
<p>
|
||||
<i>
|
||||
<span>In case of multiple files per expression, file names can contain a suffix, either separated by a dot or a
|
||||
dash.
|
||||
Examples: </span><tt>joy.png</tt>, <tt>joy-1.png</tt>, <tt>joy.expressive.png</tt>
|
||||
</i>
|
||||
</p>
|
||||
<h3 id="image_list_header">
|
||||
<strong data-i18n="Sprite set:">Sprite set:</strong> <span id="image_list_header_name"></span>
|
||||
</h3>
|
||||
|
@@ -111,6 +111,10 @@ img.expression.default {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.expression_list_image_container {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.expression_list_title {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
@@ -126,6 +130,9 @@ img.expression.default {
|
||||
flex-direction: column;
|
||||
line-height: 1;
|
||||
}
|
||||
.expression_list_custom {
|
||||
font-size: 0.66rem;
|
||||
}
|
||||
|
||||
.expression_list_buttons {
|
||||
position: absolute;
|
||||
@@ -162,11 +169,24 @@ img.expression.default {
|
||||
row-gap: 1rem;
|
||||
}
|
||||
|
||||
#image_list .success {
|
||||
#image_list .expression_list_item[data-expression-type="success"] .expression_list_title {
|
||||
color: green;
|
||||
}
|
||||
|
||||
#image_list .failure {
|
||||
#image_list .expression_list_item[data-expression-type="additional"] .expression_list_title {
|
||||
color: darkolivegreen;
|
||||
}
|
||||
#image_list .expression_list_item[data-expression-type="additional"] .expression_list_title::before {
|
||||
content: '➕';
|
||||
position: absolute;
|
||||
top: -7px;
|
||||
left: -9px;
|
||||
font-size: 14px;
|
||||
color: transparent;
|
||||
text-shadow: 0 0 0 darkolivegreen;
|
||||
}
|
||||
|
||||
#image_list .expression_list_item[data-expression-type="failure"] .expression_list_title {
|
||||
color: red;
|
||||
}
|
||||
|
||||
@@ -189,3 +209,12 @@ img.expression.default {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
#expressions_container:has(#expressions_allow_multiple:not(:checked)) #image_list .expression_list_item[data-expression-type="additional"],
|
||||
#expressions_container:has(#expressions_allow_multiple:not(:checked)) label[for="expressions_reroll_if_same"] {
|
||||
opacity: 0.3;
|
||||
transition: opacity var(--animation-duration) ease;
|
||||
}
|
||||
#expressions_container:has(#expressions_allow_multiple:not(:checked)) #image_list .expression_list_item[data-expression-type="additional"]:hover,
|
||||
#expressions_container:has(#expressions_allow_multiple:not(:checked)) #image_list .expression_list_item[data-expression-type="additional"]:focus {
|
||||
opacity: unset;
|
||||
}
|
||||
|
@@ -0,0 +1,12 @@
|
||||
<div class="m-b-1" data-i18n="upload_expression_request">Please enter a name for the sprite (without extension).</div>
|
||||
<div class="m-b-1" data-i18n="upload_expression_naming_1">
|
||||
Sprite names must follow the naming schema for the selected expression: {{expression}}
|
||||
</div>
|
||||
<div data-i18n="upload_expression_naming_2">
|
||||
For multiple expressions, the name must follow the expression name and a valid suffix. Allowed separators are '-' or dot '.'.
|
||||
</div>
|
||||
<span class="m-b-1" data-i18n="Examples:">Examples:</span> <tt>{{expression}}.png</tt>, <tt>{{expression}}-1.png</tt>, <tt>{{expression}}.expressive.png</tt>
|
||||
{{#if clickedFileName}}
|
||||
<div class="m-t-1" data-i18n="upload_expression_replace">Click 'Replace' to replace the existing expression:</div>
|
||||
<tt>{{clickedFileName}}</tt>
|
||||
{{/if}}
|
@@ -605,7 +605,7 @@ const handleOutgoingMessage = createEventHandler(translateOutgoingMessage, () =>
|
||||
const handleImpersonateReady = createEventHandler(translateImpersonate, () => shouldTranslate(incomingTypes));
|
||||
const handleMessageEdit = createEventHandler(translateMessageEdit, () => true);
|
||||
|
||||
window['translate'] = translate;
|
||||
globalThis.translate = translate;
|
||||
|
||||
jQuery(async () => {
|
||||
const html = await renderExtensionTemplateAsync('translate', 'index');
|
||||
|
@@ -27,14 +27,12 @@ import { SlashCommandEnumValue, enumTypes } from '../../slash-commands/SlashComm
|
||||
import { enumIcons } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';
|
||||
import { POPUP_TYPE, callGenericPopup } from '../../popup.js';
|
||||
import { GoogleTranslateTtsProvider } from './google-translate.js';
|
||||
export { talkingAnimation };
|
||||
|
||||
const UPDATE_INTERVAL = 1000;
|
||||
const wrapper = new ModuleWorkerWrapper(moduleWorker);
|
||||
|
||||
let voiceMapEntries = [];
|
||||
let voiceMap = {}; // {charName:voiceid, charName2:voiceid2}
|
||||
let talkingHeadState = false;
|
||||
let lastChatId = null;
|
||||
let lastMessage = null;
|
||||
let lastMessageHash = null;
|
||||
@@ -166,27 +164,6 @@ async function moduleWorker() {
|
||||
updateUiAudioPlayState();
|
||||
}
|
||||
|
||||
function talkingAnimation(switchValue) {
|
||||
if (!modules.includes('talkinghead')) {
|
||||
console.debug('Talking Animation module not loaded');
|
||||
return;
|
||||
}
|
||||
|
||||
const apiUrl = getApiUrl();
|
||||
const animationType = switchValue ? 'start' : 'stop';
|
||||
|
||||
if (switchValue !== talkingHeadState) {
|
||||
try {
|
||||
console.log(animationType + ' Talking Animation');
|
||||
doExtrasFetch(`${apiUrl}/api/talkinghead/${animationType}_talking`);
|
||||
talkingHeadState = switchValue;
|
||||
} catch (error) {
|
||||
// Handle the error here or simply ignore it to prevent logging
|
||||
}
|
||||
}
|
||||
updateUiAudioPlayState();
|
||||
}
|
||||
|
||||
function resetTtsPlayback() {
|
||||
// Stop system TTS utterance
|
||||
cancelTtsPlay();
|
||||
@@ -378,7 +355,6 @@ function onAudioControlClicked() {
|
||||
// Not pausing, doing a full stop to anything TTS is doing. Better UX as pause is not as useful
|
||||
if (!audioElement.paused || isTtsProcessing()) {
|
||||
resetTtsPlayback();
|
||||
talkingAnimation(false);
|
||||
} else {
|
||||
// Default play behavior if not processing or playing is to play the last message.
|
||||
processAndQueueTtsMessage(context.chat[context.chat.length - 1]);
|
||||
@@ -405,7 +381,6 @@ function addAudioControl() {
|
||||
function completeCurrentAudioJob() {
|
||||
audioQueueProcessorReady = true;
|
||||
currentAudioJob = null;
|
||||
talkingAnimation(false); //stop lip animation
|
||||
// updateUiPlayState();
|
||||
wrapper.update();
|
||||
}
|
||||
@@ -436,7 +411,6 @@ async function processAudioJobQueue() {
|
||||
audioQueueProcessorReady = false;
|
||||
currentAudioJob = audioJobQueue.shift();
|
||||
playAudioData(currentAudioJob);
|
||||
talkingAnimation(true);
|
||||
} catch (error) {
|
||||
toastr.error(error.toString());
|
||||
console.error(error);
|
||||
|
@@ -1,6 +1,5 @@
|
||||
import { isMobile } from '../../RossAscends-mods.js';
|
||||
import { getPreviewString } from './index.js';
|
||||
import { talkingAnimation } from './index.js';
|
||||
import { saveTtsProviderSettings } from './index.js';
|
||||
export { SystemTtsProvider };
|
||||
|
||||
@@ -70,7 +69,6 @@ var speechUtteranceChunker = function (utt, settings, callback) {
|
||||
//placing the speak invocation inside a callback fixes ordering and onend issues.
|
||||
setTimeout(function () {
|
||||
speechSynthesis.speak(newUtt);
|
||||
talkingAnimation(true);
|
||||
}, 0);
|
||||
};
|
||||
|
||||
@@ -240,7 +238,6 @@ class SystemTtsProvider {
|
||||
//some code to execute when done
|
||||
resolve(silence);
|
||||
console.log('System TTS done');
|
||||
talkingAnimation(false);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@@ -561,9 +561,9 @@ async function retrieveFileChunks(queryText, collectionId) {
|
||||
*/
|
||||
async function vectorizeFile(fileText, fileName, collectionId, chunkSize, overlapPercent) {
|
||||
try {
|
||||
if (settings.translate_files && typeof window['translate'] === 'function') {
|
||||
if (settings.translate_files && typeof globalThis.translate === 'function') {
|
||||
console.log(`Vectors: Translating file ${fileName} to English...`);
|
||||
const translatedText = await window['translate'](fileText, 'en');
|
||||
const translatedText = await globalThis.translate(fileText, 'en');
|
||||
fileText = translatedText;
|
||||
}
|
||||
|
||||
|
@@ -2167,6 +2167,14 @@ function getStreamingReply(data, state) {
|
||||
state.reasoning += (data.choices?.filter(x => x?.delta?.reasoning)?.[0]?.delta?.reasoning || '');
|
||||
}
|
||||
return data.choices?.[0]?.delta?.content ?? data.choices?.[0]?.message?.content ?? data.choices?.[0]?.text ?? '';
|
||||
} else if (oai_settings.chat_completion_source === chat_completion_sources.CUSTOM) {
|
||||
if (oai_settings.show_thoughts) {
|
||||
state.reasoning +=
|
||||
data.choices?.filter(x => x?.delta?.reasoning_content)?.[0]?.delta?.reasoning_content ??
|
||||
data.choices?.filter(x => x?.delta?.reasoning)?.[0]?.delta?.reasoning ??
|
||||
'';
|
||||
}
|
||||
return data.choices?.[0]?.delta?.content ?? data.choices?.[0]?.message?.content ?? data.choices?.[0]?.text ?? '';
|
||||
} else {
|
||||
return data.choices?.[0]?.delta?.content ?? data.choices?.[0]?.message?.content ?? data.choices?.[0]?.text ?? '';
|
||||
}
|
||||
@@ -4107,6 +4115,40 @@ function getMaxContextWindowAI(value) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the maximum context size for the Groq model
|
||||
* @param {string} model Model identifier
|
||||
* @param {boolean} isUnlocked Whether context limits are unlocked
|
||||
* @returns {number} Maximum context size in tokens
|
||||
*/
|
||||
function getGroqMaxContext(model, isUnlocked) {
|
||||
if (isUnlocked) {
|
||||
return unlocked_max;
|
||||
}
|
||||
|
||||
const contextMap = {
|
||||
'gemma2-9b-it': max_8k,
|
||||
'llama-3.3-70b-versatile': max_128k,
|
||||
'llama-3.1-8b-instant': max_128k,
|
||||
'llama3-70b-8192': max_8k,
|
||||
'llama3-8b-8192': max_8k,
|
||||
'llama-guard-3-8b': max_8k,
|
||||
'mixtral-8x7b-32768': max_32k,
|
||||
'deepseek-r1-distill-llama-70b': max_128k,
|
||||
'llama-3.3-70b-specdec': max_8k,
|
||||
'llama-3.2-1b-preview': max_128k,
|
||||
'llama-3.2-3b-preview': max_128k,
|
||||
'llama-3.2-11b-vision-preview': max_128k,
|
||||
'llama-3.2-90b-vision-preview': max_128k,
|
||||
'qwen-2.5-32b': max_128k,
|
||||
'deepseek-r1-distill-qwen-32b': max_128k,
|
||||
'deepseek-r1-distill-llama-70b-specdec': max_128k,
|
||||
};
|
||||
|
||||
// Return context size if model found, otherwise default to 128k
|
||||
return Object.entries(contextMap).find(([key]) => model.includes(key))?.[1] || max_128k;
|
||||
}
|
||||
|
||||
async function onModelChange() {
|
||||
biasCache = undefined;
|
||||
let value = String($(this).val() || '');
|
||||
@@ -4387,7 +4429,7 @@ async function onModelChange() {
|
||||
if (oai_settings.max_context_unlocked) {
|
||||
$('#openai_max_context').attr('max', unlocked_max);
|
||||
}
|
||||
else if (['sonar', 'sonar-reasoning'].includes(oai_settings.perplexity_model)) {
|
||||
else if (['sonar', 'sonar-reasoning', 'sonar-reasoning-pro', 'r1-1776'].includes(oai_settings.perplexity_model)) {
|
||||
$('#openai_max_context').attr('max', 127000);
|
||||
}
|
||||
else if (['sonar-pro'].includes(oai_settings.perplexity_model)) {
|
||||
@@ -4408,33 +4450,8 @@ async function onModelChange() {
|
||||
}
|
||||
|
||||
if (oai_settings.chat_completion_source == chat_completion_sources.GROQ) {
|
||||
if (oai_settings.max_context_unlocked) {
|
||||
$('#openai_max_context').attr('max', unlocked_max);
|
||||
} else if (oai_settings.groq_model.includes('gemma2-9b-it')) {
|
||||
$('#openai_max_context').attr('max', max_8k);
|
||||
} else if (oai_settings.groq_model.includes('llama-3.3-70b-versatile')) {
|
||||
$('#openai_max_context').attr('max', max_128k);
|
||||
} else if (oai_settings.groq_model.includes('llama-3.1-8b-instant')) {
|
||||
$('#openai_max_context').attr('max', max_128k);
|
||||
} else if (oai_settings.groq_model.includes('llama3-70b-8192')) {
|
||||
$('#openai_max_context').attr('max', max_8k);
|
||||
} else if (oai_settings.groq_model.includes('llama3-8b-8192')) {
|
||||
$('#openai_max_context').attr('max', max_8k);
|
||||
} else if (oai_settings.groq_model.includes('mixtral-8x7b-32768')) {
|
||||
$('#openai_max_context').attr('max', max_32k);
|
||||
} else if (oai_settings.groq_model.includes('deepseek-r1-distill-llama-70b')) {
|
||||
$('#openai_max_context').attr('max', max_128k);
|
||||
} else if (oai_settings.groq_model.includes('llama-3.3-70b-specdec')) {
|
||||
$('#openai_max_context').attr('max', max_8k);
|
||||
} else if (oai_settings.groq_model.includes('llama-3.2-1b-preview')) {
|
||||
$('#openai_max_context').attr('max', max_128k);
|
||||
} else if (oai_settings.groq_model.includes('llama-3.2-3b-preview')) {
|
||||
$('#openai_max_context').attr('max', max_128k);
|
||||
} else if (oai_settings.groq_model.includes('llama-3.2-11b-vision-preview')) {
|
||||
$('#openai_max_context').attr('max', max_128k);
|
||||
} else if (oai_settings.groq_model.includes('llama-3.2-90b-vision-preview')) {
|
||||
$('#openai_max_context').attr('max', max_128k);
|
||||
}
|
||||
const maxContext = getGroqMaxContext(oai_settings.groq_model, oai_settings.max_context_unlocked);
|
||||
$('#openai_max_context').attr('max', maxContext);
|
||||
oai_settings.openai_max_context = Math.min(Number($('#openai_max_context').attr('max')), oai_settings.openai_max_context);
|
||||
$('#openai_max_context').val(oai_settings.openai_max_context).trigger('input');
|
||||
oai_settings.temp_openai = Math.min(oai_max_temp, oai_settings.temp_openai);
|
||||
|
@@ -1845,14 +1845,15 @@ async function loadContextSettings() {
|
||||
|
||||
/**
|
||||
* Common function to perform fuzzy search with optional caching
|
||||
* @template T
|
||||
* @param {string} type - Type of search from fuzzySearchCategories
|
||||
* @param {any[]} data - Data array to search in
|
||||
* @param {Array<{name: string, weight: number, getFn?: (obj: any) => string}>} keys - Fuse.js keys configuration
|
||||
* @param {T[]} data - Data array to search in
|
||||
* @param {Array<{name: string, weight: number, getFn?: (obj: T) => string}>} keys - Fuse.js keys configuration
|
||||
* @param {string} searchValue - The search term
|
||||
* @param {Object.<string, { resultMap: Map<string, any> }>} [fuzzySearchCaches=null] - Optional fuzzy search caches
|
||||
* @returns {import('fuse.js').FuseResult<any>[]} Results as items with their score
|
||||
* @returns {import('fuse.js').FuseResult<T>[]} Results as items with their score
|
||||
*/
|
||||
function performFuzzySearch(type, data, keys, searchValue, fuzzySearchCaches = null) {
|
||||
export function performFuzzySearch(type, data, keys, searchValue, fuzzySearchCaches = null) {
|
||||
// Check cache if provided
|
||||
if (fuzzySearchCaches) {
|
||||
const cache = fuzzySearchCaches[type];
|
||||
|
@@ -76,6 +76,11 @@ export function extractReasoningFromData(data) {
|
||||
return data?.choices?.[0]?.message?.reasoning ?? '';
|
||||
case chat_completion_sources.MAKERSUITE:
|
||||
return data?.responseContent?.parts?.filter(part => part.thought)?.map(part => part.text)?.join('\n\n') ?? '';
|
||||
case chat_completion_sources.CUSTOM: {
|
||||
return data?.choices?.[0]?.message?.reasoning_content
|
||||
?? data?.choices?.[0]?.message?.reasoning
|
||||
?? '';
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -338,14 +343,15 @@ export class ReasoningHandler {
|
||||
return mesChanged;
|
||||
}
|
||||
|
||||
if (this.state === ReasoningState.None) {
|
||||
if (this.state === ReasoningState.None || this.#isHiddenReasoningModel) {
|
||||
// If streamed message starts with the opening, cut it out and put all inside reasoning
|
||||
if (message.mes.startsWith(power_user.reasoning.prefix) && message.mes.length > power_user.reasoning.prefix.length) {
|
||||
this.#isParsingReasoning = true;
|
||||
|
||||
// Manually set starting state here, as we might already have received the ending suffix
|
||||
this.state = ReasoningState.Thinking;
|
||||
this.startTime = this.initialTime;
|
||||
this.startTime = this.startTime ?? this.initialTime;
|
||||
this.endTime = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -679,7 +679,7 @@ export function getTokenizerModel() {
|
||||
}
|
||||
|
||||
if (oai_settings.chat_completion_source === chat_completion_sources.PERPLEXITY) {
|
||||
if (oai_settings.perplexity_model.includes('sonar-reasoning')) {
|
||||
if (oai_settings.perplexity_model.includes('sonar-reasoning') || oai_settings.perplexity_model.includes('r1-1776')) {
|
||||
return deepseekTokenizer;
|
||||
}
|
||||
if (oai_settings.perplexity_model.includes('llama-3') || oai_settings.perplexity_model.includes('llama3')) {
|
||||
@@ -694,6 +694,9 @@ export function getTokenizerModel() {
|
||||
}
|
||||
|
||||
if (oai_settings.chat_completion_source === chat_completion_sources.GROQ) {
|
||||
if (oai_settings.groq_model.includes('qwen')) {
|
||||
return qwen2Tokenizer;
|
||||
}
|
||||
if (oai_settings.groq_model.includes('llama-3') || oai_settings.groq_model.includes('llama3')) {
|
||||
return llama3Tokenizer;
|
||||
}
|
||||
|
@@ -55,6 +55,10 @@
|
||||
--interactable-outline-color: var(--white100);
|
||||
--interactable-outline-color-faint: var(--white20a);
|
||||
|
||||
--reasoning-body-color: var(--SmartThemeEmColor);
|
||||
--reasoning-em-color: color-mix(in srgb, var(--SmartThemeEmColor) 67%, var(--SmartThemeBlurTintColor) 33%);
|
||||
--reasoning-saturation: 0.5;
|
||||
|
||||
|
||||
/*Default Theme, will be changed by ToolCool Color Picker*/
|
||||
--SmartThemeBodyColor: rgb(220, 220, 210);
|
||||
@@ -348,13 +352,13 @@ input[type='checkbox']:focus-visible {
|
||||
|
||||
.mes_reasoning {
|
||||
display: block;
|
||||
border-left: 2px solid var(--SmartThemeEmColor);
|
||||
border-left: 2px solid var(--reasoning-body-color);
|
||||
border-radius: 2px;
|
||||
padding: 5px;
|
||||
padding-left: 14px;
|
||||
margin-bottom: 0.5em;
|
||||
overflow-y: auto;
|
||||
color: var(--SmartThemeEmColor);
|
||||
color: hsl(from var(--reasoning-body-color) h calc(s * var(--reasoning-saturation)) l);
|
||||
}
|
||||
|
||||
.mes_reasoning_details {
|
||||
@@ -374,18 +378,6 @@ input[type='checkbox']:focus-visible {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.mes_reasoning em,
|
||||
.mes_reasoning i,
|
||||
.mes_reasoning u,
|
||||
.mes_reasoning q,
|
||||
.mes_reasoning blockquote {
|
||||
filter: saturate(0.5);
|
||||
}
|
||||
|
||||
.mes_reasoning_details .mes_reasoning em {
|
||||
color: color-mix(in srgb, var(--SmartThemeEmColor) 67%, var(--SmartThemeBlurTintColor) 33%);
|
||||
}
|
||||
|
||||
.mes_reasoning_header_block {
|
||||
flex-grow: 1;
|
||||
}
|
||||
@@ -461,26 +453,36 @@ input[type='checkbox']:focus-visible {
|
||||
}
|
||||
|
||||
.mes_text i,
|
||||
.mes_text em,
|
||||
.mes_text em {
|
||||
color: var(--SmartThemeEmColor);
|
||||
}
|
||||
.mes_reasoning i,
|
||||
.mes_reasoning em {
|
||||
color: var(--SmartThemeEmColor);
|
||||
color: hsl(from var(--reasoning-em-color) h calc(s * var(--reasoning-saturation)) l);
|
||||
}
|
||||
|
||||
.mes_text q i,
|
||||
.mes_text q em {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.mes_text u,
|
||||
.mes_reasoning u {
|
||||
color: var(--SmartThemeUnderlineColor);
|
||||
.mes_reasoning q i,
|
||||
.mes_reasoning q em {
|
||||
color: hsl(from var(--SmartThemeQuoteColor) h calc(s * var(--reasoning-saturation)) l);
|
||||
}
|
||||
|
||||
.mes_text q,
|
||||
.mes_reasoning q {
|
||||
.mes_text u {
|
||||
color: var(--SmartThemeUnderlineColor);
|
||||
}
|
||||
.mes_reasoning u {
|
||||
color: hsl(from var(--SmartThemeUnderlineColor) h calc(s * var(--reasoning-saturation)) l);
|
||||
}
|
||||
|
||||
.mes_text q {
|
||||
color: var(--SmartThemeQuoteColor);
|
||||
}
|
||||
.mes_reasoning q {
|
||||
color: hsl(from var(--SmartThemeQuoteColor) h calc(s * var(--reasoning-saturation)) l);
|
||||
}
|
||||
|
||||
.mes_text font[color] em,
|
||||
.mes_text font[color] i,
|
||||
|
16
server.js
16
server.js
@@ -57,7 +57,8 @@ import {
|
||||
|
||||
import getWebpackServeMiddleware from './src/middleware/webpack-serve.js';
|
||||
import basicAuthMiddleware from './src/middleware/basicAuth.js';
|
||||
import whitelistMiddleware, { getAccessLogPath, migrateAccessLog } from './src/middleware/whitelist.js';
|
||||
import whitelistMiddleware from './src/middleware/whitelist.js';
|
||||
import accessLoggerMiddleware, { getAccessLogPath, migrateAccessLog } from './src/middleware/accessLogWriter.js';
|
||||
import multerMonkeyPatch from './src/middleware/multerMonkeyPatch.js';
|
||||
import initRequestProxy from './src/request-proxy.js';
|
||||
import getCacheBusterMiddleware from './src/middleware/cacheBuster.js';
|
||||
@@ -243,7 +244,6 @@ const cliArguments = yargs(hideBin(process.argv))
|
||||
describe: 'Request proxy URL (HTTP or SOCKS protocols)',
|
||||
}).option('requestProxyBypass', {
|
||||
type: 'array',
|
||||
default: null,
|
||||
describe: 'Request proxy bypass list (space separated list of hosts)',
|
||||
}).parseSync();
|
||||
|
||||
@@ -340,9 +340,17 @@ const CORS = cors({
|
||||
|
||||
app.use(CORS);
|
||||
|
||||
if (listen && basicAuthMode) app.use(basicAuthMiddleware);
|
||||
if (listen && basicAuthMode) {
|
||||
app.use(basicAuthMiddleware);
|
||||
}
|
||||
|
||||
app.use(whitelistMiddleware(enableWhitelist, listen));
|
||||
if (enableWhitelist) {
|
||||
app.use(whitelistMiddleware());
|
||||
}
|
||||
|
||||
if (listen) {
|
||||
app.use(accessLoggerMiddleware());
|
||||
}
|
||||
|
||||
if (enableCorsProxy) {
|
||||
app.use(bodyParser.json({
|
||||
|
@@ -230,7 +230,7 @@ router.post('/version', jsonParser, async (request, response) => {
|
||||
} catch (error) {
|
||||
// it is not a git repo, or has no commits yet, or is a bare repo
|
||||
// not possible to update it, most likely can't get the branch name either
|
||||
return response.send({ currentBranchName: null, currentCommitHash, isUpToDate: true, remoteUrl: null });
|
||||
return response.send({ currentBranchName: '', currentCommitHash: '', isUpToDate: true, remoteUrl: '' });
|
||||
}
|
||||
|
||||
const currentBranch = await git.cwd(extensionPath).branch();
|
||||
|
@@ -125,8 +125,14 @@ router.get('/get', jsonParser, function (request, response) {
|
||||
.map((file) => {
|
||||
const pathToSprite = path.join(spritesPath, file);
|
||||
const mtime = fs.statSync(pathToSprite).mtime?.toISOString().replace(/[^0-9]/g, '').slice(0, 14);
|
||||
|
||||
const fileName = path.parse(pathToSprite).name.toLowerCase();
|
||||
// Extract the label from the filename via regex, which can be suffixed with a sub-name, either connected with a dash or a dot.
|
||||
// Examples: joy.png, joy-1.png, joy.expressive.png
|
||||
const label = fileName.match(/^(.+?)(?:[-\\.].*?)?$/)?.[1] ?? fileName;
|
||||
|
||||
return {
|
||||
label: path.parse(pathToSprite).name.toLowerCase(),
|
||||
label: label,
|
||||
path: `/characters/${name}/${file}` + (mtime ? `?t=${mtime}` : ''),
|
||||
};
|
||||
});
|
||||
@@ -141,8 +147,9 @@ router.get('/get', jsonParser, function (request, response) {
|
||||
router.post('/delete', jsonParser, async (request, response) => {
|
||||
const label = request.body.label;
|
||||
const name = request.body.name;
|
||||
const spriteName = request.body.spriteName || label;
|
||||
|
||||
if (!label || !name) {
|
||||
if (!spriteName || !name) {
|
||||
return response.sendStatus(400);
|
||||
}
|
||||
|
||||
@@ -158,7 +165,7 @@ router.post('/delete', jsonParser, async (request, response) => {
|
||||
|
||||
// Remove existing sprite with the same label
|
||||
for (const file of files) {
|
||||
if (path.parse(file).name === label) {
|
||||
if (path.parse(file).name === spriteName) {
|
||||
fs.rmSync(path.join(spritesPath, file));
|
||||
}
|
||||
}
|
||||
@@ -221,6 +228,7 @@ router.post('/upload', urlencodedParser, async (request, response) => {
|
||||
const file = request.file;
|
||||
const label = request.body.label;
|
||||
const name = request.body.name;
|
||||
const spriteName = request.body.spriteName || label;
|
||||
|
||||
if (!file || !label || !name) {
|
||||
return response.sendStatus(400);
|
||||
@@ -243,12 +251,12 @@ router.post('/upload', urlencodedParser, async (request, response) => {
|
||||
|
||||
// Remove existing sprite with the same label
|
||||
for (const file of files) {
|
||||
if (path.parse(file).name === label) {
|
||||
if (path.parse(file).name === spriteName) {
|
||||
fs.rmSync(path.join(spritesPath, file));
|
||||
}
|
||||
}
|
||||
|
||||
const filename = label + path.parse(file.originalname).ext;
|
||||
const filename = spriteName + path.parse(file.originalname).ext;
|
||||
const spritePath = path.join(file.destination, file.filename);
|
||||
const pathToFile = path.join(spritesPath, filename);
|
||||
// Copy uploaded file to sprites folder
|
||||
|
59
src/middleware/accessLogWriter.js
Normal file
59
src/middleware/accessLogWriter.js
Normal file
@@ -0,0 +1,59 @@
|
||||
import path from 'node:path';
|
||||
import fs from 'node:fs';
|
||||
import { getRealIpFromHeader } from '../express-common.js';
|
||||
import { color, getConfigValue } from '../util.js';
|
||||
|
||||
const enableAccessLog = getConfigValue('logging.enableAccessLog', true);
|
||||
|
||||
const knownIPs = new Set();
|
||||
|
||||
export const getAccessLogPath = () => path.join(globalThis.DATA_ROOT, 'access.log');
|
||||
|
||||
export function migrateAccessLog() {
|
||||
try {
|
||||
if (!fs.existsSync('access.log')) {
|
||||
return;
|
||||
}
|
||||
const logPath = getAccessLogPath();
|
||||
if (fs.existsSync(logPath)) {
|
||||
return;
|
||||
}
|
||||
fs.renameSync('access.log', logPath);
|
||||
console.log(color.yellow('Migrated access.log to new location:'), logPath);
|
||||
} catch (e) {
|
||||
console.error('Failed to migrate access log:', e);
|
||||
console.info('Please move access.log to the data directory manually.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates middleware for logging access and new connections
|
||||
* @returns {import('express').RequestHandler}
|
||||
*/
|
||||
export default function accessLoggerMiddleware() {
|
||||
return function (req, res, next) {
|
||||
const clientIp = getRealIpFromHeader(req);
|
||||
const userAgent = req.headers['user-agent'];
|
||||
|
||||
if (!knownIPs.has(clientIp)) {
|
||||
// Log new connection
|
||||
console.info(color.yellow(`New connection from ${clientIp}; User Agent: ${userAgent}\n`));
|
||||
knownIPs.add(clientIp);
|
||||
|
||||
// Write to access log if enabled
|
||||
if (enableAccessLog) {
|
||||
const logPath = getAccessLogPath();
|
||||
const timestamp = new Date().toISOString();
|
||||
const log = `${timestamp} ${clientIp} ${userAgent}\n`;
|
||||
|
||||
fs.appendFile(logPath, log, (err) => {
|
||||
if (err) {
|
||||
console.error('Failed to write access log:', err);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
}
|
@@ -10,9 +10,6 @@ import { color, getConfigValue, safeReadFileSync } from '../util.js';
|
||||
const whitelistPath = path.join(process.cwd(), './whitelist.txt');
|
||||
const enableForwardedWhitelist = getConfigValue('enableForwardedWhitelist', false, 'boolean');
|
||||
let whitelist = getConfigValue('whitelist', []);
|
||||
let knownIPs = new Set();
|
||||
|
||||
export const getAccessLogPath = () => path.join(globalThis.DATA_ROOT, 'access.log');
|
||||
|
||||
if (fs.existsSync(whitelistPath)) {
|
||||
try {
|
||||
@@ -48,67 +45,41 @@ function getForwardedIp(req) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function migrateAccessLog() {
|
||||
try {
|
||||
if (!fs.existsSync('access.log')) {
|
||||
return;
|
||||
}
|
||||
const logPath = getAccessLogPath();
|
||||
if (fs.existsSync(logPath)) {
|
||||
return;
|
||||
}
|
||||
fs.renameSync('access.log', logPath);
|
||||
console.log(color.yellow('Migrated access.log to new location:'), logPath);
|
||||
} catch (e) {
|
||||
console.error('Failed to migrate access log:', e);
|
||||
console.info('Please move access.log to the data directory manually.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a middleware function that checks if the client IP is in the whitelist.
|
||||
* @param {boolean} whitelistMode If whitelist mode is enabled via config or command line
|
||||
* @param {boolean} listen If listen mode is enabled via config or command line
|
||||
* @returns {import('express').RequestHandler} The middleware function
|
||||
*/
|
||||
export default function whitelistMiddleware(whitelistMode, listen) {
|
||||
export default function whitelistMiddleware() {
|
||||
const forbiddenWebpage = Handlebars.compile(
|
||||
safeReadFileSync('./public/error/forbidden-by-whitelist.html') ?? '',
|
||||
);
|
||||
|
||||
const noLogPaths = [
|
||||
'/favicon.ico',
|
||||
];
|
||||
|
||||
return function (req, res, next) {
|
||||
const clientIp = getIpFromRequest(req);
|
||||
const forwardedIp = getForwardedIp(req);
|
||||
const userAgent = req.headers['user-agent'];
|
||||
|
||||
if (listen && !knownIPs.has(clientIp)) {
|
||||
console.info(color.yellow(`New connection from ${clientIp}; User Agent: ${userAgent}\n`));
|
||||
knownIPs.add(clientIp);
|
||||
|
||||
// Write access log
|
||||
const logPath = getAccessLogPath();
|
||||
const timestamp = new Date().toISOString();
|
||||
const log = `${timestamp} ${clientIp} ${userAgent}\n`;
|
||||
fs.appendFile(logPath, log, (err) => {
|
||||
if (err) {
|
||||
console.error('Failed to write access log:', err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//clientIp = req.connection.remoteAddress.split(':').pop();
|
||||
if (whitelistMode === true && !whitelist.some(x => ipMatching.matches(clientIp, ipMatching.getMatch(x)))
|
||||
|| forwardedIp && whitelistMode === true && !whitelist.some(x => ipMatching.matches(forwardedIp, ipMatching.getMatch(x)))
|
||||
if (!whitelist.some(x => ipMatching.matches(clientIp, ipMatching.getMatch(x)))
|
||||
|| forwardedIp && !whitelist.some(x => ipMatching.matches(forwardedIp, ipMatching.getMatch(x)))
|
||||
) {
|
||||
// Log the connection attempt with real IP address
|
||||
const ipDetails = forwardedIp
|
||||
? `${clientIp} (forwarded from ${forwardedIp})`
|
||||
: clientIp;
|
||||
console.warn(
|
||||
color.red(
|
||||
`Blocked connection from ${clientIp}; User Agent: ${userAgent}\n\tTo allow this connection, add its IP address to the whitelist or disable whitelist mode by editing config.yaml in the root directory of your SillyTavern installation.\n`,
|
||||
),
|
||||
);
|
||||
|
||||
if (!noLogPaths.includes(req.path)) {
|
||||
console.warn(
|
||||
color.red(
|
||||
`Blocked connection from ${clientIp}; User Agent: ${userAgent}\n\tTo allow this connection, add its IP address to the whitelist or disable whitelist mode by editing config.yaml in the root directory of your SillyTavern installation.\n`,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return res.status(403).send(forbiddenWebpage({ ipDetails }));
|
||||
}
|
||||
next();
|
||||
|
@@ -3,7 +3,7 @@ import path from 'node:path';
|
||||
import url from 'node:url';
|
||||
|
||||
import express from 'express';
|
||||
import { default as git } from 'simple-git';
|
||||
import { default as git, CheckRepoActions } from 'simple-git';
|
||||
import { sync as commandExistsSync } from 'command-exists';
|
||||
import { getConfigValue, color } from './util.js';
|
||||
|
||||
@@ -256,7 +256,7 @@ async function updatePlugins(pluginsPath) {
|
||||
const pluginPath = path.join(pluginsPath, directory);
|
||||
const pluginRepo = git(pluginPath);
|
||||
|
||||
const isRepo = await pluginRepo.checkIsRepo();
|
||||
const isRepo = await pluginRepo.checkIsRepo(CheckRepoActions.IS_REPO_ROOT);
|
||||
if (!isRepo) {
|
||||
continue;
|
||||
}
|
||||
|
@@ -805,7 +805,7 @@ export function stringToBool(str) {
|
||||
* Setup the minimum log level
|
||||
*/
|
||||
export function setupLogLevel() {
|
||||
const logLevel = getConfigValue('minLogLevel', LOG_LEVELS.DEBUG, 'number');
|
||||
const logLevel = getConfigValue('logging.minLogLevel', LOG_LEVELS.DEBUG, 'number');
|
||||
|
||||
globalThis.console.debug = logLevel <= LOG_LEVELS.DEBUG ? console.debug : () => {};
|
||||
globalThis.console.info = logLevel <= LOG_LEVELS.INFO ? console.info : () => {};
|
||||
|
Reference in New Issue
Block a user