Merge branch 'staging' into support-multiple-expressions
| @@ -85,6 +85,11 @@ cookieSecret: '' | ||
| 85 | 85 | disableCsrfProtection: false |
| 86 | 86 | # Disable startup security checks - NOT RECOMMENDED |
| 87 | 87 | securityOverride: 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 | |
| 88 | 93 | # -- ADVANCED CONFIGURATION -- |
| 89 | 94 | # Open the browser automatically |
| 90 | 95 | autorun: true |
| @@ -1,4 +1,4 @@ | ||
| 1 | 1 | import getWebpackServeMiddleware from '../src/middleware/webpack-serve.js'; |
| 2 | 2 | |
| 3 | 3 | const middleware = getWebpackServeMiddleware(); |
| 4 | 4 | await middleware.runWebpackCompiler({ forceDist: true }); |
| @@ -43,6 +43,7 @@ | ||
| 43 | 43 | "ip-matching": "^2.1.2", |
| 44 | 44 | "ip-regex": "^5.0.0", |
| 45 | 45 | "ipaddr.js": "^2.0.1", |
| 46 | + "is-docker": "^3.0.0", | |
| 46 | 47 | "jimp": "^0.22.10", |
| 47 | 48 | "localforage": "^1.10.0", |
| 48 | 49 | "lodash": "^4.17.21", |
| @@ -4649,15 +4650,15 @@ | ||
| 4649 | 4650 | "license": "MIT" |
| 4650 | 4651 | }, |
| 4651 | 4652 | "node_modules/is-docker": { |
| 4652 | 4653 | "version": "23.20.10", |
| 4653 | 4654 | "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-23.20.10.tgz", |
| 4654 | 4655 | "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQeljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", |
| 4655 | 4656 | "license": "MIT", |
| 4656 | 4657 | "bin": { |
| 4657 | 4658 | "is-docker": "cli.js" |
| 4658 | 4659 | }, |
| 4659 | 4660 | "engines": { |
| 4660 | - "node": ">=8" | |
| 4661 | + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" | |
| 4661 | 4662 | }, |
| 4662 | 4663 | "funding": { |
| 4663 | 4664 | "url": "https://github.com/sponsors/sindresorhus" |
| @@ -4734,6 +4735,21 @@ | ||
| 4734 | 4735 | "node": ">=8" |
| 4735 | 4736 | } |
| 4736 | 4737 | }, |
| 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 | + }, | |
| 4737 | 4753 | "node_modules/isarray": { |
| 4738 | 4754 | "version": "1.0.0", |
| 4739 | 4755 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", |
| @@ -5518,6 +5534,21 @@ | ||
| 5518 | 5534 | "url": "https://github.com/sponsors/sindresorhus" |
| 5519 | 5535 | } |
| 5520 | 5536 | }, |
| 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 | + }, | |
| 5521 | 5552 | "node_modules/openai": { |
| 5522 | 5553 | "version": "4.17.4", |
| 5523 | 5554 | "resolved": "https://registry.npmjs.org/openai/-/openai-4.17.4.tgz", |
| @@ -33,6 +33,7 @@ | ||
| 33 | 33 | "ip-matching": "^2.1.2", |
| 34 | 34 | "ip-regex": "^5.0.0", |
| 35 | 35 | "ipaddr.js": "^2.0.1", |
| 36 | + "is-docker": "^3.0.0", | |
| 36 | 37 | "jimp": "^0.22.10", |
| 37 | 38 | "localforage": "^1.10.0", |
| 38 | 39 | "lodash": "^4.17.21", |
| @@ -8,7 +8,7 @@ import path from 'node:path'; | ||
| 8 | 8 | import process from 'node:process'; |
| 9 | 9 | import { fileURLToPath } from 'node:url'; |
| 10 | 10 | |
| 11 | 11 | import { default as git, CheckRepoActions } from 'simple-git'; |
| 12 | 12 | import { color } from './src/util.js'; |
| 13 | 13 | |
| 14 | 14 | const __dirname = import.meta.dirname ?? path.dirname(fileURLToPath(import.meta.url)); |
| @@ -49,7 +49,7 @@ async function updatePlugins() { | ||
| 49 | 49 | const pluginPath = path.join(pluginsPath, directory); |
| 50 | 50 | const pluginRepo = git(pluginPath); |
| 51 | 51 | |
| 52 | 52 | const isRepo = await pluginRepo.checkIsRepo(CheckRepoActions.IS_REPO_ROOT); |
| 53 | 53 | if (!isRepo) { |
| 54 | 54 | console.log(`Directory ${color.yellow(directory)} is not a Git repository`); |
| 55 | 55 | continue; |
| @@ -4666,7 +4666,7 @@ | ||
| 4666 | 4666 | <small data-i18n="Enabled">Enabled</small> |
| 4667 | 4667 | </label> |
| 4668 | 4668 | <small data-i18n="Minimum generated message length">Minimum generated message length</small> |
| 4669 | 4669 | <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"> |
| 4670 | 4670 | <small data-i18n="Blacklisted words">Blacklisted words</small> |
| 4671 | 4671 | <div class="auto_swipe"> |
| 4672 | 4672 | <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> |
| @@ -24,10 +24,22 @@ if (typeof Array.prototype.indexOf === 'function') { | ||
| 24 | 24 | |
| 25 | 25 | |
| 26 | 26 | /* 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 = []) { | |
| 28 | 32 | this.events = {}; |
| 33 | + this.autoFireLastArgs = new Map(); | |
| 34 | + this.autoFireAfterEmit = new Set(autoFireAfterEmit); | |
| 29 | 35 | }; |
| 30 | 36 | |
| 37 | +/** | |
| 38 | + * Adds a listener to an event. | |
| 39 | + * @param {string} event Event name | |
| 40 | + * @param {function} listener Event listener | |
| 41 | + * @returns | |
| 42 | + */ | |
| 31 | 43 | EventEmitter.prototype.on = function (event, listener) { |
| 32 | 44 | // Unknown event used by external libraries? |
| 33 | 45 | if (event === undefined) { |
| @@ -40,6 +52,10 @@ EventEmitter.prototype.on = function (event, listener) { | ||
| 40 | 52 | } |
| 41 | 53 | |
| 42 | 54 | 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 | + } | |
| 43 | 59 | }; |
| 44 | 60 | |
| 45 | 61 | /** |
| @@ -60,6 +76,10 @@ EventEmitter.prototype.makeLast = function (event, listener) { | ||
| 60 | 76 | } |
| 61 | 77 | |
| 62 | 78 | events.push(listener); |
| 79 | + | |
| 80 | + if (this.autoFireAfterEmit.has(event) && this.autoFireLastArgs.has(event)) { | |
| 81 | + listener.apply(this, this.autoFireLastArgs.get(event)); | |
| 82 | + } | |
| 63 | 83 | } |
| 64 | 84 | |
| 65 | 85 | /** |
| @@ -80,8 +100,17 @@ EventEmitter.prototype.makeFirst = function (event, listener) { | ||
| 80 | 100 | } |
| 81 | 101 | |
| 82 | 102 | events.unshift(listener); |
| 103 | + | |
| 104 | + if (this.autoFireAfterEmit.has(event) && this.autoFireLastArgs.has(event)) { | |
| 105 | + listener.apply(this, this.autoFireLastArgs.get(event)); | |
| 106 | + } | |
| 83 | 107 | } |
| 84 | 108 | |
| 109 | +/** | |
| 110 | + * Removes a listener from an event. | |
| 111 | + * @param {string} event Event name | |
| 112 | + * @param {function} listener Event listener | |
| 113 | + */ | |
| 85 | 114 | EventEmitter.prototype.removeListener = function (event, listener) { |
| 86 | 115 | var idx; |
| 87 | 116 | |
| @@ -94,6 +123,10 @@ EventEmitter.prototype.removeListener = function (event, listener) { | ||
| 94 | 123 | } |
| 95 | 124 | }; |
| 96 | 125 | |
| 126 | +/** | |
| 127 | + * Emits an event with optional arguments. | |
| 128 | + * @param {string} event Event name | |
| 129 | + */ | |
| 97 | 130 | EventEmitter.prototype.emit = async function (event) { |
| 98 | 131 | let args = [].slice.call(arguments, 1); |
| 99 | 132 | if (localStorage.getItem('eventTracing') === 'true') { |
| @@ -118,6 +151,10 @@ EventEmitter.prototype.emit = async function (event) { | ||
| 118 | 151 | } |
| 119 | 152 | } |
| 120 | 153 | } |
| 154 | + | |
| 155 | + if (this.autoFireAfterEmit.has(event)) { | |
| 156 | + this.autoFireLastArgs.set(event, args); | |
| 157 | + } | |
| 121 | 158 | }; |
| 122 | 159 | |
| 123 | 160 | EventEmitter.prototype.emitAndWait = function (event) { |
| @@ -144,6 +181,10 @@ EventEmitter.prototype.emitAndWait = function (event) { | ||
| 144 | 181 | } |
| 145 | 182 | } |
| 146 | 183 | } |
| 184 | + | |
| 185 | + if (this.autoFireAfterEmit.has(event)) { | |
| 186 | + this.autoFireLastArgs.set(event, args); | |
| 187 | + } | |
| 147 | 188 | }; |
| 148 | 189 | |
| 149 | 190 | EventEmitter.prototype.once = function (event, listener) { |
| @@ -709,7 +709,7 @@ | ||
| 709 | 709 | "Auto-swipe": "السحب التلقائي", |
| 710 | 710 | "Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "تمكين وظيفة السحب التلقائي. الإعدادات في هذا القسم تؤثر فقط عند تمكين السحب التلقائي", |
| 711 | 711 | "Minimum generated message length": "الحد الأدنى لطول الرسالة المولدة", |
| 712 | 712 | "If the generated message is shorter than thisthese many characters, trigger an auto-swipe": "إذا كانت الرسالة المولدة أقصر من هذا، فتحريض السحب التلقائي", |
| 713 | 713 | "Blacklisted words": "الكلمات الممنوعة", |
| 714 | 714 | "words you dont want generated separated by comma ','": "الكلمات التي لا تريد توليدها مفصولة بفاصلة ','", |
| 715 | 715 | "Blacklisted word count to swipe": "عدد الكلمات الممنوعة للسحب", |
| @@ -709,7 +709,7 @@ | ||
| 709 | 709 | "Auto-swipe": "Automatisches Wischen", |
| 710 | 710 | "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", |
| 711 | 711 | "Minimum generated message length": "Minimale generierte Nachrichtenlänge", |
| 712 | 712 | "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", |
| 713 | 713 | "Blacklisted words": "Verbotene Wörter", |
| 714 | 714 | "words you dont want generated separated by comma ','": "Wörter, die du nicht generiert haben möchtest, durch Komma ',' getrennt", |
| 715 | 715 | "Blacklisted word count to swipe": "Anzahl der verbotenen Wörter, um zu wischen", |
| @@ -709,7 +709,7 @@ | ||
| 709 | 709 | "Auto-swipe": "Deslizamiento automático", |
| 710 | 710 | "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", |
| 711 | 711 | "Minimum generated message length": "Longitud mínima del mensaje generado", |
| 712 | 712 | "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", |
| 713 | 713 | "Blacklisted words": "Palabras prohibidas", |
| 714 | 714 | "words you dont want generated separated by comma ','": "palabras que no desea generar separadas por coma ','", |
| 715 | 715 | "Blacklisted word count to swipe": "Número de palabras prohibidas para deslizar", |
| @@ -656,7 +656,7 @@ | ||
| 656 | 656 | "Auto-swipe": "Balayage automatique", |
| 657 | 657 | "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é", |
| 658 | 658 | "Minimum generated message length": "Longueur minimale du message généré", |
| 659 | 659 | "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", |
| 660 | 660 | "Blacklisted words": "Mots en liste noire", |
| 661 | 661 | "words you dont want generated separated by comma ','": "mots que vous ne voulez pas générer séparés par des virgules ','", |
| 662 | 662 | "Blacklisted word count to swipe": "Nombre de mots en liste noire pour balayer", |
| @@ -709,7 +709,7 @@ | ||
| 709 | 709 | "Auto-swipe": "Sjálfvirkur sveip", |
| 710 | 710 | "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", |
| 711 | 711 | "Minimum generated message length": "Lágmarks lengd á mynduðum skilaboðum", |
| 712 | 712 | "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", |
| 713 | 713 | "Blacklisted words": "Svört orð", |
| 714 | 714 | "words you dont want generated separated by comma ','": "orð sem þú vilt ekki að framleiða aðskilin með kommu ','", |
| 715 | 715 | "Blacklisted word count to swipe": "Fjöldi svörtra orða til að sveipa", |
| @@ -709,7 +709,7 @@ | ||
| 709 | 709 | "Auto-swipe": "Auto-swipe", |
| 710 | 710 | "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", |
| 711 | 711 | "Minimum generated message length": "Lunghezza minima del messaggio generato", |
| 712 | 712 | "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", |
| 713 | 713 | "Blacklisted words": "Parole in blacklist", |
| 714 | 714 | "words you dont want generated separated by comma ','": "parole che non vuoi generate separate da virgola ','", |
| 715 | 715 | "Blacklisted word count to swipe": "Numero di parole in blacklist per attivare un'automatica rimozione", |
| @@ -709,7 +709,7 @@ | ||
| 709 | 709 | "Auto-swipe": "オートスワイプ", |
| 710 | 710 | "Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "自動スワイプ機能を有効にします。このセクションの設定は、自動スワイプが有効になっている場合にのみ効果があります", |
| 711 | 711 | "Minimum generated message length": "生成されたメッセージの最小長", |
| 712 | 712 | "If the generated message is shorter than thisthese many characters, trigger an auto-swipe": "生成されたメッセージがこれよりも短い場合、自動スワイプをトリガーします", |
| 713 | 713 | "Blacklisted words": "ブラックリストされた単語", |
| 714 | 714 | "words you dont want generated separated by comma ','": "コンマ ',' で区切られた生成したくない単語", |
| 715 | 715 | "Blacklisted word count to swipe": "スワイプするブラックリストされた単語の数", |
| @@ -724,7 +724,7 @@ | ||
| 724 | 724 | "Auto-swipe": "자동 스와이프", |
| 725 | 725 | "Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "자동 스와이프 기능을 활성화합니다. 이 섹션의 설정은 자동 스와이프가 활성화되었을 때만 영향을 미칩니다", |
| 726 | 726 | "Minimum generated message length": "생성된 메시지 최소 길이", |
| 727 | 727 | "If the generated message is shorter than thisthese many characters, trigger an auto-swipe": "생성된 메시지가이보다 짧으면 자동 스와이프를 트리거합니다", |
| 728 | 728 | "Blacklisted words": "금지어", |
| 729 | 729 | "words you dont want generated separated by comma ','": "쉼표로 구분된 생성하지 않으려는 단어", |
| 730 | 730 | "Blacklisted word count to swipe": "스와이프할 금지어 개수", |
| @@ -709,7 +709,7 @@ | ||
| 709 | 709 | "Auto-swipe": "Automatisch vegen", |
| 710 | 710 | "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", |
| 711 | 711 | "Minimum generated message length": "Minimale gegenereerde berichtlengte", |
| 712 | 712 | "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", |
| 713 | 713 | "Blacklisted words": "Verboden woorden", |
| 714 | 714 | "words you dont want generated separated by comma ','": "woorden die je niet gegenereerd wilt hebben gescheiden door komma ','", |
| 715 | 715 | "Blacklisted word count to swipe": "Aantal verboden woorden om te vegen", |
| @@ -709,7 +709,7 @@ | ||
| 709 | 709 | "Auto-swipe": "Auto-swipe", |
| 710 | 710 | "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", |
| 711 | 711 | "Minimum generated message length": "Comprimento mínimo da mensagem gerada", |
| 712 | 712 | "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", |
| 713 | 713 | "Blacklisted words": "Palavras proibidas", |
| 714 | 714 | "words you dont want generated separated by comma ','": "palavras que você não quer geradas separadas por vírgula ','", |
| 715 | 715 | "Blacklisted word count to swipe": "Contagem de palavras proibidas para swipe", |
| @@ -426,7 +426,7 @@ | ||
| 426 | 426 | "Requests logprobs from the API for the Token Probabilities feature": "Запросить логпробы из API для функции Token Probabilities.", |
| 427 | 427 | "Automatically reject and re-generate AI message based on configurable criteria": "Автоматическое отклонение и повторная генерация сообщений AI на основе настраиваемых критериев.", |
| 428 | 428 | "Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "Включить авто-свайп. Настройки в этом разделе действуют только при включенном авто-свайпе.", |
| 429 | 429 | "If the generated message is shorter than thisthese many characters, trigger an auto-swipe": "Если сгенерированное сообщение короче этого значения, срабатывает авто-свайп.", |
| 430 | 430 | "Reload and redraw the currently open chat": "Перезагрузить и перерисовать открытый в данный момент чат.", |
| 431 | 431 | "Auto-Expand Message Actions": "Развернуть действия", |
| 432 | 432 | "Persona Management": "Управление персоной", |
| @@ -709,7 +709,7 @@ | ||
| 709 | 709 | "Auto-swipe": "Автоматичний змах", |
| 710 | 710 | "Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "Вмикає функцію автоматичного змаху. Налаштування в цьому розділі діють лише тоді, коли увімкнено автоматичний змах", |
| 711 | 711 | "Minimum generated message length": "Мінімальна довжина згенерованого повідомлення", |
| 712 | 712 | "If the generated message is shorter than thisthese many characters, trigger an auto-swipe": "Якщо згенероване повідомлення коротше за це, викликайте автоматичний змаху", |
| 713 | 713 | "Blacklisted words": "Список заборонених слів", |
| 714 | 714 | "words you dont want generated separated by comma ','": "слова, які ви не хочете генерувати, розділені комою ','", |
| 715 | 715 | "Blacklisted word count to swipe": "Кількість заборонених слів для змаху", |
| @@ -709,7 +709,7 @@ | ||
| 709 | 709 | "Auto-swipe": "Tự động vuốt", |
| 710 | 710 | "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", |
| 711 | 711 | "Minimum generated message length": "Độ dài tối thiểu của tin nhắn được tạo", |
| 712 | 712 | "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", |
| 713 | 713 | "Blacklisted words": "Từ trong danh sách đen", |
| 714 | 714 | "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 ','", |
| 715 | 715 | "Blacklisted word count to swipe": "Số từ trong danh sách đen để vuốt", |
| @@ -804,7 +804,7 @@ | ||
| 804 | 804 | "Auto-swipe": "自动滑动", |
| 805 | 805 | "Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "启用自动滑动功能。仅当启用自动滑动时,本节中的设置才会生效", |
| 806 | 806 | "Minimum generated message length": "生成的消息的最小长度", |
| 807 | 807 | "If the generated message is shorter than thisthese many characters, trigger an auto-swipe": "如果生成的消息短于此长度,则触发自动滑动", |
| 808 | 808 | "Blacklisted words": "屏蔽词", |
| 809 | 809 | "words you dont want generated separated by comma ','": "不想生成的词语,用半角逗号“,”分隔", |
| 810 | 810 | "Blacklisted word count to swipe": "触发滑动的黑名单词语数量", |
| @@ -710,7 +710,7 @@ | ||
| 710 | 710 | "Auto-swipe": "自動滑動", |
| 711 | 711 | "Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "啟用自動滑動功能。此部分的設定僅在啟用自動滑動時有效。", |
| 712 | 712 | "Minimum generated message length": "生成訊息的最小長度", |
| 713 | 713 | "If the generated message is shorter than thisthese many characters, trigger an auto-swipe": "如果生成的訊息比這個短,將觸發自動滑動。", |
| 714 | 714 | "Blacklisted words": "黑名單詞語", |
| 715 | 715 | "words you dont want generated separated by comma ','": "您不想生成的文字,使用逗號分隔", |
| 716 | 716 | "Blacklisted word count to swipe": "滑動的黑名單詞語數量", |
| @@ -512,7 +512,7 @@ export const event_types = { | ||
| 512 | 512 | TOOL_CALLS_RENDERED: 'tool_calls_rendered', |
| 513 | 513 | }; |
| 514 | 514 | |
| 515 | 515 | export const eventSource = new EventEmitter([event_types.APP_READY]); |
| 516 | 516 | |
| 517 | 517 | eventSource.on(event_types.CHAT_CHANGED, processChatSlashCommands); |
| 518 | 518 | |
| @@ -1487,7 +1487,7 @@ jQuery(function () { | ||
| 1487 | 1487 | ...chat.filter(x => x?.extra?.type !== system_message_types.ASSISTANT_NOTE), |
| 1488 | 1488 | ]; |
| 1489 | 1489 | |
| 1490 | 1490 | download(JSONchatToSave.stringifymap(chatToSave,(m) null,=> 4JSON.stringify(m)).join('\n'), `Assistant - ${humanizedDateTime()}.jsonjsonl`, 'application/json'); |
| 1491 | 1491 | }); |
| 1492 | 1492 | |
| 1493 | 1493 | // Do not change. #attachFile is added by extension. |
| @@ -338,14 +338,15 @@ export class ReasoningHandler { | ||
| 338 | 338 | return mesChanged; |
| 339 | 339 | } |
| 340 | 340 | |
| 341 | 341 | if (this.state === ReasoningState.None || this.#isHiddenReasoningModel) { |
| 342 | 342 | // If streamed message starts with the opening, cut it out and put all inside reasoning |
| 343 | 343 | if (message.mes.startsWith(power_user.reasoning.prefix) && message.mes.length > power_user.reasoning.prefix.length) { |
| 344 | 344 | this.#isParsingReasoning = true; |
| 345 | 345 | |
| 346 | 346 | // Manually set starting state here, as we might already have received the ending suffix |
| 347 | 347 | this.state = ReasoningState.Thinking; |
| 348 | 348 | this.startTime = this.startTime ?? this.initialTime; |
| 349 | + this.endTime = null; | |
| 349 | 350 | } |
| 350 | 351 | } |
| 351 | 352 | |
| @@ -9,6 +9,9 @@ import { ensureImageFormatSupported, getBase64Async, humanFileSize } from './uti | ||
| 9 | 9 | export let currentUser = null; |
| 10 | 10 | export let accountsEnabled = false; |
| 11 | 11 | |
| 12 | +// Extend the session every 30 minutes | |
| 13 | +const SESSION_EXTEND_INTERVAL = 30 * 60 * 1000; | |
| 14 | + | |
| 12 | 15 | /** |
| 13 | 16 | * Enable or disable user account controls in the UI. |
| 14 | 17 | * @param {boolean} isEnabled User account controls enabled |
| @@ -894,6 +897,24 @@ async function slugify(text) { | ||
| 894 | 897 | } |
| 895 | 898 | } |
| 896 | 899 | |
| 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 | + | |
| 897 | 918 | jQuery(() => { |
| 898 | 919 | $('#logout_button').on('click', () => { |
| 899 | 920 | logout(); |
| @@ -904,4 +925,9 @@ jQuery(() => { | ||
| 904 | 925 | $('#account_button').on('click', () => { |
| 905 | 926 | openUserProfile(); |
| 906 | 927 | }); |
| 928 | + setInterval(async () => { | |
| 929 | + if (currentUser) { | |
| 930 | + await extendUserSession(); | |
| 931 | + } | |
| 932 | + }, SESSION_EXTEND_INTERVAL); | |
| 907 | 933 | }); |
| @@ -55,6 +55,10 @@ | ||
| 55 | 55 | --interactable-outline-color: var(--white100); |
| 56 | 56 | --interactable-outline-color-faint: var(--white20a); |
| 57 | 57 | |
| 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 | + | |
| 58 | 62 | |
| 59 | 63 | /*Default Theme, will be changed by ToolCool Color Picker*/ |
| 60 | 64 | --SmartThemeBodyColor: rgb(220, 220, 210); |
| @@ -348,13 +352,13 @@ input[type='checkbox']:focus-visible { | ||
| 348 | 352 | |
| 349 | 353 | .mes_reasoning { |
| 350 | 354 | display: block; |
| 351 | 355 | border-left: 2px solid var(--SmartThemeEmColorreasoning-body-color); |
| 352 | 356 | border-radius: 2px; |
| 353 | 357 | padding: 5px; |
| 354 | 358 | padding-left: 14px; |
| 355 | 359 | margin-bottom: 0.5em; |
| 356 | 360 | overflow-y: auto; |
| 357 | - color: var(--SmartThemeEmColor); | |
| 361 | + color: hsl(from var(--reasoning-body-color) h calc(s * var(--reasoning-saturation)) l); | |
| 358 | 362 | } |
| 359 | 363 | |
| 360 | 364 | .mes_reasoning_details { |
| @@ -374,18 +378,6 @@ input[type='checkbox']:focus-visible { | ||
| 374 | 378 | margin-bottom: 0; |
| 375 | 379 | } |
| 376 | 380 | |
| 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 | - | |
| 389 | 381 | .mes_reasoning_header_block { |
| 390 | 382 | flex-grow: 1; |
| 391 | 383 | } |
| @@ -461,26 +453,36 @@ input[type='checkbox']:focus-visible { | ||
| 461 | 453 | } |
| 462 | 454 | |
| 463 | 455 | .mes_text i, |
| 464 | 456 | .mes_text em, { |
| 457 | + color: var(--SmartThemeEmColor); | |
| 458 | +} | |
| 465 | 459 | .mes_reasoning i, |
| 466 | 460 | .mes_reasoning em { |
| 467 | - color: var(--SmartThemeEmColor); | |
| 461 | + color: hsl(from var(--reasoning-em-color) h calc(s * var(--reasoning-saturation)) l); | |
| 468 | 462 | } |
| 469 | 463 | |
| 470 | 464 | .mes_text q i, |
| 471 | 465 | .mes_text q em { |
| 472 | 466 | color: inherit; |
| 473 | 467 | } |
| 468 | +.mes_reasoning q i, | |
| 469 | +.mes_reasoning q em { | |
| 470 | + color: hsl(from var(--SmartThemeQuoteColor) h calc(s * var(--reasoning-saturation)) l); | |
| 471 | +} | |
| 474 | 472 | |
| 475 | 473 | .mes_text u, { |
| 476 | -.mes_reasoning u { | |
| 477 | 474 | color: var(--SmartThemeUnderlineColor); |
| 478 | 475 | } |
| 476 | +.mes_reasoning u { | |
| 477 | + color: hsl(from var(--SmartThemeUnderlineColor) h calc(s * var(--reasoning-saturation)) l); | |
| 478 | +} | |
| 479 | 479 | |
| 480 | 480 | .mes_text q, { |
| 481 | -.mes_reasoning q { | |
| 482 | 481 | color: var(--SmartThemeQuoteColor); |
| 483 | 482 | } |
| 483 | +.mes_reasoning q { | |
| 484 | + color: hsl(from var(--SmartThemeQuoteColor) h calc(s * var(--reasoning-saturation)) l); | |
| 485 | +} | |
| 484 | 486 | |
| 485 | 487 | .mes_text font[color] em, |
| 486 | 488 | .mes_text font[color] i, |
| @@ -556,7 +556,13 @@ app.use('/api/users', usersPublicRouter); | ||
| 556 | 556 | |
| 557 | 557 | // Everything below this line requires authentication |
| 558 | 558 | app.use(requireLoginMiddleware); |
| 559 | 559 | app.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 | +}); | |
| 560 | 566 | |
| 561 | 567 | // File uploads |
| 562 | 568 | app.use(multer({ dest: uploadsPath, limits: { fieldSize: 10 * 1024 * 1024 } }).single('avatar')); |
| @@ -230,7 +230,7 @@ router.post('/version', jsonParser, async (request, response) => { | ||
| 230 | 230 | } catch (error) { |
| 231 | 231 | // it is not a git repo, or has no commits yet, or is a bare repo |
| 232 | 232 | // not possible to update it, most likely can't get the branch name either |
| 233 | 233 | return response.send({ currentBranchName: null'', currentCommitHash: '', isUpToDate: true, remoteUrl: null'' }); |
| 234 | 234 | } |
| 235 | 235 | |
| 236 | 236 | const currentBranch = await git.cwd(extensionPath).branch(); |
| @@ -3,13 +3,16 @@ import crypto from 'node:crypto'; | ||
| 3 | 3 | import storage from 'node-persist'; |
| 4 | 4 | import express from 'express'; |
| 5 | 5 | import { RateLimiterMemory, RateLimiterRes } from 'rate-limiter-flexible'; |
| 6 | 6 | import { jsonParser, getIpFromRequest, getRealIpFromHeader } from '../express-common.js'; |
| 7 | 7 | import { color, Cache, getConfigValue } from '../util.js'; |
| 8 | 8 | import { KEY_PREFIX, getUserAvatar, toKey, getPasswordHash, getPasswordSalt } from '../users.js'; |
| 9 | 9 | |
| 10 | 10 | const DISCREET_LOGIN = getConfigValue('enableDiscreetLogin', false); |
| 11 | +const PREFER_REAL_IP_HEADER = getConfigValue('rateLimiting.preferRealIpHeader', false); | |
| 11 | 12 | const MFA_CACHE = new Cache(5 * 60 * 1000); |
| 12 | 13 | |
| 14 | +const getIpAddress = (request) => PREFER_REAL_IP_HEADER ? getRealIpFromHeader(request) : getIpFromRequest(request); | |
| 15 | + | |
| 13 | 16 | export const router = express.Router(); |
| 14 | 17 | const loginLimiter = new RateLimiterMemory({ |
| 15 | 18 | points: 5, |
| @@ -60,7 +63,7 @@ router.post('/login', jsonParser, async (request, response) => { | ||
| 60 | 63 | return response.status(400).json({ error: 'Missing required fields' }); |
| 61 | 64 | } |
| 62 | 65 | |
| 63 | 66 | const ip = getIpFromRequestgetIpAddress(request); |
| 64 | 67 | await loginLimiter.consume(ip); |
| 65 | 68 | |
| 66 | 69 | /** @type {import('../users.js').User} */ |
| @@ -92,7 +95,7 @@ router.post('/login', jsonParser, async (request, response) => { | ||
| 92 | 95 | return response.json({ handle: user.handle }); |
| 93 | 96 | } catch (error) { |
| 94 | 97 | if (error instanceof RateLimiterRes) { |
| 95 | 98 | console.error('Login failed: Rate limited from', getIpFromRequestgetIpAddress(request)); |
| 96 | 99 | return response.status(429).send({ error: 'Too many attempts. Try again later or recover your password.' }); |
| 97 | 100 | } |
| 98 | 101 | |
| @@ -108,7 +111,7 @@ router.post('/recover-step1', jsonParser, async (request, response) => { | ||
| 108 | 111 | return response.status(400).json({ error: 'Missing required fields' }); |
| 109 | 112 | } |
| 110 | 113 | |
| 111 | 114 | const ip = getIpFromRequestgetIpAddress(request); |
| 112 | 115 | await recoverLimiter.consume(ip); |
| 113 | 116 | |
| 114 | 117 | /** @type {import('../users.js').User} */ |
| @@ -132,7 +135,7 @@ router.post('/recover-step1', jsonParser, async (request, response) => { | ||
| 132 | 135 | return response.sendStatus(204); |
| 133 | 136 | } catch (error) { |
| 134 | 137 | if (error instanceof RateLimiterRes) { |
| 135 | 138 | console.error('Recover step 1 failed: Rate limited from', getIpFromRequestgetIpAddress(request)); |
| 136 | 139 | return response.status(429).send({ error: 'Too many attempts. Try again later or contact your admin.' }); |
| 137 | 140 | } |
| 138 | 141 | |
| @@ -150,7 +153,7 @@ router.post('/recover-step2', jsonParser, async (request, response) => { | ||
| 150 | 153 | |
| 151 | 154 | /** @type {import('../users.js').User} */ |
| 152 | 155 | const user = await storage.getItem(toKey(request.body.handle)); |
| 153 | 156 | const ip = getIpFromRequestgetIpAddress(request); |
| 154 | 157 | |
| 155 | 158 | if (!user) { |
| 156 | 159 | console.error('Recover step 2 failed: User', request.body.handle, 'not found'); |
| @@ -186,7 +189,7 @@ router.post('/recover-step2', jsonParser, async (request, response) => { | ||
| 186 | 189 | return response.sendStatus(204); |
| 187 | 190 | } catch (error) { |
| 188 | 191 | if (error instanceof RateLimiterRes) { |
| 189 | 192 | console.error('Recover step 2 failed: Rate limited from', getIpFromRequestgetIpAddress(request)); |
| 190 | 193 | return response.status(429).send({ error: 'Too many attempts. Try again later or contact your admin.' }); |
| 191 | 194 | } |
| 192 | 195 | |
| @@ -25,3 +25,17 @@ export function getIpFromRequest(req) { | ||
| 25 | 25 | } |
| 26 | 26 | return clientIp; |
| 27 | 27 | } |
| 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 | +} | |
| @@ -1,11 +1,8 @@ | ||
| 1 | 1 | import path from 'node:path'; |
| 2 | 2 | import webpack from 'webpack'; |
| 3 | 3 | import { publicLibConfig }getPublicLibConfig from '../../webpack.config.js'; |
| 4 | 4 | |
| 5 | 5 | export default function getWebpackServeMiddleware() { |
| 6 | - const outputPath = publicLibConfig.output?.path; | |
| 7 | - const outputFile = publicLibConfig.output?.filename; | |
| 8 | - | |
| 9 | 6 | /** |
| 10 | 7 | * A very spartan recreation of webpack-dev-middleware. |
| 11 | 8 | * @param {import('express').Request} req Request object. |
| @@ -14,6 +11,10 @@ export default function getWebpackServeMiddleware() { | ||
| 14 | 11 | * @type {import('express').RequestHandler} |
| 15 | 12 | */ |
| 16 | 13 | function devMiddleware(req, res, next) { |
| 14 | + const publicLibConfig = getPublicLibConfig(); | |
| 15 | + const outputPath = publicLibConfig.output?.path; | |
| 16 | + const outputFile = publicLibConfig.output?.filename; | |
| 17 | + | |
| 17 | 18 | if (req.method === 'GET' && path.parse(req.path).base === outputFile) { |
| 18 | 19 | return res.sendFile(outputFile, { root: outputPath }); |
| 19 | 20 | } |
| @@ -23,9 +24,12 @@ export default function getWebpackServeMiddleware() { | ||
| 23 | 24 | |
| 24 | 25 | /** |
| 25 | 26 | * Wait until Webpack is done compiling. |
| 27 | + * @param {object} param Parameters. | |
| 28 | + * @param {boolean} [param.forceDist] Whether to force the use the /dist folder. | |
| 26 | 29 | * @returns {Promise<void>} |
| 27 | 30 | */ |
| 28 | 31 | devMiddleware.runWebpackCompiler = ({ forceDist = false } = {}) => { |
| 32 | + const publicLibConfig = getPublicLibConfig(forceDist); | |
| 29 | 33 | const compiler = webpack(publicLibConfig); |
| 30 | 34 | |
| 31 | 35 | return new Promise((resolve) => { |
| @@ -3,7 +3,7 @@ import path from 'node:path'; | ||
| 3 | 3 | import url from 'node:url'; |
| 4 | 4 | |
| 5 | 5 | import express from 'express'; |
| 6 | 6 | import { default as git, CheckRepoActions } from 'simple-git'; |
| 7 | 7 | import { sync as commandExistsSync } from 'command-exists'; |
| 8 | 8 | import { getConfigValue, color } from './util.js'; |
| 9 | 9 | |
| @@ -256,7 +256,7 @@ async function updatePlugins(pluginsPath) { | ||
| 256 | 256 | const pluginPath = path.join(pluginsPath, directory); |
| 257 | 257 | const pluginRepo = git(pluginPath); |
| 258 | 258 | |
| 259 | 259 | const isRepo = await pluginRepo.checkIsRepo(CheckRepoActions.IS_REPO_ROOT); |
| 260 | 260 | if (!isRepo) { |
| 261 | 261 | continue; |
| 262 | 262 | } |
| @@ -1,13 +1,49 @@ | ||
| 1 | 1 | import process from 'node:process'; |
| 2 | 2 | import path from 'node:path'; |
| 3 | +import isDocker from 'is-docker'; | |
| 3 | 4 | |
| 4 | -/** @type {import('webpack').Configuration} */ | |
| 5 | +/** | |
| 5 | -export const publicLibConfig = { | |
| 6 | + * Get the Webpack configuration for the public/lib.js file. | |
| 7 | + * 1. Docker has got cache and the output file pre-baked. | |
| 8 | + * 2. Non-Docker environments use the global DATA_ROOT variable to determine the cache and output directories. | |
| 9 | + * @param {boolean} forceDist Whether to force the use the /dist folder. | |
| 10 | + * @returns {import('webpack').Configuration} | |
| 11 | + * @throws {Error} If the DATA_ROOT variable is not set. | |
| 12 | + * */ | |
| 13 | +export default function getPublicLibConfig(forceDist = false) { | |
| 14 | + function getCacheDirectory() { | |
| 15 | + if (forceDist || isDocker()) { | |
| 16 | + return path.resolve(process.cwd(), 'dist/webpack'); | |
| 17 | + } | |
| 18 | + | |
| 19 | + if (typeof globalThis.DATA_ROOT === 'string') { | |
| 20 | + return path.resolve(globalThis.DATA_ROOT, '_webpack', 'cache'); | |
| 21 | + } | |
| 22 | + | |
| 23 | + throw new Error('DATA_ROOT variable is not set.'); | |
| 24 | + } | |
| 25 | + | |
| 26 | + function getOutputDirectory() { | |
| 27 | + if (forceDist || isDocker()) { | |
| 28 | + return path.resolve(process.cwd(), 'dist'); | |
| 29 | + } | |
| 30 | + | |
| 31 | + if (typeof globalThis.DATA_ROOT === 'string') { | |
| 32 | + return path.resolve(globalThis.DATA_ROOT, '_webpack', 'output'); | |
| 33 | + } | |
| 34 | + | |
| 35 | + throw new Error('DATA_ROOT variable is not set.'); | |
| 36 | + } | |
| 37 | + | |
| 38 | + const cacheDirectory = getCacheDirectory(); | |
| 39 | + const outputDirectory = getOutputDirectory(); | |
| 40 | + | |
| 41 | + return { | |
| 6 | 42 | mode: 'production', |
| 7 | 43 | entry: './public/lib.js', |
| 8 | 44 | cache: { |
| 9 | 45 | type: 'filesystem', |
| 10 | - cacheDirectory: path.resolve(process.cwd(), 'dist/webpack'), | |
| 46 | + cacheDirectory: cacheDirectory, | |
| 11 | 47 | store: 'pack', |
| 12 | 48 | compression: 'gzip', |
| 13 | 49 | }, |
| @@ -28,8 +64,9 @@ export const publicLibConfig = { | ||
| 28 | 64 | hints: false, |
| 29 | 65 | }, |
| 30 | 66 | output: { |
| 31 | - path: path.resolve(process.cwd(), 'dist'), | |
| 67 | + path: outputDirectory, | |
| 32 | 68 | filename: 'lib.js', |
| 33 | 69 | libraryTarget: 'module', |
| 34 | 70 | }, |
| 35 | 71 | }; |
| 72 | +} | |