mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-02-22 23:18:27 +01:00
Merge branch 'staging' into persona-lorebook
This commit is contained in:
commit
3be17e2ed8
@ -12,3 +12,4 @@ access.log
|
||||
/data
|
||||
/cache
|
||||
.DS_Store
|
||||
/public/scripts/extensions/third-party
|
||||
|
2
.github/readme-de_de.md
vendored
2
.github/readme-de_de.md
vendored
@ -8,7 +8,7 @@
|
||||
|
||||
<div align="center">
|
||||
|
||||
[English](readme.md) | German | [中文](readme-zh_cn.md) | [日本語](readme-ja_jp.md) | [Русский](readme-ru_ru.md)
|
||||
[English](readme.md) | German | [中文](readme-zh_cn.md) | [繁體中文](readme-zh_tw.md) | [日本語](readme-ja_jp.md) | [Русский](readme-ru_ru.md)
|
||||
|
||||
[](https://github.com/SillyTavern/SillyTavern/stargazers)
|
||||
[](https://github.com/SillyTavern/SillyTavern/network)
|
||||
|
2
.github/readme-ja_jp.md
vendored
2
.github/readme-ja_jp.md
vendored
@ -5,7 +5,7 @@
|
||||
|
||||
<div align="center">
|
||||
|
||||
[English](readme.md) | [German](readme-de_de.md) | [中文](readme-zh_cn.md) | 日本語 | [Русский](readme-ru_ru.md)
|
||||
[English](readme.md) | [German](readme-de_de.md) | [中文](readme-zh_cn.md) | [繁體中文](readme-zh_tw.md) | 日本語 | [Русский](readme-ru_ru.md)
|
||||
|
||||
[](https://github.com/SillyTavern/SillyTavern/stargazers)
|
||||
[](https://github.com/SillyTavern/SillyTavern/network)
|
||||
|
2
.github/readme-ru_ru.md
vendored
2
.github/readme-ru_ru.md
vendored
@ -7,7 +7,7 @@
|
||||
|
||||
<div align="center">
|
||||
|
||||
[English](readme.md) | [German](readme-de_de.md) | [中文](readme-zh_cn.md) | [日本語](readme-ja_jp.md) | Русский
|
||||
[English](readme.md) | [German](readme-de_de.md) | [中文](readme-zh_cn.md) | [繁體中文](readme-zh_tw.md) | [日本語](readme-ja_jp.md) | Русский
|
||||
|
||||
[](https://github.com/SillyTavern/SillyTavern/stargazers)
|
||||
[](https://github.com/SillyTavern/SillyTavern/network)
|
||||
|
2
.github/readme-zh_cn.md
vendored
2
.github/readme-zh_cn.md
vendored
@ -5,7 +5,7 @@
|
||||
|
||||
<div align="center">
|
||||
|
||||
[English](readme.md) | [German](readme-de_de.md) | 中文 | [日本語](readme-ja_jp.md) | [Русский](readme-ru_ru.md)
|
||||
[English](readme.md) | [German](readme-de_de.md) | 中文 | [繁體中文](readme-zh_tw.md) | [日本語](readme-ja_jp.md) | [Русский](readme-ru_ru.md)
|
||||
|
||||
[](https://github.com/SillyTavern/SillyTavern/stargazers)
|
||||
[](https://github.com/SillyTavern/SillyTavern/network)
|
||||
|
381
.github/readme-zh_tw.md
vendored
Normal file
381
.github/readme-zh_tw.md
vendored
Normal file
@ -0,0 +1,381 @@
|
||||
> [!IMPORTANT]
|
||||
> 這裡的資訊可能已經過時或不完整,僅供您參考。請使用英文版本以取得最新資訊。
|
||||
|
||||
<a name="readme-top"></a>
|
||||
|
||||
![][cover]
|
||||
|
||||
<div align="center">
|
||||
|
||||
[English](readme.md) | [German](readme-de_de.md) | [中文](readme-zh_cn.md) | 繁體中文 | [日本語](readme-ja_jp.md) | [Русский](readme-ru_ru.md)
|
||||
|
||||
[](https://github.com/SillyTavern/SillyTavern/stargazers)
|
||||
[](https://github.com/SillyTavern/SillyTavern/network)
|
||||
[](https://github.com/SillyTavern/SillyTavern/issues)
|
||||
[](https://github.com/SillyTavern/SillyTavern/pulls)
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
SillyTavern 提供一個統一的前端介面,整合多種大型語言模型的 API(包括:KoboldAI/CPP、Horde、NovelAI、Ooba、Tabby、OpenAI、OpenRouter、Claude、Mistral 等)。同時具備行動裝置友善的佈局、視覺小說模式(Visual Novel Mode)、Automatic1111 與 ComfyUI 的影像生成 API 整合、TTS(語音合成)、世界資訊(Lorebook)、可自訂 UI、自動翻譯功能,以及強大的提示詞(prompt)設定選項和無限的第三方擴充潛力。
|
||||
|
||||
我們擁有一個 [官方文件網站](https://docs.sillytavern.app/) 可以幫助解答絕大多數的使用問題,並幫助您順利入門。
|
||||
|
||||
## SillyTavern 是什麼?
|
||||
|
||||
SillyTavern(簡稱 ST)是一款本地安裝的使用者介面,讓您能與大型語言模型(LLM)、影像生成引擎以及語音合成模型互動的前端。
|
||||
|
||||
SillyTavern 起源於 2023 年 2 月,作為 TavernAI 1.2.8 的分支版本發展至今。目前已有超過 100 位貢獻者,並擁有超過兩年的獨立開發歷史。如今,它已成為 AI 愛好者中備受推崇的軟體之一。
|
||||
|
||||
## 我們的願景
|
||||
|
||||
1. 我們致力於賦予使用者對 LLM 提示詞的最大控制權與實用性,並認為學習過程中的挑戰是樂趣的一部分
|
||||
2. 我們不提供任何線上或託管服務,也不會程式化追蹤任何使用者數據。
|
||||
3. SillyTavern 是由一群熱衷於 LLM 的開發者社群所打造的熱情專案,並將永遠保持免費與開源。
|
||||
|
||||
## 分支介紹
|
||||
|
||||
SillyTavern 採用雙分支開發模式,確保為所有使用者提供流暢的使用體驗。
|
||||
|
||||
* `release`(穩定版):🌟 **推薦給大部分的使用者使用。** 此分支最為穩定,僅在主要版本發布時更新。適合大多數人,通常每月更新一次。
|
||||
* `staging`(開發版):⚠️ **不建議普通使用者使用。** 此分支包含最新功能,但可能隨時出現問題。適合進階使用者與愛好者,每日多次更新。
|
||||
|
||||
如果您不熟悉 git CLI 或對分支概念不清楚,請放心對您來說,`release`(穩定版)分支永遠是首選。
|
||||
|
||||
## 使用 SillyTavern 需要什麼?
|
||||
|
||||
由於 SillyTavern 僅是一個介面,您需要一個 LLM 後端來提供推理能力。您可以使用 AI Horde 以立即開始聊天。此外,我們支持許多其他本地和雲端 LLM 後端,例如 OpenAI 兼容 API、KoboldAI、Tabby 等。更多支持的 API 資訊,請參閱 [常見問題](https://docs.sillytavern.app/usage/api-connections/)。
|
||||
|
||||
### 我需要高效能電腦才能運行 SillyTavern 嗎?
|
||||
|
||||
SillyTavern 的硬體需求相當低。任何能夠運行 NodeJS 18 或更高版本的設備都可以執行。若您打算在本地機器上進行 LLM 推理,我們建議使用擁有至少 6GB VRAM 的 3000 系列 NVIDIA 顯示卡。更多詳細資訊,請參考您使用的後端文檔。
|
||||
|
||||
### 推薦後端(僅為推薦,非官方合作和隸屬關係)
|
||||
|
||||
* [AI Horde](https://aihorde.net/):使用志願者託管的模型,無需進一步設定
|
||||
* [KoboldCpp](https://github.com/LostRuins/koboldcpp):社群推崇的選擇,可在本地運行 GGUF 模型
|
||||
* [tabbyAPI](https://github.com/theroyallab/tabbyAPI):一個流行且輕量的本地託管 exl2 推理 API
|
||||
* [OpenRouter](https://openrouter.ai):提供多個雲端 LLM 提供商(如 OpenAI、Claude、Meta Llama 等)及熱門社群模型的單一 API
|
||||
|
||||
## 有任何問題或建議?
|
||||
|
||||
### 歡迎加入我們的 Discord 伺服器
|
||||
|
||||
| [![][discord-shield-badge]][discord-link] | [加入我們的 Disocrd 伺服器](https://discord.gg/sillytavern) 以獲得技術支援、分享您喜愛的角色與提示詞。 |
|
||||
| :---------------------------------------- | :----------------------------------------------------------------------------------------------------------------- |
|
||||
|
||||
或直接聯繫開發者:
|
||||
|
||||
* Discord: cohee, rossascends, wolfsblvt
|
||||
* Reddit: [/u/RossAscends](https://www.reddit.com/user/RossAscends/), [/u/sillylossy](https://www.reddit.com/user/sillylossy/), [u/Wolfsblvt](https://www.reddit.com/user/Wolfsblvt/)
|
||||
* [提交 GitHub 問題](https://github.com/SillyTavern/SillyTavern/issues)
|
||||
|
||||
### 我喜歡這個專案,我該如何貢獻呢?
|
||||
|
||||
1. **提交拉取要求(Pull Request)**:想了解如何貢獻,請參閱 [CONTRIBUTING.md](../CONTRIBUTING.md)。
|
||||
2. **提供功能建議與問題報告**:使用本專案所提供的模板提交建議或問題報告。
|
||||
3. **仔細閱讀此 README 文件及相關文檔**:請避免提出重複問題或建議。
|
||||
|
||||
## 螢幕截圖
|
||||
|
||||
<img width="500" alt="image" src="https://github.com/user-attachments/assets/9b5f32f0-c3b3-4102-b3f5-0e9213c0f50f">
|
||||
<img width="500" alt="image" src="https://github.com/user-attachments/assets/913fdbaa-7d33-42f1-ae2c-89dca41c53d1">
|
||||
|
||||
## 角色卡
|
||||
|
||||
SillyTavern 的核心概念是「角色卡」(Character Cards)。角色卡是一組設定 LLM 行為的提示詞,用於 SillyTavern 中進行持續性對話。其功能類似於 ChatGPT 的 GPT 或 Poe 的聊天機器人。角色卡的內容可以是任何形式:抽象場景、針對特定任務設計的助手、知名人物,或者虛構角色。
|
||||
|
||||
角色卡中唯一必填的項目是名稱欄位。若想與語言模型開始一般對話,您只需創建一個名稱為「Assistant」的新卡片,其餘欄位皆可保持空白。若希望進行更具主題性的對話,則可以提供語言模型背景資訊、行為模式、寫作風格以及特定情境來啟動聊天。
|
||||
|
||||
如果您僅想進行快速對話而不選擇角色卡片,或想測試 LLM 的連線,則可在打開 SillyTavern 後,於歡迎頁面的輸入欄位中直接輸入您的提示內容。請注意,這類對話是暫時的,不會被永久保存。
|
||||
|
||||
若想了解如何設定角色卡,可參考預設角色(如 Seraphina)或從「下載擴充功能 & 資源」(Download Extensions & Assets)選單中下載社群製作的角色卡。
|
||||
|
||||
## 核心功能
|
||||
|
||||
* 進階文本生成設定:內含許多社群製作的預設設定
|
||||
* 支援世界資訊(World Info):創建豐富的背景故事,或節省角色卡片中的 Token(符記)使用
|
||||
* 群組聊天:多角色聊天室,可讓角色與您或彼此對話
|
||||
* 豐富的 UI 自定義選項:主題顏色、背景圖片、自定義 CSS 等
|
||||
* 使用者設定:讓 AI 更了解您並提升沉浸感
|
||||
* 內建 RAG 支持:可將文檔加入對話,供 AI 參考
|
||||
* 強大的聊天指令子系統:內含 [腳本引擎(Scripting Engine)](https://docs.sillytavern.app/usage/st-script/)
|
||||
|
||||
## 擴充功能
|
||||
|
||||
SillyTavern 支持多種擴充功能。
|
||||
|
||||
* 角色情感表達:使用視覺圖片(立繪)呈現情緒表達
|
||||
* 聊天記錄自動摘要
|
||||
* 自動化介面與聊天翻譯
|
||||
* 穩定擴散(Stable Diffusion)、FLUX 和 DALL-E 的影像生成整合
|
||||
* 語音合成:AI 回應訊息可透過 ElevenLabs、Silero 或系統 TTS 語音合成
|
||||
* 網頁搜尋功能:為提示詞添加真實世界的上下文資訊
|
||||
* 更多擴展:可從「下載擴充功能 & 資源」(Download Extensions & Assets)選單中下載
|
||||
|
||||
想了解如何使用這些擴充功能,請參考:[官方說明文件](https://docs.sillytavern.app/)
|
||||
|
||||
# ⌛ 安裝指南
|
||||
|
||||
> \[!WARNING]
|
||||
>
|
||||
> * 請勿將程式安裝到 Windows 的系統控制資料夾(如 Program Files、System32 等)
|
||||
> * 請勿以管理員權限執行 Start.bat
|
||||
> * 無法在 Windows 7 系統上安裝,因為它無法執行 NodeJS 18.16
|
||||
|
||||
## 🪟 Windows
|
||||
|
||||
### 使用 Git 安裝
|
||||
|
||||
1. 安裝 [NodeJS](https://nodejs.org/en)(建議使用最新的 LTS 版本)
|
||||
2. 安裝 [Git for Windows](https://gitforwindows.org/)
|
||||
3. 打開 Windows 檔案總管(`Win+E`)
|
||||
4. 創建/使用一個不受 Windows 系統控制或監控的資料夾(例如:C:\MySpecialFolder\)
|
||||
5. 在該資料夾內開啟命令提示字元(Command Prompt):點擊地址欄,輸入 `cmd` 並按下 Enter
|
||||
6. 當命令提示字元黑框彈出時,輸入以下其中一條指令後,按下 Enter:
|
||||
|
||||
* 安裝 Release(穩定版)分支:`git clone https://github.com/SillyTavern/SillyTavern -b release`
|
||||
* 安裝 Staging(開發板)分支:`git clone https://github.com/SillyTavern/SillyTavern -b staging`
|
||||
|
||||
7. 當程式碼下載完成後,雙擊 `Start.bat`,NodeJS 將自動安裝所需的依賴項
|
||||
8. 本地伺服器啟動後,SillyTavern 將自動在您的瀏覽器中打開
|
||||
|
||||
### 使用 GitHub Desktop 安裝
|
||||
|
||||
(此方式僅允許通過 GitHub Desktop 使用 git。如果您也希望在命令列中使用 `git`,則需額外安裝 [Git for Windows](https://gitforwindows.org/))
|
||||
|
||||
1. 安裝 [NodeJS](https://nodejs.org/en)(建議使用最新的 LTS 版本)
|
||||
2. 安裝 [GitHub Desktop](https://central.github.com/deployments/desktop/desktop/latest/win32)
|
||||
3. 安裝完成後,打開 GitHub Desktop,點擊 `Clone a repository from the internet....` (注意:此步驟 **無需創建 GitHub 帳號**。)
|
||||
4. 在彈出選單中,點擊「URL」選項,輸入此網址:`https://github.com/SillyTavern/SillyTavern`,然後點擊「Clone」。您可以更改「Local path」來選擇 SillyTavern 的下載位置
|
||||
6. 若想開啟 SillyTavern,需使用 Windows 檔案總管以進入您複製儲存庫的資料夾。預設位置為:`C:\Users\[您的 Windows 使用者名稱]\Documents\GitHub\SillyTavern`
|
||||
7. 雙擊 `start.bat` 文件。(請注意:若您的作業系統隱藏了 `.bat` 副檔名,該文件可能顯示為「`Start`」。這就是您需要雙擊運行的文件。)
|
||||
8. 雙擊後,將會彈出一個大型黑色的命令提示字元視窗,SillyTavern 會開始安裝其運行所需的文件與依賴
|
||||
9. 安裝完成後,若一切正常,命令提示字元視窗應顯示運行中的訊息,且您的瀏覽器會自動打開 SillyTavern 頁籤
|
||||
10. 連接到任何 SillyTavern [支援的 APIs](https://docs.sillytavern.app/usage/api-connections/) 並開始聊天吧!
|
||||
|
||||
## 🐧 Linux & 🍎 MacOS
|
||||
|
||||
對於 MacOS 和 Linux 系統,所有操作都將在終端機(Terminal)中完成。
|
||||
|
||||
1. 安裝 git 和 NodeJS(具體方法因操作系統而異)
|
||||
2. 複製儲存庫(Clone the repo):
|
||||
|
||||
* 安裝 Release(穩定版)分支:`git clone https://github.com/SillyTavern/SillyTavern -b release`
|
||||
* 安裝 Staging(開發板)分支:`git clone https://github.com/SillyTavern/SillyTavern -b staging`
|
||||
|
||||
3. 使用命令 `cd SillyTavern` 以進入安裝資料夾
|
||||
4. 使用以下其中一條命令,以執行 `start.sh` 腳本:
|
||||
|
||||
* `./start.sh`
|
||||
* `bash start.sh`
|
||||
|
||||
## ⚡ 使用 SillyTavern Launcher 安裝
|
||||
|
||||
SillyTavern Launcher 是一個安裝嚮導,協助您設定多種選項,包括安裝本地推理(inference)的後端。
|
||||
|
||||
### 對於 Windows 使用者
|
||||
|
||||
1. 在鍵盤上按下 **`WINDOWS + R`** 打開「執行」對話框,然後輸入以下指令以安裝 git:
|
||||
|
||||
```shell
|
||||
cmd /c winget install -e --id Git.Git
|
||||
```
|
||||
|
||||
2. 在鍵盤上按下 **`WINDOWS + E`** 打開檔案總管,導航至您想要安裝 Launcher 的資料夾。在目標資料夾的地址欄輸入 `cmd` 並按下 Enter。接著執行以下命令:
|
||||
|
||||
```shell
|
||||
git clone https://github.com/SillyTavern/SillyTavern-Launcher.git && cd SillyTavern-Launcher && start installer.bat
|
||||
```
|
||||
|
||||
### 對於 Linux 使用者
|
||||
|
||||
1. 打開您喜歡的終端機(Terminal),安裝 git
|
||||
2. 使用以下指令以複製 Sillytavern-Launcher:
|
||||
|
||||
```shell
|
||||
git clone https://github.com/SillyTavern/SillyTavern-Launcher.git && cd SillyTavern-Launcher
|
||||
```
|
||||
|
||||
3. 執行安裝腳本(installer.sh):
|
||||
|
||||
```shell
|
||||
chmod +x install.sh && ./install.sh
|
||||
```
|
||||
|
||||
4. 安裝完成後,執行啟動腳本(launcher.sh):
|
||||
|
||||
```shell
|
||||
chmod +x launcher.sh && ./launcher.sh
|
||||
```
|
||||
|
||||
### 對於 Mac 使用者
|
||||
|
||||
1. 打開終端機(Terminal),並使用以下指令安裝 Homebrew:
|
||||
|
||||
```shell
|
||||
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
|
||||
```
|
||||
|
||||
2. 使用 Homebrew 以安裝 git:
|
||||
|
||||
```shell
|
||||
brew install git
|
||||
```
|
||||
|
||||
3. 使用以下指令以複製 Sillytavern-Launcher:
|
||||
|
||||
```shell
|
||||
git clone https://github.com/SillyTavern/SillyTavern-Launcher.git && cd SillyTavern-Launcher
|
||||
```
|
||||
|
||||
4. 執行安裝腳本(installer.sh):
|
||||
|
||||
```shell
|
||||
chmod +x install.sh && ./install.sh
|
||||
```
|
||||
|
||||
5. 安裝完成後,執行啟動腳本(launcher.sh):
|
||||
|
||||
```shell
|
||||
chmod +x launcher.sh && ./launcher.sh
|
||||
```
|
||||
|
||||
## 🐋 使用 Docker 安裝
|
||||
|
||||
以下指南已假設您安裝 Docker,能夠訪問命令列進行容器安裝,並熟悉 Docker 的基本使用。
|
||||
|
||||
### 自行構建映像
|
||||
|
||||
我們提供了一份完整的 [SillyTavern Docker 使用指南](http://docs.sillytavern.app/installation/docker/)。該指南涵蓋了 Windows、macOS 和 Linux 的安裝過程。若您希望自行構建映像,建議先閱讀該文檔。
|
||||
|
||||
### 使用 GitHub 容器註冊表(最簡易的方式)
|
||||
|
||||
您需要設定兩個必要的目錄映射(directory mappings)和一個端口映射(port mapping)來使 SillyTavern 正常運行。在執行指令時,請將以下佔位符替換為您的實際配置:
|
||||
|
||||
#### 容器變數
|
||||
|
||||
##### 目錄映射(Volume Mappings)
|
||||
|
||||
* [config]:用於存放 SillyTavern 設定文件的本地資料夾
|
||||
* [data]:用於存放 SillyTavern 使用者數據(包括角色)的本地資料夾
|
||||
* [plugins](可選):用於存放 SillyTavern 擴充功能的本地資料夾
|
||||
|
||||
##### 端口映射(Port Mappings)
|
||||
|
||||
* [PublicPort]:對外流量的訪問端口。這是必需的,因為您將從虛擬機容器外部訪問實例。除非實施了額外的安全服務,否則請勿將此端口暴露於網路
|
||||
|
||||
##### 其他設定(Additional Settings)
|
||||
|
||||
* [DockerNet]:容器應連接的 Docker 網路。如果您不熟悉此概念,請參閱 [Docker 官方說明文件](https://docs.docker.com/reference/cli/docker/network/)
|
||||
* [version]:在 GitHub 頁面的右側,您可以找到「Packages」。選擇「sillytavern」包,然後查看映像版本。「latest」標籤會使您保持與當前版本同步。您也可以選擇「staging」或「release」標籤,但這可能不適用於依賴擴充功能的使用者,因為擴充功能可能需要時間進行更新
|
||||
|
||||
#### 安裝命令
|
||||
|
||||
1. 打開命令列(Command Line)
|
||||
2. 執行以下指令:
|
||||
|
||||
`docker create --name='sillytavern' --net='[DockerNet]' -p '8000:8000/tcp' -v '[plugins]':'/home/node/app/plugins':'rw' -v '[config]':'/home/node/app/config':'rw' -v '[data]':'/home/node/app/data':'rw' 'ghcr.io/sillytavern/sillytavern:[version]'`
|
||||
|
||||
> 請注意:默認的監聽端口為 8000。如果您在設定文件中更改了此端口,請務必使用適當的端口號
|
||||
|
||||
## 📱 於 Android 系統中使用 Termux 安裝
|
||||
|
||||
> \[!NOTE]
|
||||
> **雖然您可以在 Android 設備上使用 Termux 直接運行 SillyTavern,但這不在我們的官方支持範圍內。**
|
||||
>
|
||||
> **請參閱 ArroganceComplex#2659 所提供的指南:**
|
||||
>
|
||||
> * <https://rentry.org/STAI-Termux>
|
||||
|
||||
**不支援:Android ARM LEtime-web。** 32 位 Android 系統需要額外的依賴項,這無法通過 npm 安裝。請使用以下命令安裝:`pkg install esbuild`。完成後,請按照普通的安裝步驟進行操作
|
||||
|
||||
## API 金鑰管理
|
||||
|
||||
SillyTavern 將您的 API 金鑰(Keys)保存在使用者數據目錄中的 `secrets.json` 文件內(默認路徑為`/data/default-user/secrets.json`)
|
||||
|
||||
默認情況下,API 金鑰在您保存並重新載入頁面後,將不會自介面中顯示
|
||||
|
||||
如需啟用查看金鑰功能:
|
||||
|
||||
1. 在 `config.yaml` 文件中,將 `allowKeysExposure` 的「值」設為 `true`
|
||||
2. 重新啟動 SillyTavern 伺服器
|
||||
3. 點擊 API 連線頁面右下角的「查看隱藏的 API 金鑰(View hidden API keys)」超連結
|
||||
|
||||
## 命令列參數(Command-line Arguments)
|
||||
|
||||
您可以在啟動 SillyTavern 伺服器時傳遞命令列參數,以覆蓋 `config.yaml` 文件中的某些設定。
|
||||
|
||||
### 範例
|
||||
|
||||
```shell
|
||||
node server.js --port 8000 --listen false
|
||||
# or
|
||||
npm run start -- --port 8000 --listen false
|
||||
# or(僅適用於 Windows)
|
||||
Start.bat --port 8000 --listen false
|
||||
```
|
||||
|
||||
### Supported arguments
|
||||
|
||||
| Option | Description | Type |
|
||||
|-------------------------|------------------------------------------------------------------------------------------------------|----------|
|
||||
| `--version` | 顯示版本序號 | boolean |
|
||||
| `--enableIPv6` | 啟用 IPv6 | boolean |
|
||||
| `--enableIPv4` | 啟用 IPv4 | boolean |
|
||||
| `--port` | 設定 SillyTavern 運行的端口。若未提供,則預設使用 `config.yaml` 中的 'port' | number
|
||||
| `--dnsPreferIPv6` | 偏好使用 IPv6 解析 DNS。未提供則默認使用 `config.yaml` 中的 'preferIPv6' | boolean |
|
||||
| `--autorun` | 自動在瀏覽器中啟動 SillyTavern。未提供則默認使用 `config.yaml` 中的 'autorun' | boolean |
|
||||
| `--autorunHostname` | 自動啟動時的主機名稱,通常建議保持為 'auto' | string |
|
||||
| `--autorunPortOverride` | 覆蓋自動啟動的端口設定 | string |
|
||||
| `--listen` | SillyTavern 是否可監聽所有網路接口。若未提供,則默認使用 `config.yaml` 中的 'listen' | boolean |
|
||||
| `--corsProxy` | 啟用 CORS 代理。若未提供,則默認使用 `config.yaml` 中的 'enableCorsProxy' | boolean |
|
||||
| `--disableCsrf` | 停用 CSRF 保護 | boolean |
|
||||
| `--ssl` | 啟用 SSL | boolean |
|
||||
| `--certPath` | 設定您證書文件的路徑 | string |
|
||||
| `--keyPath` | 設定您私人金鑰文件的路徑 | string |
|
||||
| `--whitelist` | 啟用白名單模式 | boolean |
|
||||
| `--dataRoot` | 設定數據儲存的根目錄 | string |
|
||||
| `--avoidLocalhost` | 在自動模式下避免使用 'localhost' | boolean |
|
||||
| `--basicAuthMode` | 啟用基本身份驗證模式 | boolean |
|
||||
| `--requestProxyEnabled` | 啟用代理以處理外部請求 | boolean |
|
||||
| `--requestProxyUrl` | 設定請求代理的 URL(支持 HTTP 或 SOCKS 協議) | string |
|
||||
| `--requestProxyBypass` | 請求代理的例外主機清單(主機列表需以空格分隔) | array |
|
||||
|
||||
## 遠端連線
|
||||
|
||||
遠端連線功能最常用於希望在手機上使用 SillyTavern 的使用者。此時伺服器將由同一 Wi-Fi 網路上的 PC 運行。不過,您也可以設定來自其他網路的遠端連線。
|
||||
|
||||
詳細設定指南請參閱 [官方說明文件](https://docs.sillytavern.app/usage/remoteconnections/)。
|
||||
|
||||
您還可以選擇設定 SillyTavern 的使用者檔案,並開啟密碼保護(可選):[使用者設定指南](https://docs.sillytavern.app/installation/st-1.12.0-migration-guide/#users)。
|
||||
|
||||
## 遇到任何效能問題?
|
||||
|
||||
1. 在「使用者設定」選單(設定介面主題)中,禁用模糊效果(Blur Effect),並開啟「減少動畫效果」(Reduced Motion)
|
||||
2. 若使用響應串流傳輸,請將串流的 FPS 設定為較低的值(建議設定為 10-15 FPS)
|
||||
3. 確保瀏覽器已啟用 GPU 加速以進行渲染
|
||||
|
||||
## 授權與致謝
|
||||
|
||||
**本程式(SillyTavern)的發布是基於其可能對使用者有所幫助的期許,但不提供任何形式的保證;包括但不限於對可銷售性(marketability)或特定用途適用性的隱含保證。如需更多詳情,請參閱 GNU Affero 通用公共許可證。**
|
||||
|
||||
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
|
||||
|
||||
* [TavernAI](https://github.com/TavernAI/TavernAI) 1.2.8 由 Humi 提供:MIT 許可
|
||||
* 經授權使用部分來自 CncAnon 的 TavernAITurbo 模組
|
||||
* 視覺小說模式(Visual Novel Mode)的靈感,來源於 PepperTaco 的貢獻(<https://github.com/peppertaco/Tavern/>)
|
||||
* Noto Sans 字體由 Google 提供(OFL 許可)
|
||||
* 主題圖示由 Font Awesome <https://fontawesome.com> 提供(圖示:CC BY 4.0,字體:SIL OFL 1.1,代碼:MIT 許可)
|
||||
* 預設資源來源於 @OtisAlejandro(包含角色 Seraphina 與知識書)與 @kallmeflocc(SillyTavern 官方 Discord 伺服器成員突破 10K 的慶祝背景)
|
||||
* Docker 安裝指南由 [@mrguymiah](https://github.com/mrguymiah) 和 [@Bronya-Rand](https://github.com/Bronya-Rand) 編寫
|
||||
|
||||
## 主要貢獻者
|
||||
|
||||
[](https://github.com/SillyTavern/SillyTavern/graphs/contributors)
|
||||
|
||||
<!-- LINK GROUP -->
|
||||
[cover]: https://github.com/user-attachments/assets/01a6ae9a-16aa-45f2-8bff-32b5dc587e44
|
||||
[discord-link]: https://discord.gg/sillytavern
|
||||
[discord-shield-badge]: https://img.shields.io/discord/1100685673633153084?color=5865F2&label=discord&labelColor=black&logo=discord&logoColor=white&style=for-the-badge
|
2
.github/readme.md
vendored
2
.github/readme.md
vendored
@ -4,7 +4,7 @@
|
||||
|
||||
<div align="center">
|
||||
|
||||
English | [German](readme-de_de.md) | [中文](readme-zh_cn.md) | [日本語](readme-ja_jp.md) | [Русский](readme-ru_ru.md)
|
||||
English | [German](readme-de_de.md) | [中文](readme-zh_cn.md) | [繁體中文](readme-zh_tw.md) | [日本語](readme-ja_jp.md) | [Русский](readme-ru_ru.md)
|
||||
|
||||
[](https://github.com/SillyTavern/SillyTavern/stargazers)
|
||||
[](https://github.com/SillyTavern/SillyTavern/network)
|
||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -50,3 +50,5 @@ public/css/user.css
|
||||
/default/scaffold
|
||||
public/scripts/extensions/third-party
|
||||
/certs
|
||||
.aider*
|
||||
.env
|
||||
|
@ -11,3 +11,4 @@ access.log
|
||||
.github
|
||||
.vscode
|
||||
.git
|
||||
/public/scripts/extensions/third-party
|
||||
|
@ -65,7 +65,7 @@ label[for="extensions_autoconnect"] {
|
||||
}
|
||||
|
||||
.extensions_info .extension_enabled {
|
||||
color: green;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.extensions_info .extension_disabled {
|
||||
@ -76,13 +76,44 @@ label[for="extensions_autoconnect"] {
|
||||
color: gray;
|
||||
}
|
||||
|
||||
input.extension_missing[type="checkbox"] {
|
||||
opacity: 0.5;
|
||||
.extensions_info .extension_modules {
|
||||
font-size: 0.8em;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
#extensions_list .disabled {
|
||||
text-decoration: line-through;
|
||||
color: lightgray;
|
||||
.extensions_info .extension_block {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
padding: 5px;
|
||||
margin-bottom: 5px;
|
||||
border: 1px solid var(--SmartThemeBorderColor);
|
||||
border-radius: 10px;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.extensions_info .extension_name {
|
||||
font-size: 1.05em;
|
||||
}
|
||||
|
||||
.extensions_info .extension_version {
|
||||
opacity: 0.8;
|
||||
font-size: 0.8em;
|
||||
font-weight: normal;
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
.extensions_info .extension_block a {
|
||||
color: var(--SmartThemeBodyColor);
|
||||
}
|
||||
|
||||
.extensions_info .extension_name.update_available {
|
||||
color: limegreen;
|
||||
}
|
||||
|
||||
input.extension_missing[type="checkbox"] {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.update-button {
|
||||
@ -105,3 +136,13 @@ input.extension_missing[type="checkbox"] {
|
||||
#extensionsMenu>div.extension_container:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.extensions_info .extension_text_block {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.extensions_info .extension_actions {
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
@ -1774,6 +1774,35 @@
|
||||
<span data-i18n="Load default order">Load default order</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="sampler_priority_block_aphrodite" data-tg-type="aphrodite" class="range-block flexFlowColumn wide100p">
|
||||
<hr class="wide100p">
|
||||
<h4 class="range-block-title justifyCenter">
|
||||
<span data-i18n="Sampler Order">Sampler Order</span>
|
||||
<div class="margin5 fa-solid fa-circle-info opacity50p" title="Aphrodite only. Determines the order of samplers. Skew is always applied post-softmax, so it's not included here." data-i18n="[title]Aphrodite only. Determines the order of samplers. Skew is always applied post-softmax, so it's not included here."></div>
|
||||
</h4>
|
||||
<div class="toggle-description widthUnset" data-i18n="Aphrodite only. Determines the order of samplers.">
|
||||
Aphrodite only. Determines the order of samplers.
|
||||
</div>
|
||||
<div id="sampler_priority_container_aphrodite" class="prompt_order">
|
||||
<div data-name="dry" draggable="true"><span>DRY</span><small></small></div>
|
||||
<div data-name="penalties" draggable="true"><span>Penalties</span><small></small></div>
|
||||
<div data-name="no_repeat_ngram" draggable="true"><span>No Repeat Ngram</span><small></small></div>
|
||||
<div data-name="temperature" draggable="true"><span>Dynatemp & Temperature</span><small></small></div>
|
||||
<div data-name="top_nsigma" draggable="true"><span>Top Nsigma</span><small></small></div>
|
||||
<div data-name="top_p_top_k" draggable="true"><span>Top P & Top K</span><small></small></div>
|
||||
<div data-name="top_a" draggable="true"><span>Top A</span><small></small></div>
|
||||
<div data-name="min_p" draggable="true"><span>Min P</span><small></small></div>
|
||||
<div data-name="tfs" draggable="true"><span>Tail-Free Sampling</span><small></small></div>
|
||||
<div data-name="eta_cutoff" draggable="true"><span>Eta Cutoff</span><small></small></div>
|
||||
<div data-name="epsilon_cutoff" draggable="true"><span>Epsilon Cutoff</span><small></small></div>
|
||||
<div data-name="typical_p" draggable="true"><span>Typical P</span><small></small></div>
|
||||
<div data-name="quadratic" draggable="true"><span>Cubic and Quadratic Sampling</span><small></small></div>
|
||||
<div data-name="xtc" draggable="true"><span>XTC</span><small></small></div>
|
||||
</div>
|
||||
<div id="aphrodite_default_order" class="menu_button menu_button_icon">
|
||||
<span data-i18n="Load default order">Load default order</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div><!-- end of textgen settings-->
|
||||
<div id="openai_settings">
|
||||
@ -3071,6 +3100,9 @@
|
||||
</div>
|
||||
<h4 data-i18n="Groq Model">Groq Model</h4>
|
||||
<select id="model_groq_select">
|
||||
<optgroup label="Llama 3.3">
|
||||
<option value="llama-3.3-70b-versatile">llama-3.3-70b-versatile</option>
|
||||
</optgroup>
|
||||
<optgroup label="Llama 3.2">
|
||||
<option value="llama-3.2-1b-preview">llama-3.2-1b-preview</option>
|
||||
<option value="llama-3.2-3b-preview">llama-3.2-3b-preview</option>
|
||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -697,6 +697,11 @@ async function moduleWorker() {
|
||||
return;
|
||||
}
|
||||
|
||||
// If using LLM api then check if streamingProcessor is finished to avoid sending multiple requests to the API
|
||||
if (extension_settings.expressions.api === EXPRESSION_API.llm && context.streamingProcessor && !context.streamingProcessor.isFinished) {
|
||||
return;
|
||||
}
|
||||
|
||||
// API is busy
|
||||
if (inApiCall) {
|
||||
console.debug('Classification API is busy');
|
||||
@ -995,6 +1000,11 @@ function sampleClassifyText(text) {
|
||||
// Replace macros, remove asterisks and quotes
|
||||
let result = substituteParams(text).replace(/[*"]/g, '');
|
||||
|
||||
// If using LLM api there is no need to check length of characters
|
||||
if (extension_settings.expressions.api === EXPRESSION_API.llm) {
|
||||
return result.trim();
|
||||
}
|
||||
|
||||
const SAMPLE_THRESHOLD = 500;
|
||||
const HALF_SAMPLE_THRESHOLD = SAMPLE_THRESHOLD / 2;
|
||||
|
||||
|
0
public/scripts/extensions/third-party/.gitkeep
vendored
Normal file
0
public/scripts/extensions/third-party/.gitkeep
vendored
Normal file
@ -262,13 +262,13 @@ class AllTalkTtsProvider {
|
||||
console.debug('AllTalkTTS: Settings loaded');
|
||||
try {
|
||||
// Check if TTS provider is ready
|
||||
this.setupEventListeners();
|
||||
this.updateLanguageDropdown();
|
||||
await this.checkReady();
|
||||
await this.updateSettingsFromServer(); // Fetch dynamic settings from the TTS server
|
||||
await this.fetchTtsVoiceObjects(); // Fetch voices only if service is ready
|
||||
await this.fetchRvcVoiceObjects(); // Fetch RVC voices
|
||||
this.updateNarratorVoicesDropdown();
|
||||
this.updateLanguageDropdown();
|
||||
this.setupEventListeners();
|
||||
this.applySettingsToHTML();
|
||||
updateStatus('Ready');
|
||||
} catch (error) {
|
||||
|
@ -448,6 +448,5 @@ export class FilterHelper {
|
||||
for (const cache of Object.values(this.fuzzySearchCaches)) {
|
||||
cache.resultMap.clear();
|
||||
}
|
||||
console.log('All fuzzy search caches cleared');
|
||||
}
|
||||
}
|
||||
|
@ -1178,7 +1178,8 @@ function preparePromptsForChatCompletion({ Scenario, charPersonality, name2, wor
|
||||
|
||||
// Apply character-specific main prompt
|
||||
const systemPrompt = prompts.get('main') ?? null;
|
||||
if (systemPromptOverride && systemPrompt && systemPrompt.forbid_overrides !== true) {
|
||||
const isSystemPromptDisabled = promptManager.isPromptDisabledForActiveCharacter('main');
|
||||
if (systemPromptOverride && systemPrompt && systemPrompt.forbid_overrides !== true && !isSystemPromptDisabled) {
|
||||
const mainOriginalContent = systemPrompt.content;
|
||||
systemPrompt.content = systemPromptOverride;
|
||||
const mainReplacement = promptManager.preparePrompt(systemPrompt, mainOriginalContent);
|
||||
@ -1187,7 +1188,8 @@ function preparePromptsForChatCompletion({ Scenario, charPersonality, name2, wor
|
||||
|
||||
// Apply character-specific jailbreak
|
||||
const jailbreakPrompt = prompts.get('jailbreak') ?? null;
|
||||
if (jailbreakPromptOverride && jailbreakPrompt && jailbreakPrompt.forbid_overrides !== true) {
|
||||
const isJailbreakPromptDisabled = promptManager.isPromptDisabledForActiveCharacter('jailbreak');
|
||||
if (jailbreakPromptOverride && jailbreakPrompt && jailbreakPrompt.forbid_overrides !== true && !isJailbreakPromptDisabled) {
|
||||
const jbOriginalContent = jailbreakPrompt.content;
|
||||
jailbreakPrompt.content = jailbreakPromptOverride;
|
||||
const jbReplacement = promptManager.preparePrompt(jailbreakPrompt, jbOriginalContent);
|
||||
@ -4269,7 +4271,7 @@ async function onModelChange() {
|
||||
else if (oai_settings.groq_model.includes('llama-3.2') && oai_settings.groq_model.includes('-preview')) {
|
||||
$('#openai_max_context').attr('max', max_8k);
|
||||
}
|
||||
else if (oai_settings.groq_model.includes('llama-3.2') || oai_settings.groq_model.includes('llama-3.1')) {
|
||||
else if (oai_settings.groq_model.includes('llama-3.3') || oai_settings.groq_model.includes('llama-3.2') || oai_settings.groq_model.includes('llama-3.1')) {
|
||||
$('#openai_max_context').attr('max', max_128k);
|
||||
}
|
||||
else if (oai_settings.groq_model.includes('llama3-groq')) {
|
||||
|
@ -1757,7 +1757,7 @@ async function loadContextSettings() {
|
||||
} else {
|
||||
$element.val(power_user.context[control.property]);
|
||||
}
|
||||
console.log(`Setting ${$element.prop('id')} to ${power_user.context[control.property]}`);
|
||||
console.debug(`Setting ${$element.prop('id')} to ${power_user.context[control.property]}`);
|
||||
|
||||
// If the setting already exists, no need to duplicate it
|
||||
// TODO: Maybe check the power_user object for the setting instead of a flag?
|
||||
@ -1768,7 +1768,7 @@ async function loadContextSettings() {
|
||||
} else {
|
||||
power_user.context[control.property] = value;
|
||||
}
|
||||
console.log(`Setting ${$element.prop('id')} to ${value}`);
|
||||
console.debug(`Setting ${$element.prop('id')} to ${value}`);
|
||||
if (!CSS.supports('field-sizing', 'content') && $(this).is('textarea')) {
|
||||
await resetScrollHeight($(this));
|
||||
}
|
||||
|
@ -129,6 +129,10 @@ function setSamplerListListeners() {
|
||||
relatedDOMElement = $('#sampler_priority_block_ooba');
|
||||
}
|
||||
|
||||
if (samplerName === 'samplers_priorities') { //this is for aphrodite's sampler priority
|
||||
relatedDOMElement = $('#sampler_priority_block_aphrodite');
|
||||
}
|
||||
|
||||
if (samplerName === 'penalty_alpha') { //contrastive search only has one sampler, does it need its own block?
|
||||
relatedDOMElement = $('#contrastiveSearchBlock');
|
||||
}
|
||||
@ -237,6 +241,11 @@ async function listSamplers(main_api, arrayOnly = false) {
|
||||
displayname = 'Ooba Sampler Priority Block';
|
||||
}
|
||||
|
||||
if (sampler === 'samplers_priorities') { //this is for aphrodite's sampler priority
|
||||
targetDOMelement = $('#sampler_priority_block_aphrodite');
|
||||
displayname = 'Aphrodite Sampler Priority Block';
|
||||
}
|
||||
|
||||
if (sampler === 'penalty_alpha') { //contrastive search only has one sampler, does it need its own block?
|
||||
targetDOMelement = $('#contrastiveSearchBlock');
|
||||
displayname = 'Contrast Search Block';
|
||||
@ -373,6 +382,10 @@ export async function validateDisabledSamplers(redraw = false) {
|
||||
relatedDOMElement = $('#sampler_priority_block_ooba');
|
||||
}
|
||||
|
||||
if (sampler === 'samplers_priorities') { //this is for aphrodite's sampler priority
|
||||
relatedDOMElement = $('#sampler_priority_block_aphrodite');
|
||||
}
|
||||
|
||||
if (sampler === 'dry_multiplier') {
|
||||
relatedDOMElement = $('#dryBlock');
|
||||
targetDisplayType = 'block';
|
||||
|
@ -16,7 +16,7 @@ import { power_user, registerDebugFunction } from './power-user.js';
|
||||
import { getEventSourceStream } from './sse-stream.js';
|
||||
import { getCurrentDreamGenModelTokenizer, getCurrentOpenRouterModelTokenizer } from './textgen-models.js';
|
||||
import { ENCODE_TOKENIZERS, TEXTGEN_TOKENIZERS, getTextTokens, tokenizers } from './tokenizers.js';
|
||||
import { getSortableDelay, onlyUnique } from './utils.js';
|
||||
import { getSortableDelay, onlyUnique, arraysEqual } from './utils.js';
|
||||
|
||||
export const textgen_types = {
|
||||
OOBA: 'ooba',
|
||||
@ -82,6 +82,22 @@ const OOBA_DEFAULT_ORDER = [
|
||||
'encoder_repetition_penalty',
|
||||
'no_repeat_ngram',
|
||||
];
|
||||
const APHRODITE_DEFAULT_ORDER = [
|
||||
'dry',
|
||||
'penalties',
|
||||
'no_repeat_ngram',
|
||||
'temperature',
|
||||
'top_nsigma',
|
||||
'top_p_top_k',
|
||||
'top_a',
|
||||
'min_p',
|
||||
'tfs',
|
||||
'eta_cutoff',
|
||||
'epsilon_cutoff',
|
||||
'typical_p',
|
||||
'quadratic',
|
||||
'xtc',
|
||||
];
|
||||
const BIAS_KEY = '#textgenerationwebui_api-settings';
|
||||
|
||||
// Maybe let it be configurable in the future?
|
||||
@ -163,6 +179,7 @@ const settings = {
|
||||
banned_tokens: '',
|
||||
sampler_priority: OOBA_DEFAULT_ORDER,
|
||||
samplers: LLAMACPP_DEFAULT_ORDER,
|
||||
samplers_priorities: APHRODITE_DEFAULT_ORDER,
|
||||
ignore_eos_token: false,
|
||||
spaces_between_special_tokens: true,
|
||||
speculative_ngram: false,
|
||||
@ -256,6 +273,7 @@ export const setting_names = [
|
||||
'sampler_order',
|
||||
'sampler_priority',
|
||||
'samplers',
|
||||
'samplers_priorities',
|
||||
'n',
|
||||
'logit_bias',
|
||||
'custom_model',
|
||||
@ -553,6 +571,20 @@ function sortOobaItemsByOrder(orderArray) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorts the Aphrodite sampler items by the given order.
|
||||
* @param {string[]} orderArray Sampler order array.
|
||||
*/
|
||||
function sortAphroditeItemsByOrder(orderArray) {
|
||||
console.debug('Preset samplers order: ', orderArray);
|
||||
const $container = $('#sampler_priority_container_aphrodite');
|
||||
|
||||
orderArray.forEach((name) => {
|
||||
const $item = $container.find(`[data-name="${name}"]`).detach();
|
||||
$container.append($item);
|
||||
});
|
||||
}
|
||||
|
||||
jQuery(function () {
|
||||
$('#koboldcpp_order').sortable({
|
||||
delay: getSortableDelay(),
|
||||
@ -606,6 +638,19 @@ jQuery(function () {
|
||||
},
|
||||
});
|
||||
|
||||
$('#sampler_priority_container_aphrodite').sortable({
|
||||
delay: getSortableDelay(),
|
||||
stop: function () {
|
||||
const order = [];
|
||||
$('#sampler_priority_container_aphrodite').children().each(function () {
|
||||
order.push($(this).data('name'));
|
||||
});
|
||||
settings.samplers_priorities = order;
|
||||
console.log('Samplers reordered:', settings.samplers_priorities);
|
||||
saveSettingsDebounced();
|
||||
},
|
||||
});
|
||||
|
||||
$('#tabby_json_schema').on('input', function () {
|
||||
const json_schema_string = String($(this).val());
|
||||
|
||||
@ -624,6 +669,13 @@ jQuery(function () {
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$('#aphrodite_default_order').on('click', function () {
|
||||
sortAphroditeItemsByOrder(APHRODITE_DEFAULT_ORDER);
|
||||
settings.samplers_priorities = APHRODITE_DEFAULT_ORDER;
|
||||
console.log('Default samplers order loaded:', settings.samplers_priorities);
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$('#textgen_type').on('change', function () {
|
||||
const type = String($(this).val());
|
||||
settings.type = type;
|
||||
@ -832,6 +884,14 @@ function setSettingByName(setting, value, trigger) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ('samplers_priorities' === setting) {
|
||||
value = Array.isArray(value) ? value : APHRODITE_DEFAULT_ORDER;
|
||||
insertMissingArrayItems(APHRODITE_DEFAULT_ORDER, value);
|
||||
sortAphroditeItemsByOrder(value);
|
||||
settings.samplers_priorities = value;
|
||||
return;
|
||||
}
|
||||
|
||||
if ('samplers' === setting) {
|
||||
value = Array.isArray(value) ? value : LLAMACPP_DEFAULT_ORDER;
|
||||
insertMissingArrayItems(LLAMACPP_DEFAULT_ORDER, value);
|
||||
@ -1256,6 +1316,11 @@ export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate,
|
||||
'nsigma': settings.nsigma,
|
||||
'custom_token_bans': toIntArray(banned_tokens),
|
||||
'no_repeat_ngram_size': settings.no_repeat_ngram_size,
|
||||
'sampler_priority': settings.type === APHRODITE && !arraysEqual(
|
||||
settings.samplers_priorities,
|
||||
APHRODITE_DEFAULT_ORDER)
|
||||
? settings.samplers_priorities
|
||||
: undefined,
|
||||
};
|
||||
|
||||
if (settings.type === OPENROUTER) {
|
||||
|
@ -31,7 +31,7 @@ export async function setUserControls(isEnabled) {
|
||||
* Check if the current user is an admin.
|
||||
* @returns {boolean} True if the current user is an admin
|
||||
*/
|
||||
function isAdmin() {
|
||||
export function isAdmin() {
|
||||
if (!currentUser) {
|
||||
return false;
|
||||
}
|
||||
|
@ -2173,3 +2173,20 @@ export function getCharIndex(char) {
|
||||
if (index === -1) throw new Error(`Character not found: ${char.avatar}`);
|
||||
return index;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares two arrays for equality
|
||||
* @param {any[]} a - The first array
|
||||
* @param {any[]} b - The second array
|
||||
* @returns {boolean} True if the arrays are equal, false otherwise
|
||||
*/
|
||||
export function arraysEqual(a, b) {
|
||||
if (a === b) return true;
|
||||
if (a == null || b == null) return false;
|
||||
if (a.length !== b.length) return false;
|
||||
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
if (a[i] !== b[i]) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ export const PUBLIC_DIRECTORIES = {
|
||||
backups: 'backups/',
|
||||
sounds: 'public/sounds',
|
||||
extensions: 'public/scripts/extensions',
|
||||
globalExtensions: 'public/scripts/extensions/third-party',
|
||||
};
|
||||
|
||||
export const SETTINGS_FILE = 'settings.json';
|
||||
|
@ -158,7 +158,8 @@ async function tryReadImage(imgPath, crop) {
|
||||
return image;
|
||||
}
|
||||
// If it's an unsupported type of image (APNG) - just read the file as buffer
|
||||
catch {
|
||||
catch (error) {
|
||||
console.log(`Failed to read image: ${imgPath}`, error);
|
||||
return fs.readFileSync(imgPath);
|
||||
}
|
||||
}
|
||||
|
@ -73,8 +73,19 @@ router.post('/install', jsonParser, async (request, response) => {
|
||||
fs.mkdirSync(path.join(request.user.directories.extensions));
|
||||
}
|
||||
|
||||
const url = request.body.url;
|
||||
const extensionPath = path.join(request.user.directories.extensions, path.basename(url, '.git'));
|
||||
if (!fs.existsSync(PUBLIC_DIRECTORIES.globalExtensions)) {
|
||||
fs.mkdirSync(PUBLIC_DIRECTORIES.globalExtensions);
|
||||
}
|
||||
|
||||
const { url, global } = request.body;
|
||||
|
||||
if (global && !request.user.profile.admin) {
|
||||
console.warn(`User ${request.user.profile.handle} does not have permission to install global extensions.`);
|
||||
return response.status(403).send('Forbidden: No permission to install global extensions.');
|
||||
}
|
||||
|
||||
const basePath = global ? PUBLIC_DIRECTORIES.globalExtensions : request.user.directories.extensions;
|
||||
const extensionPath = path.join(basePath, sanitize(path.basename(url, '.git')));
|
||||
|
||||
if (fs.existsSync(extensionPath)) {
|
||||
return response.status(409).send(`Directory already exists at ${extensionPath}`);
|
||||
@ -83,10 +94,8 @@ router.post('/install', jsonParser, async (request, response) => {
|
||||
await git.clone(url, extensionPath, { '--depth': 1 });
|
||||
console.log(`Extension has been cloned at ${extensionPath}`);
|
||||
|
||||
|
||||
const { version, author, display_name } = await getManifest(extensionPath);
|
||||
|
||||
|
||||
return response.send({ version, author, display_name, extensionPath });
|
||||
} catch (error) {
|
||||
console.log('Importing custom content failed', error);
|
||||
@ -112,8 +121,15 @@ router.post('/update', jsonParser, async (request, response) => {
|
||||
}
|
||||
|
||||
try {
|
||||
const extensionName = request.body.extensionName;
|
||||
const extensionPath = path.join(request.user.directories.extensions, extensionName);
|
||||
const { extensionName, global } = request.body;
|
||||
|
||||
if (global && !request.user.profile.admin) {
|
||||
console.warn(`User ${request.user.profile.handle} does not have permission to update global extensions.`);
|
||||
return response.status(403).send('Forbidden: No permission to update global extensions.');
|
||||
}
|
||||
|
||||
const basePath = global ? PUBLIC_DIRECTORIES.globalExtensions : request.user.directories.extensions;
|
||||
const extensionPath = path.join(basePath, extensionName);
|
||||
|
||||
if (!fs.existsSync(extensionPath)) {
|
||||
return response.status(404).send(`Directory does not exist at ${extensionPath}`);
|
||||
@ -122,7 +138,6 @@ router.post('/update', jsonParser, async (request, response) => {
|
||||
const { isUpToDate, remoteUrl } = await checkIfRepoIsUpToDate(extensionPath);
|
||||
const currentBranch = await git.cwd(extensionPath).branch();
|
||||
if (!isUpToDate) {
|
||||
|
||||
await git.cwd(extensionPath).pull('origin', currentBranch.current);
|
||||
console.log(`Extension has been updated at ${extensionPath}`);
|
||||
} else {
|
||||
@ -140,6 +155,50 @@ router.post('/update', jsonParser, async (request, response) => {
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/move', jsonParser, async (request, response) => {
|
||||
try {
|
||||
const { extensionName, source, destination } = request.body;
|
||||
|
||||
if (!extensionName || !source || !destination) {
|
||||
return response.status(400).send('Bad Request. Not all required parameters are provided.');
|
||||
}
|
||||
|
||||
if (!request.user.profile.admin) {
|
||||
console.warn(`User ${request.user.profile.handle} does not have permission to move extensions.`);
|
||||
return response.status(403).send('Forbidden: No permission to move extensions.');
|
||||
}
|
||||
|
||||
const sourceDirectory = source === 'global' ? PUBLIC_DIRECTORIES.globalExtensions : request.user.directories.extensions;
|
||||
const destinationDirectory = destination === 'global' ? PUBLIC_DIRECTORIES.globalExtensions : request.user.directories.extensions;
|
||||
const sourcePath = path.join(sourceDirectory, sanitize(extensionName));
|
||||
const destinationPath = path.join(destinationDirectory, sanitize(extensionName));
|
||||
|
||||
if (!fs.existsSync(sourcePath) || !fs.statSync(sourcePath).isDirectory()) {
|
||||
console.error(`Source directory does not exist at ${sourcePath}`);
|
||||
return response.status(404).send('Source directory does not exist.');
|
||||
}
|
||||
|
||||
if (fs.existsSync(destinationPath)) {
|
||||
console.error(`Destination directory already exists at ${destinationPath}`);
|
||||
return response.status(409).send('Destination directory already exists.');
|
||||
}
|
||||
|
||||
if (source === destination) {
|
||||
console.error('Source and destination directories are the same');
|
||||
return response.status(409).send('Source and destination directories are the same.');
|
||||
}
|
||||
|
||||
fs.cpSync(sourcePath, destinationPath, { recursive: true, force: true });
|
||||
fs.rmSync(sourcePath, { recursive: true, force: true });
|
||||
console.log(`Extension has been moved from ${sourcePath} to ${destinationPath}`);
|
||||
|
||||
return response.sendStatus(204);
|
||||
} catch (error) {
|
||||
console.log('Moving extension failed', error);
|
||||
return response.status(500).send('Internal Server Error. Try again later.');
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* HTTP POST handler function to get the current git commit hash and branch name for a given extension.
|
||||
* It checks whether the repository is up-to-date with the remote, and returns the status along with
|
||||
@ -157,19 +216,28 @@ router.post('/version', jsonParser, async (request, response) => {
|
||||
}
|
||||
|
||||
try {
|
||||
const extensionName = request.body.extensionName;
|
||||
const extensionPath = path.join(request.user.directories.extensions, extensionName);
|
||||
const { extensionName, global } = request.body;
|
||||
const basePath = global ? PUBLIC_DIRECTORIES.globalExtensions : request.user.directories.extensions;
|
||||
const extensionPath = path.join(basePath, sanitize(extensionName));
|
||||
|
||||
if (!fs.existsSync(extensionPath)) {
|
||||
return response.status(404).send(`Directory does not exist at ${extensionPath}`);
|
||||
}
|
||||
|
||||
let currentCommitHash;
|
||||
try {
|
||||
currentCommitHash = await git.cwd(extensionPath).revparse(['HEAD']);
|
||||
} catch (error) {
|
||||
// it is not a git repo, or has no commits yet, or is a bare repo
|
||||
// not possible to update it, most likely can't get the branch name either
|
||||
return response.send({ currentBranchName: null, currentCommitHash, isUpToDate: true, remoteUrl: null });
|
||||
}
|
||||
|
||||
const currentBranch = await git.cwd(extensionPath).branch();
|
||||
// get only the working branch
|
||||
const currentBranchName = currentBranch.current;
|
||||
await git.cwd(extensionPath).fetch('origin');
|
||||
const currentCommitHash = await git.cwd(extensionPath).revparse(['HEAD']);
|
||||
console.log(currentBranch, currentCommitHash);
|
||||
console.log(extensionName, currentBranchName, currentCommitHash);
|
||||
const { isUpToDate, remoteUrl } = await checkIfRepoIsUpToDate(extensionPath);
|
||||
|
||||
return response.send({ currentBranchName, currentCommitHash, isUpToDate, remoteUrl });
|
||||
@ -193,11 +261,16 @@ router.post('/delete', jsonParser, async (request, response) => {
|
||||
return response.status(400).send('Bad Request: extensionName is required in the request body.');
|
||||
}
|
||||
|
||||
// Sanitize the extension name to prevent directory traversal
|
||||
const extensionName = sanitize(request.body.extensionName);
|
||||
|
||||
try {
|
||||
const extensionPath = path.join(request.user.directories.extensions, extensionName);
|
||||
const { extensionName, global } = request.body;
|
||||
|
||||
if (global && !request.user.profile.admin) {
|
||||
console.warn(`User ${request.user.profile.handle} does not have permission to delete global extensions.`);
|
||||
return response.status(403).send('Forbidden: No permission to delete global extensions.');
|
||||
}
|
||||
|
||||
const basePath = global ? PUBLIC_DIRECTORIES.globalExtensions : request.user.directories.extensions;
|
||||
const extensionPath = path.join(basePath, sanitize(extensionName));
|
||||
|
||||
if (!fs.existsSync(extensionPath)) {
|
||||
return response.status(404).send(`Directory does not exist at ${extensionPath}`);
|
||||
@ -219,26 +292,38 @@ router.post('/delete', jsonParser, async (request, response) => {
|
||||
* If the folder is called third-party, search for subfolders instead
|
||||
*/
|
||||
router.get('/discover', jsonParser, function (request, response) {
|
||||
// get all folders in the extensions folder, except third-party
|
||||
const extensions = fs
|
||||
.readdirSync(PUBLIC_DIRECTORIES.extensions)
|
||||
.filter(f => fs.statSync(path.join(PUBLIC_DIRECTORIES.extensions, f)).isDirectory())
|
||||
.filter(f => f !== 'third-party');
|
||||
|
||||
// get all folders in the third-party folder, if it exists
|
||||
|
||||
if (!fs.existsSync(path.join(request.user.directories.extensions))) {
|
||||
return response.send(extensions);
|
||||
fs.mkdirSync(path.join(request.user.directories.extensions));
|
||||
}
|
||||
|
||||
const thirdPartyExtensions = fs
|
||||
if (!fs.existsSync(PUBLIC_DIRECTORIES.globalExtensions)) {
|
||||
fs.mkdirSync(PUBLIC_DIRECTORIES.globalExtensions);
|
||||
}
|
||||
|
||||
// Get all folders in system extensions folder, excluding third-party
|
||||
const builtInExtensions = fs
|
||||
.readdirSync(PUBLIC_DIRECTORIES.extensions)
|
||||
.filter(f => fs.statSync(path.join(PUBLIC_DIRECTORIES.extensions, f)).isDirectory())
|
||||
.filter(f => f !== 'third-party')
|
||||
.map(f => ({ type: 'system', name: f }));
|
||||
|
||||
// Get all folders in local extensions folder
|
||||
const userExtensions = fs
|
||||
.readdirSync(path.join(request.user.directories.extensions))
|
||||
.filter(f => fs.statSync(path.join(request.user.directories.extensions, f)).isDirectory());
|
||||
.filter(f => fs.statSync(path.join(request.user.directories.extensions, f)).isDirectory())
|
||||
.map(f => ({ type: 'local', name: `third-party/${f}` }));
|
||||
|
||||
// add the third-party extensions to the extensions array
|
||||
extensions.push(...thirdPartyExtensions.map(f => `third-party/${f}`));
|
||||
console.log(extensions);
|
||||
// Get all folders in global extensions folder
|
||||
// In case of a conflict, the extension will be loaded from the user folder
|
||||
const globalExtensions = fs
|
||||
.readdirSync(PUBLIC_DIRECTORIES.globalExtensions)
|
||||
.filter(f => fs.statSync(path.join(PUBLIC_DIRECTORIES.globalExtensions, f)).isDirectory())
|
||||
.map(f => ({ type: 'global', name: `third-party/${f}` }))
|
||||
.filter(f => !userExtensions.some(e => e.name === f.name));
|
||||
|
||||
// Combine all extensions
|
||||
const allExtensions = [...builtInExtensions, ...userExtensions, ...globalExtensions];
|
||||
console.log(allExtensions);
|
||||
|
||||
return response.send(extensions);
|
||||
return response.send(allExtensions);
|
||||
});
|
||||
|
30
src/users.js
30
src/users.js
@ -782,6 +782,34 @@ function createRouteHandler(directoryFn) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a route handler for serving extensions.
|
||||
* @param {(req: import('express').Request) => string} directoryFn A function that returns the directory path to serve files from
|
||||
* @returns {import('express').RequestHandler}
|
||||
*/
|
||||
function createExtensionsRouteHandler(directoryFn) {
|
||||
return async (req, res) => {
|
||||
try {
|
||||
const directory = directoryFn(req);
|
||||
const filePath = decodeURIComponent(req.params[0]);
|
||||
|
||||
const existsLocal = fs.existsSync(path.join(directory, filePath));
|
||||
if (existsLocal) {
|
||||
return res.sendFile(filePath, { root: directory });
|
||||
}
|
||||
|
||||
const existsGlobal = fs.existsSync(path.join(PUBLIC_DIRECTORIES.globalExtensions, filePath));
|
||||
if (existsGlobal) {
|
||||
return res.sendFile(filePath, { root: PUBLIC_DIRECTORIES.globalExtensions });
|
||||
}
|
||||
|
||||
return res.sendStatus(404);
|
||||
} catch (error) {
|
||||
return res.sendStatus(500);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that the current user is an admin.
|
||||
* @param {import('express').Request} request Request object
|
||||
@ -872,4 +900,4 @@ router.use('/User%20Avatars/*', createRouteHandler(req => req.user.directories.a
|
||||
router.use('/assets/*', createRouteHandler(req => req.user.directories.assets));
|
||||
router.use('/user/images/*', createRouteHandler(req => req.user.directories.userImages));
|
||||
router.use('/user/files/*', createRouteHandler(req => req.user.directories.files));
|
||||
router.use('/scripts/extensions/third-party/*', createRouteHandler(req => req.user.directories.extensions));
|
||||
router.use('/scripts/extensions/third-party/*', createExtensionsRouteHandler(req => req.user.directories.extensions));
|
||||
|
Loading…
x
Reference in New Issue
Block a user