Merge branch 'staging' into support-multiple-expressions

1adde74f38ff51083a652edd87cdb39891cb6ed8

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

34 files changed, +269 -98Ignore whitespace
default/config.yaml+5 -0
@@ -85,6 +85,11 @@ cookieSecret: ''
8585disableCsrfProtection: false
8686# Disable startup security checks - NOT RECOMMENDED
8787securityOverride: false
88+# -- RATE LIMITING CONFIGURATION --
89+rateLimiting:
90+ # Use X-Real-IP header instead of socket IP for rate limiting
91+ # Only enable this if you are using a properly configured reverse proxy (like Nginx/traefik/Caddy)
92+ preferRealIpHeader: false
8893# -- ADVANCED CONFIGURATION --
8994# Open the browser automatically
9095autorun: true
docker/build-lib.js+1 -1
@@ -1,4 +1,4 @@
11import getWebpackServeMiddleware from '../src/middleware/webpack-serve.js';
22
33const middleware = getWebpackServeMiddleware();
44await middleware.runWebpackCompiler({ forceDist: true });
package-lock.json+35 -4
@@ -43,6 +43,7 @@
4343 "ip-matching": "^2.1.2",
4444 "ip-regex": "^5.0.0",
4545 "ipaddr.js": "^2.0.1",
46+ "is-docker": "^3.0.0",
4647 "jimp": "^0.22.10",
4748 "localforage": "^1.10.0",
4849 "lodash": "^4.17.21",
@@ -4649,15 +4650,15 @@
46494650 "license": "MIT"
46504651 },
46514652 "node_modules/is-docker": {
46524653 "version": "23.20.10",
46534654 "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-23.20.10.tgz",
46544655 "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQeljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==",
46554656 "license": "MIT",
46564657 "bin": {
46574658 "is-docker": "cli.js"
46584659 },
46594660 "engines": {
4660- "node": ">=8"
4661+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
46614662 },
46624663 "funding": {
46634664 "url": "https://github.com/sponsors/sindresorhus"
@@ -4734,6 +4735,21 @@
47344735 "node": ">=8"
47354736 }
47364737 },
4738+ "node_modules/is-wsl/node_modules/is-docker": {
4739+ "version": "2.2.1",
4740+ "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz",
4741+ "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==",
4742+ "license": "MIT",
4743+ "bin": {
4744+ "is-docker": "cli.js"
4745+ },
4746+ "engines": {
4747+ "node": ">=8"
4748+ },
4749+ "funding": {
4750+ "url": "https://github.com/sponsors/sindresorhus"
4751+ }
4752+ },
47374753 "node_modules/isarray": {
47384754 "version": "1.0.0",
47394755 "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
@@ -5518,6 +5534,21 @@
55185534 "url": "https://github.com/sponsors/sindresorhus"
55195535 }
55205536 },
5537+ "node_modules/open/node_modules/is-docker": {
5538+ "version": "2.2.1",
5539+ "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz",
5540+ "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==",
5541+ "license": "MIT",
5542+ "bin": {
5543+ "is-docker": "cli.js"
5544+ },
5545+ "engines": {
5546+ "node": ">=8"
5547+ },
5548+ "funding": {
5549+ "url": "https://github.com/sponsors/sindresorhus"
5550+ }
5551+ },
55215552 "node_modules/openai": {
55225553 "version": "4.17.4",
55235554 "resolved": "https://registry.npmjs.org/openai/-/openai-4.17.4.tgz",
package.json+1 -0
@@ -33,6 +33,7 @@
3333 "ip-matching": "^2.1.2",
3434 "ip-regex": "^5.0.0",
3535 "ipaddr.js": "^2.0.1",
36+ "is-docker": "^3.0.0",
3637 "jimp": "^0.22.10",
3738 "localforage": "^1.10.0",
3839 "lodash": "^4.17.21",
plugins.js+2 -2
@@ -8,7 +8,7 @@ import path from 'node:path';
88import process from 'node:process';
99import { fileURLToPath } from 'node:url';
1010
1111import { default as git, CheckRepoActions } from 'simple-git';
1212import { color } from './src/util.js';
1313
1414const __dirname = import.meta.dirname ?? path.dirname(fileURLToPath(import.meta.url));
@@ -49,7 +49,7 @@ async function updatePlugins() {
4949 const pluginPath = path.join(pluginsPath, directory);
5050 const pluginRepo = git(pluginPath);
5151
5252 const isRepo = await pluginRepo.checkIsRepo(CheckRepoActions.IS_REPO_ROOT);
5353 if (!isRepo) {
5454 console.log(`Directory ${color.yellow(directory)} is not a Git repository`);
5555 continue;
public/index.html+1 -1
@@ -4666,7 +4666,7 @@
46664666 <small data-i18n="Enabled">Enabled</small>
46674667 </label>
46684668 <small data-i18n="Minimum generated message length">Minimum generated message length</small>
46694669 <input id="auto_swipe_minimum_length" name="auto_swipe_minimum_length" type="number" min="0" step="1" value="0" class="text_pole" title="If the generated message is shorter than thisthese many characters, trigger an auto-swipe." data-i18n="[title]If the generated message is shorter than thisthese many characters, trigger an auto-swipe">
46704670 <small data-i18n="Blacklisted words">Blacklisted words</small>
46714671 <div class="auto_swipe">
46724672 <textarea id="auto_swipe_blacklist" name="auto_swipe_blacklist" data-i18n="[placeholder]words you dont want generated separated by comma ','" placeholder="words you don't want generated separated by comma ','" class="text_pole textarea_compact" value="" autocomplete="off" rows="3"></textarea>
public/lib/eventemitter.js+43 -2
@@ -24,10 +24,22 @@ if (typeof Array.prototype.indexOf === 'function') {
2424
2525
2626/* Polyfill EventEmitter. */
27-var EventEmitter = function () {
27+/**
28+ * Creates an event emitter.
29+ * @param {string[]} autoFireAfterEmit Auto-fire event names
30+ */
31+var EventEmitter = function (autoFireAfterEmit = []) {
2832 this.events = {};
33+ this.autoFireLastArgs = new Map();
34+ this.autoFireAfterEmit = new Set(autoFireAfterEmit);
2935};
3036
37+/**
38+ * Adds a listener to an event.
39+ * @param {string} event Event name
40+ * @param {function} listener Event listener
41+ * @returns
42+ */
3143EventEmitter.prototype.on = function (event, listener) {
3244 // Unknown event used by external libraries?
3345 if (event === undefined) {
@@ -40,6 +52,10 @@ EventEmitter.prototype.on = function (event, listener) {
4052 }
4153
4254 this.events[event].push(listener);
55+
56+ if (this.autoFireAfterEmit.has(event) && this.autoFireLastArgs.has(event)) {
57+ listener.apply(this, this.autoFireLastArgs.get(event));
58+ }
4359};
4460
4561/**
@@ -60,6 +76,10 @@ EventEmitter.prototype.makeLast = function (event, listener) {
6076 }
6177
6278 events.push(listener);
79+
80+ if (this.autoFireAfterEmit.has(event) && this.autoFireLastArgs.has(event)) {
81+ listener.apply(this, this.autoFireLastArgs.get(event));
82+ }
6383}
6484
6585/**
@@ -80,8 +100,17 @@ EventEmitter.prototype.makeFirst = function (event, listener) {
80100 }
81101
82102 events.unshift(listener);
103+
104+ if (this.autoFireAfterEmit.has(event) && this.autoFireLastArgs.has(event)) {
105+ listener.apply(this, this.autoFireLastArgs.get(event));
106+ }
83107}
84108
109+/**
110+ * Removes a listener from an event.
111+ * @param {string} event Event name
112+ * @param {function} listener Event listener
113+ */
85114EventEmitter.prototype.removeListener = function (event, listener) {
86115 var idx;
87116
@@ -94,6 +123,10 @@ EventEmitter.prototype.removeListener = function (event, listener) {
94123 }
95124};
96125
126+/**
127+ * Emits an event with optional arguments.
128+ * @param {string} event Event name
129+ */
97130EventEmitter.prototype.emit = async function (event) {
98131 let args = [].slice.call(arguments, 1);
99132 if (localStorage.getItem('eventTracing') === 'true') {
@@ -118,6 +151,10 @@ EventEmitter.prototype.emit = async function (event) {
118151 }
119152 }
120153 }
154+
155+ if (this.autoFireAfterEmit.has(event)) {
156+ this.autoFireLastArgs.set(event, args);
157+ }
121158};
122159
123160EventEmitter.prototype.emitAndWait = function (event) {
@@ -144,10 +181,14 @@ EventEmitter.prototype.emitAndWait = function (event) {
144181 }
145182 }
146183 }
184+
185+ if (this.autoFireAfterEmit.has(event)) {
186+ this.autoFireLastArgs.set(event, args);
187+ }
147188};
148189
149190EventEmitter.prototype.once = function (event, listener) {
150191 this.on(event, function g () {
151192 this.removeListener(event, g);
152193 listener.apply(this, arguments);
153194 });
public/locales/ar-sa.json+1 -1
@@ -709,7 +709,7 @@
709709 "Auto-swipe": "السحب التلقائي",
710710 "Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "تمكين وظيفة السحب التلقائي. الإعدادات في هذا القسم تؤثر فقط عند تمكين السحب التلقائي",
711711 "Minimum generated message length": "الحد الأدنى لطول الرسالة المولدة",
712712 "If the generated message is shorter than thisthese many characters, trigger an auto-swipe": "إذا كانت الرسالة المولدة أقصر من هذا، فتحريض السحب التلقائي",
713713 "Blacklisted words": "الكلمات الممنوعة",
714714 "words you dont want generated separated by comma ','": "الكلمات التي لا تريد توليدها مفصولة بفاصلة ','",
715715 "Blacklisted word count to swipe": "عدد الكلمات الممنوعة للسحب",
public/locales/de-de.json+1 -1
@@ -709,7 +709,7 @@
709709 "Auto-swipe": "Automatisches Wischen",
710710 "Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "Aktiviere die Auto-Wisch-Funktion. Einstellungen in diesem Abschnitt haben nur dann Auswirkungen, wenn das automatische Wischen aktiviert ist",
711711 "Minimum generated message length": "Minimale generierte Nachrichtenlänge",
712712 "If the generated message is shorter than thisthese many characters, trigger an auto-swipe": "Wenn die generierte Nachricht kürzer ist als diese, löse automatisches Wischen aus",
713713 "Blacklisted words": "Verbotene Wörter",
714714 "words you dont want generated separated by comma ','": "Wörter, die du nicht generiert haben möchtest, durch Komma ',' getrennt",
715715 "Blacklisted word count to swipe": "Anzahl der verbotenen Wörter, um zu wischen",
public/locales/es-es.json+1 -1
@@ -709,7 +709,7 @@
709709 "Auto-swipe": "Deslizamiento automático",
710710 "Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "Habilitar la función de deslizamiento automático. La configuración en esta sección solo tiene efecto cuando el deslizamiento automático está habilitado",
711711 "Minimum generated message length": "Longitud mínima del mensaje generado",
712712 "If the generated message is shorter than thisthese many characters, trigger an auto-swipe": "Si el mensaje generado es más corto que esto, activar un deslizamiento automático",
713713 "Blacklisted words": "Palabras prohibidas",
714714 "words you dont want generated separated by comma ','": "palabras que no desea generar separadas por coma ','",
715715 "Blacklisted word count to swipe": "Número de palabras prohibidas para deslizar",
public/locales/fr-fr.json+1 -1
@@ -656,7 +656,7 @@
656656 "Auto-swipe": "Balayage automatique",
657657 "Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "Activer la fonction de balayage automatique. Les paramètres de cette section n'ont d'effet que lorsque le balayage automatique est activé",
658658 "Minimum generated message length": "Longueur minimale du message généré",
659659 "If the generated message is shorter than thisthese many characters, trigger an auto-swipe": "Si le message généré est plus court que cela, déclenchez un balayage automatique",
660660 "Blacklisted words": "Mots en liste noire",
661661 "words you dont want generated separated by comma ','": "mots que vous ne voulez pas générer séparés par des virgules ','",
662662 "Blacklisted word count to swipe": "Nombre de mots en liste noire pour balayer",
public/locales/is-is.json+1 -1
@@ -709,7 +709,7 @@
709709 "Auto-swipe": "Sjálfvirkur sveip",
710710 "Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "Virkjaðu sjálfvirka sveiflugerð. Stillingar í þessum hluta hafa aðeins áhrif þegar sjálfvirkur sveiflugerð er virk",
711711 "Minimum generated message length": "Lágmarks lengd á mynduðum skilaboðum",
712712 "If the generated message is shorter than thisthese many characters, trigger an auto-swipe": "Ef mynduðu skilaboðin eru styttri en þessi, kallaðu fram sjálfvirkar sveiflugerðar",
713713 "Blacklisted words": "Svört orð",
714714 "words you dont want generated separated by comma ','": "orð sem þú vilt ekki að framleiða aðskilin með kommu ','",
715715 "Blacklisted word count to swipe": "Fjöldi svörtra orða til að sveipa",
public/locales/it-it.json+1 -1
@@ -709,7 +709,7 @@
709709 "Auto-swipe": "Auto-swipe",
710710 "Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "Abilita la funzione di auto-swipe. Le impostazioni in questa sezione hanno effetto solo quando l'auto-swipe è abilitato",
711711 "Minimum generated message length": "Lunghezza minima del messaggio generato",
712712 "If the generated message is shorter than thisthese many characters, trigger an auto-swipe": "Se il messaggio generato è più breve di questo, attiva un'automatica rimozione",
713713 "Blacklisted words": "Parole in blacklist",
714714 "words you dont want generated separated by comma ','": "parole che non vuoi generate separate da virgola ','",
715715 "Blacklisted word count to swipe": "Numero di parole in blacklist per attivare un'automatica rimozione",
public/locales/ja-jp.json+1 -1
@@ -709,7 +709,7 @@
709709 "Auto-swipe": "オートスワイプ",
710710 "Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "自動スワイプ機能を有効にします。このセクションの設定は、自動スワイプが有効になっている場合にのみ効果があります",
711711 "Minimum generated message length": "生成されたメッセージの最小長",
712712 "If the generated message is shorter than thisthese many characters, trigger an auto-swipe": "生成されたメッセージがこれよりも短い場合、自動スワイプをトリガーします",
713713 "Blacklisted words": "ブラックリストされた単語",
714714 "words you dont want generated separated by comma ','": "コンマ ',' で区切られた生成したくない単語",
715715 "Blacklisted word count to swipe": "スワイプするブラックリストされた単語の数",
public/locales/ko-kr.json+1 -1
@@ -724,7 +724,7 @@
724724 "Auto-swipe": "자동 스와이프",
725725 "Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "자동 스와이프 기능을 활성화합니다. 이 섹션의 설정은 자동 스와이프가 활성화되었을 때만 영향을 미칩니다",
726726 "Minimum generated message length": "생성된 메시지 최소 길이",
727727 "If the generated message is shorter than thisthese many characters, trigger an auto-swipe": "생성된 메시지가이보다 짧으면 자동 스와이프를 트리거합니다",
728728 "Blacklisted words": "금지어",
729729 "words you dont want generated separated by comma ','": "쉼표로 구분된 생성하지 않으려는 단어",
730730 "Blacklisted word count to swipe": "스와이프할 금지어 개수",
public/locales/nl-nl.json+1 -1
@@ -709,7 +709,7 @@
709709 "Auto-swipe": "Automatisch vegen",
710710 "Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "Schakel de automatische-vegen functie in. Instellingen in dit gedeelte hebben alleen effect wanneer automatisch vegen is ingeschakeld",
711711 "Minimum generated message length": "Minimale gegenereerde berichtlengte",
712712 "If the generated message is shorter than thisthese many characters, trigger an auto-swipe": "Als het gegenereerde bericht korter is dan dit, activeer dan een automatische veeg",
713713 "Blacklisted words": "Verboden woorden",
714714 "words you dont want generated separated by comma ','": "woorden die je niet gegenereerd wilt hebben gescheiden door komma ','",
715715 "Blacklisted word count to swipe": "Aantal verboden woorden om te vegen",
public/locales/pt-pt.json+1 -1
@@ -709,7 +709,7 @@
709709 "Auto-swipe": "Auto-swipe",
710710 "Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "Ativar a função de auto-swipe. As configurações nesta seção só têm efeito quando o auto-swipe está ativado",
711711 "Minimum generated message length": "Comprimento mínimo da mensagem gerada",
712712 "If the generated message is shorter than thisthese many characters, trigger an auto-swipe": "Se a mensagem gerada for mais curta que isso, acione um auto-swipe",
713713 "Blacklisted words": "Palavras proibidas",
714714 "words you dont want generated separated by comma ','": "palavras que você não quer geradas separadas por vírgula ','",
715715 "Blacklisted word count to swipe": "Contagem de palavras proibidas para swipe",
public/locales/ru-ru.json+2 -2
@@ -426,7 +426,7 @@
426426 "Requests logprobs from the API for the Token Probabilities feature": "Запросить логпробы из API для функции Token Probabilities.",
427427 "Automatically reject and re-generate AI message based on configurable criteria": "Автоматическое отклонение и повторная генерация сообщений AI на основе настраиваемых критериев.",
428428 "Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "Включить авто-свайп. Настройки в этом разделе действуют только при включенном авто-свайпе.",
429429 "If the generated message is shorter than thisthese many characters, trigger an auto-swipe": "Если сгенерированное сообщение короче этого значения, срабатывает авто-свайп.",
430430 "Reload and redraw the currently open chat": "Перезагрузить и перерисовать открытый в данный момент чат.",
431431 "Auto-Expand Message Actions": "Развернуть действия",
432432 "Persona Management": "Управление персоной",
@@ -2205,4 +2205,4 @@
22052205 "Tokenized text:": "Токенизированный текст:",
22062206 "Token IDs:": "Идентификаторы токенов:",
22072207 "Tokens:": "Токенов:"
2208-}
2208 \ No newline at end of file
2208+}
public/locales/uk-ua.json+1 -1
@@ -709,7 +709,7 @@
709709 "Auto-swipe": "Автоматичний змах",
710710 "Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "Вмикає функцію автоматичного змаху. Налаштування в цьому розділі діють лише тоді, коли увімкнено автоматичний змах",
711711 "Minimum generated message length": "Мінімальна довжина згенерованого повідомлення",
712712 "If the generated message is shorter than thisthese many characters, trigger an auto-swipe": "Якщо згенероване повідомлення коротше за це, викликайте автоматичний змаху",
713713 "Blacklisted words": "Список заборонених слів",
714714 "words you dont want generated separated by comma ','": "слова, які ви не хочете генерувати, розділені комою ','",
715715 "Blacklisted word count to swipe": "Кількість заборонених слів для змаху",
public/locales/vi-vn.json+1 -1
@@ -709,7 +709,7 @@
709709 "Auto-swipe": "Tự động vuốt",
710710 "Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "Bật chức năng tự động vuốt. Các cài đặt trong phần này chỉ có tác dụng khi tự động vuốt được bật",
711711 "Minimum generated message length": "Độ dài tối thiểu của tin nhắn được tạo",
712712 "If the generated message is shorter than thisthese many characters, trigger an auto-swipe": "Nếu tin nhắn được tạo ra ngắn hơn điều này, kích hoạt tự động vuốt",
713713 "Blacklisted words": "Từ trong danh sách đen",
714714 "words you dont want generated separated by comma ','": "các từ bạn không muốn được tạo ra được phân tách bằng dấu phẩy ','",
715715 "Blacklisted word count to swipe": "Số từ trong danh sách đen để vuốt",
public/locales/zh-cn.json+1 -1
@@ -804,7 +804,7 @@
804804 "Auto-swipe": "自动滑动",
805805 "Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "启用自动滑动功能。仅当启用自动滑动时,本节中的设置才会生效",
806806 "Minimum generated message length": "生成的消息的最小长度",
807807 "If the generated message is shorter than thisthese many characters, trigger an auto-swipe": "如果生成的消息短于此长度,则触发自动滑动",
808808 "Blacklisted words": "屏蔽词",
809809 "words you dont want generated separated by comma ','": "不想生成的词语,用半角逗号“,”分隔",
810810 "Blacklisted word count to swipe": "触发滑动的黑名单词语数量",
public/locales/zh-tw.json+1 -1
@@ -710,7 +710,7 @@
710710 "Auto-swipe": "自動滑動",
711711 "Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "啟用自動滑動功能。此部分的設定僅在啟用自動滑動時有效。",
712712 "Minimum generated message length": "生成訊息的最小長度",
713713 "If the generated message is shorter than thisthese many characters, trigger an auto-swipe": "如果生成的訊息比這個短,將觸發自動滑動。",
714714 "Blacklisted words": "黑名單詞語",
715715 "words you dont want generated separated by comma ','": "您不想生成的文字,使用逗號分隔",
716716 "Blacklisted word count to swipe": "滑動的黑名單詞語數量",
public/script.js+1 -1
@@ -512,7 +512,7 @@ export const event_types = {
512512 TOOL_CALLS_RENDERED: 'tool_calls_rendered',
513513};
514514
515515export const eventSource = new EventEmitter([event_types.APP_READY]);
516516
517517eventSource.on(event_types.CHAT_CHANGED, processChatSlashCommands);
518518
public/scripts/chats.js+1 -1
@@ -1487,7 +1487,7 @@ jQuery(function () {
14871487 ...chat.filter(x => x?.extra?.type !== system_message_types.ASSISTANT_NOTE),
14881488 ];
14891489
14901490 download(JSONchatToSave.stringifymap(chatToSave,(m) null,=> 4JSON.stringify(m)).join('\n'), `Assistant - ${humanizedDateTime()}.jsonjsonl`, 'application/json');
14911491 });
14921492
14931493 // Do not change. #attachFile is added by extension.
public/scripts/reasoning.js+3 -2
@@ -338,14 +338,15 @@ export class ReasoningHandler {
338338 return mesChanged;
339339 }
340340
341341 if (this.state === ReasoningState.None || this.#isHiddenReasoningModel) {
342342 // If streamed message starts with the opening, cut it out and put all inside reasoning
343343 if (message.mes.startsWith(power_user.reasoning.prefix) && message.mes.length > power_user.reasoning.prefix.length) {
344344 this.#isParsingReasoning = true;
345345
346346 // Manually set starting state here, as we might already have received the ending suffix
347347 this.state = ReasoningState.Thinking;
348348 this.startTime = this.startTime ?? this.initialTime;
349+ this.endTime = null;
349350 }
350351 }
351352
public/scripts/user.js+26 -0
@@ -9,6 +9,9 @@ import { ensureImageFormatSupported, getBase64Async, humanFileSize } from './uti
99export let currentUser = null;
1010export let accountsEnabled = false;
1111
12+// Extend the session every 30 minutes
13+const SESSION_EXTEND_INTERVAL = 30 * 60 * 1000;
14+
1215/**
1316 * Enable or disable user account controls in the UI.
1417 * @param {boolean} isEnabled User account controls enabled
@@ -894,6 +897,24 @@ async function slugify(text) {
894897 }
895898}
896899
900+/**
901+ * Pings the server to extend the user session.
902+ */
903+async function extendUserSession() {
904+ try {
905+ const response = await fetch('/api/ping?extend=1', {
906+ method: 'GET',
907+ headers: getRequestHeaders(),
908+ });
909+
910+ if (!response.ok) {
911+ throw new Error('Ping did not succeed', { cause: response.status });
912+ }
913+ } catch (error) {
914+ console.error('Failed to extend user session', error);
915+ }
916+}
917+
897918jQuery(() => {
898919 $('#logout_button').on('click', () => {
899920 logout();
@@ -904,4 +925,9 @@ jQuery(() => {
904925 $('#account_button').on('click', () => {
905926 openUserProfile();
906927 });
928+ setInterval(async () => {
929+ if (currentUser) {
930+ await extendUserSession();
931+ }
932+ }, SESSION_EXTEND_INTERVAL);
907933});
public/style.css+22 -20
@@ -55,6 +55,10 @@
5555 --interactable-outline-color: var(--white100);
5656 --interactable-outline-color-faint: var(--white20a);
5757
58+ --reasoning-body-color: var(--SmartThemeEmColor);
59+ --reasoning-em-color: color-mix(in srgb, var(--SmartThemeEmColor) 67%, var(--SmartThemeBlurTintColor) 33%);
60+ --reasoning-saturation: 0.5;
61+
5862
5963 /*Default Theme, will be changed by ToolCool Color Picker*/
6064 --SmartThemeBodyColor: rgb(220, 220, 210);
@@ -348,13 +352,13 @@ input[type='checkbox']:focus-visible {
348352
349353.mes_reasoning {
350354 display: block;
351355 border-left: 2px solid var(--SmartThemeEmColorreasoning-body-color);
352356 border-radius: 2px;
353357 padding: 5px;
354358 padding-left: 14px;
355359 margin-bottom: 0.5em;
356360 overflow-y: auto;
357- color: var(--SmartThemeEmColor);
361+ color: hsl(from var(--reasoning-body-color) h calc(s * var(--reasoning-saturation)) l);
358362}
359363
360364.mes_reasoning_details {
@@ -374,18 +378,6 @@ input[type='checkbox']:focus-visible {
374378 margin-bottom: 0;
375379}
376380
377-.mes_reasoning em,
378-.mes_reasoning i,
379-.mes_reasoning u,
380-.mes_reasoning q,
381-.mes_reasoning blockquote {
382- filter: saturate(0.5);
383-}
384-
385-.mes_reasoning_details .mes_reasoning em {
386- color: color-mix(in srgb, var(--SmartThemeEmColor) 67%, var(--SmartThemeBlurTintColor) 33%);
387-}
388-
389381.mes_reasoning_header_block {
390382 flex-grow: 1;
391383}
@@ -461,26 +453,36 @@ input[type='checkbox']:focus-visible {
461453}
462454
463455.mes_text i,
464456.mes_text em, {
457+ color: var(--SmartThemeEmColor);
458+}
465459.mes_reasoning i,
466460.mes_reasoning em {
467- color: var(--SmartThemeEmColor);
461+ color: hsl(from var(--reasoning-em-color) h calc(s * var(--reasoning-saturation)) l);
468462}
469463
470464.mes_text q i,
471465.mes_text q em {
472466 color: inherit;
473467}
468+.mes_reasoning q i,
469+.mes_reasoning q em {
470+ color: hsl(from var(--SmartThemeQuoteColor) h calc(s * var(--reasoning-saturation)) l);
471+}
474472
475473.mes_text u, {
476-.mes_reasoning u {
477474 color: var(--SmartThemeUnderlineColor);
478475}
476+.mes_reasoning u {
477+ color: hsl(from var(--SmartThemeUnderlineColor) h calc(s * var(--reasoning-saturation)) l);
478+}
479479
480480.mes_text q, {
481-.mes_reasoning q {
482481 color: var(--SmartThemeQuoteColor);
483482}
483+.mes_reasoning q {
484+ color: hsl(from var(--SmartThemeQuoteColor) h calc(s * var(--reasoning-saturation)) l);
485+}
484486
485487.mes_text font[color] em,
486488.mes_text font[color] i,
server.js+7 -1
@@ -556,7 +556,13 @@ app.use('/api/users', usersPublicRouter);
556556
557557// Everything below this line requires authentication
558558app.use(requireLoginMiddleware);
559559app.get('/api/ping', (_request, response) => response.sendStatus(204));{
560+ if (request.query.extend && request.session) {
561+ request.session.touch = Date.now();
562+ }
563+
564+ response.sendStatus(204);
565+});
560566
561567// File uploads
562568app.use(multer({ dest: uploadsPath, limits: { fieldSize: 10 * 1024 * 1024 } }).single('avatar'));
src/endpoints/extensions.js+1 -1
@@ -230,7 +230,7 @@ router.post('/version', jsonParser, async (request, response) => {
230230 } catch (error) {
231231 // it is not a git repo, or has no commits yet, or is a bare repo
232232 // not possible to update it, most likely can't get the branch name either
233233 return response.send({ currentBranchName: null'', currentCommitHash: '', isUpToDate: true, remoteUrl: null'' });
234234 }
235235
236236 const currentBranch = await git.cwd(extensionPath).branch();
src/endpoints/users-public.js+10 -7
@@ -3,13 +3,16 @@ import crypto from 'node:crypto';
33import storage from 'node-persist';
44import express from 'express';
55import { RateLimiterMemory, RateLimiterRes } from 'rate-limiter-flexible';
66import { jsonParser, getIpFromRequest, getRealIpFromHeader } from '../express-common.js';
77import { color, Cache, getConfigValue } from '../util.js';
88import { KEY_PREFIX, getUserAvatar, toKey, getPasswordHash, getPasswordSalt } from '../users.js';
99
1010const DISCREET_LOGIN = getConfigValue('enableDiscreetLogin', false);
11+const PREFER_REAL_IP_HEADER = getConfigValue('rateLimiting.preferRealIpHeader', false);
1112const MFA_CACHE = new Cache(5 * 60 * 1000);
1213
14+const getIpAddress = (request) => PREFER_REAL_IP_HEADER ? getRealIpFromHeader(request) : getIpFromRequest(request);
15+
1316export const router = express.Router();
1417const loginLimiter = new RateLimiterMemory({
1518 points: 5,
@@ -60,7 +63,7 @@ router.post('/login', jsonParser, async (request, response) => {
6063 return response.status(400).json({ error: 'Missing required fields' });
6164 }
6265
6366 const ip = getIpFromRequestgetIpAddress(request);
6467 await loginLimiter.consume(ip);
6568
6669 /** @type {import('../users.js').User} */
@@ -92,7 +95,7 @@ router.post('/login', jsonParser, async (request, response) => {
9295 return response.json({ handle: user.handle });
9396 } catch (error) {
9497 if (error instanceof RateLimiterRes) {
9598 console.error('Login failed: Rate limited from', getIpFromRequestgetIpAddress(request));
9699 return response.status(429).send({ error: 'Too many attempts. Try again later or recover your password.' });
97100 }
98101
@@ -108,7 +111,7 @@ router.post('/recover-step1', jsonParser, async (request, response) => {
108111 return response.status(400).json({ error: 'Missing required fields' });
109112 }
110113
111114 const ip = getIpFromRequestgetIpAddress(request);
112115 await recoverLimiter.consume(ip);
113116
114117 /** @type {import('../users.js').User} */
@@ -132,7 +135,7 @@ router.post('/recover-step1', jsonParser, async (request, response) => {
132135 return response.sendStatus(204);
133136 } catch (error) {
134137 if (error instanceof RateLimiterRes) {
135138 console.error('Recover step 1 failed: Rate limited from', getIpFromRequestgetIpAddress(request));
136139 return response.status(429).send({ error: 'Too many attempts. Try again later or contact your admin.' });
137140 }
138141
@@ -150,7 +153,7 @@ router.post('/recover-step2', jsonParser, async (request, response) => {
150153
151154 /** @type {import('../users.js').User} */
152155 const user = await storage.getItem(toKey(request.body.handle));
153156 const ip = getIpFromRequestgetIpAddress(request);
154157
155158 if (!user) {
156159 console.error('Recover step 2 failed: User', request.body.handle, 'not found');
@@ -186,7 +189,7 @@ router.post('/recover-step2', jsonParser, async (request, response) => {
186189 return response.sendStatus(204);
187190 } catch (error) {
188191 if (error instanceof RateLimiterRes) {
189192 console.error('Recover step 2 failed: Rate limited from', getIpFromRequestgetIpAddress(request));
190193 return response.status(429).send({ error: 'Too many attempts. Try again later or contact your admin.' });
191194 }
192195
src/express-common.js+14 -0
@@ -25,3 +25,17 @@ export function getIpFromRequest(req) {
2525 }
2626 return clientIp;
2727}
28+
29+/**
30+ * Gets the IP address of the client when behind reverse proxy using x-real-ip header, falls back to socket remote address.
31+ * This function should be used when the application is running behind a reverse proxy (e.g., Nginx, traefik, Caddy...).
32+ * @param {import('express').Request} req Request object
33+ * @returns {string} IP address of the client
34+ */
35+export function getRealIpFromHeader(req) {
36+ if (req.headers['x-real-ip']) {
37+ return req.headers['x-real-ip'].toString();
38+ }
39+
40+ return getIpFromRequest(req);
41+}
src/middleware/webpack-serve.js+9 -5
@@ -1,11 +1,8 @@
11import path from 'node:path';
22import webpack from 'webpack';
33import { publicLibConfig }getPublicLibConfig from '../../webpack.config.js';
44
55export default function getWebpackServeMiddleware() {
6- const outputPath = publicLibConfig.output?.path;
7- const outputFile = publicLibConfig.output?.filename;
8-
96 /**
107 * A very spartan recreation of webpack-dev-middleware.
118 * @param {import('express').Request} req Request object.
@@ -14,6 +11,10 @@ export default function getWebpackServeMiddleware() {
1411 * @type {import('express').RequestHandler}
1512 */
1613 function devMiddleware(req, res, next) {
14+ const publicLibConfig = getPublicLibConfig();
15+ const outputPath = publicLibConfig.output?.path;
16+ const outputFile = publicLibConfig.output?.filename;
17+
1718 if (req.method === 'GET' && path.parse(req.path).base === outputFile) {
1819 return res.sendFile(outputFile, { root: outputPath });
1920 }
@@ -23,9 +24,12 @@ export default function getWebpackServeMiddleware() {
2324
2425 /**
2526 * Wait until Webpack is done compiling.
27+ * @param {object} param Parameters.
28+ * @param {boolean} [param.forceDist] Whether to force the use the /dist folder.
2629 * @returns {Promise<void>}
2730 */
2831 devMiddleware.runWebpackCompiler = ({ forceDist = false } = {}) => {
32+ const publicLibConfig = getPublicLibConfig(forceDist);
2933 const compiler = webpack(publicLibConfig);
3034
3135 return new Promise((resolve) => {
src/plugin-loader.js+2 -2
@@ -3,7 +3,7 @@ import path from 'node:path';
33import url from 'node:url';
44
55import express from 'express';
66import { default as git, CheckRepoActions } from 'simple-git';
77import { sync as commandExistsSync } from 'command-exists';
88import { getConfigValue, color } from './util.js';
99
@@ -256,7 +256,7 @@ async function updatePlugins(pluginsPath) {
256256 const pluginPath = path.join(pluginsPath, directory);
257257 const pluginRepo = git(pluginPath);
258258
259259 const isRepo = await pluginRepo.checkIsRepo(CheckRepoActions.IS_REPO_ROOT);
260260 if (!isRepo) {
261261 continue;
262262 }
webpack.config.js+69 -32
@@ -1,35 +1,72 @@
11import process from 'node:process';
22import path from 'node:path';
3+import isDocker from 'is-docker';
34
4-/** @type {import('webpack').Configuration} */
5+/**
5-export const publicLibConfig = {
6+ * Get the Webpack configuration for the public/lib.js file.
6- mode: 'production',
7+ * 1. Docker has got cache and the output file pre-baked.
7- entry: './public/lib.js',
8+ * 2. Non-Docker environments use the global DATA_ROOT variable to determine the cache and output directories.
8- cache: {
9+ * @param {boolean} forceDist Whether to force the use the /dist folder.
9- type: 'filesystem',
10+ * @returns {import('webpack').Configuration}
10- cacheDirectory: path.resolve(process.cwd(), 'dist/webpack'),
11+ * @throws {Error} If the DATA_ROOT variable is not set.
11- store: 'pack',
12+ * */
12- compression: 'gzip',
13+export default function getPublicLibConfig(forceDist = false) {
13- },
14+ function getCacheDirectory() {
14- devtool: false,
15+ if (forceDist || isDocker()) {
15- watch: false,
16+ return path.resolve(process.cwd(), 'dist/webpack');
16- module: {},
17+ }
17- stats: {
18+
18- preset: 'minimal',
19+ if (typeof globalThis.DATA_ROOT === 'string') {
19- assets: false,
20+ return path.resolve(globalThis.DATA_ROOT, '_webpack', 'cache');
20- modules: false,
21+ }
21- colors: true,
22+
22- timings: true,
23+ throw new Error('DATA_ROOT variable is not set.');
2324 },
24- experiments: {
25+
25- outputModule: true,
26+ function getOutputDirectory() {
26- },
27+ if (forceDist || isDocker()) {
27- performance: {
28+ return path.resolve(process.cwd(), 'dist');
28- hints: false,
29+ }
29- },
30+
30- output: {
31+ if (typeof globalThis.DATA_ROOT === 'string') {
3132 path: return path.resolve(processglobalThis.cwd()DATA_ROOT, 'dist_webpack'), 'output');
32- filename: 'lib.js',
33+ }
33- libraryTarget: 'module',
34+
34- },
35+ throw new Error('DATA_ROOT variable is not set.');
3536 };
37+
38+ const cacheDirectory = getCacheDirectory();
39+ const outputDirectory = getOutputDirectory();
40+
41+ return {
42+ mode: 'production',
43+ entry: './public/lib.js',
44+ cache: {
45+ type: 'filesystem',
46+ cacheDirectory: cacheDirectory,
47+ store: 'pack',
48+ compression: 'gzip',
49+ },
50+ devtool: false,
51+ watch: false,
52+ module: {},
53+ stats: {
54+ preset: 'minimal',
55+ assets: false,
56+ modules: false,
57+ colors: true,
58+ timings: true,
59+ },
60+ experiments: {
61+ outputModule: true,
62+ },
63+ performance: {
64+ hints: false,
65+ },
66+ output: {
67+ path: outputDirectory,
68+ filename: 'lib.js',
69+ libraryTarget: 'module',
70+ },
71+ };
72+}