Merge branch 'staging' of https://github.com/SillyTavern/SillyTavern into staging

This commit is contained in:
WBlair1 2024-07-03 16:31:15 -07:00
commit 1af76af4d7
30 changed files with 393 additions and 143 deletions

View File

@ -1,4 +1,6 @@
.git
.github
.vscode
node_modules
npm-debug.log
readme*

View File

@ -1,5 +1,5 @@
name: Bug Report 🐛
description: Report something that's not working the intended way. Support requests for external programs (reverse proxies, 3rd party servers, other peoples' forks) will be refused!
description: Report something that's not working the intended way. Support requests for external programs (reverse proxies, 3rd party servers, other peoples' forks) will be refused! Please use English only.
title: '[BUG] <title>'
labels: ['🐛 Bug']
body:

View File

@ -1,5 +1,5 @@
name: Feature Request ✨
description: Suggest an idea for future development of this project
description: Suggest an idea for future development of this project. Please use English only.
title: '[FEATURE_REQUEST] <title>'
labels: ['🦄 Feature Request']

5
.github/pull_request_template.md vendored Normal file
View File

@ -0,0 +1,5 @@
<!-- Put X in the box below to confirm -->
## Checklist:
- [ ] I have read the [Contributing guidelines](https://github.com/SillyTavern/SillyTavern/blob/release/CONTRIBUTING.md).

View File

@ -8,3 +8,6 @@ secrets.json
/data
/cache
access.log
.github
.vscode
.git

32
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,32 @@
# How to contribute to SillyTavern
## Setting up the dev environment
1. Required software: git and node.
2. Recommended editor: Visual Studio Code.
3. You can also use GitHub Codespaces which sets up everything for you.
## Getting the code ready
1. Register a GitHub account.
2. Fork this repository under your account.
3. Clone the fork onto your machine.
4. Open the cloned repository in the code editor.
5. Create a git branch (recommended).
6. Make your changes and test them locally.
7. Commit the changes and push the branch to the remote repo.
8. Go to GitHub, and open a pull request, targeting the upstream branch.
## Contribution guidelines
1. Our standards are pretty low, but make sure the code is not too ugly:
- Run VS Code's autoformat when you're done.
- Check with ESLint by running `npm run lint`, then fix the errors.
- Use common sense and follow existing naming conventions.
2. Create pull requests for the staging branch, 99% of contributions should go there. That way people could test your code before the next stable release.
3. You can still send a pull request for release in the following scenarios:
- Updating README.
- Updating GitHub Actions.
- Hotfixing a critical bug.
4. Project maintainers will test and can change your code before merging.
5. Mind the license. Your contributions will be licensed under the GNU Affero General Public License. If you don't know what that implies, consult your lawyer.

View File

@ -19,6 +19,10 @@
"filename": "themes/Dark V 1.0.json",
"type": "theme"
},
{
"filename": "themes/Azure.json",
"type": "theme"
},
{
"filename": "backgrounds/__transparent.png",
"type": "background"

View File

@ -0,0 +1,35 @@
{
"name": "Azure",
"blur_strength": 11,
"main_text_color": "rgba(171, 198, 223, 1)",
"italics_text_color": "rgba(255, 255, 255, 1)",
"underline_text_color": "rgba(188, 231, 207, 1)",
"quote_text_color": "rgba(111, 133, 253, 1)",
"blur_tint_color": "rgba(23, 30, 33, 0.61)",
"chat_tint_color": "rgba(23, 23, 23, 0)",
"user_mes_blur_tint_color": "rgba(0, 28, 174, 0.2)",
"bot_mes_blur_tint_color": "rgba(0, 13, 57, 0.22)",
"shadow_color": "rgba(0, 0, 0, 1)",
"shadow_width": 5,
"border_color": "rgba(0, 0, 0, 0.5)",
"font_scale": 1,
"fast_ui_mode": false,
"waifuMode": false,
"avatar_style": 1,
"chat_display": 1,
"noShadows": false,
"chat_width": 50,
"timer_enabled": true,
"timestamps_enabled": true,
"timestamp_model_icon": false,
"mesIDDisplay_enabled": true,
"message_token_count_enabled": false,
"expand_message_actions": false,
"enableZenSliders": false,
"enableLabMode": false,
"hotswap_enabled": true,
"custom_css": "",
"bogus_folders": false,
"reduced_motion": false,
"compact_input_area": false
}

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{
"name": "sillytavern",
"version": "1.12.2",
"version": "1.12.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "sillytavern",
"version": "1.12.2",
"version": "1.12.3",
"hasInstallScript": true,
"license": "AGPL-3.0",
"dependencies": {

View File

@ -70,7 +70,7 @@
"type": "git",
"url": "https://github.com/SillyTavern/SillyTavern.git"
},
"version": "1.12.2",
"version": "1.12.3",
"scripts": {
"start": "node server.js",
"start:no-csrf": "node server.js --disableCsrf",

View File

@ -10,9 +10,9 @@
width: 100svw;
height: 100svh;
background-color: var(--SmartThemeBlurTintColor);
color: var(--SmartThemeBodyColor);
/*for some reason the full screen blur does not work on iOS*/
backdrop-filter: blur(30px);
color: var(--SmartThemeBodyColor);
opacity: 1;
}

View File

@ -1,8 +1,10 @@
/* iPhone copium land */
@media screen and (max-width: 1000px) {
.ios .popup .popup-body {
height: fit-content;
max-height: 90vh;
max-height: 90svh;
}
body.safari .popup .popup-body:has(.maximized_textarea) {
height: 100%;
}
body.safari .popup .popup-body {
height: fit-content;
max-height: 90vh;
max-height: 90svh;
}

View File

@ -1969,7 +1969,7 @@
<small data-i18n="Example: http://127.0.0.1:5000/api ">Example: http://127.0.0.1:5000/api </small>
<input id="api_url_text" name="api_url" class="text_pole" placeholder="http://127.0.0.1:5000/api" maxlength="500" value="" autocomplete="off" data-server-history="kobold">
<div id="koboldcpp_hint" class="neutral_warning displayNone">
We have a dedicated KoboldCpp support under Text Completion ⇒ KoboldCpp.
KoboldCpp works better when you select the Text Completion API and then KoboldCpp as a type!
</div>
<div class="flex-container">
<div id="api_button" class="api_button menu_button" type="submit" data-i18n="Connect" data-server-connect="kobold">Connect</div>

View File

@ -1,6 +1,6 @@
{
"Favorite": "お気に入り",
"Tag": "鬼ごっこ",
"Tag": "タグ",
"Duplicate": "重複",
"Persona": "ペルソナ",
"Delete": "削除",
@ -29,13 +29,13 @@
"Text Adventure": "テキストアドベンチャー",
"response legth(tokens)": "応答の長さ(トークン数)",
"Streaming": "ストリーミング",
"Streaming_desc": "生成された応答をビット単位で表示します。",
"Streaming_desc": "生成された応答を逐次表示します。",
"context size(tokens)": "コンテキストのサイズ(トークン数)",
"unlocked": "ロック解除",
"Only enable this if your model supports context sizes greater than 4096 tokens": "モデルが4096トークンを超えるコンテキストサイズをサポートしている場合にのみ有効にします",
"Max prompt cost:": "最大プロンプトコスト:",
"Display the response bit by bit as it is generated.": "生成されるたびに、応答をビットごとに表示します。",
"When this is off, responses will be displayed all at once when they are complete.": "この機能がオフの場合、応答は完全になるとすぐにすべて一度に表示されます。",
"Display the response bit by bit as it is generated.": "生成されるたびに、応答を逐次表示します。",
"When this is off, responses will be displayed all at once when they are complete.": "この機能がオフの場合、応答は完全に生成されたときに一度ですべて表示されます。",
"Temperature": "温度",
"rep.pen": "繰り返しペナルティ",
"Rep. Pen. Range.": "繰り返しペナルティの範囲",
@ -46,10 +46,10 @@
"Phrase Repetition Penalty": "フレーズの繰り返しペナルティ",
"Off": "オフ",
"Very light": "非常に軽い",
"Light": "ライト",
"Medium": "ミディアム",
"Aggressive": "攻撃的",
"Very aggressive": "非常に攻撃的",
"Light": "軽め",
"Medium": "中程度",
"Aggressive": "強め",
"Very aggressive": "非常に強い",
"Unlocked Context Size": "ロック解除されたコンテキストサイズ",
"Unrestricted maximum value for the context slider": "コンテキストスライダーの制限なしの最大値",
"Context Size (tokens)": "コンテキストサイズ(トークン数)",
@ -132,7 +132,7 @@
"CFG Scale": "CFGスケール",
"Negative Prompt": "ネガティブプロンプト",
"Add text here that would make the AI generate things you don't want in your outputs.": "出力に望ましくないものを生成させるAIを作成するテキストをここに追加します。",
"Used if CFG Scale is unset globally, per chat or character": "CFGスケールがグローバル、チャットごと、または文字ごとに設定されていない場合に使用されます",
"Used if CFG Scale is unset globally, per chat or character": "CFGスケールがグローバル、チャットごと、またはキャラクターごとに設定されていない場合に使用されます",
"Mirostat Tau": "Mirostat Tau",
"Mirostat LR": "ミロスタットLR",
"Min Length": "最小長",
@ -214,7 +214,7 @@
"Sampler Priority": "サンプラー優先度",
"Ooba only. Determines the order of samplers.": "Oobaのみ。サンプラーの順序を決定します。",
"Character Names Behavior": "キャラクター名の動作",
"Helps the model to associate messages with characters.": "モデルがメッセージを文字に関連付けるのに役立ちます。",
"Helps the model to associate messages with characters.": "モデルがメッセージをキャラクターに関連付けるのに役立ちます。",
"None": "なし",
"character_names_none": "グループと過去のペルソナを除きます。それ以外の場合は、プロンプトに名前を必ず入力してください。",
"Don't add character names.": "キャラクター名を追加しないでください。",
@ -222,7 +222,7 @@
"character_names_completion": "制限事項: ラテン英数字とアンダースコアのみ。すべてのソースで機能するわけではありません。特に、Claude、MistralAI、Google では機能しません。",
"Add character names to completion objects.": "完了オブジェクトにキャラクター名を追加します。",
"Message Content": "メッセージ内容",
"Prepend character names to message contents.": "メッセージの内容の先頭に文字名を追加します。",
"Prepend character names to message contents.": "メッセージの内容の先頭にキャラクター名を追加します。",
"Continue Postfix": "ポストフィックスの継続",
"The next chunk of the continued message will be appended using this as a separator.": "継続メッセージの次のチャンクは、これを区切り文字として使用して追加されます。",
"Space": "空間",
@ -270,9 +270,9 @@
"Text Completion": "テキスト補完",
"Chat Completion": "チャット完了",
"NovelAI": "NovelAI",
"KoboldAI Horde": "KoboldAIホルド",
"KoboldAI Horde": "KoboldAI Horde",
"KoboldAI": "KoboldAI",
"Avoid sending sensitive information to the Horde.": "ホルドに機密情報を送信しないでください。",
"Avoid sending sensitive information to the Horde.": "Hordeに機密情報を送信しないでください。",
"Review the Privacy statement": "プライバシー声明を確認する",
"Register a Horde account for faster queue times": "キュー待ち時間を短縮するためにHordeアカウントを登録する",
"Learn how to contribute your idle GPU cycles to the Horde": "アイドルのGPUサイクルをホルドに貢献する方法を学びます",
@ -633,7 +633,7 @@
"Tags_as_Folders_desc": "最近の変更: タグは、タグ管理メニューでフォルダーとしてマークされて初めてフォルダーとして表示されます。ここをクリックして表示します。",
"Character Handling": "キャラクター処理",
"If set in the advanced character definitions, this field will be displayed in the characters list.": "高度なキャラクター定義で設定されている場合、このフィールドがキャラクターリストに表示されます。",
"Char List Subheader": "文字リストサブヘッダー",
"Char List Subheader": "キャラクターリストサブヘッダー",
"Character Version": "キャラクターバージョン",
"Created by": "作成者",
"Use fuzzy matching, and search characters in the list by all data fields, not just by a name substring": "曖昧な一致を使用し、名前の部分文字列ではなく、すべてのデータフィールドでリスト内のキャラクターを検索する",
@ -729,8 +729,8 @@
"Automatically hide details": "詳細を自動的に非表示にする",
"Determines how entries are found for autocomplete.": "オートコンプリートのエントリの検索方法を決定します。",
"Autocomplete Matching": "マッチング",
"Starts with": "始まりは",
"Includes": "含まれるもの",
"Starts with": "前方一致",
"Includes": "部分一致",
"Fuzzy": "ファジー",
"Sets the style of the autocomplete.": "オートコンプリートのスタイルを設定します。",
"Autocomplete Style": "スタイル",
@ -755,8 +755,8 @@
"Auto-select": "自動選択",
"System Backgrounds": "システムの背景",
"Chat Backgrounds": "チャットの背景",
"bg_chat_hint_1": "チャットの背景は、",
"bg_chat_hint_2": "拡張子がここに表示されます。",
"bg_chat_hint_1": "",
"bg_chat_hint_2": "拡張機能で生成したチャットの背景はここに表示されます。",
"Extensions": "拡張機能",
"Notify on extension updates": "拡張機能の更新時に通知",
"Manage extensions": "拡張機能を管理",
@ -770,9 +770,9 @@
"How do I use this?": "これをどのように使用しますか?",
"Click for stats!": "統計をクリック!",
"Usage Stats": "使用状況統計",
"Backup your personas to a file": "キャラクタをファイルにバックアップします",
"Backup your personas to a file": "キャラクタをファイルにバックアップします",
"Backup": "バックアップ",
"Restore your personas from a file": "ファイルからキャラクタを復元します",
"Restore your personas from a file": "ファイルからキャラクタを復元します",
"Restore": "復元",
"Create a dummy persona": "ダミーのペルソナを作成",
"Create": "作成",
@ -839,7 +839,7 @@
"Describe your character's physical and mental traits here.": "ここにキャラクターの身体的および精神的特徴を説明します。",
"First message": "最初のメッセージ",
"Click to set additional greeting messages": "追加の挨拶メッセージを設定するにはクリック",
"Alt. Greetings": "挨拶",
"Alt. Greetings": "他の挨拶",
"This will be the first message from the character that starts every chat.": "これはすべてのチャットを開始するキャラクターからの最初のメッセージになります。",
"Group Controls": "グループコントロール",
"Chat Name (Optional)": "チャット名(任意)",
@ -889,7 +889,7 @@
"popup-button-yes": "はい",
"popup-button-no": "いいえ",
"popup-button-cancel": "キャンセル",
"popup-button-import": "輸入",
"popup-button-import": "インポート",
"Advanced Defininitions": "高度な定義",
"Prompt Overrides": "プロンプトのオーバーライド",
"(For Chat Completion and Instruct Mode)": "(チャット補完と指示モード用)",
@ -937,7 +937,7 @@
"Type here...": "ここに入力...",
"Chat Lorebook": "チャットロアブック",
"Chat Lorebook for": "チャットロアブック",
"chat_world_template_txt": "選択したワールド情報はこのチャットにバインドされます。AI の返信を生成する際、\nグローバルおよびキャラクターの伝承書のエントリと結合されます。",
"chat_world_template_txt": "選択したワールド情報はこのチャットにバインドされます。AI の返信を生成する際、\nグローバルおよびキャラクターのロアブックのエントリと結合されます。",
"Select a World Info file for": "次のためにワールド情報ファイルを選択",
"Primary Lorebook": "プライマリロアブック",
"A selected World Info will be bound to this character as its own Lorebook.": "選択したワールド情報は、このキャラクターにその独自のロアブックとしてバインドされます。",
@ -1065,9 +1065,9 @@
"Change it later in the 'User Settings' panel.": "後で「ユーザー設定」パネルで変更します。",
"Enable simple UI mode": "シンプルUIモードを有効にする",
"Looking for AI characters?": "AIキャラクターをお探しですか?",
"onboarding_import": "輸入",
"onboarding_import": "インポート",
"from supported sources or view": "サポートされているソースからまたは表示",
"Sample characters": "サンプル文字",
"Sample characters": "サンプルキャラクター",
"Your Persona": "あなたのペルソナ",
"Before you get started, you must select a persona name.": "始める前に、ペルソナ名を選択する必要があります。",
"welcome_message_part_8": "これはいつでも変更可能です。",
@ -1081,7 +1081,7 @@
"View character card": "キャラクターカードを表示",
"Remove from group": "グループから削除",
"Add to group": "グループに追加",
"Alternate Greetings": "代わりの挨拶",
"Alternate Greetings": "挨拶のバリエーション",
"Alternate_Greetings_desc": "これらは、新しいチャットを開始するときに最初のメッセージにスワイプとして表示されます。\nグループのメンバーは、そのうちの 1 つを選択して会話を開始できます。",
"Alternate Greetings Hint": "ボタンをクリックして始めましょう!",
"(This will be the first message from the character that starts every chat)": "(これはすべてのチャットを開始するキャラクターからの最初のメッセージになります)",
@ -1118,11 +1118,11 @@
"Will be used as the default CFG options for every chat unless overridden.": "上書きされない限り、すべてのチャットのデフォルトの CFG オプションとして使用されます。",
"CFG Prompt Cascading": "CFG プロンプト カスケード",
"Combine positive/negative prompts from other boxes.": "他のボックスからの肯定的/否定的なプロンプトを組み合わせます。",
"For example, ticking the chat, global, and character boxes combine all negative prompts into a comma-separated string.": "たとえば、チャット、グローバル、および文字のボックスにチェックを入れると、すべての否定プロンプトがコンマ区切りの文字列に結合されます。",
"For example, ticking the chat, global, and character boxes combine all negative prompts into a comma-separated string.": "たとえば、チャット、グローバル、およびキャラクターのボックスにチェックを入れると、すべてのネガティブプロンプトがコンマ区切りの文字列に結合されます。",
"Always Include": "常に含めます",
"Chat Negatives": "チャットのネガティブ",
"Character Negatives": "性格のマイナス面",
"Global Negatives": "世界的なマイナス",
"Character Negatives": "キャラクターのネガティブ",
"Global Negatives": "グローバルネガティブ",
"Custom Separator:": "カスタムセパレーター:",
"Insertion Depth:": "挿入深さ:",
"Token Probabilities": "トークン確率",
@ -1236,7 +1236,7 @@
"ext_regex_title": "正規表現",
"ext_regex_new_global_script": "+ グローバル",
"ext_regex_new_scoped_script": "+ スコープ付き",
"ext_regex_import_script": "輸入",
"ext_regex_import_script": "インポート",
"ext_regex_global_scripts": "グローバルスクリプト",
"ext_regex_global_scripts_desc": "すべてのキャラクターで使用可能。ローカル設定に保存されます。",
"ext_regex_scoped_scripts": "スコープ付きスクリプト",
@ -1301,8 +1301,8 @@
"Authentication (optional)": "認証(オプション)",
"Example: username:password": "例: ユーザー名:パスワード",
"Important:": "重要:",
"sd_auto_auth_warning_1": "SD Web UIを実行する",
"sd_auto_auth_warning_2": "フラグ! サーバーは SillyTavern ホスト マシンからアクセスできる必要があります。",
"sd_auto_auth_warning_1": "SD Web UIを",
"sd_auto_auth_warning_2": "フラグを指定して実行してください! サーバーは SillyTavern ホスト マシンからアクセスできる必要があります。",
"sd_drawthings_url": "例: {{drawthings_url}}",
"sd_drawthings_auth_txt": "UI で HTTP API スイッチを有効にして DrawThings アプリを実行します。サーバーは SillyTavern ホスト マシンからアクセスできる必要があります。",
"sd_vlad_url": "例: {{vlad_url}}",
@ -1326,36 +1326,36 @@
"Enhance": "強化する",
"Refine": "リファイン",
"Decrisper": "デクリスパー",
"Sampling steps": "サンプリング手順 ()",
"Width": "幅 ",
"Height": "身長 ",
"Resolution": "解",
"Sampling steps": "サンプリングステップ数",
"Width": "幅",
"Height": "高さ",
"Resolution": "解像度",
"Model": "モデル",
"Sampling method": "サンプリング方法",
"Karras (not all samplers supported)": "Karras (すべてのサンプラーがサポートされているわけではありません)",
"SMEA versions of samplers are modified to perform better at high resolution.": "SMEA バージョンのサンプラーは、高解像度でより優れたパフォーマンスを発揮するように変更されています。",
"SMEA": "中小企業庁",
"SMEA": "SMEA",
"DYN variants of SMEA samplers often lead to more varied output, but may fail at very high resolutions.": "SMEA サンプラーの DYN バリアントは、多くの場合、より多様な出力をもたらしますが、非常に高い解像度では失敗する可能性があります。",
"DYN": "ダイナミック",
"Scheduler": "スケジューラ",
"Restore Faces": "顔を復元する",
"Hires. Fix": "雇用。修正",
"Scheduler": "スケジューラ",
"Restore Faces": "顔の修復",
"Hires. Fix": "高解像度補助",
"Upscaler": "アップスケーラー",
"Upscale by": "高級化",
"Upscale by": "アップスケール倍率",
"Denoising strength": "ノイズ除去の強さ",
"Hires steps (2nd pass)": "採用手順2回目のパス",
"Preset for prompt prefix and negative prompt": "プロンプトプレフィックスと否定プロンプトのプリセット",
"Hires steps (2nd pass)": "高解像度でのステップ数",
"Preset for prompt prefix and negative prompt": "プロンプトプレフィックスとネガティブプロンプトのプリセット",
"Style": "スタイル",
"Save style": "スタイルを保存",
"Delete style": "スタイルを削除",
"Common prompt prefix": "一般的なプロンプトプレフィックス",
"Common prompt prefix": "共通のプロンプトプレフィックス",
"sd_prompt_prefix_placeholder": "生成されたプロンプトを挿入する場所を指定するには、{prompt}を使用します。",
"Negative common prompt prefix": "否定の共通プロンプト接頭辞",
"Character-specific prompt prefix": "文字固有のプロンプトプレフィックス",
"Negative common prompt prefix": "共通のネガティブプロンプトプレフィックス",
"Character-specific prompt prefix": "キャラクター固有のプロンプトプレフィックス",
"Won't be used in groups.": "グループでは使用されません。",
"sd_character_prompt_placeholder": "現在選択されているキャラクターを説明する任意の特性。共通のプロンプト プレフィックスの後に追加されます。\n例: 女性、緑の目、茶色の髪、ピンクのシャツ",
"Character-specific negative prompt prefix": "文字固有の否定プロンプト接頭辞",
"sd_character_negative_prompt_placeholder": "選択したキャラクターに表示されるべきではない特性。否定の共通プロンプト接頭辞の後に追加されます。\n例: ジュエリー、靴、メガネ",
"sd_character_prompt_placeholder": "現在選択されているキャラクターを説明する特徴。共通のプロンプトプレフィックスの後に追加されます。\n例: 女性、緑の目、茶色の髪、ピンクのシャツ",
"Character-specific negative prompt prefix": "キャラクター固有のネガティブプロンプトプレフィックス",
"sd_character_negative_prompt_placeholder": "選択したキャラクターに表示されるべきではない特徴。共通のネガティブプロンプトプレフィックスの後に追加されます。\n例: ジュエリー、靴、メガネ",
"Shareable": "共有可能",
"Image Prompt Templates": "画像プロンプトテンプレート",
"Vectors Model Warning": "チャットの途中でモデルを変更する場合は、ベクトルを消去することをお勧めします。そうしないと、標準以下の結果になります。",
@ -1380,22 +1380,22 @@
"Warning:": "警告:",
"This action is irreversible.": "この操作は元に戻せません。",
"Type the user's handle below to confirm:": "確認するには、以下のユーザーのハンドルを入力してください。",
"Import Characters": "文字をインポートする",
"Import Characters": "キャラクターをインポートする",
"Enter the URL of the content to import": "インポートするコンテンツの URL を入力します",
"Supported sources:": "サポートされているソース:",
"char_import_1": "チャブキャラクター直接リンクまたはID",
"char_import_1": "Chub キャラクター 直接リンクまたはID",
"char_import_example": "例:",
"char_import_2": "チャブの伝承集 (直接リンクまたは ID)",
"char_import_2": "Chub ロアブック (直接リンクまたは ID)",
"char_import_3": "JanitorAI キャラクター (直接リンクまたは UUID)",
"char_import_4": "Pygmalion.chat キャラクター (直接リンクまたは UUID)",
"char_import_5": "AICharacterCard.com キャラクター (直接リンクまたは ID)",
"char_import_6": "直接PNGリンク参照",
"char_import_7": "許可されたホストの場合)",
"char_import_8": "RisuRealm キャラクター (直接リンク)",
"Supports importing multiple characters.": "複数の文字のインポートをサポートします。",
"Supports importing multiple characters.": "複数のキャラクターのインポートをサポートします。",
"Write each URL or ID into a new line.": "各 URL または ID を新しい行に入力します。",
"Export for character": "文字のエクスポート",
"Export prompts for this character, including their order.": "この文字のプロンプトを順序も含めてエクスポートします。",
"Export for character": "キャラクターのエクスポート",
"Export prompts for this character, including their order.": "このキャラクターのプロンプトを順序も含めてエクスポートします。",
"Export all": "すべてをエクスポート",
"Export all your prompts to a file": "すべてのプロンプトをファイルにエクスポートする",
"Insert prompt": "プロンプトを挿入",

View File

@ -334,6 +334,9 @@
"vLLM API key": "vLLM API 密钥",
"Example: 127.0.0.1:8000": "例如http://127.0.0.1:8000",
"vLLM Model": "vLLM 模型",
"HuggingFace Token": "HuggingFace 代币",
"Endpoint URL": "端点 URL",
"Example: https://****.endpoints.huggingface.cloud": "例如https://****.endpoints.huggingface.cloud",
"PygmalionAI/aphrodite-engine": "PygmalionAI/aphrodite-engine用于OpenAI API的包装器",
"Aphrodite API key": "Aphrodite API 密钥",
"Aphrodite Model": "Aphrodite 模型",
@ -419,6 +422,8 @@
"Prompt Post-Processing": "提示词后处理",
"Applies additional processing to the prompt before sending it to the API.": "在将提示词发送到 API 之前对其进行额外处理。",
"prompt_post_processing_none": "未选择",
"01.AI API Key": "01.AI API密钥",
"01.AI Model": "01.AI模型",
"Additional Parameters": "附加参数",
"Verifies your API connection by sending a short test message. Be aware that you'll be credited for it!": "通过发送简短的测试消息验证您的API连接。请注意您将因此而消耗额度",
"Test Message": "发送测试消息",
@ -1033,6 +1038,8 @@
"Sticky": "粘性",
"Entries with a cooldown can't be activated N messages after being triggered.": "具有冷却时间的条目在触发后 N 条消息内无法被激活。",
"Cooldown": "冷却",
"Entries with a delay can't be activated until there are N messages present in the chat.": "直到聊天中出现 N 条消息时,延迟的条目才能被激活。",
"Delay": "延迟",
"Filter to Character(s)": "应用到角色",
"Character Exclusion": "反选角色",
"-- Characters not found --": "-- 未找到角色 --",
@ -1077,6 +1084,7 @@
"Move message up": "将消息上移",
"Move message down": "将消息下移",
"Enlarge": "放大",
"Caption": "标题",
"Welcome to SillyTavern!": "欢迎来到 SillyTavern",
"welcome_message_part_1": "阅读",
"welcome_message_part_2": "官方文档",
@ -1113,10 +1121,6 @@
"alternate_greetings_hint_2": "按钮即可开始!",
"Alternate Greeting #": "额外问候语 #",
"(This will be the first message from the character that starts every chat)": "(这将是角色在每次聊天开始时发送的第一条消息)",
"Forbid Media Override explanation": "当前角色/群组在聊天中使用外部媒体的能力。",
"Forbid Media Override subtitle": "媒体:图像、视频、音频。外部:不在本地服务器上托管。",
"Always forbidden": "始终禁止",
"Always allowed": "始终允许",
"View contents": "查看内容",
"Remove the file": "删除文件",
"Unique to this chat": "此聊天独有",
@ -1240,6 +1244,7 @@
"Message Template": "消息模板",
"(use _space": "(使用",
"macro)": "宏指令)",
"Automatically caption images": "自动为图像添加标题",
"Edit captions before saving": "保存前编辑标题",
"Character Expressions": "角色表情",
"Translate text to English before classification": "分类之前将文本翻译成英文",
@ -1579,6 +1584,10 @@
"Warning:": "警告:",
"This action is irreversible.": "此操作不可逆。",
"Type the user's handle below to confirm:": "在下面输入用户的名称以确认:",
"Forbid Media Override explanation": "当前角色/群组在聊天中使用外部媒体的能力。",
"Forbid Media Override subtitle": "媒体:图像、视频、音频。外部:不在本地服务器上托管。",
"Always forbidden": "始终禁止",
"Always allowed": "始终允许",
"help_format_1": "文本格式化命令:",
"help_format_2": "*文本*",
"help_format_3": "显示为",

View File

@ -227,7 +227,7 @@ import { appendFileContent, hasPendingFileAttachment, populateFileAttachment, de
import { initPresetManager } from './scripts/preset-manager.js';
import { MacrosParser, evaluateMacros } from './scripts/macros.js';
import { currentUser, setUserControls } from './scripts/user.js';
import { POPUP_TYPE, Popup, callGenericPopup, fixToastrForDialogs } from './scripts/popup.js';
import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup, fixToastrForDialogs } from './scripts/popup.js';
import { renderTemplate, renderTemplateAsync } from './scripts/templates.js';
import { ScraperManager } from './scripts/scrapers.js';
import { SlashCommandParser } from './scripts/slash-commands/SlashCommandParser.js';
@ -520,6 +520,7 @@ const chatElement = $('#chat');
let dialogueResolve = null;
let dialogueCloseStop = false;
export let chat_metadata = {};
/** @type {StreamingProcessor} */
export let streamingProcessor = null;
let crop_data = undefined;
let is_delete_mode = false;
@ -837,6 +838,7 @@ export let main_api;// = "kobold";
//novel settings
export let novelai_settings;
export let novelai_setting_names;
/** @type {AbortController} */
let abortController;
//css
@ -4380,6 +4382,25 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
}
}
/**
* Stops the generation and any streaming if it is currently running.
*/
export function stopGeneration() {
let stopped = false;
if (streamingProcessor) {
streamingProcessor.onStopStreaming();
streamingProcessor = null;
stopped = true;
}
if (abortController) {
abortController.abort('Clicked stop button');
hideStopButton();
stopped = true;
}
eventSource.emit(event_types.GENERATION_STOPPED);
return stopped;
}
/**
* Injects extension prompts into chat messages.
* @param {object[]} messages Array of chat messages
@ -7163,7 +7184,8 @@ function onScenarioOverrideRemoveClick() {
* @param {string} inputValue - Value to set the input to.
* @param {PopupOptions} options - Options for the popup.
* @typedef {{okButton?: string, rows?: number, wide?: boolean, wider?: boolean, large?: boolean, allowHorizontalScrolling?: boolean, allowVerticalScrolling?: boolean, cropAspect?: number }} PopupOptions - Options for the popup.
* @returns
* @returns {Promise<any>} A promise that resolves when the popup is closed.
* @deprecated Use `callGenericPopup` instead.
*/
export function callPopup(text, type, inputValue = '', { okButton, rows, wide, wider, large, allowHorizontalScrolling, allowVerticalScrolling, cropAspect } = {}) {
function getOkButtonText() {
@ -7794,6 +7816,7 @@ window['SillyTavern'].getContext = function () {
eventTypes: event_types,
addOneMessage: addOneMessage,
generate: Generate,
stopGeneration: stopGeneration,
getTokenCount: getTokenCount,
extensionPrompts: extension_prompts,
setExtensionPrompt: setExtensionPrompt,
@ -7849,6 +7872,8 @@ window['SillyTavern'].getContext = function () {
* @deprecated Legacy snake-case naming, compatibility with old extensions
*/
event_types: event_types,
POPUP_TYPE: POPUP_TYPE,
POPUP_RESULT: POPUP_RESULT,
};
};
@ -10342,15 +10367,7 @@ jQuery(async function () {
});
$(document).on('click', '.mes_stop', function () {
if (streamingProcessor) {
streamingProcessor.onStopStreaming();
streamingProcessor = null;
}
if (abortController) {
abortController.abort('Clicked stop button');
hideStopButton();
}
eventSource.emit(event_types.GENERATION_STOPPED);
stopGeneration();
});
$(document).on('click', '#form_sheld .stscript_continue', function () {
@ -10839,3 +10856,4 @@ jQuery(async function () {
initCustomSelectedSamplers();
});

View File

@ -725,8 +725,14 @@ export function initRossMods() {
RA_autoconnect();
}
if (getParsedUA()?.os?.name === 'iOS') {
document.body.classList.add('ios');
const userAgent = getParsedUA();
console.debug('User Agent', userAgent);
const isMobileSafari = /iPad|iPhone|iPod/.test(navigator.platform) || (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);
const isDesktopSafari = userAgent?.browser?.name === 'Safari' && userAgent?.platform?.type === 'desktop';
const isIOS = userAgent?.os?.name === 'iOS';
if (isIOS || isMobileSafari || isDesktopSafari) {
document.body.classList.add('safari');
}
$('#main_api').change(function () {

View File

@ -1430,7 +1430,7 @@ jQuery(function () {
wrapper.classList.add('flexFlowColumn', 'justifyCenter', 'alignitemscenter');
const textarea = document.createElement('textarea');
textarea.value = String(bro.val());
textarea.classList.add('height100p', 'wide100p');
textarea.classList.add('height100p', 'wide100p', 'maximized_textarea');
bro.hasClass('monospace') && textarea.classList.add('monospace');
textarea.addEventListener('input', function () {
bro.val(textarea.value).trigger('input');

View File

@ -154,7 +154,7 @@ export function initDynamicStyles() {
// Process all stylesheets on initial load
Array.from(document.styleSheets).forEach(sheet => {
try {
applyDynamicFocusStyles(sheet, { fromExtension: sheet.href.toLowerCase().includes('scripts/extensions') });
applyDynamicFocusStyles(sheet, { fromExtension: sheet.href?.toLowerCase().includes('scripts/extensions') == true });
} catch (e) {
console.warn('Failed to process stylesheet on initial load:', e);
}

View File

@ -3017,25 +3017,25 @@ async function generateComfyImage(prompt, negativePrompt) {
const text = await workflowResponse.text();
toastr.error(`Failed to load workflow.\n\n${text}`);
}
let workflow = (await workflowResponse.json()).replace('"%prompt%"', JSON.stringify(prompt));
workflow = workflow.replace('"%negative_prompt%"', JSON.stringify(negativePrompt));
let workflow = (await workflowResponse.json()).replaceAll('"%prompt%"', JSON.stringify(prompt));
workflow = workflow.replaceAll('"%negative_prompt%"', JSON.stringify(negativePrompt));
const seed = extension_settings.sd.seed >= 0 ? extension_settings.sd.seed : Math.round(Math.random() * Number.MAX_SAFE_INTEGER);
workflow = workflow.replaceAll('"%seed%"', JSON.stringify(seed));
placeholders.forEach(ph => {
workflow = workflow.replace(`"%${ph}%"`, JSON.stringify(extension_settings.sd[ph]));
workflow = workflow.replaceAll(`"%${ph}%"`, JSON.stringify(extension_settings.sd[ph]));
});
(extension_settings.sd.comfy_placeholders ?? []).forEach(ph => {
workflow = workflow.replace(`"%${ph.find}%"`, JSON.stringify(substituteParams(ph.replace)));
workflow = workflow.replaceAll(`"%${ph.find}%"`, JSON.stringify(substituteParams(ph.replace)));
});
if (/%user_avatar%/gi.test(workflow)) {
const response = await fetch(getUserAvatarUrl());
if (response.ok) {
const avatarBlob = await response.blob();
const avatarBase64 = await getBase64Async(avatarBlob);
workflow = workflow.replace('"%user_avatar%"', JSON.stringify(avatarBase64));
workflow = workflow.replaceAll('"%user_avatar%"', JSON.stringify(avatarBase64));
} else {
workflow = workflow.replace('"%user_avatar%"', JSON.stringify(PNG_PIXEL));
workflow = workflow.replaceAll('"%user_avatar%"', JSON.stringify(PNG_PIXEL));
}
}
if (/%char_avatar%/gi.test(workflow)) {
@ -3043,9 +3043,9 @@ async function generateComfyImage(prompt, negativePrompt) {
if (response.ok) {
const avatarBlob = await response.blob();
const avatarBase64 = await getBase64Async(avatarBlob);
workflow = workflow.replace('"%char_avatar%"', JSON.stringify(avatarBase64));
workflow = workflow.replaceAll('"%char_avatar%"', JSON.stringify(avatarBase64));
} else {
workflow = workflow.replace('"%char_avatar%"', JSON.stringify(PNG_PIXEL));
workflow = workflow.replaceAll('"%char_avatar%"', JSON.stringify(PNG_PIXEL));
}
}
console.log(`{

View File

@ -178,8 +178,37 @@ async function loadGroupChat(chatId) {
return [];
}
async function validateGroup(group) {
if (!group) return;
// Validate that all members exist as characters
let dirty = false;
group.members = group.members.filter(member => {
const character = characters.find(x => x.avatar === member || x.name === member);
if (!character) {
const msg = `Warning: Listed member ${member} does not exist as a character. It will be removed from the group.`;
toastr.warning(msg, 'Group Validation');
console.warn(msg);
dirty = true;
}
return character;
});
if (dirty) {
await editGroup(group.id, true, false);
}
}
export async function getGroupChat(groupId, reload = false) {
const group = groups.find((x) => x.id === groupId);
if (!group) {
console.warn('Group not found', groupId);
return;
}
// Run validation before any loading
validateGroup(group);
const chat_id = group.chat_id;
const data = await loadGroupChat(chat_id);
let freshChat = false;
@ -197,7 +226,6 @@ export async function getGroupChat(groupId, reload = false) {
if (group && Array.isArray(group.members)) {
for (let member of group.members) {
const character = characters.find(x => x.avatar === member || x.name === member);
if (!character) {
continue;
}
@ -219,10 +247,8 @@ export async function getGroupChat(groupId, reload = false) {
freshChat = true;
}
if (group) {
let metadata = group.chat_metadata ?? {};
updateChatMetadata(metadata, true);
}
let metadata = group.chat_metadata ?? {};
updateChatMetadata(metadata, true);
if (reload) {
select_group_chats(groupId, true);

View File

@ -1,7 +1,5 @@
import { POPUP_RESULT, POPUP_TYPE, Popup } from './popup.js';
const ELEMENT_ID = 'loader';
/** @type {Popup} */
let loaderPopup;
@ -31,7 +29,7 @@ export async function hideLoader() {
return new Promise((resolve) => {
// Spinner blurs/fades out
$('#load-spinner').on('transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd', function () {
$(`#${ELEMENT_ID}`).remove();
$('#loader').remove();
// Yoink preloader entirely; it only exists to cover up unstyled content while loading JS
// If it's present, we remove it once and then it's gone.
yoinkPreloader();

View File

@ -254,7 +254,7 @@ function getCurrentSwipeId() {
// For swipe macro, we are accepting using the message that is currently being swiped
const mid = getLastMessageId({ exclude_swipe_in_propress: false });
const swipeId = chat[mid]?.swipe_id;
return swipeId ? swipeId + 1 : null;
return swipeId !== null ? swipeId + 1 : null;
}
/**
@ -401,7 +401,7 @@ function timeDiffReplace(input) {
const time2 = moment(matchPart2);
const timeDifference = moment.duration(time1.diff(time2));
return timeDifference.humanize();
return timeDifference.humanize(true);
});
return output;

View File

@ -689,7 +689,7 @@ function formatWorldInfo(value) {
return '';
}
if (!oai_settings.wi_format) {
if (!oai_settings.wi_format.trim()) {
return value;
}

View File

@ -40,7 +40,7 @@ import { tokenizers } from './tokenizers.js';
import { BIAS_CACHE } from './logit-bias.js';
import { renderTemplateAsync } from './templates.js';
import { countOccurrences, debounce, delay, download, getFileText, isOdd, isTrueBoolean, onlyUnique, resetScrollHeight, shuffle, sortMoments, stringToRange, timestampToMoment } from './utils.js';
import { countOccurrences, debounce, delay, download, getFileText, getStringHash, isOdd, isTrueBoolean, onlyUnique, resetScrollHeight, shuffle, sortMoments, stringToRange, timestampToMoment } from './utils.js';
import { FILTER_TYPES } from './filters.js';
import { PARSER_FLAG, SlashCommandParser } from './slash-commands/SlashCommandParser.js';
import { SlashCommand } from './slash-commands/SlashCommand.js';
@ -335,6 +335,8 @@ const storage_keys = {
compact_input_area: 'compact_input_area',
auto_connect_legacy: 'AutoConnectEnabled',
auto_load_chat_legacy: 'AutoLoadChatEnabled',
storyStringValidationCache: 'StoryStringValidationCache',
};
const contextControls = [
@ -2105,6 +2107,9 @@ export function fuzzySearchGroups(searchValue) {
*/
export function renderStoryString(params) {
try {
// Validate and log possible warnings/errors
validateStoryString(power_user.context.story_string, params);
// compile the story string template into a function, with no HTML escaping
const compiledTemplate = Handlebars.compile(power_user.context.story_string, { noEscape: true });
@ -2132,6 +2137,55 @@ export function renderStoryString(params) {
}
}
/**
* Validate the story string for possible warnings or issues
*
* @param {string} storyString - The story string
* @param {Object} params - The story string parameters
*/
function validateStoryString(storyString, params) {
/** @type {{hashCache: {[hash: string]: {fieldsWarned: {[key: string]: boolean}}}}} */
const cache = JSON.parse(localStorage.getItem(storage_keys.storyStringValidationCache)) ?? { hashCache: {} };
const hash = getStringHash(storyString);
// Initialize the cache for the current hash if it doesn't exist
if (!cache.hashCache[hash]) {
cache.hashCache[hash] = { fieldsWarned: {} };
}
const currentCache = cache.hashCache[hash];
const fieldsToWarn = [];
function validateMissingField(field, fallbackLegacyField = null) {
const contains = storyString.includes(`{{${field}}}`) || (!!fallbackLegacyField && storyString.includes(`{{${fallbackLegacyField}}}`));
if (!contains && params[field]) {
const wasLogged = currentCache.fieldsWarned[field];
if (!wasLogged) {
fieldsToWarn.push(field);
currentCache.fieldsWarned[field] = true;
}
console.warn(`The story string does not contain {{${field}}}, but it would contain content:\n`, params[field]);
}
}
validateMissingField('description');
validateMissingField('personality');
validateMissingField('persona');
validateMissingField('scenario');
validateMissingField('system');
validateMissingField('wiBefore', 'loreBefore');
validateMissingField('wiAfter', 'loreAfter');
if (fieldsToWarn.length > 0) {
const fieldsList = fieldsToWarn.map(field => `{{${field}}}`).join(', ');
toastr.warning(`The story string does not contain the following fields, but they would contain content: ${fieldsList}`, 'Story String Validation');
}
localStorage.setItem(storage_keys.storyStringValidationCache, JSON.stringify(cache));
}
const sortFunc = (a, b) => power_user.sort_order == 'asc' ? compareFunc(a, b) : compareFunc(b, a);
const compareFunc = (first, second) => {
const a = first[power_user.sort_field];

View File

@ -33,6 +33,7 @@ import {
setCharacterName,
setExtensionPrompt,
setUserName,
stopGeneration,
substituteParams,
system_avatar,
system_message_types,
@ -898,6 +899,24 @@ export function initDefaultSlashCommands() {
],
helpString: 'Adds a swipe to the last chat message.',
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'stop',
callback: () => {
const stopped = stopGeneration();
return String(stopped);
},
returns: 'true/false, whether the generation was running and got stopped',
helpString: `
<div>
Stops the generation and any streaming if it is currently running.
</div>
<div>
Note: This command cannot be executed from the chat input, as sending any message or script from there is blocked during generation.
But it can be executed via automations or QR scripts/buttons.
</div>
`,
aliases: ['generate-stop'],
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'abort',
callback: abortCallback,

View File

@ -263,6 +263,8 @@ export const setting_names = [
'bypass_status_check',
];
const DYNATEMP_BLOCK = document.getElementById('dynatemp_block_ooba');
export function validateTextGenUrl() {
const selector = SERVER_INPUTS[settings.type];
@ -1045,6 +1047,10 @@ export function isJsonSchemaSupported() {
return [TABBY, LLAMACPP].includes(settings.type) && main_api === 'textgenerationwebui';
}
function isDynamicTemperatureSupported() {
return settings.dynatemp && DYNATEMP_BLOCK?.dataset?.tgType?.includes(settings.type);
}
function getLogprobsNumber() {
if (settings.type === VLLM || settings.type === INFERMATICAI) {
return 5;
@ -1055,6 +1061,7 @@ function getLogprobsNumber() {
export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate, isContinue, cfgValues, type) {
const canMultiSwipe = !isContinue && !isImpersonate && type !== 'quiet';
const dynatemp = isDynamicTemperatureSupported();
const { banned_tokens, banned_strings } = getCustomTokenBans();
let params = {
@ -1063,7 +1070,7 @@ export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate,
'max_new_tokens': maxTokens,
'max_tokens': maxTokens,
'logprobs': power_user.request_token_probabilities ? getLogprobsNumber() : undefined,
'temperature': settings.dynatemp ? (settings.min_temp + settings.max_temp) / 2 : settings.temp,
'temperature': dynatemp ? (settings.min_temp + settings.max_temp) / 2 : settings.temp,
'top_p': settings.top_p,
'typical_p': settings.typical_p,
'typical': settings.typical_p,
@ -1081,11 +1088,11 @@ export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate,
'length_penalty': settings.length_penalty,
'early_stopping': settings.early_stopping,
'add_bos_token': settings.add_bos_token,
'dynamic_temperature': settings.dynatemp ? true : undefined,
'dynatemp_low': settings.dynatemp ? settings.min_temp : undefined,
'dynatemp_high': settings.dynatemp ? settings.max_temp : undefined,
'dynatemp_range': settings.dynatemp ? (settings.max_temp - settings.min_temp) / 2 : undefined,
'dynatemp_exponent': settings.dynatemp ? settings.dynatemp_exponent : undefined,
'dynamic_temperature': dynatemp ? true : undefined,
'dynatemp_low': dynatemp ? settings.min_temp : undefined,
'dynatemp_high': dynatemp ? settings.max_temp : undefined,
'dynatemp_range': dynatemp ? (settings.max_temp - settings.min_temp) / 2 : undefined,
'dynatemp_exponent': dynatemp ? settings.dynatemp_exponent : undefined,
'smoothing_factor': settings.smoothing_factor,
'smoothing_curve': settings.smoothing_curve,
'dry_allowed_length': settings.dry_allowed_length,

View File

@ -803,7 +803,7 @@ export function getImageSizeFromDataURL(dataUrl) {
export function getCharaFilename(chid) {
const context = getContext();
const fileName = context.characters[chid ?? context.characterId].avatar;
const fileName = context.characters[chid ?? context.characterId]?.avatar;
if (fileName) {
return fileName.replace(/\.[^/.]+$/, '');

View File

@ -609,10 +609,6 @@ const postSetupTasks = async function () {
console.warn(color.yellow('Basic Authentication is enabled, but username or password is not set or empty!'));
}
}
if (listen && !basicAuthMode && enableAccounts) {
await userModule.checkAccountsProtection();
}
};
/**
@ -631,16 +627,6 @@ async function loadPlugins() {
}
}
if (listen && !enableWhitelist && !basicAuthMode) {
if (getConfigValue('securityOverride', false)) {
console.warn(color.red('Security has been overridden. If it\'s not a trusted network, change the settings.'));
}
else {
console.error(color.red('Your SillyTavern is currently unsecurely open to the public. Enable whitelisting or basic authentication.'));
process.exit(1);
}
}
/**
* Set the title of the terminal window
* @param {string} title Desired title for the window
@ -654,10 +640,53 @@ function setWindowTitle(title) {
}
}
/**
* Prints an error message and exits the process if necessary
* @param {string} message The error message to print
* @returns {void}
*/
function logSecurityAlert(message) {
if (basicAuthMode || enableWhitelist) return; // safe!
console.error(color.red(message));
if (getConfigValue('securityOverride', false)) {
console.warn(color.red('Security has been overridden. If it\'s not a trusted network, change the settings.'));
return;
}
process.exit(1);
}
async function verifySecuritySettings() {
// Skip all security checks as listen is set to false
if (!listen) {
return;
}
if (!enableAccounts) {
logSecurityAlert('Your SillyTavern is currently insecurely open to the public. Enable whitelisting, basic authentication or user accounts.');
}
const users = await userModule.getAllEnabledUsers();
const unprotectedUsers = users.filter(x => !x.password);
const unprotectedAdminUsers = unprotectedUsers.filter(x => x.admin);
if (unprotectedUsers.length > 0) {
console.warn(color.blue('A friendly reminder that the following users are not password protected:'));
unprotectedUsers.map(x => `${color.yellow(x.handle)} ${color.red(x.admin ? '(admin)' : '')}`).forEach(x => console.warn(x));
console.log();
console.warn(`Consider setting a password in the admin panel or by using the ${color.blue('recover.js')} script.`);
console.log();
if (unprotectedAdminUsers.length > 0) {
logSecurityAlert('If you are not using basic authentication or whitelisting, you should set a password for all admin users.');
}
}
}
// User storage module needs to be initialized before starting the server
userModule.initUserStorage(dataRoot)
.then(userModule.ensurePublicDirectoriesExist)
.then(userModule.migrateUserData)
.then(verifySecuritySettings)
.then(preSetupTasks)
.finally(() => {
if (cliArguments.ssl) {

View File

@ -681,27 +681,27 @@ async function createBackupArchive(handle, response) {
}
/**
* Checks if any admin users are not password protected. If so, logs a warning.
* @returns {Promise<void>}
* Gets all of the users.
* @returns {Promise<User[]>}
*/
async function checkAccountsProtection() {
async function getAllUsers() {
if (!ENABLE_ACCOUNTS) {
return;
return [];
}
/**
* @type {User[]}
*/
const users = await storage.values();
const unprotectedUsers = users.filter(x => x.enabled && x.admin && !x.password);
if (unprotectedUsers.length > 0) {
console.warn(color.red('The following admin users are not password protected:'));
unprotectedUsers.forEach(x => console.warn(color.yellow(x.handle)));
console.log();
console.warn('Please disable them or set a password in the admin panel.');
console.log();
await delay(3000);
}
return users;
}
/**
* Gets all of the enabled users.
* @returns {Promise<User[]>}
*/
async function getAllEnabledUsers() {
const users = await getAllUsers();
return users.filter(x => x.enabled);
}
/**
@ -738,6 +738,7 @@ module.exports = {
shouldRedirectToLogin,
createBackupArchive,
tryAutoLogin,
checkAccountsProtection,
getAllUsers,
getAllEnabledUsers,
router,
};