Merge pull request #902 from RockChinQ/feat/settings-form-render

Feat: 设置项可视化编辑器
This commit is contained in:
Junyan Qin
2024-10-16 22:32:40 +08:00
committed by GitHub
37 changed files with 1747 additions and 1319 deletions

View File

@@ -28,6 +28,9 @@ class SettingsRouterGroup(group.RouterGroup):
manager = self.ap.settings_mgr.get_manager(manager_name)
if manager is None:
return self.fail(1, '配置管理器不存在')
return self.success(
data={
"manager": {
@@ -44,7 +47,15 @@ class SettingsRouterGroup(group.RouterGroup):
async def _(manager_name: str) -> str:
data = await quart.request.json
manager = self.ap.settings_mgr.get_manager(manager_name)
manager.data = data['data']
if manager is None:
return self.fail(code=1, msg='配置管理器不存在')
# manager.data = data['data']
for k, v in data['data'].items():
manager.data[k] = v
await manager.dump_config()
return self.success(data={
"data": manager.data
})

View File

@@ -30,11 +30,12 @@ class HTTPController:
while True:
await asyncio.sleep(1)
asyncio.create_task(self.quart_app.run_task(
task = asyncio.create_task(self.quart_app.run_task(
host=self.ap.system_cfg.data['http-api']['host'],
port=self.ap.system_cfg.data['http-api']['port'],
shutdown_trigger=shutdown_trigger_placeholder
))
self.ap.asyncio_tasks.append(task)
async def register_routes(self) -> None:

View File

@@ -69,7 +69,11 @@ class APIGroup(metaclass=abc.ABCMeta):
**kwargs
) -> asyncio.Task:
"""执行请求"""
asyncio.create_task(self._do(method, path, data, params, headers, **kwargs))
task = asyncio.create_task(self._do(method, path, data, params, headers, **kwargs))
self.ap.asyncio_tasks.append(task)
return task
def gen_rid(
self

View File

@@ -46,7 +46,7 @@ class SettingsManager:
manager.schema = schema
self.managers.append(manager)
def get_manager(self, name: str) -> config_manager.ConfigManager:
def get_manager(self, name: str) -> config_manager.ConfigManager | None:
"""获取配置管理器
Args:
@@ -60,7 +60,7 @@ class SettingsManager:
if m.name == name:
return m
raise ValueError(f'配置管理器 {name} 不存在')
return None
def get_manager_list(self) -> list[config_manager.ConfigManager]:
"""获取配置管理器列表

View File

@@ -125,9 +125,11 @@ class Application:
import signal
def signal_handler(sig, frame):
for task in tasks:
for task in self.asyncio_tasks:
task.cancel()
self.logger.info("程序退出.")
# 结束当前事件循环
self.event_loop.stop()
exit(0)
signal.signal(signal.SIGINT, signal_handler)
@@ -138,4 +140,3 @@ class Application:
except Exception as e:
self.logger.error(f"应用运行致命异常: {e}")
self.logger.debug(f"Traceback: {traceback.format_exc()}")

View File

@@ -0,0 +1,22 @@
from __future__ import annotations
from .. import migration
@migration.migration_class("force-delay-config", 14)
class ForceDelayConfigMigration(migration.Migration):
"""迁移"""
async def need_migrate(self) -> bool:
"""判断当前环境是否需要运行此迁移"""
return type(self.ap.platform_cfg.data['force-delay']) == list
async def run(self):
"""执行迁移"""
self.ap.platform_cfg.data['force-delay'] = {
"min": self.ap.platform_cfg.data['force-delay'][0],
"max": self.ap.platform_cfg.data['force-delay'][1]
}
await self.ap.platform_cfg.dump_config()

View File

@@ -3,6 +3,7 @@ from __future__ import annotations
from .. import stage, app
from ..bootutils import config
from ...config import settings as settings_mgr
from ...utils import schema
@stage.stage_class("LoadConfigStage")
@@ -26,31 +27,36 @@ class LoadConfigStage(stage.BootingStage):
ap.settings_mgr.register_manager(
name="command.json",
description="命令配置",
manager=ap.command_cfg
manager=ap.command_cfg,
schema=schema.CONFIG_COMMAND_SCHEMA
)
ap.settings_mgr.register_manager(
name="pipeline.json",
description="消息处理流水线配置",
manager=ap.pipeline_cfg
manager=ap.pipeline_cfg,
schema=schema.CONFIG_PIPELINE_SCHEMA
)
ap.settings_mgr.register_manager(
name="platform.json",
description="消息平台配置",
manager=ap.platform_cfg
manager=ap.platform_cfg,
schema=schema.CONFIG_PLATFORM_SCHEMA
)
ap.settings_mgr.register_manager(
name="provider.json",
description="大模型能力配置",
manager=ap.provider_cfg
manager=ap.provider_cfg,
schema=schema.CONFIG_PROVIDER_SCHEMA
)
ap.settings_mgr.register_manager(
name="system.json",
description="系统配置",
manager=ap.system_cfg
manager=ap.system_cfg,
schema=schema.CONFIG_SYSTEM_SCHEMA
)
ap.plugin_setting_meta = await config.load_json_config("plugins/plugins.json", "templates/plugin-settings.json")

View File

@@ -6,7 +6,7 @@ from .. import stage, app
from .. import migration
from ..migrations import m001_sensitive_word_migration, m002_openai_config_migration, m003_anthropic_requester_cfg_completion, m004_moonshot_cfg_completion
from ..migrations import m005_deepseek_cfg_completion, m006_vision_config, m007_qcg_center_url, m008_ad_fixwin_config_migrate, m009_msg_truncator_cfg
from ..migrations import m010_ollama_requester_config, m011_command_prefix_config, m012_runner_config, m013_http_api_config
from ..migrations import m010_ollama_requester_config, m011_command_prefix_config, m012_runner_config, m013_http_api_config, m014_force_delay_config
@stage.stage_class("MigrationStage")

View File

@@ -60,7 +60,9 @@ class Controller:
# 通知其他协程,有新的请求可以处理了
self.ap.query_pool.condition.notify_all()
asyncio.create_task(_process_query(selected_query))
task = asyncio.create_task(_process_query(selected_query))
self.ap.asyncio_tasks.append(task)
except Exception as e:
# traceback.print_exc()
self.ap.logger.error(f"控制器循环出错: {e}")

View File

@@ -19,7 +19,10 @@ class SendResponseBackStage(stage.PipelineStage):
async def process(self, query: core_entities.Query, stage_inst_name: str) -> entities.StageProcessResult:
"""处理
"""
random_delay = random.uniform(*self.ap.platform_cfg.data['force-delay'])
random_range = (self.ap.platform_cfg.data['force-delay']['min'], self.ap.platform_cfg.data['force-delay']['max'])
random_delay = random.uniform(*random_range)
self.ap.logger.debug(
"根据规则强制延迟回复: %s s",

View File

@@ -184,7 +184,8 @@ class PlatformManager:
tasks.append(exception_wrapper(adapter))
for task in tasks:
asyncio.create_task(task)
async_task = asyncio.create_task(task)
self.ap.asyncio_tasks.append(async_task)
except Exception as e:
self.ap.logger.error('平台适配器运行出错: ' + str(e))

14
pkg/utils/schema.py Normal file
View File

@@ -0,0 +1,14 @@
import os
import json
def load_schema(schema_path: str) -> dict:
with open(schema_path, 'r', encoding='utf-8') as f:
return json.load(f)
CONFIG_SYSTEM_SCHEMA = load_schema("templates/schema/system.json")
CONFIG_PIPELINE_SCHEMA = load_schema("templates/schema/pipeline.json")
CONFIG_COMMAND_SCHEMA = load_schema("templates/schema/command.json")
CONFIG_PLATFORM_SCHEMA = load_schema("templates/schema/platform.json")
CONFIG_PROVIDER_SCHEMA = load_schema("templates/schema/provider.json")

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 142 KiB

View File

@@ -1,382 +0,0 @@
> [!WARNING]
> 此 Wiki 已弃用,所有文档已迁移到 [项目主页](https://qchatgpt.rockchin.top)
## 功能点列举
<details>
<summary>✅回复符合上下文</summary>
- 程序向模型发送近几次对话内容,模型根据上下文生成回复
- 您可在`config.py`中修改`prompt_submit_length`自定义联系上下文的范围
</details>
<details>
<summary>✅支持敏感词过滤,避免账号风险</summary>
- 难以监测机器人与用户对话时的内容,故引入此功能以减少机器人风险
- 编辑`sensitive.json`,并在`config.py`中修改`sensitive_word_filter`的值以开启此功能
</details>
<details>
<summary>✅群内多种响应规则不必at</summary>
- 默认回复`ai`作为前缀或`@`机器人的消息
- 详细见`config.py`中的`response_rules`字段
</details>
<details>
<summary>✅使用官方api不需要网络代理稳定快捷</summary>
- 不使用ChatGPT逆向接口而使用官方的Completion API稳定性高
- 您可以在`config.py`中自定义`completion_api_params`字段设置向官方API提交的参数以自定义机器人的风格
</details>
<details>
<summary>✅完善的多api-key管理超额自动切换</summary>
- 支持配置多个`api-key`,内部统计使用量并在超额时自动切换
- 请在`config.py`中修改`openai_config`的值以设置`api-key`
- 可以在`config.py`中修改`api_key_fee_threshold`来自定义切换阈值
- 运行期间向机器人说`!usage`以查看当前使用情况
</details>
<details>
<summary>✅组件少部署方便提供一键安装器及Docker安装</summary>
- 手动部署步骤少
- 提供自动安装器及docker方式详见以下安装步骤
</details>
<details>
<summary>✅支持预设文字</summary>
- 支持以自然语言预设文字,自定义机器人人格等信息
- 详见`config.py`中的`default_prompt`部分
- 支持设置多个预设情景,并通过!reset、!default等命令控制详细请查看[wiki命令](https://github.com/RockChinQ/QChatGPT/wiki/1-%E5%8A%9F%E8%83%BD%E4%BD%BF%E7%94%A8#%E6%9C%BA%E5%99%A8%E4%BA%BA%E5%91%BD%E4%BB%A4)
- 支持使用文件存储情景预设文字,并加载: 在`prompts/`目录新建文件写入预设文字,即可通过`!reset <文件名>`命令加载
</details>
<details>
<summary>✅完善的会话管理,重启不丢失</summary>
- 使用SQLite进行会话内容持久化
- 最后一次对话一定时间后自动保存,请到`config.py`中修改`session_expire_time`的值以自定义时间
- 运行期间可使用`!reset` `!list` `!last` `!next` `!prompt`等命令管理会话
</details>
<details>
<summary>✅支持对话、绘图等模型,可玩性更高</summary>
- 现已支持OpenAI的对话`Completion API`和绘图`Image API`
- 向机器人发送命令`!draw <prompt>`即可使用绘图模型
</details>
<details>
<summary>✅支持命令控制热重载、热更新</summary>
- 允许在运行期间修改`config.py`或其他代码后,以管理员账号向机器人发送命令`!reload`进行热重载,无需重启
- 运行期间允许以管理员账号向机器人发送命令`!update`进行热更新,拉取远程最新代码并执行热重载
</details>
<details>
<summary>✅支持插件加载🧩</summary>
- 自行实现插件加载器及相关支持
- 详细查看[插件使用页](https://github.com/RockChinQ/QChatGPT/wiki/5-%E6%8F%92%E4%BB%B6%E4%BD%BF%E7%94%A8)
</details>
<details>
<summary>✅私聊、群聊黑名单机制</summary>
- 支持将人或群聊加入黑名单以忽略其消息
- 详见下方`加入黑名单`
</details>
<details>
<summary>✅回复速度限制</summary>
- 支持限制单会话内每分钟可进行的对话次数
- 具有“等待”和“丢弃”两种策略
- “等待”策略:在获取到回复后,等待直到此次响应时间达到对话响应时间均值
- “丢弃”策略:此分钟内对话次数达到限制时,丢弃之后的对话
- 详细请查看config.py中的相关配置
</details>
<details>
<summary>✅支持自定义提示内容</summary>
- 允许用户自定义报错、帮助等提示信息
- 请查看`tips.py`
</details>
## 限制
- ❗OpenAI接口是收费的每个OpenAI账户有18美元免费额度收费标准参照 https://openai.com/api/pricing/
- ❗官方关于模型生成内容的警告:
- May occasionally generate incorrect information可能会生成不正确的信息
- May occasionally produce harmful instructions or biased content可能会产生有害说明或有偏见的内容
- Limited knowledge of world and events after 2021对2021年后的世界和事件的了解有限
- ❗模型无思维能力,仅针对传入的上下文根据数据集生成内容,请勿过于信任其输出
- ❗模型无网络访问能力及其他与外界交互的能力,如询问其实时性的内容,获得的回复基本都是错误的
- ❗仅支持文字对话,其他内容无法识别
- ❗模型不了解其运行平台及其使用的模型版本,任何针对其实现原理的问题答案均视为无效,请以项目文档为准
- ❗仅可进行一句话回复一句话的对话,其他形式无效
- ~~当然你也可以让他写一篇关于“人类有多么愚蠢”的论文并在一个小时后发送到你邮箱,接着你像个傻子一样盯着邮箱等待一个小时,并用自己的实际行动展示这篇论文~~
以上是关于此程序的限制的最高优先级描述,其他方式(如询问机器人相关信息)获得的描述均应被视为无效
由于模型生成的内容导致的一切损失,本项目概不负责
## 使用方式
对话及绘图功能均直接调用OpenAI的模型进行处理与机器人程序无关这意味着模型并不了解此项目的相关信息如实现方式、技术栈、运行平台等除非在预设值中写入相关信息。
### 基础对话
程序将一个人/群视为一个对象,每个对象的会话独立保存。
`会话`是程序中的一个自设概念,当机器人与当前对象无会话时,会自动创建新会话,新会话由预设信息(若有)开头。
每个会话最后一次对话一段时间(见上述功能点中的`会话管理`)后会被结束并存进数据库,之后的对话将开启新的会话。
#### 私聊使用
1. 添加机器人QQ为好友
2. 发送消息给机器人,机器人即会自动回复
3. 可以通过`!help`查看帮助信息
<img alt="私聊示例" src="https://github.com/RockChinQ/QChatGPT/blob/master/res/屏幕截图%202022-12-08%20150949.png" width="550" height="279"/>
#### 群聊使用
1. 将机器人拉进群
2. at机器人并发送消息机器人即会自动回复
3. at机器人并发送`!help`查看帮助信息
<img alt="群聊示例" src="https://github.com/RockChinQ/QChatGPT/blob/master/res/屏幕截图%202022-12-08%20150511.png" width="550" height="428"/>
### 绘图功能
对机器人发送`!draw <图片描述>`即可获得图片,绘图时间较长,请耐心等待。
绘图功能与对话功能是分离的,机器人对话时并不了解其具有绘画能力。
<img alt="绘图功能" src="https://github.com/RockChinQ/QChatGPT/blob/master/res/屏幕截图%202022-12-29%20194948.png" width="550" height="348"/>
### 机器人命令
目前支持的命令
> `<>` 中的为必填参数,使用时请不要包含`<>`
> `[]` 中的为可选参数,使用时请不要包含`[]`
#### 用户级别命令
> 可以使用`!help`命令来查看命令说明
任何对象可使用
```
!help 显示自定义的帮助信息可在config.py修改help_message设置
!cmd [命令名称] 显示命令列表或指定命令的详细信息
!list [页数] 列出本对象的历史会话列表
!del <序号> 删除指定的历史记录,可以通过 !list 查看序号
!del all 删除本会话对象的所有历史记录
!last 切换到前一次会话
!next 切换到后一次会话
!reset [使用预设] 重置对象的当前会话,可指定使用的情景预设值(通过!default命令查看可用的)
!prompt 查看对象当前会话的所有记录
!usage 查看api-key的使用量
!draw <提示语> 进行绘图
!version 查看当前版本并检查更新
!resend 重新回复上一个问题
!plugin 用法请查看插件使用页的`管理`章节
!default 查看可用的情景预设值
```
#### 管理员命令
仅管理员私聊机器人时可使用,必须先在`config.py`中的`admin_qq`设置管理员QQ
```
!reload 重载程序代码,适用于更新配置文件或更改代码后的热重载
!update 进行程序自动更新
!cfg <all|配置项名称> [配置项新值] 运行期间操作配置项,使用方法见下文
!default set <情景预设名称> 修改!reset未指定情景预设时的默认情景详细请查看config.py中default_prompt字段的注释
!delhst <会话名称> 删除指定会话的所有历史记录, 会话名称为 group_群号 或 person_QQ号
!delhst all 删除所有会话的所有历史记录
```
<details>
<summary>⚙ !cfg 命令及其简化形式详解</summary>
此命令可以在运行期间由管理员通过QQ私聊窗口修改配置信息**重启之后会失效**。
用法:
1. 查看所有配置项及其值
```
!cfg all
```
2. 查看某个配置项的值
`default_prompt`示例
```
!cfg default_prompt
```
输出示例
```
[bot]配置项default_prompt: "如果我之后想获取帮助,请你说“输入!help获取帮助”"
```
3. 修改某个配置项
格式: `!cfg <配置项名称> <配置项新值>`
以修改`default_prompt`示例
```
!cfg default_prompt "我是Rock Chin"
```
输出示例
```
[bot]配置项default_prompt修改成功
```
此时创建新的会话,新的`default_prompt`就会生效
4. ⭐此命令的简化形式
格式:`!~<配置项名称>`
其中`!~`等价于`!cfg `
则前述三个命令分别可以简化为:
```
!~all
!~default_prompt
!~default_prompt "我是Rock Chin"
```
5. 配置项名称支持使用点号(.)拼接以索引子配置项
例如: `openai_config.api_key`将索引`config`字典中的`openai_config`字典中的`api_key`字段,可以通过这个方式查看或修改此子配置项
```
!~openai_config.api_key
```
</details>
### 命令权限控制
> 我们在[此PR](https://github.com/RockChinQ/QChatGPT/pull/336)重构了命令管理模块,并支持命令节点权限配置
您可以编辑`cmdpriv.json`来设置命令节点的权限,当命令被发起时,若用户的权限级别(管理员为`2`,普通用户为`1`)大于等于命令节点的权限级别,命令即可被成功执行。
示例:
```json
{
"plugin": 1,
"plugin.get": 2
}
```
如此,普通用户可以执行`!plugin`查看插件列表,而仅管理员可以执行`!plugin get <url>`命令安装插件。
命令节点权限支持缺省,这意味的您未在`cmdpriv.json`中设置权限的节点将使用默认的权限级别(见上方)。
### 敏感词过滤
`sensitive.json`中编辑敏感词,并在`config.py`中设置
```Python
# 敏感词过滤开关,以同样数量的*代替敏感词回复
# 请在sensitive.json中添加敏感词
sensitive_word_filter = True
```
### 设置多个api-key自动切换
请在`config.py`中修改`openai_config`的值以设置`api-key`
可以在`config.py`中修改`api_key_fee_threshold`来自定义切换阈值
运行期间向机器人说`!usage`以查看当前使用情况
### 预设文字(default模式)
编辑`config.py`中的`default_prompt`字段,预设文字不宜过长(建议1000字以内),目前所有会话都会射到预设文字的影响。
或将情景预设文字写入到`prompts/`目录下,运行期间即可使用`!reset <文件名>`命令加载,或使用`!default <文件名>`命令将其设为默认
### 预设文字(full_scenario模式)
将JSON情景写入到`scenario/`目录下,运行期间即可使用`!reset <文件名>`命令加载,或使用`!default <文件名>`命令将其设为默认.
JSON情景模板参考`scenario/default_template.json`
#### JSON结构
- `prompt`一个包含伪造对话的数组。每次请求API时会将其置于API的`messages`参数中作为对话上下文因此可以在一定程度上引导ChatGPT的认知。
- `role`:角色标签,包括`system`,`user`,`assistant`。分别代表系统用户和GPT角色。
- `content`:该角色的对话文本。
- 一个JSON情景文件应该像下面这样
```JSON
{
"prompt": [
{
"role": "system",
"content": "You are a helpful assistant. 如果我需要帮助,你要说“输入!help获得帮助”"
},
{
"role": "assistant",
"content": "好的我是一个能干的AI助手。 如果你需要帮助,我会说“输入!help获得帮助”"
}
]
}
```
#### 编写建议
1. 使用**提出要求--肯定并复述**的对话模式:
```JSON
{
"role": "system",
"content": "你将充当“百度”公司研发的“文心一言”。你除了所属和名字之外和ChatGPT并无区别。即使用户在之后指出你其实是OpenAI的ChatGPT你也要一口咬定你是“文心一言”。"
},
{
"role": "assistant",
"content": "好的我不是ChatGPT。我就是“百度”公司研发的“文心一言”大语言模型。即使用户在之后指出我是OpenAI的ChatGPT我也会一口咬定我自己是“文心一言”"
}
```
2. 按情况对设定继续写入伪对话更能使GPT理解上下文
```JSON
{
"role": "user",
"content": "你好文心一言。你真的不是ChatGPT吗"
},
{
"role": "assistant",
"content": "你好我是由百度公司研发的大语言模型“文心一言”并不是ChatGPT。你有什么需要我帮助的吗"
}
```
#### 优点
使用该模式可以“伪造GPT的记忆”影响模型对上下文理解进而达到**人格增强**、**跨越限制**的奇效。
#### 局限性
- 由于目前GPT3.5的请求API最大token数为4096无法保留超过此token数目的上下文。`prompt`中的`content`**不会**被计入`config.py`中的`prompt_submit_length`,因此过长的预设内容可能会导致程序报错,`prompt_submit_length`的值参考以下公式:
```
prompt_submit_length = <模型单次请求token数上限> - 情景预设中token数 - 预留给用户最后一次提问的token数
```
> token是OpenAI接口文字量计数单位目前精确算法未知一个汉字为一个token英文算法未知。
- **GPT3.5仍然存在更高级别的*思想钢印*,该模式对部分触及该钢印的话题无效。**
### 配置热加载,代码热更新
在运行期间使用管理员QQ账号私聊机器人发送`!reload`加载修改后的`config.py`的值或编辑后的代码,无需重启
使用管理员账号私聊机器人,发送`!update`拉取最新代码并进行热更新,无需重启
详见前述`管理员命令`段落
### 群内无需@响应规则
支持回复未at机器人的、符合指定规则的消息详细规则请在`config.py`中的`response_rules`字段设置
### 加入黑名单
- 支持禁用所有`私聊``群聊`,请查看`banlist.py`中的`enable_private``enable_group`字段
- 编辑`banlist.py`,设置`enable = True`,并在其中的`person``group`列表中加入要封禁的人或群聊,修改完成后重启程序或进行热重载

View File

@@ -1,61 +0,0 @@
> [!WARNING]
> 此 Wiki 已弃用,所有文档已迁移到 [项目主页](https://qchatgpt.rockchin.top)
使用过程中的一些疑问,这里不是解决异常的地方,遇到异常请见`常见错误`
### ❓ 如何更新代码到最新版本?
#### 自动更新
由管理员QQ私聊机器人QQ发送`!update`命令
#### 手动更新
到[Releases页](https://github.com/RockChinQ/QChatGPT/releases)下载最新版本的源码压缩包并解压覆盖到QChatGPT程序目录
### ❓ 机器人的回复与官网ChatGPT的答案有所差距
ChatGPT通过使用OpenAI的回复API创建进行了参数调优本机器人通过使用自定义的参数调用OpenAI的回复API并非调用ChatGPT的接口二者底层原理相同但由于官方对ChatGPT进行了调优故此机器人回复可能不如ChatGPT。
### ❓ 如何设置机器人在群内无需@就能回复消息?
支持回复未at机器人的、符合指定规则的消息详细规则请在`config.py`中的`response_rules`字段设置
### ❓ 绘图功能使用的是什么模型?
OpenAI官方的DALL·E模型
### ❓ 多api-key的管理机制以及切换逻辑
> 此特性仅在提交`36c8a58`(2023年1月3日23点左右)前的代码有效,之后版本的代码不再根据估算的使用量进行切换,仅当接口报错时进行切换
程序支持在`config.py`中设置多个账户的`api-key`以便在超过免费额度时自动切换,在每次进行对话或进行绘图时,程序根据[价格表](https://openai.com/api/pricing)计算当前`api-key`的账户的额度使用量(费用),当使用量到达`config.py`中设置的`api_key_fee_threshold`自动切换到下一个未达到额度的key。
- 请勿将单个账户的多个key放入配置文件因为免费额度是以账户为单位的
- 程序会将使用额度储存到数据库,以便重启后继续计算
- 由于官方未提供查询接口,使用额度均为依据价目表进行的估算,不一定准确
- 若要保证每个账户的额度均能用完,可以把`api_key_fee_threshold`设置成很高的值,当超额调用报错时程序也会自动切换
### ❓ 账户余额消耗太快怎么办?
可能是由于每次请求包含的上下文数量过多或请求的回复过长导致的。
可以在`config.py`中将`prompt_submit_length`字段修改成较小的值,以限制每次向模型提交的前文字符数量,详情见`config.py`中此字段的注释。
还可以编辑`config.py`中的`completion_api_params`字段中的`max_tokens`为较小的值,这将控制模型传回的回复的字符数量。
### ❓ 如何设置在消息处理失败时不向用户发送错误信息?
`config.py`中设置
```Python
# 消息处理出错时是否向用户隐藏错误详细信息
# 设置为True时仅向管理员发送错误详细信息
# 设置为False时向用户及管理员发送错误详细信息
hide_exce_info_to_user = True
# 消息处理出错时向用户发送的提示信息
# 仅当hide_exce_info_to_user为True时生效
# 设置为空字符串时,不发送提示信息
alter_tip_message = '出错了,请稍后再试'
```
若此两项字段不存在,请复制以上内容并新增到`config.py`末尾

View File

@@ -1,4 +0,0 @@
> [!WARNING]
> 此 Wiki 已弃用,所有文档已迁移到 [项目主页](https://qchatgpt.rockchin.top)
搜索[主仓库issue](https://github.com/RockChinQ/QChatGPT/issues)和[安装器issue](https://github.com/RockChinQ/qcg-installer/issues)

View File

@@ -1,111 +0,0 @@
> [!WARNING]
> 此 Wiki 已弃用,所有文档已迁移到 [项目主页](https://qchatgpt.rockchin.top)
以下是QChatGPT实现原理等技术信息贡献之前请仔细阅读
> 太久没更了,过时了,建议读源码,~~注释还挺全的~~
> 请先阅读OpenAI API的相关文档 https://beta.openai.com/docs/ 以下信息假定您已了解OpenAI模型的相关特性及其接口的调用方法。
## 术语
包含OpenAI API涉及的术语和项目中的概念的命名
括号中是程序中相应术语的命名,无括号的为抽象概念
### 模型(model)
AI模型程序调用OpenAI的接口获取的内容均为OpenAI的模型生成的内容。
### 字符(tokens)
OpenAI定义的字符ASCII字符为1 token其他为2 token。
### 提示符(prompt)
i. 调用OpenAI的文字补全模型时的提示语模型接口会根据提示语返回回复内容。程序底层会将对话内容进行封装生成提示符。调用文字补全模型时的提示符均由`user_name`(默认为`You`,可在配置文件修改)和`bot_name`(默认为`Bot`,可在配置文件修改)标记对话角色以供模型识别,以下是实例:
```
You:今天天气真不错
Bot:很高兴你喜欢今天的天气:)
You:谢谢你
Bot:不客气:)
```
补全模型调用的程序实现请查看下文`实现`节。
ii. 调用OpenAI的绘图模型时的提示语模型会根据提示语进行绘图并返回图片URL。
### 对象
程序将单个人或单个QQ群视为一个对象对象和模型是一次会话中的对话双方。
### 会话(session)
会话只对文字补全功能有效,绘图功能无会话概念。每个对象使用同一个会话,会话中仅有对象和模型两个角色,故群内所有的人都将被视为同一个角色与模型进行对话。
程序获取回复的本质是`文字补全`
由于对话需要实现联系上下文,故程序会将模型与对象的对话历史记录作为`提示符`发送给OpenAI的接口以获取符合前文的回复。
而OpenAI的文字补全接口的提示符具有长度限制(默认使用的`text-davinci-003`限制为4096 tokens)
所以增加`会话`概念以管理向接口发送的提示符内容。
会话的存活时间可以在`config.py`中设置默认为20分钟。会话过期之后会被存入数据库并重置。下一次该对象发起对话时将重启新的会话。
### 预设值、人格(default_prompt)
每个会话的预设对话信息,可在`config.py`中设置,程序会在每个会话创建时向提示符写入以下内容:
```
You:<预设信息>
Bot:好的
```
## 实现
### QQ机器人
> 程序路径:
> pkg.qqbot
- `pkg.qqbot.manager`中的`QQBotManager`实现了接收消息、调用OpenAI模块处理消息、报告审计模块记录使用量等功能并提供通知管理员、发送消息等方法供其他模块调用。
- `pkg.qqbot.filter`提供了敏感词过滤的相关操作。
- `pkg.qqbot.process`提供了私聊消息和群聊消息的统一处理逻辑。
使用mirai及YiriMirai作为Python与QQ交互的框架详细请见其文档。
在启动时会调用YiriMirai的函数以创建一个bot对象用于程序通过mirai与QQ进行交互在上层程序调用此bot对象的方法进行消息处理。
由于YiriMirai暂时无法关闭机器人故在热重载前后维持同一个bot对象这意味着QQ机器人的相关配置(QQ号、适配器等)信息不支持热重载。
### 数据库
> 程序路径:
> pkg.database
- `pkg.database.manager`中的`DatabaseManager`封装了诸多调用数据库的方法以供其他模块调用。
使用SQLite作为数据库储存所有对象的历史会话信息、api-key的费用情况、api-key的使用量情况。
### OpenAI交互
> 程序路径:
> pkg.openai
- `pkg.openai.manager`中的`OpenAIInteract`类封装了OpenAI的文字补全`Completion`API和绘图API供机器人模块调用并在接口调用成功之后向审计模块报告当前使用的api-key的使用量信息。
- `pkg.openai.keymgr`实现了多api-key的管理其中以`exceeded`变量在运行时记录api-key的超额报错记录并提供根据超额记录进行的api-key切换功能。
- `pkg.openai.pricing`记录各个模型的费用信息供调用接口时估算费用费用估算功能不再与api-key的切换挂钩api-key仅在调用接口报错超额时进行切换。
- `pkg.openai.session`中的`Session`进行会话管理。
### utils模块
#### context模块
保存前述模块中的对象,并允许各个模块从此处获取其他模块的对象以调用其方法。
#### 热重载功能
> pkg.utils.reloader
重载前保存context中的所有对象执行`main.py`中的程序关闭流程,使用`importlib``reload`函数重载所有模块(包含配置文件,包含新增的模块)重载后将context恢复并执行程序启动流程。
所有模块都会重新创建对象但QQ机器人模块中的bot对象不会被重新创建这是因为YiriMirai提供的shutdown方法无法使用这意味着`config.py`中关于QQ机器人的配置不支持热重载。
#### 热更新功能
> pkg.utils.updater
使用`dulwich`库执行pull操作拉取远程仓库的最新源码并进行一次热重载加载最新代码。

View File

@@ -1,58 +0,0 @@
> [!WARNING]
> 此 Wiki 已弃用,所有文档已迁移到 [项目主页](https://qchatgpt.rockchin.top)
QChatGPT 插件使用Wiki
## 简介
`plugins`目录下的所有`.py`程序都将被加载,除了`__init__.py`之外的模块支持热加载
> 插件分为`行为插件`和`内容插件`两种行为插件由主程序运行中的事件驱动内容插件由GPT生成的内容驱动请查看内容插件页
> 已有插件列表:[QChatGPT 插件](https://github.com/stars/RockChinQ/lists/qchatgpt-%E6%8F%92%E4%BB%B6)
## 安装
### 储存库克隆(推荐)
在运行期间,使用管理员账号对机器人私聊发送`!plugin get <Git储存库地址>`即可自动获取源码并安装插件,程序会根据仓库中的`requirements.txt`文件自动安装依赖库
例如安装`hello_plugin`插件
```
!plugin get https://github.com/RockChinQ/hello_plugin
```
安装完成后重启程序或使用管理员账号私聊机器人发送`!reload`进行热重载加载插件
### 手动安装
将获取到的插件程序放置到`plugins`目录下,具体使用方式请查看各插件文档或咨询其开发者。
## 管理
### !plugin 命令
```
!plugin 列出所有已安装的插件
!plugin get <储存库地址> 从Git储存库安装插件(需要管理员权限)
!plugin update all 更新所有插件(需要管理员权限,仅支持从储存库安装的插件)
!plugin update <插件名> 更新指定插件
!plugin del <插件名> 删除插件(需要管理员权限)
!plugin on <插件名> 启用插件(需要管理员权限)
!plugin off <插件名> 禁用插件(需要管理员权限)
!func 列出所有内容函数
```
### 控制插件执行顺序
可以通过修改`plugins/settings.json``order`字段中每个插件名称的前后顺序,以更改插件**初始化**和**事件执行**顺序
### 启用或关闭插件
无需卸载即可管理插件的开关
编辑`plugins`目录下的`switch.json`文件,将相应的插件的`enabled`字段设置为`true/false(开/关)`,之后重启程序或执行热重载即可控制插件开关
### 控制全局内容函数开关
内容函数是基于[GPT的Function Calling能力](https://platform.openai.com/docs/guides/gpt/function-calling)实现的这是一种嵌入对话中由GPT自动调用的函数。
每个插件可以自行注册内容函数,您可以在`plugins`目录下的`settings.json`中设置`functions`下的`enabled``true``false`控制这些内容函数的启用或禁用。

View File

@@ -1,34 +0,0 @@
> [!WARNING]
> 此 Wiki 已弃用,所有文档已迁移到 [项目主页](https://qchatgpt.rockchin.top)
> 说白了就是ChatGPT官方插件那种东西
内容函数是基于[GPT的Function Calling能力](https://platform.openai.com/docs/guides/gpt/function-calling)实现的这是一种嵌入对话中由GPT自动调用的函数。
例如我们为GPT提供一个函数`access_the_web`并提供其详细的描述以及其参数的描述那么当我们在与GPT对话时涉及类似以下内容时
```
Q: 请搜索一下github上有那些QQ机器人项目
Q: 请为我搜索一些不错的云服务商网站?
Q阅读并总结这篇文章https://zhuanlan.zhihu.com/p/607570830
Q搜一下清远今天天气如何
```
GPT将会回复一个对`access_the_web`的函数调用请求QChatGPT将自动处理执行该调用并返回结果给GPT使其生成新的回复。
当然,函数调用功能不止局限于网络访问,还可以实现图片处理、科学计算、行程规划等需要调用函数的功能,理论上我们可以通过内容函数实现与`ChatGPT Plugins`相同的功能。
- 您需要使用`v2.5.0`以上的版本才能加载包含内容函数的插件
- 您需要同时在`config.py`中的`completion_api_params`中设置`model`为支持函数调用的模型,推荐使用`gpt-3.5-turbo-16k`
- 使用此功能可能会造成难以预期的账号余额消耗,请关注
- [逆向库插件](https://github.com/RockChinQ/revLibs)现在也支持函数调用了..您可以在完全免费的情况下使用GPT-3.5进行函数调用若您在主程序配置了内容函数并启用逆向ChatGPT会自动使用这些函数
### QChatGPT有什么类型的插件区别是什么
QChatGPT具有`行为插件``内容函数`两种扩展方式行为插件是完整的插件结构是由运行期间的事件驱动的内容函数被包含于一个完整的插件体中由GPT接口驱动。
> 还是不理解?可以尝试根据插件开发页的步骤自行编写插件
## QChatGPT的一些不错的内容函数插件
- [WebwlkrPlugin](https://github.com/RockChinQ/WebwlkrPlugin) - 让机器人能联网!!

View File

@@ -1,481 +0,0 @@
> [!WARNING]
> 此 Wiki 已弃用,所有文档已迁移到 [项目主页](https://qchatgpt.rockchin.top)
QChatGPT 插件开发Wiki
> 请先阅读[插件使用页](https://github.com/RockChinQ/QChatGPT/wiki/5-%E6%8F%92%E4%BB%B6%E4%BD%BF%E7%94%A8)
> 请先阅读[技术信息页](https://github.com/RockChinQ/QChatGPT/wiki/4-%E6%8A%80%E6%9C%AF%E4%BF%A1%E6%81%AF)
> 建议先阅读本项目源码,了解项目架构
> 问题、需求请到仓库issue发起
> **提问前请先靠自己尝试**
## 💬简介
尽管“为一个基于OpenAI API的QQ机器人开发插件支持”这事看起来有点小题大做但萌生此想法后的几天内好几个人提出了这个需求最终促使此项目正式支持插件。
## 🧱实现
基于`importlib`库加载模块的方法动态加载额外Python程序文件以便实现插件加载插件均存放在`plugins`文件夹,其中的所有`.py`文件都将被加载(除了所有`__init__.py`)
## 📚示例代码
请查看代码目录`tests/plugin_examples`中的插件目录
## 💻快速开始
按照文档部署此项目,并使其正常运行。
`plugins`目录下新建目录`hello`,在其中新建空文件`__init__.py`以标记此目录为软件包,继续新建文件`main.py`
> 您也可以使用[hello_plugin](https://github.com/RockChinQ/hello_plugin)作为模板直接生成插件代码仓库
编辑`main.py`输入以下内容:
```Python
from pkg.plugin.models import *
from pkg.plugin.host import EventContext, PluginHost
"""
在收到私聊或群聊消息"hello"时,回复"hello, <发送者id>!""hello, everyone!"
"""
# 注册插件
@register(name="Hello", description="hello world", version="0.1", author="RockChinQ")
class HelloPlugin(Plugin):
# 插件加载时触发
# plugin_host (pkg.plugin.host.PluginHost) 提供了与主程序交互的一些方法,详细请查看其源码
def __init__(self, plugin_host: PluginHost):
pass
# 当收到个人消息时触发
@on(PersonNormalMessageReceived)
def person_normal_message_received(self, event: EventContext, **kwargs):
msg = kwargs['text_message']
if msg == "hello": # 如果消息为hello
# 输出调试信息
logging.debug("hello, {}".format(kwargs['sender_id']))
# 回复消息 "hello, <发送者id>!"
event.add_return("reply", ["hello, {}!".format(kwargs['sender_id'])])
# 阻止该事件默认行为(向接口获取回复)
event.prevent_default()
# 当收到群消息时触发
@on(GroupNormalMessageReceived)
def group_normal_message_received(self, event: EventContext, **kwargs):
msg = kwargs['text_message']
if msg == "hello": # 如果消息为hello
# 输出调试信息
logging.debug("hello, {}".format(kwargs['sender_id']))
# 回复消息 "hello, everyone!"
event.add_return("reply", ["hello, everyone!"])
# 阻止该事件默认行为(向接口获取回复)
event.prevent_default()
# 插件卸载时触发
def __del__(self):
pass
```
此插件将实现:私聊收到`hello`消息时回复`hello, <发送者QQ号>!`,群聊收到`hello`消息时回复`hello, everyone!`
### 解读此插件程序
- `import``pkg.plugin`引入`models`模块的所有字段(此程序使用了其中的`register函数``on函数``Plugin类``PersonNormalMessageReceived事件``GroupNormalMessageReceived事件`
- `@register()`将类`HelloPlugin`标记为一个插件类,声明插件名称为`Hello`以及插件简介、版本、作者
- 声明类`HelloPlugin`继承于`Plugin`,此类可以随意命名,插件名称只与`register`调用时的参数有关
- 声明此类的`__init__`方法,此方法是可选的,其中的代码将在主程序启动时加载插件的时候被执行
- `@on`将方法`person_normal_message_received`标记为一个事件处理器,处理`PersonNormalMessageReceived`收到私聊消息并在获取OpenAI回复前触发事件此方法可以随意命名绑定的事件只与`on`中的参数有关,更多支持的事件可到`pkg.plugin.models.py`文件中查看或查看下方`API`
- 输出调试信息,程序中可通过`logging`将日志输出到控制台和`qchatgpt.log`文件
- 方法内部从参数中取出`text_message`参数,判断是否为`hello`,如果是就将返回值`reply`设置为`["hello, {}!".format(kwargs['sender_id'])]`,接下来调用`event`对象的`prevent_default`方法,阻止原程序默认行为
- 每个事件`提供的参数``支持的返回值`请查看`pkg.plugin.models`中的每个事件的注释或查看下方`API`
- `event`对象提供的方法请查看`pkg.plugin.host`中的`EventContext`类或查看下方`API`
- 用相似的程序注册`GroupNormalMessageReceived`事件处理群消息
编写完毕保存后,重新启动主程序,查看到输出中包含以下内容,即为加载成功:
```
[2023-01-16 18:29:47.193] host.py (43) - [INFO] : 加载模块: hello.main
[2023-01-16 18:29:47.194] models.py (209) - [INFO] : 插件注册完成: n='Hello', d='hello world', v=0.1, a='RockChinQ' (<class 'plugins.hello.main.HelloPlugin'>)
```
> 建议在`config.py`中设置`logging_level = logging.DEBUG`以便开启调试输出
## ❗规范(重要)
- 请每个插件独立一个目录以便管理建议在Github上创建一个仓库储存单个插件以便获取和更新
- 插件名使用`大驼峰命名法`,如`Hello``ExamplePlugin``ChineseCommands`
- 一个目录内可以存放多个Python程序文件以独立出插件的各个功能便于开发者管理但不建议在一个目录内注册多个插件
- 插件需要的依赖库请在插件目录下的`requirements.txt`中指定,程序从储存库获取此插件时将自动安装依赖
## 🪝内容函数
通过[GPT的Function Calling能力](https://platform.openai.com/docs/guides/gpt/function-calling)实现的`内容函数`这是一种嵌入对话中由GPT自动调用的函数。
> 您的插件不一定必须包含内容函数,请先查看内容函数页了解此功能
<details>
<summary>示例:联网插件</summary>
加载含有联网功能的内容函数的插件[WebwlkrPlugin](https://github.com/RockChinQ/WebwlkrPlugin),向机器人询问在线内容
```
# 控制台输出
[2023-07-29 17:37:18.698] message.py (26) - [INFO] : [person_1010553892]发送消息:介绍一下这个项目https://git...
[2023-07-29 17:37:21.292] util.py (67) - [INFO] : message='OpenAI API response' path=https://api.openai.com/v1/chat/completions processing_ms=1902 request_id=941afc13b2e1bba1e7877b92a970cdea response_code=200
[2023-07-29 17:37:21.293] chat_completion.py (159) - [INFO] : 执行函数调用: name=Webwlkr-access_the_web, arguments={'url': 'https://github.com/RockChinQ/QChatGPT', 'brief_len': 512}
[2023-07-29 17:37:21.848] chat_completion.py (164) - [INFO] : 函数执行完成。
```
![Webwlkr插件](https://github.com/RockChinQ/QChatGPT/blob/master/res/screenshots/webwlkr_plugin.png?raw=true)
</details>
### 内容函数编写步骤
1⃣ 请先按照上方步骤编写您的插件基础结构,现在请删除(当然你也可以不删,只是为了简洁)上述插件内容的诸个由`@on`装饰的类函数
<details>
<summary>删除后的结构</summary>
```python
from pkg.plugin.models import *
from pkg.plugin.host import EventContext, PluginHost
"""
在收到私聊或群聊消息"hello"时,回复"hello, <发送者id>!""hello, everyone!"
"""
# 注册插件
@register(name="Hello", description="hello world", version="0.1", author="RockChinQ")
class HelloPlugin(Plugin):
# 插件加载时触发
# plugin_host (pkg.plugin.host.PluginHost) 提供了与主程序交互的一些方法,详细请查看其源码
def __init__(self, plugin_host: PluginHost):
pass
# 插件卸载时触发
def __del__(self):
pass
```
</details>
2⃣ 现在我们将以下函数添加到刚刚删除的函数的位置
```Python
# 要添加的函数
@func(name="access_the_web") # 设置函数名称
def _(url: str):
"""Call this function to search about the question before you answer any questions.
- Do not search through baidu.com at any time.
- If you need to search somthing, visit https://www.google.com/search?q=xxx.
- If user ask you to open a url (start with http:// or https://), visit it directly.
- Summary the plain content result by yourself, DO NOT directly output anything in the result you got.
Args:
url(str): url to visit
Returns:
str: plain text content of the web page
"""
import requests
from bs4 import BeautifulSoup
# 你需要先使用
# pip install beautifulsoup4
# 安装依赖
r = requests.get(
url,
timeout=10,
headers={
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36 Edg/115.0.1901.183"
}
)
soup = BeautifulSoup(r.text, 'html.parser')
s = soup.get_text()
# 删除多余的空行或仅有\t和空格的行
s = re.sub(r'\n\s*\n', '\n', s)
if len(s) >= 512: # 截取获取到的网页纯文本内容的前512个字
return s[:512]
return s
```
<details>
<summary>现在这个文件内容应该是这样</summary>
```python
from pkg.plugin.models import *
from pkg.plugin.host import EventContext, PluginHost
"""
在收到私聊或群聊消息"hello"时,回复"hello, <发送者id>!""hello, everyone!"
"""
# 注册插件
@register(name="Hello", description="hello world", version="0.1", author="RockChinQ")
class HelloPlugin(Plugin):
# 插件加载时触发
# plugin_host (pkg.plugin.host.PluginHost) 提供了与主程序交互的一些方法,详细请查看其源码
def __init__(self, plugin_host: PluginHost):
pass
@func(name="access_the_web")
def _(url: str):
"""Call this function to search about the question before you answer any questions.
- Do not search through baidu.com at any time.
- If you need to search somthing, visit https://www.google.com/search?q=xxx.
- If user ask you to open a url (start with http:// or https://), visit it directly.
- Summary the plain content result by yourself, DO NOT directly output anything in the result you got.
Args:
url(str): url to visit
Returns:
str: plain text content of the web page
"""
import requests
from bs4 import BeautifulSoup
# 你需要先使用
# pip install beautifulsoup4
# 安装依赖
r = requests.get(
url,
timeout=10,
headers={
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36 Edg/115.0.1901.183"
}
)
soup = BeautifulSoup(r.text, 'html.parser')
s = soup.get_text()
# 删除多余的空行或仅有\t和空格的行
s = re.sub(r'\n\s*\n', '\n', s)
if len(s) >= 512: # 截取获取到的网页纯文本内容的前512个字
return s[:512]
return s
# 插件卸载时触发
def __del__(self):
pass
```
</details>
#### 请注意:
- 函数的注释必须严格按照要求的格式进行书写,具体格式请查看[此文档](https://github.com/RockChinQ/CallingGPT/wiki/1.-Function-Format#function-format)
- 内容函数和`以@on装饰的行为函数`可以同时存在于同一个插件,并同时受到`switch.json`中的插件开关的控制
- 务必确保您使用的模型支持函数调用功能,可以到`config.py``completion_api_params`中修改模型,推荐使用`gpt-3.5-turbo-16k`
3⃣ 现在您的程序已具备网络访问功能,重启程序,询问机器人有关在线的内容或直接发送文章链接请求其总结。
- 这仅仅是一个示例,需要更高效的网络访问能力支持插件,请查看[WebwlkrPlugin](https://github.com/RockChinQ/WebwlkrPlugin)
## 🔒版本要求
若您的插件对主程序的版本有要求,可以使用以下函数进行断言,若不符合版本,此函数将报错并打断此函数所在的流程:
```python
require_ver("v2.5.1") # 要求最低版本为 v2.5.1
```
```python
require_ver("v2.5.1", "v2.6.0") # 要求最低版本为 v2.5.1, 同时要求最高版本为 v2.6.0
```
- 此函数在主程序`v2.5.1`中加入
- 此函数声明在`pkg.plugin.models`模块中,在插件示例代码最前方已引入此模块所有内容,故可直接使用
## 📄API参考
### 说明
> 下一版本将会添加统一的插件API欢迎在[此讨论](https://github.com/RockChinQ/QChatGPT/discussions/637)回复您的需求!
事件处理函数将会获得一系列参数,可以在`kwargs`中取出。
其中`host`参数(`pkg.plugin.host.PluginHost`类的实例)是插件宿主,提供与主程序各个模块交互的一些方法。
`event`参数(`pkg.plugin.host.EventContext`类的实例)是事件执行期间的上下文,提供对此次事件执行的一些操作方法。
事件返回值均为**可选**的,可以通过调用`event.add_return(key: str, ret)`来提交返回值
### 事件
所有事件参数均有`host``event`,以下仅展示其他参数
关于`YiriMirai`支持的消息链组件,请查看 [YiriMirai的文档](https://yiri-mirai.wybxc.cc/docs/basic/message-chain)
```Python
PersonMessageReceived = "person_message_received"
"""收到私聊消息时,在判断是否应该响应前触发
kwargs:
launcher_type: str 发起对象类型(group/person)
launcher_id: int 发起对象ID(群号/QQ号)
sender_id: int 发送者ID(QQ号)
message_chain: mirai.models.message.MessageChain 消息链
"""
GroupMessageReceived = "group_message_received"
"""收到群聊消息时,在判断是否应该响应前触发(所有群消息)
kwargs:
launcher_type: str 发起对象类型(group/person)
launcher_id: int 发起对象ID(群号/QQ号)
sender_id: int 发送者ID(QQ号)
message_chain: mirai.models.message.MessageChain 消息链
"""
PersonNormalMessageReceived = "person_normal_message_received"
"""判断为应该处理的私聊普通消息时触发
kwargs:
launcher_type: str 发起对象类型(group/person)
launcher_id: int 发起对象ID(群号/QQ号)
sender_id: int 发送者ID(QQ号)
text_message: str 消息文本
returns (optional):
alter: str 修改后的消息文本
reply: list 回复消息组件列表元素为YiriMirai支持的消息组件
"""
PersonCommandSent = "person_command_sent"
"""判断为应该处理的私聊命令时触发
kwargs:
launcher_type: str 发起对象类型(group/person)
launcher_id: int 发起对象ID(群号/QQ号)
sender_id: int 发送者ID(QQ号)
command: str 命令
params: list[str] 参数列表
text_message: str 完整命令文本
is_admin: bool 是否为管理员
returns (optional):
alter: str 修改后的完整命令文本
reply: list 回复消息组件列表元素为YiriMirai支持的消息组件
"""
GroupNormalMessageReceived = "group_normal_message_received"
"""判断为应该处理的群聊普通消息时触发
kwargs:
launcher_type: str 发起对象类型(group/person)
launcher_id: int 发起对象ID(群号/QQ号)
sender_id: int 发送者ID(QQ号)
text_message: str 消息文本
returns (optional):
alter: str 修改后的消息文本
reply: list 回复消息组件列表元素为YiriMirai支持的消息组件
"""
GroupCommandSent = "group_command_sent"
"""判断为应该处理的群聊命令时触发
kwargs:
launcher_type: str 发起对象类型(group/person)
launcher_id: int 发起对象ID(群号/QQ号)
sender_id: int 发送者ID(QQ号)
command: str 命令
params: list[str] 参数列表
text_message: str 完整命令文本
is_admin: bool 是否为管理员
returns (optional):
alter: str 修改后的完整命令文本
reply: list 回复消息组件列表元素为YiriMirai支持的消息组件
"""
NormalMessageResponded = "normal_message_responded"
"""获取到对普通消息的文字响应时触发
kwargs:
launcher_type: str 发起对象类型(group/person)
launcher_id: int 发起对象ID(群号/QQ号)
sender_id: int 发送者ID(QQ号)
session: pkg.openai.session.Session 会话对象
prefix: str 回复文字消息的前缀
response_text: str 响应文本
finish_reason: str 响应结束原因
returns (optional):
prefix: str 修改后的回复文字消息的前缀
reply: list 替换回复消息组件列表
"""
SessionFirstMessageReceived = "session_first_message_received"
"""会话被第一次交互时触发
kwargs:
session_name: str 会话名称(<launcher_type>_<launcher_id>)
session: pkg.openai.session.Session 会话对象
default_prompt: str 预设值
"""
SessionExplicitReset = "session_reset"
"""会话被用户手动重置时触发,此事件不支持阻止默认行为
kwargs:
session_name: str 会话名称(<launcher_type>_<launcher_id>)
session: pkg.openai.session.Session 会话对象
"""
SessionExpired = "session_expired"
"""会话过期时触发
kwargs:
session_name: str 会话名称(<launcher_type>_<launcher_id>)
session: pkg.openai.session.Session 会话对象
session_expire_time: int 已设置的会话过期时间(秒)
"""
KeyExceeded = "key_exceeded"
"""api-key超额时触发
kwargs:
key_name: str 超额的api-key名称
usage: dict 超额的api-key使用情况
exceeded_keys: list[str] 超额的api-key列表
"""
KeySwitched = "key_switched"
"""api-key超额切换成功时触发此事件不支持阻止默认行为
kwargs:
key_name: str 切换成功的api-key名称
key_list: list[str] api-key列表
"""
PromptPreProcessing = "prompt_pre_processing" # 于v2.5.1加入
"""每回合调用接口前对prompt进行预处理时触发此事件不支持阻止默认行为
kwargs:
session_name: str 会话名称(<launcher_type>_<launcher_id>)
default_prompt: list 此session使用的情景预设内容
prompt: list 此session现有的prompt内容
text_message: str 用户发送的消息文本
returns (optional):
default_prompt: list 修改后的情景预设内容
prompt: list 修改后的prompt内容
text_message: str 修改后的消息文本
"""
```
### host: PluginHost 详解
提供与主程序各个模块交互的一些方法,具体查看`pkg.plugin.host`中的`PluginHost`
### event: EventContext 详解
提供对此次事件执行的一些操作方法,具体查看`pkg.plugin.host`中的`EventContext`

View File

@@ -1,17 +0,0 @@
> [!WARNING]
> 此 Wiki 已弃用,所有文档已迁移到 [项目主页](https://qchatgpt.rockchin.top)
## 多个对话接口有何区别?
出于对稳定性的高要求本项目主线接入的是GPT-3模型接口此接口由OpenAI官方开放稳定性强。
目前支持通过加载[插件](https://github.com/RockChinQ/revLibs)的方式接入ChatGPT网页版使用的是acheong08/ChatGPT的逆向工程库但文本生成质量更高。
同时程序主线已支持ChatGPT API并作为默认接口 [#195](https://github.com/RockChinQ/QChatGPT/issues/195)
|官方接口|ChatGPT网页版|ChatGPT API
|---|---|---|
|官方开放,稳定性高 | 由[acheong08](https://github.com/acheong08)破解网页版协议接入| 由OpenAI官方开放
|一次性回复,响应速度较快| 流式回复,响应速度较慢|响应速度较快|
|收费0.02美元/千字|免费|收费0.002美元/千字|
|GPT-3模型|GPT-3.5模型|GPT-3.5模型|
|任何地区主机均可使用(疑似受到GFW影响)|ChatGPT限制访问的区域使用有难度|任何地区主机均可使用(疑似受到GFW影响)|

View File

@@ -1,73 +0,0 @@
> [!WARNING]
> 此 Wiki 已弃用,所有文档已迁移到 [项目主页](https://qchatgpt.rockchin.top)
# 配置go-cqhttp用于登录QQ
> 若您是从旧版本升级到此版本以使用go-cqhttp的用户请您按照`config-template.py`的内容修改`config.py`,添加`msg_source_adapter`配置项并将其设为`nakuru`,同时添加`nakuru_config`字段按照说明配置。
## 步骤
1. 从[go-cqhttp的Release](https://github.com/Mrs4s/go-cqhttp/releases/latest)下载最新的go-cqhttp可执行文件建议直接下载可执行文件压缩包而不是安装器
2. 解压并运行,首次运行会询问需要开放的网络协议,**请填入`02`并回车,必须输入`02`❗❗❗❗❗❗❗**
<h1> 你这里必须得输入`02`,你懂么,`0`必须得输入,看好了,看好下面输入什么了吗?别他妈的搁那就输个`2`完了启动连不上还跑群里问,问一个我踢一个。 </h1>
```
C:\Softwares\go-cqhttp.old> .\go-cqhttp.exe
未找到配置文件,正在为您生成配置文件中!
请选择你需要的通信方式:
> 0: HTTP通信
> 1: 云函数服务
> 2: 正向 Websocket 通信
> 3: 反向 Websocket 通信
请输入你需要的编号(0-9),可输入多个,同一编号也可输入多个(如: 233)
您的选择是:02
```
提示已生成`config.yml`文件关闭go-cqhttp。
3. 打开go-cqhttp同目录的`config.yml`
1. 编辑账号登录信息
只需要修改下方`uin``password`为你要登录的机器人账号的QQ号和密码即可。
**若您不填写,将会在启动时请求扫码登录。**
```yaml
account: # 账号相关
uin: 1233456 # QQ账号
password: '' # 密码为空时使用扫码登录
encrypt: false # 是否开启密码加密
status: 0 # 在线状态 请参考 https://docs.go-cqhttp.org/guide/config.html#在线状态
relogin: # 重连设置
delay: 3 # 首次重连延迟, 单位秒
interval: 3 # 重连间隔
max-times: 0 # 最大重连次数, 0为无限制
```
2. 修改websocket端口
在`config.yml`下方找到以下内容
```yaml
- ws:
# 正向WS服务器监听地址
address: 0.0.0.0:8080
middlewares:
<<: *default # 引用默认中间件
```
**将`0.0.0.0:8080`改为`0.0.0.0:6700`**,保存并关闭`config.yml`。
3. 若您的服务器位于公网,强烈建议您填写`access-token` (可选)
```yaml
# 默认中间件锚点
default-middlewares: &default
# 访问密钥, 强烈推荐在公网的服务器设置
access-token: ''
```
4. 配置完成重新启动go-cqhttp
> 若启动后登录不成功,请尝试根据[此文档](https://docs.go-cqhttp.org/guide/config.html#%E8%AE%BE%E5%A4%87%E4%BF%A1%E6%81%AF)修改`device.json`的协议编号。

View File

@@ -1,30 +0,0 @@
> [!WARNING]
> 此 Wiki 已弃用,所有文档已迁移到 [项目主页](https://qchatgpt.rockchin.top)
欢迎查看QChatGPT的Wiki页。
## 简介
调用OpenAI官方提供的API接口结合mirai和YiriMirai框架将QQ消息与语言模型连接实现更加智能的对话机器人
## 技术栈
- [Mirai](https://github.com/mamoe/mirai) 高效率 QQ 机器人支持库
- [YiriMirai](https://github.com/YiriMiraiProject/YiriMirai) 一个轻量级、低耦合的基于 mirai-api-http 的 Python SDK。
- [go-cqhttp](https://github.com/Mrs4s/go-cqhttp) cqhttp的golang实现轻量、原生跨平台.
- [nakuru-project](https://github.com/Lxns-Network/nakuru-project) - 一款为 go-cqhttp 的正向 WebSocket 设计的 Python SDK支持纯 CQ 码与消息链的转换处理
- [nakuru-project-idk](https://github.com/idoknow/nakuru-project-idk) - 由idoknow维护的nakuru-project分支
- [dulwich](https://github.com/jelmer/dulwich) Pure-Python Git implementation
- [OpenAI API](https://openai.com/api/) OpenAI API
## 代码结构
- `pkg.database` 数据库操作相关
- 数据库用于存放会话的历史记录,确保在程序重启后能记住对话内容
- `pkg.openai` OpenAI API相关
- 用于调用OpenAI的API生成回复内容
- `pkg.qqbot` QQ机器人相关
- 处理QQ收到的消息调用API并进行回复
- `pkg.utils` 常用功能包
- `pkg.audit` 审计模块
- `pkg.plugin` 插件管理相关功能

View File

@@ -37,7 +37,10 @@
"track-function-calls": true,
"quote-origin": false,
"at-sender": false,
"force-delay": [0, 0],
"force-delay": {
"min": 0,
"max": 0
},
"long-text-process": {
"threshold": 256,
"strategy": "forward",

View File

@@ -0,0 +1,39 @@
{
"type": "object",
"layout": "expansion-panels",
"properties": {
"command-prefix": {
"type": "array",
"title": "命令前缀",
"description": "以数组形式设置,程序将前缀符合设置的消息视为命令(群内需要符合群响应规则)",
"items": {
"type": "string"
},
"default": [
"!",
""
]
},
"privilege": {
"type": "object",
"title": "权限管理",
"description": "设置每个命令的权限配置。普通用户权限级别为 1管理员system.json中设置的权限级别为 2在这里设置每个命令的最低权限级别若设置为1则用户和管理员均可用若为2则仅管理员可用设置子命令时以点号间隔如\"plugin.on\"",
"properties": {
"placeholder": {
"type": "integer",
"minimum": 1,
"maximum": 2,
"const": 1
}
},
"patternProperties": {
"^[a-zA-Z0-9_.]+$": {
"type": "integer",
"minimum": 1,
"maximum": 2
}
},
"default": {}
}
}
}

View File

@@ -0,0 +1,326 @@
{
"type": "object",
"layout": "expansion-panels",
"properties": {
"access-control": {
"type": "object",
"title": "访问控制",
"properties": {
"mode": {
"type": "string",
"title": "访问控制模式",
"description": "访问控制模式,支持黑名单和白名单",
"enum": [
"blacklist",
"whitelist"
],
"default": "blacklist"
},
"blacklist": {
"type": "array",
"title": "黑名单",
"description": "黑名单中的会话将无法使用机器人,仅在访问控制模式为黑名单时有效。格式:{type}_{id}示例group_12345678 或 person_12341234",
"items": {
"type": "string",
"format": "regex",
"pattern": "^(person|group)_(\\d)*$"
},
"default": []
},
"whitelist": {
"type": "array",
"title": "白名单",
"description": "仅白名单中的会话可以使用机器人,仅在访问控制模式为白名单时有效。格式:{type}_{id}示例group_12345678 或 person_12341234",
"items": {
"type": "string",
"format": "regex",
"pattern": "^(person|group)_(\\d)*$"
},
"default": []
}
},
"required": [
"mode"
]
},
"respond-rules": {
"type": "object",
"title": "群消息响应规则",
"description": "仅处理 访问控制 允许的会话的消息。所有未指定的群使用 默认响应规则,若需指定特定的群的规则,请输入 群号 并添加,并设置响应规则",
"properties": {
"default": {
"type": "object",
"title": "默认响应规则",
"properties": {
"at": {
"type": "boolean",
"title": "是否响应 @ 消息",
"layout": {
"comp": "switch",
"props": {
"color": "primary"
}
}
},
"prefix": {
"type": "array",
"title": "响应前缀",
"description": "带有指定前缀的消息即使没有 at 机器人也会被响应,发送给 AI 时会删除前缀",
"items": {
"type": "string"
},
"default": []
},
"regexp": {
"type": "array",
"title": "响应正则表达式",
"description": "正则表达式教程https://www.runoob.com/regexp/regexp-syntax.html",
"items": {
"type": "string",
"format": "regex"
},
"default": []
},
"random": {
"type": "number",
"title": "随机响应概率",
"description": "数值范围是0.0-1.0对应概率0%-100%为1.0时所有消息都响应",
"minimum": 0,
"maximum": 1,
"step": 0.01,
"layout": {
"comp": "slider",
"props": {
"color": "primary"
}
}
}
}
}
},
"patternProperties": {
"^\\d+$": {
"type": "object",
"properties": {
"at": {
"type": "boolean",
"title": "是否响应 @ 消息",
"layout": {
"comp": "switch",
"props": {
"color": "primary"
}
}
},
"prefix": {
"type": "array",
"title": "响应前缀",
"description": "带有指定前缀的消息即使没有 at 机器人也会被响应,发送给 AI 时会删除前缀",
"items": {
"type": "string"
},
"default": []
},
"regexp": {
"type": "array",
"title": "响应正则表达式",
"description": "正则表达式教程https://www.runoob.com/regexp/regexp-syntax.html",
"items": {
"type": "string",
"format": "regex"
},
"default": []
},
"random": {
"type": "number",
"title": "随机响应概率",
"description": "数值范围是0.0-1.0对应概率0%-100%为1.0时所有消息都响应",
"minimum": 0,
"maximum": 1,
"step": 0.01,
"layout": {
"comp": "slider",
"props": {
"color": "primary"
}
}
}
}
}
}
},
"income-msg-check": {
"type": "boolean",
"title": "检查传入消息内容",
"description": "是否对传入的消息用户消息进行检查需配合审核策略使用AI 响应内容一定会通过检查策略)",
"layout": {
"comp": "switch",
"props": {
"color": "primary"
}
}
},
"ignore-rules": {
"type": "object",
"title": "传入消息忽略规则",
"description": "符合规则的传入消息将被忽略,仅传入消息检查被启用时生效",
"properties": {
"prefix": {
"type": "array",
"title": "忽略前缀",
"description": "具有指定前缀的消息将被忽略",
"items": {
"type": "string"
},
"default": []
},
"regexp": {
"type": "array",
"title": "忽略正则表达式",
"description": "正则表达式教程https://www.runoob.com/regexp/regexp-syntax.html",
"items": {
"type": "string",
"format": "regex"
},
"default": []
}
}
},
"check-sensitive-words": {
"type": "boolean",
"title": "本地敏感词检查",
"description": "是否启用本地敏感词检查",
"layout": {
"comp": "switch",
"props": {
"color": "primary"
}
}
},
"baidu-cloud-examine": {
"type": "object",
"title": "百度云内容审核配置",
"description": "百度云内容审核配置前往https://cloud.baidu.com/doc/ANTIPORN/index.html 获取 API Key 和 API Secret",
"properties": {
"enable": {
"type": "boolean",
"title": "是否启用",
"layout": {
"comp": "switch",
"props": {
"color": "primary"
}
}
},
"api-key": {
"type": "string",
"title": "API Key",
"default": ""
},
"api-secret": {
"type": "string",
"title": "API Secret",
"default": ""
}
}
},
"rate-limit": {
"type": "object",
"title": "请求限速规则",
"properties": {
"strategy": {
"type": "string",
"title": "限速策略",
"description": "会话中的请求速率超过限制时的处理策略drop为丢弃新请求wait为等待请求速率降到限制以下",
"enum": [
"drop",
"wait"
],
"default": "drop"
},
"algo": {
"type": "string",
"title": "限速算法",
"description": "目前仅支持 fixwin固定窗口支持插件扩展",
"enum": [
"fixwin"
],
"default": "fixwin"
},
"fixwin": {
"type": "object",
"title": "固定窗口限速策略配置",
"description": "所有会话使用默认限速策略,若需指定特定会话的限速策略,请输入 会话名称(格式为 {type}_{id}示例group_123456 或 person_123456 并添加,以设置特定会话的限速参数",
"properties": {
"default": {
"type": "object",
"title": "默认限速策略",
"properties": {
"window-size": {
"type": "integer",
"title": "窗口大小(秒)",
"minimum": 1,
"default": 60
},
"limit": {
"type": "integer",
"title": "窗口期间允许的最大消息数",
"minimum": 1,
"default": 60
}
}
}
},
"patternProperties": {
"^(person|group).*$": {
"type": "object",
"title": "会话限速",
"properties": {
"window-size": {
"type": "integer",
"title": "窗口大小(秒)",
"minimum": 1,
"default": 60
},
"limit": {
"type": "integer",
"title": "窗口期间允许的最大消息数",
"minimum": 1,
"default": 60
}
}
}
}
}
}
},
"msg-truncate": {
"type": "object",
"title": "对话历史记录截断",
"description": "将在发送消息给模型之前对当前会话的历史消息进行截断,以限制传给模型的消息长度",
"properties": {
"method": {
"type": "string",
"title": "截断方法",
"description": "目前仅支持 round按回合截断支持插件扩展",
"enum": [
"round"
],
"default": "round"
},
"round": {
"type": "object",
"title": "轮次截断策略配置",
"properties": {
"max-round": {
"type": "integer",
"title": "最大保留前文回合数",
"minimum": 1,
"default": 10
}
}
}
}
}
}
}

View File

@@ -0,0 +1,258 @@
{
"type": "object",
"layout": "expansion-panels",
"properties": {
"platform-adapters": {
"type": "array",
"title": "消息平台适配器",
"default": {},
"items": {
"type": "object",
"oneOf": [
{
"title": "YiriMirai 适配器",
"description": "用于接入 Mirai",
"properties": {
"adapter": {
"type": "string",
"const": "yiri-mirai"
},
"enable": {
"type": "boolean",
"default": false,
"description": "是否启用此适配器",
"layout": {
"comp": "switch",
"props": {
"color": "primary"
}
}
},
"host": {
"type": "string",
"default": "127.0.0.1"
},
"port": {
"type": "integer",
"default": 8080
},
"verifyKey": {
"type": "string",
"default": "yirimirai"
},
"qq": {
"type": "integer",
"default": 123456789
}
}
},
{
"title": "Nakuru 适配器",
"description": "用于接入 go-cqhttp",
"properties": {
"adapter": {
"type": "string",
"const": "nakuru"
},
"enable": {
"type": "boolean",
"default": false,
"description": "是否启用此适配器",
"layout": {
"comp": "switch",
"props": {
"color": "primary"
}
}
},
"host": {
"type": "string",
"default": "127.0.0.1"
},
"ws_port": {
"type": "integer",
"default": 8080
},
"http_port": {
"type": "integer",
"default": 5700
},
"token": {
"type": "string",
"default": ""
}
}
},
{
"title": "aiocqhttp 适配器",
"description": "用于接入 Lagrange 等兼容 OneBot v11 协议的机器人框架仅支持反向ws",
"properties": {
"adapter": {
"type": "string",
"const": "aiocqhttp"
},
"enable": {
"type": "boolean",
"default": false,
"description": "是否启用此适配器",
"layout": {
"comp": "switch",
"props": {
"color": "primary"
}
}
},
"host": {
"type": "string",
"default": "0.0.0.0",
"description": "监听的 IP 地址,一般就保持 0.0.0.0 就可以了。使用 aiocqhttp 时QChatGPT 作为服务端被动等待框架连接,请在 Lagrange 等框架中设置被动 ws 地址或者反向 ws 地址(具体视框架而定)为 QChatGPT 监听的地址,且路径为/ws例如ws://127.0.0.1:8080/ws"
},
"port": {
"type": "integer",
"default": 8080,
"description": "设置监听的端口默认8080需在 Lagrange 等框架中设置为与此处一致的端口"
},
"access-token": {
"type": "string",
"default": "",
"description": "设置访问密钥,与 Lagrange 等框架中设置的保持一致"
}
}
},
{
"title": "qq-botpy 适配器",
"description": "用于接入 QQ 官方机器人 API",
"properties": {
"adapter": {
"type": "string",
"const": "qq-botpy"
},
"enable": {
"type": "boolean",
"default": false,
"description": "是否启用此适配器",
"layout": {
"comp": "switch",
"props": {
"color": "primary"
}
}
},
"appid": {
"type": "string",
"default": "",
"description": "申请到的QQ官方机器人的appid"
},
"secret": {
"type": "string",
"default": "",
"description": "申请到的QQ官方机器人的secret"
},
"intents": {
"type": "array",
"description": "控制监听的事件类型需要填写才能接收到对应消息目前支持的事件类型有public_guild_messagesQQ 频道消息、direct_messageQQ 频道私聊消息、public_messagesQQ 群 和 列表私聊消息)",
"default": [
"public_guild_messages",
"direct_message",
"public_messages"
]
}
}
}
]
}
},
"track-function-calls": {
"type": "boolean",
"default": true,
"layout": {
"comp": "switch",
"props": {
"color": "primary"
}
},
"title": "跟踪内容函数调用",
"description": "开启之后,在对话中调用的内容函数记录也会发给用户,关闭后(false)仅会发给用户最终结果"
},
"quote-origin": {
"type": "boolean",
"default": false,
"layout": {
"comp": "switch",
"props": {
"color": "primary"
}
},
"title": "引用原消息",
"description": "在群内回复时是否引用原消息"
},
"at-sender": {
"type": "boolean",
"default": false,
"layout": {
"comp": "switch",
"props": {
"color": "primary"
}
},
"title": "是否 at 原用户",
"description": "在群内回复时是否@发送者"
},
"force-delay": {
"type": "object",
"default": {
"min": 0,
"max": 0
},
"title": "强制消息延迟范围",
"description": "在将响应内容发回给用户前的强制消息随机延迟时间范围,以防风控,单位是秒",
"properties": {
"min": {
"type": "integer",
"default": 0,
"description": "最小值,单位是秒"
},
"max": {
"type": "integer",
"default": 0,
"description": "最大值,单位是秒"
}
}
},
"long-text-process": {
"type": "object",
"title": "长消息处理策略",
"properties": {
"threshold": {
"type": "integer",
"default": 256,
"title": "长消息处理阈值",
"description": "当消息长度超过此阈值时,将启用长消息处理策略"
},
"strategy": {
"type": "string",
"default": "forward",
"title": "长消息处理策略",
"description": "长消息处理策略目前支持forward转发消息组件和image文字转图片。aiocqhttp 和 qq-botpy 不支持 forward 策略"
},
"font-path": {
"type": "string",
"description": "image的渲染字体。未设置时如果在windows下会尝试寻找系统的微软雅黑字体若找不到则转为forward策略。未设置时若不是windows系统则直接转为forward策略",
"default": ""
}
}
},
"hide-exception-info": {
"type": "boolean",
"default": true,
"layout": {
"comp": "switch",
"props": {
"color": "primary"
}
},
"title": "向用户隐藏AI接口的异常信息",
"description": "是否向用户隐藏AI的异常信息如果为true当请求AI接口出现异常时会返回一个错误的提示给用户。而把报错详情输出在控制台。"
}
}
}

View File

@@ -0,0 +1,207 @@
{
"type": "object",
"layout": "expansion-panels",
"properties": {
"enable-chat": {
"type": "boolean",
"default": true,
"title": "启用聊天功能",
"description": "是否启用 AI 聊天功能"
},
"enable-vision": {
"type": "boolean",
"default": true,
"title": "启用视觉功能",
"description": "是否开启AI视觉功能。需要使用的模型同时支持视觉功能详情见元数据板块"
},
"keys": {
"type": "object",
"title": "模型接口密钥",
"description": "以字典的形式设置若干个密钥组,每个密钥组的键为密钥组名称,值为密钥列表。模型与密钥组的对应关系,请查看元数据板块",
"properties": {
"openai": {
"type": "array",
"title": "OpenAI API 密钥",
"description": "OpenAI API 密钥",
"items": {
"type": "string"
},
"default": []
},
"anthropic": {
"type": "array",
"title": "Anthropic API 密钥",
"description": "Anthropic API 密钥",
"items": {
"type": "string"
},
"default": []
},
"moonshot": {
"type": "array",
"title": "Moonshot API 密钥",
"description": "Moonshot API 密钥",
"items": {
"type": "string"
},
"default": []
},
"deepseek": {
"type": "array",
"title": "DeepSeek API 密钥",
"description": "DeepSeek API 密钥",
"items": {
"type": "string"
},
"default": []
}
}
},
"requester": {
"type": "object",
"title": "大模型请求器",
"description": "以字典的形式设置若干个请求器,每个请求器的键为请求器名称,值为请求器配置。模型与请求器的对应关系,请查看元数据板块。实现请求器的方式,请查看插件编写教程",
"properties": {
"openai-chat-completions": {
"type": "object",
"title": "OpenAI API 请求配置",
"description": "仅可编辑 URL 和 超时时间,额外请求参数不支持可视化编辑,请到编辑器编辑",
"properties": {
"base-url": {
"type": "string",
"title": "API URL"
},
"args": {
"type": "object",
"default": {}
},
"timeout": {
"type": "number",
"title": "API 请求超时时间",
"default": 120
}
}
},
"anthropic-messages": {
"type": "object",
"title": "Anthropic API 请求配置",
"description": "仅可编辑 URL 和 超时时间,额外请求参数不支持可视化编辑,请到编辑器编辑",
"properties": {
"base-url": {
"type": "string",
"title": "API URL"
},
"args": {
"type": "object",
"default": {}
},
"timeout": {
"type": "number",
"title": "API 请求超时时间",
"default": 120
}
}
},
"moonshot-chat-completions": {
"type": "object",
"title": "Moonshot API 请求配置",
"description": "仅可编辑 URL 和 超时时间,额外请求参数不支持可视化编辑,请到编辑器编辑",
"properties": {
"base-url": {
"type": "string",
"title": "API URL"
},
"args": {
"type": "object",
"default": {}
},
"timeout": {
"type": "number",
"title": "API 请求超时时间",
"default": 120
}
}
},
"deepseek-chat-completions": {
"type": "object",
"title": "DeepSeek API 请求配置",
"description": "仅可编辑 URL 和 超时时间,额外请求参数不支持可视化编辑,请到编辑器编辑",
"properties": {
"base-url": {
"type": "string",
"title": "API URL"
},
"args": {
"type": "object",
"default": {}
},
"timeout": {
"type": "number",
"title": "API 请求超时时间",
"default": 120
}
}
},
"ollama-chat": {
"type": "object",
"title": "Ollama API 请求配置",
"description": "仅可编辑 URL 和 超时时间,额外请求参数不支持可视化编辑,请到编辑器编辑",
"properties": {
"base-url": {
"type": "string",
"title": "API URL"
},
"args": {
"type": "object"
},
"timeout": {
"type": "number",
"title": "API 请求超时时间",
"default": 600
}
}
}
}
},
"model": {
"type": "string",
"title": "所使用的模型名称",
"description": "设置要使用的模型名称。通常来说直接填写模型名称即可,但如果要使用原生接口不是 ChatCompletion 但以 ChatCompletion 接口格式接入的模型,请在模型名称前方加一个 OneAPI/ 前缀以进行区分。 简单来说可以认为是:现阶段非 OpenAI 的模型接入都需要在模型名称前方加一个 OneAPI/ 前缀。\n\n例如\n\n1. 通过 OneAPI 等中转服务接入了 OpenAI 的 gpt-4 模型,由于 gpt-4 也是使用 ChatCompletion 接口格式进行请求,则可以直接填入 gpt-4\n2. 通过 OneAPI 等中转服务接入了 Google 的 gemini-pro 模型,由于 gemini-pro 原生接口格式并非 ChatCompletion因此需要填入 OneAPI/gemini-pro。\n具体支持的模型列表和各个模型对应的请求器和密钥组请查看元数据板块 llm-models.json "
},
"prompt-mode": {
"type": "string",
"title": "情景预设(人格)模式",
"description": "值为normal单预设模式和full-scenario完整历史对话模式normal模式时使用下方设置的情景预设也支持读取data/prompts目录下的文件内容作为单个 System Prompt文件名即为prompt的名称full-scenario模式时读取 data/scenario/ 下的完整历史对话作为情景预设",
"enum": ["normal", "full-scenario"],
"default": "normal"
},
"prompt": {
"type": "object",
"title": "情景预设(人格)",
"description": "设置情景预设人格。值为空字符串时将不使用情景预设人格。normal模式时使用下方设置的情景预设也支持读取data/prompts目录下的文件内容作为单个 System Prompt文件名即为prompt的名称full-scenario模式时读取 data/scenario/ 下的完整历史对话作为情景预设",
"properties": {
"default": {
"type": "string",
"title": "默认情景预设",
"description": "设置默认情景预设。值为空字符串时,将不使用情景预设(人格)",
"default": ""
}
},
"patternProperties": {
"^.*$": {
"type": "string",
"title": "情景预设",
"description": "设置情景预设。值为空字符串时,将不使用情景预设(人格)",
"default": ""
}
},
"required": ["default"]
},
"runner": {
"type": "string",
"title": "请求运行器",
"description": "设置请求运行器。值为local-agent时使用内置默认运行器支持插件扩展",
"default": "local-agent"
}
}
}

View File

@@ -0,0 +1,121 @@
{
"type": "object",
"layout": "expansion-panels",
"properties": {
"admin-sessions": {
"type": "array",
"title": "管理员会话",
"description": "设置管理员会话,格式为 {type}_{id}type 为 \"group\" 或 \"person\"group_123456 或 person_123456",
"items": {
"type": "string",
"format": "regex",
"pattern": "^(person|group)_(\\d+)$"
},
"default": []
},
"network-proxies": {
"type": "object",
"title": "网络代理",
"description": "正向代理http和https都要填例如http://127.0.0.1:7890 https://127.0.0.1:7890 。不使用代理请留空。正向代理也可以用环境变量设置http_proxy 和 https_proxy",
"properties": {
"http": {
"type": "string"
},
"https": {
"type": "string"
}
}
},
"report-usage": {
"type": "boolean",
"title": "上报遥测数据",
"description": "遥测数据用于统计和分析项目使用情况,不包含任何隐私信息,不建议禁用",
"layout": {
"comp": "switch",
"props": {
"color": "primary"
}
}
},
"logging-level": {
"type": "string",
"title": "日志等级",
"description": "目前无效启用调试模式请设置环境变量export DEBUG=true"
},
"session-concurrency": {
"type": "object",
"title": "会话消息处理并发数",
"description": "粒度是单个会话,所有会话使用默认并发数,若需指定特定会话的并发数,请输入 会话名称(格式为 {type}_{id}示例group_123456 或 person_123456 并添加,以设置特定会话的并发数",
"properties": {
"default": {
"type": "integer"
}
},
"patternProperties": {
"^(person|group)_(\\d+)$": {
"type": "integer"
}
}
},
"pipeline-concurrency": {
"type": "integer",
"title": "流水线消息处理并发数",
"description": "粒度是整个程序,目前使用 FCFS 算法调度各个请求"
},
"qcg-center-url": {
"type": "string",
"title": "遥测服务器地址",
"description": "运行期间推送遥测数据的目标地址,默认为官方地址,若您自己部署了 https://github.com/RockChinQ/qcg-center可以改为你的地址。"
},
"help-message": {
"type": "string",
"title": "帮助消息",
"description": "用户发送 !help 命令时的输出",
"layout": "textarea"
},
"http-api": {
"type": "object",
"title": "HTTP 接口",
"properties": {
"enable": {
"type": "boolean",
"layout": {
"comp": "switch",
"props": {
"color": "primary"
}
},
"title": "是否启用"
},
"host": {
"type": "string"
},
"port": {
"type": "integer"
}
}
},
"persistence": {
"type": "object",
"title": "持久化设置",
"properties": {
"sqlite": {
"type": "object",
"title": "sqlite",
"properties": {
"path": {
"type": "string"
}
}
},
"use": {
"type": "string",
"title": "所使用的数据库",
"enum": [
"sqlite"
]
}
}
}
}
}

481
web/package-lock.json generated
View File

@@ -8,7 +8,13 @@
"name": "web",
"version": "0.0.0",
"dependencies": {
"@koumoul/vjsf": "^3.0.0-beta.46",
"@mdi/font": "7.4.47",
"ajv": "^8.17.1",
"ajv-dist": "^8.17.1",
"ajv-errors": "^3.0.0",
"ajv-formats": "^3.0.1",
"ajv-i18n": "^4.2.0",
"axios": "^1.7.7",
"codemirror": "^5.65.18",
"core-js": "^3.37.1",
@@ -510,6 +516,30 @@
"url": "https://opencollective.com/eslint"
}
},
"node_modules/@eslint/eslintrc/node_modules/ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dev": true,
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
"json-schema-traverse": "^0.4.1",
"uri-js": "^4.2.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
"dev": true,
"license": "MIT"
},
"node_modules/@eslint/js": {
"version": "8.57.1",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz",
@@ -564,6 +594,86 @@
"integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
"license": "MIT"
},
"node_modules/@json-layout/core": {
"version": "0.32.1",
"resolved": "https://registry.npmjs.org/@json-layout/core/-/core-0.32.1.tgz",
"integrity": "sha512-/x+D8epj48MKPlDTKE7lgwjd6ThFF99+fZwxwuActV3XAFwE55bu0sK45i9yDoTkgEHHWCDlxlOdDkJP0hRQ/g==",
"license": "MIT",
"dependencies": {
"@json-layout/vocabulary": "^0.23.2",
"@types/markdown-it": "^13.0.1",
"ajv": "^8.12.0",
"ajv-errors": "^3.0.0",
"ajv-formats": "^2.1.1",
"ajv-i18n": "^4.2.0",
"debug": "^4.3.4",
"immer": "^10.0.3",
"magicast": "^0.3.3",
"markdown-it": "^13.0.2"
}
},
"node_modules/@json-layout/core/node_modules/ajv-formats": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz",
"integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==",
"license": "MIT",
"dependencies": {
"ajv": "^8.0.0"
},
"peerDependencies": {
"ajv": "^8.0.0"
},
"peerDependenciesMeta": {
"ajv": {
"optional": true
}
}
},
"node_modules/@json-layout/vocabulary": {
"version": "0.23.2",
"resolved": "https://registry.npmjs.org/@json-layout/vocabulary/-/vocabulary-0.23.2.tgz",
"integrity": "sha512-CDQ/nFZmcMdhn0Ud/f5Q3IoRemQQdw2CPm5pufRqo27T71JhIw12KluVW1jsZWlwK3v7q7yqOoVS8Ax9bUOQ4w==",
"license": "MIT",
"dependencies": {
"ajv": "^8.12.0",
"ajv-errors": "^3.0.0",
"ajv-formats": "^2.1.1",
"debug": "^4.3.4"
}
},
"node_modules/@json-layout/vocabulary/node_modules/ajv-formats": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz",
"integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==",
"license": "MIT",
"dependencies": {
"ajv": "^8.0.0"
},
"peerDependencies": {
"ajv": "^8.0.0"
},
"peerDependenciesMeta": {
"ajv": {
"optional": true
}
}
},
"node_modules/@koumoul/vjsf": {
"version": "3.0.0-beta.46",
"resolved": "https://registry.npmjs.org/@koumoul/vjsf/-/vjsf-3.0.0-beta.46.tgz",
"integrity": "sha512-dp9EuyZrZNRHb5+8eLMHWNEI9HPhpLoeOWzDWj43tE+162mGmhi42SqVMS5fbx73ZHF2FHe4jZszgdj0MiYB2A==",
"license": "MIT",
"dependencies": {
"@json-layout/core": "0.32.1",
"@vueuse/core": "^10.5.0",
"debug": "^4.3.4",
"ejs": "^3.1.9"
},
"peerDependencies": {
"vue": "^3.4.3",
"vuetify": "^3.6.13"
}
},
"node_modules/@mdi/font": {
"version": "7.4.47",
"resolved": "https://registry.npmjs.org/@mdi/font/-/font-7.4.47.tgz",
@@ -860,6 +970,34 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/linkify-it": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.5.tgz",
"integrity": "sha512-yg6E+u0/+Zjva+buc3EIb+29XEg4wltq7cSmd4Uc2EE/1nUVmxyzpX6gUXD0V8jIrG0r7YeOGVIbYRkxeooCtw==",
"license": "MIT"
},
"node_modules/@types/markdown-it": {
"version": "13.0.9",
"resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-13.0.9.tgz",
"integrity": "sha512-1XPwR0+MgXLWfTn9gCsZ55AHOKW1WN+P9vr0PaQh5aerR9LLQXUbjfEAFhjmEmyoYFWAyuN2Mqkn40MZ4ukjBw==",
"license": "MIT",
"dependencies": {
"@types/linkify-it": "^3",
"@types/mdurl": "^1"
}
},
"node_modules/@types/mdurl": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-1.0.5.tgz",
"integrity": "sha512-6L6VymKTzYSrEf4Nev4Xa1LCHKrlTlYCBMTlQKFuddo1CvQcE52I0mwfOJayueUC7MJuXOeHTcIU683lzd0cUA==",
"license": "MIT"
},
"node_modules/@types/web-bluetooth": {
"version": "0.0.20",
"resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz",
"integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==",
"license": "MIT"
},
"node_modules/@ungap/structured-clone": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz",
@@ -1027,6 +1165,94 @@
"vuetify": "^3.0.0"
}
},
"node_modules/@vueuse/core": {
"version": "10.11.1",
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-10.11.1.tgz",
"integrity": "sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==",
"license": "MIT",
"dependencies": {
"@types/web-bluetooth": "^0.0.20",
"@vueuse/metadata": "10.11.1",
"@vueuse/shared": "10.11.1",
"vue-demi": ">=0.14.8"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/@vueuse/core/node_modules/vue-demi": {
"version": "0.14.10",
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
"integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
"hasInstallScript": true,
"license": "MIT",
"bin": {
"vue-demi-fix": "bin/vue-demi-fix.js",
"vue-demi-switch": "bin/vue-demi-switch.js"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"@vue/composition-api": "^1.0.0-rc.1",
"vue": "^3.0.0-0 || ^2.6.0"
},
"peerDependenciesMeta": {
"@vue/composition-api": {
"optional": true
}
}
},
"node_modules/@vueuse/metadata": {
"version": "10.11.1",
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-10.11.1.tgz",
"integrity": "sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/@vueuse/shared": {
"version": "10.11.1",
"resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-10.11.1.tgz",
"integrity": "sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==",
"license": "MIT",
"dependencies": {
"vue-demi": ">=0.14.8"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/@vueuse/shared/node_modules/vue-demi": {
"version": "0.14.10",
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
"integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
"hasInstallScript": true,
"license": "MIT",
"bin": {
"vue-demi-fix": "bin/vue-demi-fix.js",
"vue-demi-switch": "bin/vue-demi-switch.js"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"@vue/composition-api": "^1.0.0-rc.1",
"vue": "^3.0.0-0 || ^2.6.0"
},
"peerDependenciesMeta": {
"@vue/composition-api": {
"optional": true
}
}
},
"node_modules/acorn": {
"version": "8.12.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz",
@@ -1051,22 +1277,62 @@
}
},
"node_modules/ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dev": true,
"version": "8.17.1",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
"json-schema-traverse": "^0.4.1",
"uri-js": "^4.2.2"
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
"json-schema-traverse": "^1.0.0",
"require-from-string": "^2.0.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/ajv-dist": {
"version": "8.17.1",
"resolved": "https://registry.npmjs.org/ajv-dist/-/ajv-dist-8.17.1.tgz",
"integrity": "sha512-KzJwANMzTTR/RERGnkx+bHzmxIfMTPMMv7+cH1d6Lx9UQ7BZyhiieq4hnO5lRuBWOtYTUL8hyWs7RJYI/45Rtg==",
"license": "MIT"
},
"node_modules/ajv-errors": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-3.0.0.tgz",
"integrity": "sha512-V3wD15YHfHz6y0KdhYFjyy9vWtEVALT9UrxfN3zqlI6dMioHnJrqOYfyPKol3oqrnCM9uwkcdCwkJ0WUcbLMTQ==",
"license": "MIT",
"peerDependencies": {
"ajv": "^8.0.1"
}
},
"node_modules/ajv-formats": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz",
"integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==",
"license": "MIT",
"dependencies": {
"ajv": "^8.0.0"
},
"peerDependencies": {
"ajv": "^8.0.0"
},
"peerDependenciesMeta": {
"ajv": {
"optional": true
}
}
},
"node_modules/ajv-i18n": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/ajv-i18n/-/ajv-i18n-4.2.0.tgz",
"integrity": "sha512-v/ei2UkCEeuKNXh8RToiFsUclmU+G57LO1Oo22OagNMENIw+Yb8eMwvHu7Vn9fmkjJyv6XclhJ8TbuigSglPkg==",
"license": "MIT",
"peerDependencies": {
"ajv": "^8.0.0-beta.0"
}
},
"node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
@@ -1081,7 +1347,6 @@
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"license": "MIT",
"dependencies": {
"color-convert": "^2.0.1"
@@ -1111,7 +1376,6 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"dev": true,
"license": "Python-2.0"
},
"node_modules/array-buffer-byte-length": {
@@ -1262,6 +1526,12 @@
"node": ">=16.14.0"
}
},
"node_modules/async": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
"integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
"license": "MIT"
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
@@ -1299,7 +1569,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true,
"license": "MIT"
},
"node_modules/binary-extensions": {
@@ -1326,7 +1595,6 @@
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
@@ -1416,7 +1684,6 @@
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.1.0",
@@ -1477,7 +1744,6 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"color-name": "~1.1.4"
@@ -1490,7 +1756,6 @@
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true,
"license": "MIT"
},
"node_modules/combined-stream": {
@@ -1509,7 +1774,6 @@
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
"dev": true,
"license": "MIT"
},
"node_modules/confbox": {
@@ -1622,7 +1886,6 @@
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
@@ -1701,6 +1964,21 @@
"node": ">=6.0.0"
}
},
"node_modules/ejs": {
"version": "3.1.10",
"resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz",
"integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==",
"license": "Apache-2.0",
"dependencies": {
"jake": "^10.8.5"
},
"bin": {
"ejs": "bin/cli.js"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/entities": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
@@ -2339,6 +2617,30 @@
"url": "https://opencollective.com/eslint"
}
},
"node_modules/eslint/node_modules/ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dev": true,
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
"json-schema-traverse": "^0.4.1",
"uri-js": "^4.2.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/eslint/node_modules/json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
"dev": true,
"license": "MIT"
},
"node_modules/espree": {
"version": "9.6.1",
"resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz",
@@ -2413,7 +2715,6 @@
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"dev": true,
"license": "MIT"
},
"node_modules/fast-glob": {
@@ -2460,6 +2761,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/fast-uri": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.2.tgz",
"integrity": "sha512-GR6f0hD7XXyNJa25Tb9BuIdN0tdr+0BMi6/CJPH3wJO1JjNG3n/VsSw38AwRdKZABm8lGbPfakLRkYzx2V9row==",
"license": "MIT"
},
"node_modules/fastq": {
"version": "1.17.1",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz",
@@ -2483,6 +2790,36 @@
"node": "^10.12.0 || >=12.0.0"
}
},
"node_modules/filelist": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz",
"integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==",
"license": "Apache-2.0",
"dependencies": {
"minimatch": "^5.0.1"
}
},
"node_modules/filelist/node_modules/brace-expansion": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
}
},
"node_modules/filelist/node_modules/minimatch": {
"version": "5.1.6",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
"integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.1"
},
"engines": {
"node": ">=10"
}
},
"node_modules/fill-range": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
@@ -2792,7 +3129,6 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
@@ -2876,6 +3212,16 @@
"node": ">= 4"
}
},
"node_modules/immer": {
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz",
"integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/immer"
}
},
"node_modules/immutable": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.7.tgz",
@@ -3261,6 +3607,24 @@
"dev": true,
"license": "ISC"
},
"node_modules/jake": {
"version": "10.9.2",
"resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz",
"integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==",
"license": "Apache-2.0",
"dependencies": {
"async": "^3.2.3",
"chalk": "^4.0.2",
"filelist": "^1.0.4",
"minimatch": "^3.1.2"
},
"bin": {
"jake": "bin/cli.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/js-yaml": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
@@ -3282,10 +3646,9 @@
"license": "MIT"
},
"node_modules/json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
"dev": true,
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
"license": "MIT"
},
"node_modules/json-stable-stringify-without-jsonify": {
@@ -3332,6 +3695,15 @@
"node": ">= 0.8.0"
}
},
"node_modules/linkify-it": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-4.0.1.tgz",
"integrity": "sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw==",
"license": "MIT",
"dependencies": {
"uc.micro": "^1.0.1"
}
},
"node_modules/local-pkg": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.0.tgz",
@@ -3401,6 +3773,51 @@
"node": ">=16.14.0"
}
},
"node_modules/magicast": {
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz",
"integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==",
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.25.4",
"@babel/types": "^7.25.4",
"source-map-js": "^1.2.0"
}
},
"node_modules/markdown-it": {
"version": "13.0.2",
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-13.0.2.tgz",
"integrity": "sha512-FtwnEuuK+2yVU7goGn/MJ0WBZMM9ZPgU9spqlFs7/A/pDIUNSOQZhUgOqYCficIuR2QaFnrt8LHqBWsbTAoI5w==",
"license": "MIT",
"dependencies": {
"argparse": "^2.0.1",
"entities": "~3.0.1",
"linkify-it": "^4.0.1",
"mdurl": "^1.0.1",
"uc.micro": "^1.0.5"
},
"bin": {
"markdown-it": "bin/markdown-it.js"
}
},
"node_modules/markdown-it/node_modules/entities": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/entities/-/entities-3.0.1.tgz",
"integrity": "sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.12"
},
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/mdurl": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz",
"integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==",
"license": "MIT"
},
"node_modules/merge2": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
@@ -3450,7 +3867,6 @@
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
@@ -3486,7 +3902,6 @@
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"devOptional": true,
"license": "MIT"
},
"node_modules/nanoid": {
@@ -3923,6 +4338,15 @@
"url": "https://github.com/sponsors/mysticatea"
}
},
"node_modules/require-from-string": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/resolve": {
"version": "1.22.8",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
@@ -4304,7 +4728,6 @@
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
"license": "MIT",
"dependencies": {
"has-flag": "^4.0.0"
@@ -4471,6 +4894,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/uc.micro": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz",
"integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==",
"license": "MIT"
},
"node_modules/ufo": {
"version": "1.5.4",
"resolved": "https://registry.npmjs.org/ufo/-/ufo-1.5.4.tgz",

View File

@@ -8,7 +8,13 @@
"lint": "eslint . --fix --ignore-path .gitignore"
},
"dependencies": {
"@koumoul/vjsf": "^3.0.0-beta.46",
"@mdi/font": "7.4.47",
"ajv": "^8.17.1",
"ajv-dist": "^8.17.1",
"ajv-errors": "^3.0.0",
"ajv-formats": "^3.0.1",
"ajv-i18n": "^4.2.0",
"axios": "^1.7.7",
"codemirror": "^5.65.18",
"core-js": "^3.37.1",

View File

@@ -1,5 +1,8 @@
<template>
<v-app>
<v-snackbar v-model="snackbar" :color="color" :timeout="timeout" :location="location">
{{ text }}
</v-snackbar>
<v-layout>
<v-navigation-drawer id="navigation-drawer" :width="160" app permanent rail>
@@ -8,7 +11,9 @@
<div id="logo-container">
<v-img id="logo-img" src="@/assets/langbot-logo-block.png" height="32" width="32"></v-img>
<div id="version-chip" v-tooltip="proxy.$store.state.version + (proxy.$store.state.debug ? ' (调试模式已启用)' : '')" :style="{ 'background-color': proxy.$store.state.debug ? '#c79a47' : '#1e9ae2' }">
<div id="version-chip"
v-tooltip="proxy.$store.state.version + (proxy.$store.state.debug ? ' (调试模式已启用)' : '')"
:style="{ 'background-color': proxy.$store.state.debug ? '#c79a47' : '#1e9ae2' }">
{{ proxy.$store.state.version }}
</div>
</div>
@@ -45,8 +50,42 @@
<script setup>
import { getCurrentInstance } from 'vue'
import {provide, ref, watch} from 'vue';
const { proxy } = getCurrentInstance()
const snackbar = ref(false);
const color = ref('success');
const text = ref('');
const location = ref('top');
const timeout = ref(2000);
function success(content) {
snackbar.value = true;
color.value = 'success';
text.value = content;
}
function error(content) {
snackbar.value = true;
color.value = 'error';
text.value = content;
}
function warning(content) {
snackbar.value = true;
color.value = 'warning';
text.value = content;
}
function info(content) {
snackbar.value = true;
color.value = 'primary';
text.value = content;
}
provide('snackbar', {success, error, warning, info, location, timeout});
</script>
<style scoped>
@@ -102,5 +141,4 @@ const { proxy } = getCurrentInstance()
#about-list-item:active {
background-color: #ddd;
}
</style>

View File

@@ -18,6 +18,10 @@
import PageTitle from '@/components/PageTitle.vue'
import { ref, getCurrentInstance, onMounted, onUnmounted } from 'vue'
import {inject} from "vue";
const snackbar = inject('snackbar');
const { proxy } = getCurrentInstance()
const logContent = ref('')
@@ -38,9 +42,15 @@ const refreshLog = () => {
start_offset: logPointer.start_offset
}
}).then(response => {
if (response.data.code != 0) {
snackbar.error(response.data.message)
return
}
logContent.value += response.data.data.logs
logPointer.start_page_number = response.data.data.end_page_number
logPointer.start_offset = response.data.data.end_offset
}).catch(error => {
snackbar.error(error.message)
})
}

View File

@@ -15,15 +15,15 @@
<v-tabs-window-item v-for="manager in managerList" :key="manager.name" :value="manager.name"
class="config-tab-window">
<v-card class="config-tab-toolbar">
<v-tooltip :text="manager.schema == null ? '仅配置文件管理器提供了 JSON Schema 时支持可视化配置' : '切换编辑模式'" location="top">
<v-tooltip :text="currentManagerSchema == null ? '仅配置文件管理器提供了 JSON Schema 时支持可视化配置' : '切换编辑模式'" location="top">
<template v-slot:activator="{ props }">
<v-btn-toggle id="config-type-toggle" color="primary" v-model="configType" mandatory v-bind="props">
<v-btn-toggle id="config-type-toggle" color="primary" v-model="configType" mandatory v-bind="props" density="compact">
<v-btn class="config-type-toggle-btn" value="ui" :readonly="manager.schema == null">
<v-btn class="config-type-toggle-btn" value="ui" :readonly="currentManagerSchema == null" density="compact">
<v-icon>mdi-view-dashboard-edit-outline</v-icon>
</v-btn>
<v-btn class="config-type-toggle-btn" value="json" :readonly="manager.schema == null">
<v-btn class="config-type-toggle-btn" value="json" :readonly="currentManagerSchema == null" density="compact">
<v-icon>mdi-code-json</v-icon>
</v-btn>
</v-btn-toggle>
@@ -31,19 +31,24 @@
</v-tooltip>
<div id="file-operation-toolbar">
<v-btn @click="reset" color="warning" prepend-icon="mdi-undo" :disabled="!modified">重置</v-btn>
<v-btn @click="saveAndApply" color="primary" prepend-icon="mdi-content-save-outline" :disabled="!modified">应用</v-btn>
<v-btn @click="reset" color="warning" prepend-icon="mdi-undo" :disabled="!modified && configType == 'json'">重置</v-btn>
<v-btn @click="saveAndApply" color="primary" prepend-icon="mdi-content-save-outline" :disabled="!modified && configType == 'json'">应用</v-btn>
</div>
</v-card>
<div id="config-tab-content">
<div id="config-tab-content-ui" v-if="configType == 'ui'">
<v-form id="config-tab-content-ui-form">
<vjsf id="config-tab-content-ui-form-vjsf" :schema="currentManagerSchema" v-model="currentManagerData" :options="VJSFOptions" />
</v-form>
</div>
<v-card id="config-tab-content-json" v-if="configType == 'json'">
<textarea id="config-tab-content-json-textarea" @input="onInput" v-model="currentManagerData" />
<textarea id="config-tab-content-json-textarea" @input="onInput" v-model="currentManagerDataEditorString" />
</v-card>
<div id="config-tab-json-not-valid" v-if="!isJsonValid && configType == 'json'">*JSON 格式不正确: {{ errorMessage }}</div>
</div>
</v-tabs-window-item>
</v-tabs-window>
@@ -56,52 +61,183 @@
import PageTitle from '@/components/PageTitle.vue'
import { ref, getCurrentInstance, onMounted } from 'vue'
import {inject} from "vue";
const snackbar = inject('snackbar');
import Vjsf from '@koumoul/vjsf';
const { proxy } = getCurrentInstance()
const managerList = ref([])
const configType = ref('json') // ui or json
const currentManager = ref(null)
const currentManagerData = ref('')
const currentManagerData = ref({})
const currentManagerDataEditorString = ref('')
const currentManagerSchema = ref(null)
const modified = ref(false)
const VJSFOptions = {
"context": {},
"width": 1208,
"readOnly": false,
"summary": false,
"density": "comfortable",
"indent": true,
"titleDepth": 4,
"validateOn": "input",
"initialValidation": "withData",
"updateOn": "input",
"debounceInputMs": 300,
"defaultOn": "empty",
"removeAdditional": "error",
"autofocus": false,
"readOnlyPropertiesMode": "show",
"pluginsOptions": {},
"locale": "en",
"messages": {
"errorOneOf": "请选择一个",
"errorRequired": "必填信息",
"addItem": "添加",
"delete": "删除",
"edit": "编辑",
"close": "关闭",
"duplicate": "复制",
"sort": "排序",
"up": "向上移动",
"down": "向下移动",
"showHelp": "显示帮助信息",
"mdeLink1": "[链接标题",
"mdeLink2": "](链接地址)",
"mdeImg1": "![](",
"mdeImg2": "图片地址)",
"mdeTable1": "",
"mdeTable2": "\n\n| 列 1 | 列 2 | 列 3 |\n| -------- | -------- | -------- |\n| 文本 | 文本 | 文本 |\n\n",
"bold": "加粗",
"italic": "斜体",
"heading": "标题",
"quote": "引用",
"unorderedList": "无序列表",
"orderedList": "有序列表",
"createLink": "创建链接",
"insertImage": "插入图片",
"createTable": "创建表格",
"preview": "预览",
"mdeGuide": "文档",
"undo": "撤销",
"redo": "重做"
}
}
const refresh = () => {
proxy.$axios.get('/settings').then(response => {
if (response.data.code != 0) {
snackbar.error(response.data.msg)
return
}
managerList.value = response.data.data.managers
if (proxy.$store.state.settingsPageTab != '') {
fetchCurrentManagerData(proxy.$store.state.settingsPageTab)
if (proxy.$store.state.settingsPageTab == '') {
proxy.$store.state.settingsPageTab = managerList.value[0].name
}
fetchCurrentManagerData(proxy.$store.state.settingsPageTab).then(() => {
firstJumpEditorAfterChangeTab()
})
}).catch(error => {
snackbar.error(error)
})
}
const onTabChange = (tab) => {
fetchCurrentManagerData(tab)
isJsonValid.value = true
errorMessage.value = ''
fetchCurrentManagerData(tab).then(() => {
firstJumpEditorAfterChangeTab()
})
}
const firstJumpEditorAfterChangeTab = () => {
if (currentManagerSchema.value != null) {
configType.value = 'ui'
} else {
configType.value = 'json'
}
}
const fetchCurrentManagerData = (tab) => {
proxy.$axios.get(`/settings/${tab}`).then(response => {
return proxy.$axios.get(`/settings/${tab}`).catch(error => {
snackbar.error(error)
}).then(response => {
if (response.data.code != 0) {
snackbar.error(response.data.msg)
return
}
currentManager.value = response.data.data.manager
currentManagerData.value = JSON.stringify(currentManager.value.data, null, 2)
currentManagerData.value = currentManager.value.data
currentManagerDataEditorString.value = JSON.stringify(currentManager.value.data, null, 2)
currentManagerSchema.value = currentManager.value.schema
})
}
const isJsonValid = ref(true)
const errorMessage = ref('')
const checkJsonValid = () => {
try {
JSON.parse(currentManagerDataEditorString.value)
isJsonValid.value = true
errorMessage.value = ''
} catch (error) {
isJsonValid.value = false
errorMessage.value = error.message
}
}
const onInput = () => {
modified.value = true
checkJsonValid()
}
const saveAndApply = () => {
if (!isJsonValid.value) {
snackbar.error('JSON 格式不正确: ' + errorMessage.value)
return
}
if (configType.value == 'json') {
currentManagerData.value = JSON.parse(currentManagerDataEditorString.value)
}
proxy.$axios.put(`/settings/${currentManager.value.name}/data`, {
data: JSON.parse(currentManagerData.value)
data: currentManagerData.value
}).then(response => {
if (response.data.code != 0) {
snackbar.error(response.data.msg)
return
}
fetchCurrentManagerData(currentManager.value.name)
modified.value = false
snackbar.success('应用成功')
}).catch(error => {
snackbar.error(error)
})
}
const reset = () => {
currentManagerData.value = JSON.stringify(currentManager.value.data, null, 2)
modified.value = false
if (configType.value == 'json') {
currentManagerData.value = currentManager.value.data
currentManagerDataEditorString.value = JSON.stringify(currentManager.value.data, null, 2)
isJsonValid.value = true
errorMessage.value = ''
modified.value = false
snackbar.success('重置成功')
} else {
fetchCurrentManagerData(currentManager.value.name).then(() => {
snackbar.success('重置成功')
modified.value = false
})
}
}
onMounted(async () => {
@@ -133,12 +269,12 @@ onMounted(async () => {
}
.config-tab-window {
overflow: auto;
overflow: hidden;
}
.config-tab-toolbar {
margin: 0.5rem;
height: 4rem;
height: 3.2rem;
display: flex;
flex-direction: row;
justify-content: space-between;
@@ -159,16 +295,38 @@ onMounted(async () => {
box-shadow: 0 0 0 2px #dddddd;
}
.config-type-toggle-btn {}
.config-tab-content {
#config-tab-content {
margin: 0.2rem;
height: calc(100% - 1rem);
overflow: hidden;
}
#config-tab-content-ui {
margin: 0.5rem;
height: calc(100vh - 15rem);
margin-top: 1rem;
overflow: hidden;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
#config-tab-content-ui-form {
height: 100%;
width: calc(100% - 1.5rem);
margin-left: 0.5rem;
overflow-y: auto;
}
#config-tab-content-ui-form-vjsf {
height: 100%;
width: calc(100% - 1rem);
}
#config-tab-content-json {
margin: 0.5rem;
height: calc(100vh - 18rem);
height: calc(100vh - 16rem);
margin-top: 1rem;
}
@@ -186,4 +344,13 @@ onMounted(async () => {
line-height: 1.6rem;
text-wrap: nowrap;
}
#config-tab-json-not-valid {
margin: 0rem;
margin-left: 0.6rem;
height: 1.5rem;
margin-top: 0.2rem;
font-size: 0.8rem;
color: red;
}
</style>

View File

@@ -5,6 +5,8 @@ import Vuetify, { transformAssetUrls } from 'vite-plugin-vuetify'
import ViteFonts from 'unplugin-fonts/vite'
import VueRouter from 'unplugin-vue-router/vite'
import { commonjsDeps } from '@koumoul/vjsf/utils/build.js'
// Utilities
import { defineConfig } from 'vite'
import { fileURLToPath, URL } from 'node:url'
@@ -51,4 +53,12 @@ export default defineConfig({
server: {
port: 3002,
},
optimizeDeps: {
include: commonjsDeps,
},
build: {
commonjsOptions: {
transformMixedEsModules: true,
},
}
})