update by Silence_Lurker
Init plugin
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -136,3 +136,4 @@ dist
|
||||
.yarn/install-state.gz
|
||||
.pnp.*
|
||||
|
||||
.history
|
||||
185
README.md
185
README.md
@@ -1,3 +1,188 @@
|
||||
# Amily-Databin
|
||||
|
||||
Databin组件,优化数据存储持久化功能,不直接提供用户界面,默认仅供开发者进行调用。
|
||||
|
||||
该组件的意义为降低开发者对数据存储的复杂度,提供一种简单的数据存储方式,并支持数据持久化。该组件维护且仅维护存储和读取数据,不提供用户界面。(当然,并不意味着该组件不可用于用户界面,我们鼓励用户自行设计用户界面)
|
||||
|
||||
## 安装
|
||||
|
||||
### 通过链接安装 (推荐)
|
||||
|
||||
1. 在 SillyTavern 的扩展页面,点击 "Install from URL"。
|
||||
2. 输入以下链接: `https://amily-gitlab.amily49.cc/slvccans/Amily-Databin`
|
||||
3. 点击 "Install"。
|
||||
4. 重启 SillyTavern.
|
||||
|
||||
### 手动安装
|
||||
|
||||
1. 下载本仓库的 ZIP 压缩包。
|
||||
2. 解压 ZIP 文件。
|
||||
3. 将 `Amily-Databin` 文件夹复制到 `SillyTavern/public/extensions` 目录下。
|
||||
4. 重启 SillyTavern.
|
||||
|
||||
## API 用法
|
||||
|
||||
本插件会在客户端环境中注入一个全局的 `window.AmilyDatabin` 对象,用于进行精细化的数据交互。
|
||||
|
||||
**架构说明:** `SourceType` 常量表现在通过根目录的 `sourcetype.json` 文件进行动态管理。客户端会在加载时自动从服务器获取此配置,这使得系统具有很强的扩展性,未来增加新的数据类型也无需修改客户端代码。
|
||||
|
||||
### `AmilyDatabin.SourceType`
|
||||
|
||||
一个用于定义数据来源的常量表。
|
||||
|
||||
```javascript
|
||||
{
|
||||
CHAT_LOG: 1, // 用于聊天文件存储
|
||||
CHARACTER_CARD: 2, // 用于角色卡存储
|
||||
WORLD_BOOK: 3 // 用于世界书存储
|
||||
}
|
||||
```
|
||||
|
||||
### `AmilyDatabin.getData(source, fileName, objName = null)`
|
||||
|
||||
从服务器获取特定数据。`objName` 是可选的,如果省略,插件会从当前上下文中自动检测。
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* 从服务器获取特定数据。
|
||||
* @param {number} source - 数据来源标识 (使用 AmilyDatabin.SourceType)。
|
||||
* @param {string} fileName - 文件名或唯一键。
|
||||
* @param {string} [objName=null] - 可选。指定对象名(如角色ID)。若为null,则自动检测。**注意:对于 `WORLD_BOOK` 类型,`objName` 无法自动检测,必须明确提供。**
|
||||
* @returns {Promise<object|null>} 一个Promise,解析为请求到的数据对象;如果未找到则为 null。
|
||||
*/
|
||||
async function retrieveCurrentCharacterMemory() {
|
||||
try {
|
||||
// 此处省略了 objName,插件将自动使用当前角色的ID
|
||||
const memory = await window.AmilyDatabin.getData(
|
||||
window.AmilyDatabin.SourceType.CHARACTER_CARD,
|
||||
"character_memory.json"
|
||||
);
|
||||
|
||||
if (memory) {
|
||||
console.log("成功获取当前角色记忆:", memory);
|
||||
} else {
|
||||
console.log("没有找到当前角色的记忆。");
|
||||
}
|
||||
return memory;
|
||||
} catch (error) {
|
||||
console.error("获取数据失败:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 自动上下文调用示例:
|
||||
retrieveCurrentCharacterMemory();
|
||||
```
|
||||
|
||||
### `AmilyDatabin.saveData(source, fileName, fileContent, objName = null)`
|
||||
|
||||
将一个JavaScript对象保存到服务器的指定路径。`objName` 是可选的。
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* 将一个数据对象保存到服务器的指定路径。
|
||||
* @param {number} source - 数据来源标识 (使用 AmilyDatabin.SourceType)。
|
||||
* @param {string} fileName - 文件名或唯一键。
|
||||
* @param {string} [objName=null] - 可选。指定对象名。若为null,则自动检测。**注意:对于 `WORLD_BOOK` 类型,`objName` 无法自动检测,必须明确提供。**
|
||||
* @returns {Promise<boolean>} 一个Promise,如果保存成功则解析为 true,否则为 false。
|
||||
*/
|
||||
async function saveCurrentChatLog(chatHistory) {
|
||||
try {
|
||||
// 此处省略了 objName,它将自动解析为当前聊天的ID
|
||||
const success = await window.AmilyDatabin.saveData(
|
||||
window.AmilyDatabin.SourceType.CHAT_LOG,
|
||||
`log_${new Date().getTime()}.json`, // 唯一键示例
|
||||
{ history: chatHistory, savedAt: new Date().toISOString() }
|
||||
);
|
||||
|
||||
if (success) {
|
||||
console.log("当前聊天的记录已成功保存。");
|
||||
} else {
|
||||
console.error("聊天记录保存失败。");
|
||||
}
|
||||
return success;
|
||||
} catch (error) {
|
||||
console.error("保存数据时出错:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 调用示例:
|
||||
const currentChat = ["用户: 你好!", "角色: 你好!"];
|
||||
saveCurrentChatLog(currentChat);
|
||||
```
|
||||
|
||||
### `AmilyDatabin.deleteData(source, fileName, objName = null)`
|
||||
|
||||
从服务器删除一个指定的数据文件。`objName` 是可选的。
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* 从服务器删除一个特定的数据文件。
|
||||
* @param {number} source - 数据来源标识 (使用 AmilyDatabin.SourceType)。
|
||||
* @param {string} fileName - 要删除的数据的文件名或唯一键。
|
||||
* @param {string} [objName=null] - 可选。指定对象名。若为null,则自动检测。**注意:对于 `WORLD_BOOK` 类型,`objName` 无法自动检测,必须明确提供。**
|
||||
* @returns {Promise<boolean>} 一个Promise,如果删除成功则解析为 true,否则为 false。
|
||||
*/
|
||||
async function deleteCurrentCharacterMemory() {
|
||||
try {
|
||||
const success = await window.AmilyDatabin.deleteData(
|
||||
window.AmilyDatabin.SourceType.CHARACTER_CARD,
|
||||
"character_memory.json"
|
||||
);
|
||||
|
||||
if (success) {
|
||||
console.log("成功删除当前角色记忆。");
|
||||
} else {
|
||||
console.log("删除角色记忆失败。");
|
||||
}
|
||||
return success;
|
||||
} catch (error) {
|
||||
console.error("删除数据失败:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 调用示例:
|
||||
deleteCurrentCharacterMemory();
|
||||
```
|
||||
|
||||
## 调试方法
|
||||
|
||||
这些方法设计用于在浏览器控制台中进行快速调试。它们不与后端API交互。
|
||||
|
||||
### `AmilyDatabin.getDebugInstance()`
|
||||
|
||||
将 `AmilyDatabin` 对象本身输出到控制台。可用于检查其当前状态、`SourceType` 映射和可用方法。
|
||||
|
||||
```javascript
|
||||
// 在浏览器控制台调用:
|
||||
AmilyDatabin.getDebugInstance();
|
||||
// 预期输出: { SourceType: { CHAT_LOG: 1, ... }, initialize: f, ... }
|
||||
```
|
||||
|
||||
### `AmilyDatabin.getDebugChatId()`
|
||||
|
||||
尝试使用与主API方法相同的逻辑解析当前聊天ID,并将其记录到控制台。
|
||||
|
||||
```javascript
|
||||
// 在浏览器控制台调用:
|
||||
AmilyDatabin.getDebugChatId();
|
||||
// 预期输出: [Amily-Databin] Current Chat ID (Debug): "你的聊天ID"
|
||||
```
|
||||
|
||||
### `AmilyDatabin.getDebugCardId()`
|
||||
|
||||
尝试使用与主API方法相同的逻辑解析当前角色卡ID,并将其记录到控制台。
|
||||
|
||||
```javascript
|
||||
// 在浏览器控制台调用:
|
||||
AmilyDatabin.getDebugCardId();
|
||||
// 预期输出: [Amily-Databin] Current Character Card ID (Debug): "你的角色卡ID"
|
||||
```
|
||||
|
||||
## 开发者
|
||||
|
||||
主要逻辑分别位于 `index.js` (服务器端) 和 `public/client.js` (客户端)。
|
||||
|
||||
你可以直接通过全局的 `window.AmilyDatabin` 对象与此插件交互。
|
||||
|
||||
192
README_en.md
Normal file
192
README_en.md
Normal file
@@ -0,0 +1,192 @@
|
||||
# Amily-Databin
|
||||
|
||||
A Databin component designed to optimize data storage persistence. It does not directly provide a user interface but is intended for developers to call for data operations.
|
||||
|
||||
The purpose of this component is to simplify data storage for developers, offering a straightforward method for data storage with persistence support. This component solely manages data storage and retrieval; it does not offer a user interface. (However, this does not mean the component cannot be used for user interfaces; users are encouraged to design their own user interfaces.)
|
||||
|
||||
## Installation
|
||||
|
||||
### Install via Link (Recommended)
|
||||
|
||||
1. In the SillyTavern extensions page, click "Install from URL".
|
||||
2. Enter the following link: `https://amily-gitlab.amily49.cc/slvccans/Amily-Databin`
|
||||
3. Click "Install".
|
||||
4. Restart SillyTavern.
|
||||
|
||||
### Manual Installation
|
||||
|
||||
1. Download the ZIP archive of this repository.
|
||||
2. Extract the ZIP file.
|
||||
3. Copy the `Amily-Databin` folder to the `SillyTavern/public/extensions` directory.
|
||||
4. Restart SillyTavern.
|
||||
|
||||
## Usage
|
||||
|
||||
This plugin acts as a data bucket component and does not provide a user interface. It is primarily intended for developers to call.
|
||||
|
||||
## API Usage
|
||||
|
||||
This plugin injects a global `window.AmilyDatabin` object into the client-side environment for simplified, granular data interaction.
|
||||
|
||||
**Architectural Note:** The `SourceType` constants table is dynamically managed via the `sourcetype.json` file. The client automatically fetches this configuration from the server upon loading, making the system extensible without requiring client-side code changes.
|
||||
|
||||
### `AmilyDatabin.SourceType`
|
||||
|
||||
A constants table used to define the source of the data.
|
||||
|
||||
```javascript
|
||||
{
|
||||
CHAT_LOG: 1, // For chat log storage
|
||||
CHARACTER_CARD: 2, // For character card storage
|
||||
WORLD_BOOK: 3 // For world book storage
|
||||
}
|
||||
```
|
||||
|
||||
### `AmilyDatabin.getData(source, fileName, objName = null)`
|
||||
|
||||
Retrieves specific data from the server. The `objName` is optional and will be auto-detected from the current context if omitted.
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* Fetches specific data from the server.
|
||||
* @param {number} source - The data source identifier (use AmilyDatabin.SourceType).
|
||||
* @param {string} fileName - The file name or unique key for the data.
|
||||
* @param {string} [objName=null] - Optional. The specific object name (e.g., character ID). If null, it's auto-detected. **Note: For `WORLD_BOOK` type, `objName` cannot be automatically detected and must be provided explicitly.**
|
||||
* @returns {Promise<object|null>} A Promise that resolves to the requested data object, or null if not found.
|
||||
*/
|
||||
async function retrieveCurrentCharacterMemory() {
|
||||
try {
|
||||
// We omit objName, so the plugin will automatically use the current character's ID.
|
||||
const memory = await window.AmilyDatabin.getData(
|
||||
window.AmilyDatabin.SourceType.CHARACTER_CARD,
|
||||
"character_memory.json"
|
||||
);
|
||||
|
||||
if (memory) {
|
||||
console.log("Retrieved current character memory:", memory);
|
||||
} else {
|
||||
console.log("No memory found for the current character.");
|
||||
}
|
||||
return memory;
|
||||
} catch (error) {
|
||||
console.error("Failed to retrieve data:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Example call for automatic context:
|
||||
retrieveCurrentCharacterMemory();
|
||||
```
|
||||
|
||||
### `AmilyDatabin.saveData(source, fileName, fileContent, objName = null)`
|
||||
|
||||
Saves a JavaScript object to a specific path on the server. The `objName` is optional.
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* Saves a data object to a specific path on the server.
|
||||
* @param {number} source - The data source identifier (use AmilyDatabin.SourceType).
|
||||
* @param {string} fileName - The file name or unique key for the data.
|
||||
* @param {object} fileContent - The JavaScript object to save.
|
||||
* @param {string} [objName=null] - Optional. The specific object name. If null, it's auto-detected. **Note: For `WORLD_BOOK` type, `objName` cannot be automatically detected and must be provided explicitly.**
|
||||
* @returns {Promise<boolean>} A Promise that resolves to true if successful, false otherwise.
|
||||
*/
|
||||
async function saveCurrentChatLog(chatHistory) {
|
||||
try {
|
||||
// objName is omitted here, it will be resolved to the current chat's ID automatically.
|
||||
const success = await window.AmilyDatabin.saveData(
|
||||
window.AmilyDatabin.SourceType.CHAT_LOG,
|
||||
`log_${new Date().getTime()}.json`, // Example of a unique key
|
||||
{ history: chatHistory, savedAt: new Date().toISOString() }
|
||||
);
|
||||
|
||||
if (success) {
|
||||
console.log("Chat log for current chat saved successfully.");
|
||||
} else {
|
||||
console.error("Failed to save chat log.");
|
||||
}
|
||||
return success;
|
||||
} catch (error) {
|
||||
console.error("Error during data saving:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Example call:
|
||||
const currentChat = ["User: Hi!", "Character: Hello!"];
|
||||
saveCurrentChatLog(currentChat);
|
||||
```
|
||||
|
||||
### `AmilyDatabin.deleteData(source, fileName, objName = null)`
|
||||
|
||||
Deletes a specific data file from the server. The `objName` is optional.
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* Deletes a specific data file from the server.
|
||||
* @param {number} source - The data source identifier (use AmilyDatabin.SourceType).
|
||||
* @param {string} [objName=null] - Optional. The specific object name. If null, it's auto-detected. **Note: For `WORLD_BOOK` type, `objName` cannot be automatically detected and must be provided explicitly.**
|
||||
* @returns {Promise<boolean>} A Promise that resolves to true if successful, false otherwise.
|
||||
*/
|
||||
async function deleteCurrentCharacterMemory() {
|
||||
try {
|
||||
const success = await window.AmilyDatabin.deleteData(
|
||||
window.AmilyDatabin.SourceType.CHARACTER_CARD,
|
||||
"character_memory.json"
|
||||
);
|
||||
|
||||
if (success) {
|
||||
console.log("Successfully deleted current character memory.");
|
||||
} else {
|
||||
console.log("Failed to delete character memory.");
|
||||
}
|
||||
return success;
|
||||
} catch (error) {
|
||||
console.error("Failed to delete data:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Example call:
|
||||
deleteCurrentCharacterMemory();
|
||||
```
|
||||
|
||||
## Debug Methods
|
||||
|
||||
These methods are designed for quick debugging in the browser console. They do not interact with the backend API.
|
||||
|
||||
### `AmilyDatabin.getDebugInstance()`
|
||||
|
||||
Outputs the `AmilyDatabin` object itself to the console. Useful for inspecting its current state, `SourceType` map, and available methods.
|
||||
|
||||
```javascript
|
||||
// Call in browser console:
|
||||
AmilyDatabin.getDebugInstance();
|
||||
// Expected output: { SourceType: { CHAT_LOG: 1, ... }, initialize: f, ... }
|
||||
```
|
||||
|
||||
### `AmilyDatabin.getDebugChatId()`
|
||||
|
||||
Attempts to resolve the current chat ID using the same logic as the main API methods and logs it to the console.
|
||||
|
||||
```javascript
|
||||
// Call in browser console:
|
||||
AmilyDatabin.getDebugChatId();
|
||||
// Expected output: [Amily-Databin] Current Chat ID (Debug): "your_chat_id_here"
|
||||
```
|
||||
|
||||
### `AmilyDatabin.getDebugCardId()`
|
||||
|
||||
Attempts to resolve the current character card ID using the same logic as the main API methods and logs it to the console.
|
||||
|
||||
```javascript
|
||||
// Call in browser console:
|
||||
AmilyDatabin.getDebugCardId();
|
||||
// Expected output: [Amily-Databin] Current Character Card ID (Debug): "your_character_id_here"
|
||||
```
|
||||
|
||||
## Developer
|
||||
|
||||
The main logic is located in `index.js` (server-side) and `public/client.js` (client-side). The plugin registers itself with SillyTavern's extension API.
|
||||
|
||||
You can interact with this plugin via the global `window.AmilyDatabin` object directly.
|
||||
5
config.yaml
Normal file
5
config.yaml
Normal file
@@ -0,0 +1,5 @@
|
||||
name: "Amily-Databin"
|
||||
author: "Silence_Lurker"
|
||||
description: "A data bin extension for SillyTavern extension."
|
||||
version: "1.0.0"
|
||||
st_version: "1.11.0"
|
||||
134
index.js
Normal file
134
index.js
Normal file
@@ -0,0 +1,134 @@
|
||||
const express = require('express');
|
||||
const fs = require('fs').promises;
|
||||
const path = require('path');
|
||||
|
||||
const {
|
||||
plugins,
|
||||
plugin_name_to_folder_name,
|
||||
plugin_data_path,
|
||||
} = require('../../../public/script.js');
|
||||
|
||||
const extensionName = "Amily-Databin";
|
||||
const extensionVersion = "0.0.1-a";
|
||||
|
||||
const storageBasePath = path.join(plugin_data_path, 'cc.amily49.amily-databin');
|
||||
let sourceTypes = {}; // Will be loaded from sourcetype.json
|
||||
|
||||
/**
|
||||
* Builds a safe file path and prevents path traversal attacks.
|
||||
*/
|
||||
function getSafeFilePath(source, objName, file) {
|
||||
if (typeof source !== 'string' && typeof source !== 'number' || typeof objName !== 'string' || typeof file !== 'string') {
|
||||
return null;
|
||||
}
|
||||
const sanitize = (part) => String(part).replace(/\.\./g, '').replace(/[/\]/g, '');
|
||||
const safeSource = sanitize(source);
|
||||
const safeObjName = sanitize(objName);
|
||||
const safeFile = sanitize(file);
|
||||
|
||||
if (!safeSource || !safeObjName || !safeFile) return null;
|
||||
|
||||
return path.join(storageBasePath, safeSource, safeObjName, safeFile);
|
||||
}
|
||||
|
||||
// #region Server-side Business Logic per SourceType
|
||||
// These functions implement the "strategy" for each source type.
|
||||
|
||||
/** Generic save handler */
|
||||
async function defaultSaveStrategy(filePath, data) {
|
||||
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
||||
await fs.writeFile(filePath, JSON.stringify(data, null, 2), 'utf8');
|
||||
}
|
||||
|
||||
// #endregion
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
// Load source types from the file on startup.
|
||||
const sourceTypesRaw = await fs.readFile(path.join(__dirname, 'sourcetype.json'), 'utf8');
|
||||
sourceTypes = JSON.parse(sourceTypesRaw);
|
||||
|
||||
// Ensure the base storage directory exists.
|
||||
await fs.mkdir(storageBasePath, { recursive: true });
|
||||
} catch (error) {
|
||||
console.error(`[${extensionName}] Critical startup error:`, error);
|
||||
return; // Stop loading the plugin
|
||||
}
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// Endpoint to serve the source type map to the client
|
||||
router.get('/sourcetype', (req, res) => {
|
||||
res.json(sourceTypes);
|
||||
});
|
||||
|
||||
router.get('/data', async (req, res) => {
|
||||
const filePath = getSafeFilePath(req.query.source, req.query.objName, req.query.file);
|
||||
if (!filePath) return res.status(400).send("Invalid parameters.");
|
||||
|
||||
try {
|
||||
const fileContent = await fs.readFile(filePath, 'utf8');
|
||||
res.json(JSON.parse(fileContent));
|
||||
} catch (error) {
|
||||
if (error.code === 'ENOENT') return res.status(404).send('Not found.');
|
||||
console.error(`[${extensionName}] Error reading file ${filePath}:`, error);
|
||||
res.status(500).send('Internal Server Error');
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/data', async (req, res) => {
|
||||
const { source, objName, file, data } = req.body;
|
||||
const filePath = getSafeFilePath(source, objName, file);
|
||||
|
||||
if (!filePath) return res.status(400).send("Invalid parameters.");
|
||||
if (data === undefined) return res.status(400).send("'data' is required.");
|
||||
|
||||
const sourceType = parseInt(source, 10);
|
||||
|
||||
try {
|
||||
// Strategy pattern based on source type
|
||||
switch (sourceType) {
|
||||
case sourceTypes.CHAT_LOG:
|
||||
case sourceTypes.CHARACTER_CARD:
|
||||
case sourceTypes.WORLD_BOOK:
|
||||
// For now, all implemented types use the default save strategy.
|
||||
await defaultSaveStrategy(filePath, data);
|
||||
res.status(200).send('Data saved successfully.');
|
||||
break;
|
||||
|
||||
default:
|
||||
// If the source type is known but not implemented, or unknown.
|
||||
res.status(501).send(`Save handler for source type '${sourceType}' is not implemented.`);
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[${extensionName}] Error in POST /data for source ${sourceType}:`, error);
|
||||
res.status(500).send('Internal Server Error');
|
||||
}
|
||||
});
|
||||
|
||||
router.delete('/data', async (req, res) => {
|
||||
const filePath = getSafeFilePath(req.query.source, req.query.objName, req.query.file);
|
||||
if (!filePath) return res.status(400).send("Invalid parameters.");
|
||||
|
||||
try {
|
||||
await fs.unlink(filePath);
|
||||
res.status(200).send('Data deleted successfully.');
|
||||
} catch (error) {
|
||||
if (error.code === 'ENOENT') return res.status(200).send('Not found.');
|
||||
console.error(`[${extensionName}] Error deleting file ${filePath}:`, error);
|
||||
res.status(500).send('Internal Server Error');
|
||||
}
|
||||
});
|
||||
|
||||
plugins[extensionName] = {
|
||||
name: extensionName,
|
||||
version: extensionVersion,
|
||||
is_enabled: true,
|
||||
folder_name: plugin_name_to_folder_name[extensionName],
|
||||
router: router,
|
||||
assets: ['client.js'],
|
||||
};
|
||||
|
||||
console.log(`[${extensionName}] version ${extensionVersion} loaded. Storage path: ${storageBasePath}`);
|
||||
})();
|
||||
7
manifest.json
Normal file
7
manifest.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "Amily-Databin",
|
||||
"author": "Amily",
|
||||
"version": "0.0.1-a",
|
||||
"description": "A SillyTavern plugin for data management.",
|
||||
"entry": "index.js"
|
||||
}
|
||||
14
package.json
Normal file
14
package.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "amily-databin",
|
||||
"version": "0.0.1-a",
|
||||
"description": "A SillyTavern plugin for data management.",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [
|
||||
"SillyTavern"
|
||||
],
|
||||
"author": "Amily",
|
||||
"license": "ISC"
|
||||
}
|
||||
173
public/client.js
Normal file
173
public/client.js
Normal file
@@ -0,0 +1,173 @@
|
||||
(function() {
|
||||
const extensionName = "Amily-Databin";
|
||||
const apiBaseUrl = `/api/plugins/${extensionName}`;
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// Private Helper Functions
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Retrieves the current context from SillyTavern and maps it to the expected object name.
|
||||
* This function attempts to get context dynamically from SillyTavern's global objects.
|
||||
*/
|
||||
function _getDynamicObjectName(source) {
|
||||
const { SillyTavern } = window;
|
||||
let context = null;
|
||||
if (SillyTavern && typeof SillyTavern.getContext === 'function') {
|
||||
context = SillyTavern.getContext();
|
||||
} else {
|
||||
console.error(`[${extensionName}] window.SillyTavern.getContext() not found. Dynamic context resolution is not possible.`);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!context) {
|
||||
console.error(`[${extensionName}] SillyTavern context is null or undefined.`);
|
||||
return null;
|
||||
}
|
||||
|
||||
let resolvedObjectName = null;
|
||||
|
||||
switch (source) {
|
||||
case AmilyDatabin.SourceType.CHAT_LOG:
|
||||
resolvedObjectName = context.chatId;
|
||||
break;
|
||||
case AmilyDatabin.SourceType.CHARACTER_CARD:
|
||||
resolvedObjectName = context.characterId;
|
||||
break;
|
||||
case AmilyDatabin.SourceType.WORLD_BOOK:
|
||||
// From the provided getContext() structure, there is no direct 'current world book name' key.
|
||||
// The user must provide objName for WORLD_BOOK type, or implement custom logic to detect it.
|
||||
console.warn(`[${extensionName}] 'WORLD_BOOK' type's object name cannot be automatically resolved from SillyTavern.getContext(). Please provide 'objName' explicitly.`);
|
||||
break;
|
||||
default:
|
||||
console.error(`[${extensionName}] Unknown source type for dynamic resolution: ${source}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!resolvedObjectName) {
|
||||
console.warn(`[${extensionName}] Could not dynamically resolve object name for source type ${source}. Context key might be missing or not active.`);
|
||||
}
|
||||
return resolvedObjectName;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// Public API Method Implementations
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
async function initialize() {
|
||||
try {
|
||||
const response = await fetch(`${apiBaseUrl}/sourcetype`);
|
||||
if (!response.ok) throw new Error(`Failed to fetch source types: ${response.status}`);
|
||||
this.SourceType = await response.json(); // 'this' refers to AmilyDatabin object
|
||||
console.log(`[${extensionName}] Client initialized with source types:`, this.SourceType);
|
||||
} catch (error) {
|
||||
console.error(`[${extensionName}] Initialization failed:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
async function getData(source, fileName, objName = null) {
|
||||
const objectName = objName ?? _getDynamicObjectName(source);
|
||||
if (!objectName) {
|
||||
console.error(`[${extensionName}] 'objName' could not be resolved for getData (Source: ${source}, File: ${fileName}).`);
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const queryParams = new URLSearchParams({ source, objName: objectName, file: fileName });
|
||||
const response = await fetch(`${apiBaseUrl}/data?${queryParams.toString()}`);
|
||||
if (response.status === 404) return null;
|
||||
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error(`[${extensionName}] getData failed (Source: ${source}, ObjName: ${objectName}, File: ${fileName}):`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function saveData(source, fileName, fileContent, objName = null) {
|
||||
const objectName = objName ?? _getDynamicObjectName(source);
|
||||
if (!objectName) {
|
||||
console.error(`[${extensionName}] 'objName' could not be resolved for saveData (Source: ${source}, File: ${fileName}).`);
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
const response = await fetch(`${apiBaseUrl}/data`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ source, objName: objectName, file: fileName, data: fileContent }),
|
||||
});
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`HTTP error! status: ${response.status}, message: ${errorText}`);
|
||||
}
|
||||
console.log(`[${extensionName}] Data saved to: ${source}/${objectName}/${fileName}`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(`[${extensionName}] saveData failed (Source: ${source}, ObjName: ${objectName}, File: ${fileName}):`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteData(source, fileName, objName = null) {
|
||||
const objectName = objName ?? _getDynamicObjectName(source);
|
||||
if (!objectName) {
|
||||
console.error(`[${extensionName}] 'objName' could not be resolved for deleteData (Source: ${source}, File: ${fileName}).`);
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
const queryParams = new URLSearchParams({ source, objName: objectName, file: fileName });
|
||||
const response = await fetch(`${apiBaseUrl}/data?${queryParams.toString()}`, { method: 'DELETE' });
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`HTTP error! status: ${response.status}, message: ${errorText}`);
|
||||
}
|
||||
console.log(`[${extensionName}] Data deleted from: ${source}/${objectName}/${fileName}`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(`[${extensionName}] deleteData failed (Source: ${source}, ObjName: ${objectName}, File: ${fileName}):`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// Debug Methods
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
function getDebugInstance() {
|
||||
console.log(`[${extensionName}] AmilyDatabin Instance:`, AmilyDatabin);
|
||||
return AmilyDatabin;
|
||||
}
|
||||
|
||||
function getDebugChatId() {
|
||||
const chatId = _getDynamicObjectName(AmilyDatabin.SourceType.CHAT_LOG);
|
||||
console.log(`[${extensionName}] Current Chat ID (Debug):`, chatId);
|
||||
return chatId;
|
||||
}
|
||||
|
||||
function getDebugCardId() {
|
||||
const cardId = _getDynamicObjectName(AmilyDatabin.SourceType.CHARACTER_CARD);
|
||||
console.log(`[${extensionName}] Current Character Card ID (Debug):`, cardId);
|
||||
return cardId;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// Global Object Definition
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
const AmilyDatabin = {
|
||||
SourceType: {},
|
||||
initialize,
|
||||
getData,
|
||||
saveData,
|
||||
deleteData,
|
||||
// Debug methods
|
||||
getDebugInstance,
|
||||
getDebugChatId,
|
||||
getDebugCardId
|
||||
};
|
||||
|
||||
window.AmilyDatabin = AmilyDatabin;
|
||||
|
||||
// Automatically initialize the module.
|
||||
window.AmilyDatabin.initialize.bind(window.AmilyDatabin)();
|
||||
|
||||
})();
|
||||
161
showdisplay.tmp
Normal file
161
showdisplay.tmp
Normal file
@@ -0,0 +1,161 @@
|
||||
export function getContext() {
|
||||
return {
|
||||
accountStorage,
|
||||
chat,
|
||||
characters,
|
||||
groups,
|
||||
name1,
|
||||
name2,
|
||||
characterId: this_chid,
|
||||
groupId: selected_group,
|
||||
chatId: selected_group
|
||||
? groups.find(x => x.id == selected_group)?.chat_id
|
||||
: (characters[this_chid]?.chat),
|
||||
getCurrentChatId,
|
||||
getRequestHeaders,
|
||||
reloadCurrentChat,
|
||||
renameChat,
|
||||
saveSettingsDebounced,
|
||||
onlineStatus: online_status,
|
||||
maxContext: Number(max_context),
|
||||
chatMetadata: chat_metadata,
|
||||
saveMetadataDebounced,
|
||||
streamingProcessor,
|
||||
eventSource,
|
||||
eventTypes: event_types,
|
||||
addOneMessage,
|
||||
deleteLastMessage,
|
||||
deleteMessage,
|
||||
generate: Generate,
|
||||
sendStreamingRequest,
|
||||
sendGenerationRequest,
|
||||
stopGeneration,
|
||||
tokenizers,
|
||||
getTextTokens,
|
||||
/** @deprecated Use getTokenCountAsync instead */
|
||||
getTokenCount,
|
||||
getTokenCountAsync,
|
||||
extensionPrompts: extension_prompts,
|
||||
setExtensionPrompt,
|
||||
updateChatMetadata,
|
||||
saveChat: saveChatConditional,
|
||||
openCharacterChat,
|
||||
openGroupChat,
|
||||
saveMetadata,
|
||||
sendSystemMessage,
|
||||
activateSendButtons,
|
||||
deactivateSendButtons,
|
||||
saveReply,
|
||||
substituteParams,
|
||||
substituteParamsExtended,
|
||||
SlashCommandParser,
|
||||
SlashCommand,
|
||||
SlashCommandArgument,
|
||||
SlashCommandNamedArgument,
|
||||
ARGUMENT_TYPE,
|
||||
executeSlashCommandsWithOptions,
|
||||
/** @deprecated Use SlashCommandParser.addCommandObject() instead */
|
||||
registerSlashCommand,
|
||||
/** @deprecated Use executeSlashCommandWithOptions instead */
|
||||
executeSlashCommands,
|
||||
timestampToMoment,
|
||||
/** @deprecated Handlebars for extensions are no longer supported. */
|
||||
registerHelper: () => { },
|
||||
registerMacro: MacrosParser.registerMacro.bind(MacrosParser),
|
||||
unregisterMacro: MacrosParser.unregisterMacro.bind(MacrosParser),
|
||||
registerFunctionTool: ToolManager.registerFunctionTool.bind(ToolManager),
|
||||
unregisterFunctionTool: ToolManager.unregisterFunctionTool.bind(ToolManager),
|
||||
isToolCallingSupported: ToolManager.isToolCallingSupported.bind(ToolManager),
|
||||
canPerformToolCalls: ToolManager.canPerformToolCalls.bind(ToolManager),
|
||||
ToolManager,
|
||||
registerDebugFunction,
|
||||
/** @deprecated Use renderExtensionTemplateAsync instead. */
|
||||
renderExtensionTemplate,
|
||||
renderExtensionTemplateAsync,
|
||||
registerDataBankScraper: ScraperManager.registerDataBankScraper.bind(ScraperManager),
|
||||
/** @deprecated Use callGenericPopup or Popup instead. */
|
||||
callPopup,
|
||||
callGenericPopup,
|
||||
showLoader,
|
||||
hideLoader,
|
||||
mainApi: main_api,
|
||||
extensionSettings: extension_settings,
|
||||
ModuleWorkerWrapper,
|
||||
getTokenizerModel,
|
||||
generateQuietPrompt,
|
||||
generateRaw,
|
||||
writeExtensionField,
|
||||
getThumbnailUrl,
|
||||
selectCharacterById,
|
||||
messageFormatting,
|
||||
shouldSendOnEnter,
|
||||
isMobile,
|
||||
t,
|
||||
translate,
|
||||
getCurrentLocale,
|
||||
addLocaleData,
|
||||
tags,
|
||||
tagMap: tag_map,
|
||||
menuType: menu_type,
|
||||
createCharacterData: create_save,
|
||||
/** @deprecated Legacy snake-case naming, compatibility with old extensions */
|
||||
event_types: event_types,
|
||||
Popup,
|
||||
POPUP_TYPE,
|
||||
POPUP_RESULT,
|
||||
chatCompletionSettings: oai_settings,
|
||||
textCompletionSettings: textgenerationwebui_settings,
|
||||
powerUserSettings: power_user,
|
||||
getCharacters,
|
||||
getCharacterCardFields,
|
||||
uuidv4,
|
||||
humanizedDateTime,
|
||||
updateMessageBlock,
|
||||
appendMediaToMessage,
|
||||
ensureMessageMediaIsArray,
|
||||
getMediaDisplay,
|
||||
getMediaIndex,
|
||||
swipe: {
|
||||
left: swipe_left,
|
||||
right: swipe_right,
|
||||
show: showSwipeButtons,
|
||||
hide: hideSwipeButtons,
|
||||
refresh: refreshSwipeButtons,
|
||||
isAllowed: () => isSwipingAllowed,
|
||||
},
|
||||
variables: {
|
||||
local: {
|
||||
get: getLocalVariable,
|
||||
set: setLocalVariable,
|
||||
},
|
||||
global: {
|
||||
get: getGlobalVariable,
|
||||
set: setGlobalVariable,
|
||||
},
|
||||
},
|
||||
loadWorldInfo,
|
||||
saveWorldInfo,
|
||||
reloadWorldInfoEditor: reloadEditor,
|
||||
updateWorldInfoList,
|
||||
convertCharacterBook,
|
||||
getWorldInfoPrompt,
|
||||
CONNECT_API_MAP,
|
||||
getTextGenServer,
|
||||
extractMessageFromData,
|
||||
getPresetManager,
|
||||
getChatCompletionModel,
|
||||
printMessages,
|
||||
clearChat,
|
||||
ChatCompletionService,
|
||||
TextCompletionService,
|
||||
ConnectionManagerRequestService,
|
||||
updateReasoningUI,
|
||||
parseReasoningFromString,
|
||||
unshallowCharacter,
|
||||
unshallowGroupMembers,
|
||||
openThirdPartyExtensionMenu,
|
||||
symbols: {
|
||||
ignore: IGNORE_SYMBOL,
|
||||
},
|
||||
};
|
||||
}
|
||||
5
sourcetype.json
Normal file
5
sourcetype.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"CHAT_LOG": 1,
|
||||
"CHARACTER_CARD": 2,
|
||||
"WORLD_BOOK": 3
|
||||
}
|
||||
Reference in New Issue
Block a user