Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d87d586c0a | |||
| 410789311a |
@@ -11,7 +11,7 @@ jobs:
|
||||
runs-on: ubuntu-latest # 运行环境
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@master
|
||||
- name: nodejs installation
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
run: npm run docs:build
|
||||
working-directory: './docs'
|
||||
- name: scp
|
||||
uses: appleboy/scp-action@v1.0.0
|
||||
uses: appleboy/scp-action@master
|
||||
with:
|
||||
host: ${{ secrets.HOST_NEKO }}
|
||||
username: ${{ secrets.USERNAME }}
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
source: 'docs/.vitepress/dist/*'
|
||||
target: '/tmp/'
|
||||
- name: script
|
||||
uses: appleboy/ssh-action@v1.2.5
|
||||
uses: appleboy/ssh-action@master
|
||||
with:
|
||||
host: ${{ secrets.HOST_NEKO }}
|
||||
username: ${{ secrets.USERNAME }}
|
||||
|
||||
@@ -45,7 +45,7 @@ jobs:
|
||||
|
||||
- name: Create GitHub Release
|
||||
if: github.event_name == 'push'
|
||||
uses: ncipollo/release-action@v1.20.0
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
tag: release-${{ github.sha }}
|
||||
owner: AstrBotDevs
|
||||
|
||||
@@ -64,20 +64,20 @@ jobs:
|
||||
echo "build_date=$build_date" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Set QEMU
|
||||
uses: docker/setup-qemu-action@v4.0.0
|
||||
uses: docker/setup-qemu-action@v4
|
||||
|
||||
- name: Set Docker Buildx
|
||||
uses: docker/setup-buildx-action@v4.0.0
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Log in to DockerHub
|
||||
uses: docker/login-action@v4.0.0
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
if: env.HAS_GHCR_TOKEN == 'true'
|
||||
uses: docker/login-action@v4.0.0
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ env.GHCR_OWNER }}
|
||||
@@ -98,7 +98,7 @@ jobs:
|
||||
echo "EOF" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Build and Push Nightly Image
|
||||
uses: docker/build-push-action@v7.0.0
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
@@ -163,27 +163,27 @@ jobs:
|
||||
cp -r dashboard/dist data/
|
||||
|
||||
- name: Set QEMU
|
||||
uses: docker/setup-qemu-action@v4.0.0
|
||||
uses: docker/setup-qemu-action@v4
|
||||
|
||||
- name: Set Docker Buildx
|
||||
uses: docker/setup-buildx-action@v4.0.0
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Log in to DockerHub
|
||||
uses: docker/login-action@v4.0.0
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
if: env.HAS_GHCR_TOKEN == 'true'
|
||||
uses: docker/login-action@v4.0.0
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ env.GHCR_OWNER }}
|
||||
password: ${{ secrets.GHCR_GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and Push Release Image
|
||||
uses: docker/build-push-action@v7.0.0
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
|
||||
@@ -50,7 +50,7 @@ jobs:
|
||||
echo "tag=$tag" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4.3.0
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10.28.2
|
||||
|
||||
|
||||
+1
-1
@@ -62,4 +62,4 @@ GenieData/
|
||||
.opencode/
|
||||
.kilocode/
|
||||
.worktrees/
|
||||
|
||||
docs/plans/
|
||||
|
||||
@@ -234,8 +234,7 @@ pre-commit install
|
||||
- Group 7: 743746109
|
||||
- Group 8: 1030353265
|
||||
|
||||
- Developer Group(Chit-chat): 975206796
|
||||
- Developer Group(Formal): 1039761811
|
||||
- Developer Group: 975206796
|
||||
|
||||
### Discord Server
|
||||
|
||||
|
||||
@@ -222,7 +222,6 @@ pre-commit install
|
||||
- Groupe 5 : 822130018
|
||||
- Groupe 6 : 753075035
|
||||
- Groupe développeurs : 975206796
|
||||
- Groupe développeurs (officiel) : 1039761811
|
||||
|
||||
### Serveur Discord
|
||||
|
||||
|
||||
@@ -223,7 +223,6 @@ pre-commit install
|
||||
- 5群: 822130018
|
||||
- 6群: 753075035
|
||||
- 開発者群: 975206796
|
||||
- 開発者群(正式): 1039761811
|
||||
|
||||
### Discord サーバー
|
||||
|
||||
|
||||
@@ -222,7 +222,6 @@ pre-commit install
|
||||
- Группа 5: 822130018
|
||||
- Группа 6: 753075035
|
||||
- Группа разработчиков: 975206796
|
||||
- Группа разработчиков (официальная): 1039761811
|
||||
|
||||
### Сервер Discord
|
||||
|
||||
|
||||
+1
-2
@@ -225,8 +225,7 @@ pre-commit install
|
||||
- 6 群:753075035
|
||||
- 7 群:743746109
|
||||
- 8 群:1030353265
|
||||
- 開發者群(闲聊吹水):975206796
|
||||
- 開發者群(正式):1039761811
|
||||
- 開發者群:975206796
|
||||
|
||||
### Discord 群組
|
||||
|
||||
|
||||
+1
-2
@@ -226,8 +226,7 @@ pre-commit install
|
||||
- 6 群:753075035
|
||||
- 7 群:743746109
|
||||
- 8 群:1030353265
|
||||
- 开发者群(偏闲聊吹水):975206796
|
||||
- 开发者群(正式):1039761811
|
||||
- 开发者群:975206796
|
||||
|
||||
### Discord 频道
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "4.20.0"
|
||||
__version__ = "4.19.5"
|
||||
|
||||
@@ -204,7 +204,7 @@ class SendMessageToUserTool(FunctionTool[AstrAgentContext]):
|
||||
"type": "string",
|
||||
"description": (
|
||||
"Component type. One of: "
|
||||
"plain, image, record, video, file, mention_user. Record is voice message."
|
||||
"plain, image, record, file, mention_user"
|
||||
),
|
||||
},
|
||||
"text": {
|
||||
@@ -320,19 +320,6 @@ class SendMessageToUserTool(FunctionTool[AstrAgentContext]):
|
||||
components.append(Comp.Record.fromURL(url=url))
|
||||
else:
|
||||
return f"error: messages[{idx}] must include path or url for record component."
|
||||
elif msg_type == "video":
|
||||
path = msg.get("path")
|
||||
url = msg.get("url")
|
||||
if path:
|
||||
(
|
||||
local_path,
|
||||
file_from_sandbox,
|
||||
) = await self._resolve_path_from_sandbox(context, path)
|
||||
components.append(Comp.Video.fromFileSystem(path=local_path))
|
||||
elif url:
|
||||
components.append(Comp.Video.fromURL(url=url))
|
||||
else:
|
||||
return f"error: messages[{idx}] must include path or url for video component."
|
||||
elif msg_type == "file":
|
||||
path = msg.get("path")
|
||||
url = msg.get("url")
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import locale
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
@@ -53,31 +52,6 @@ def _ensure_safe_path(path: str) -> str:
|
||||
return abs_path
|
||||
|
||||
|
||||
def _decode_shell_output(output: bytes | None) -> str:
|
||||
if output is None:
|
||||
return ""
|
||||
|
||||
preferred = locale.getpreferredencoding(False) or "utf-8"
|
||||
try:
|
||||
return output.decode("utf-8")
|
||||
except (LookupError, UnicodeDecodeError):
|
||||
pass
|
||||
|
||||
if os.name == "nt":
|
||||
for encoding in ("mbcs", "cp936", "gbk", "gb18030"):
|
||||
try:
|
||||
return output.decode(encoding)
|
||||
except (LookupError, UnicodeDecodeError):
|
||||
continue
|
||||
|
||||
try:
|
||||
return output.decode(preferred)
|
||||
except (LookupError, UnicodeDecodeError):
|
||||
pass
|
||||
|
||||
return output.decode("utf-8", errors="replace")
|
||||
|
||||
|
||||
@dataclass
|
||||
class LocalShellComponent(ShellComponent):
|
||||
async def exec(
|
||||
@@ -98,32 +72,28 @@ class LocalShellComponent(ShellComponent):
|
||||
run_env.update({str(k): str(v) for k, v in env.items()})
|
||||
working_dir = _ensure_safe_path(cwd) if cwd else get_astrbot_root()
|
||||
if background:
|
||||
# `command` is intentionally executed through the current shell so
|
||||
# local computer-use behavior matches existing tool semantics.
|
||||
# Safety relies on `_is_safe_command()` and the allowed-root checks.
|
||||
proc = subprocess.Popen( # noqa: S602 # nosemgrep: python.lang.security.audit.dangerous-subprocess-use-audit
|
||||
proc = subprocess.Popen(
|
||||
command,
|
||||
shell=shell,
|
||||
cwd=working_dir,
|
||||
env=run_env,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
)
|
||||
return {"pid": proc.pid, "stdout": "", "stderr": "", "exit_code": None}
|
||||
# `command` is intentionally executed through the current shell so
|
||||
# local computer-use behavior matches existing tool semantics.
|
||||
# Safety relies on `_is_safe_command()` and the allowed-root checks.
|
||||
result = subprocess.run( # noqa: S602 # nosemgrep: python.lang.security.audit.dangerous-subprocess-use-audit
|
||||
result = subprocess.run(
|
||||
command,
|
||||
shell=shell,
|
||||
cwd=working_dir,
|
||||
env=run_env,
|
||||
timeout=timeout,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
return {
|
||||
"stdout": _decode_shell_output(result.stdout),
|
||||
"stderr": _decode_shell_output(result.stderr),
|
||||
"stdout": result.stdout,
|
||||
"stderr": result.stderr,
|
||||
"exit_code": result.returncode,
|
||||
}
|
||||
|
||||
|
||||
@@ -422,12 +422,6 @@ async def get_booter(
|
||||
) -> ComputerBooter:
|
||||
config = context.get_config(umo=session_id)
|
||||
|
||||
runtime = config.get("provider_settings", {}).get("computer_use_runtime", "local")
|
||||
if runtime == "local":
|
||||
return get_local_booter()
|
||||
elif runtime == "none":
|
||||
raise RuntimeError("Sandbox runtime is disabled by configuration.")
|
||||
|
||||
sandbox_cfg = config.get("provider_settings", {}).get("sandbox", {})
|
||||
booter_type = sandbox_cfg.get("booter", "shipyard_neo")
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ from typing import Any, TypedDict
|
||||
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
||||
|
||||
VERSION = "4.20.0"
|
||||
VERSION = "4.19.5"
|
||||
DB_PATH = os.path.join(get_astrbot_data_path(), "data_v4.db")
|
||||
|
||||
WEBHOOK_SUPPORTED_PLATFORMS = [
|
||||
@@ -219,9 +219,6 @@ DEFAULT_CONFIG = {
|
||||
"telegram": {
|
||||
"pre_ack_emoji": {"enable": False, "emojis": ["✍️"]},
|
||||
},
|
||||
"discord": {
|
||||
"pre_ack_emoji": {"enable": False, "emojis": ["🤔"]},
|
||||
},
|
||||
},
|
||||
"wake_prefix": ["/"],
|
||||
"log_level": "INFO",
|
||||
|
||||
@@ -18,7 +18,7 @@ from botpy.types.message import MarkdownPayload, Media
|
||||
|
||||
from astrbot.api import logger
|
||||
from astrbot.api.event import AstrMessageEvent, MessageChain
|
||||
from astrbot.api.message_components import File, Image, Plain, Record, Video
|
||||
from astrbot.api.message_components import Image, Plain, Record
|
||||
from astrbot.api.platform import AstrBotMessage, PlatformMetadata
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_temp_path
|
||||
from astrbot.core.utils.io import download_image_by_url, file_to_base64
|
||||
@@ -47,11 +47,6 @@ _patch_qq_botpy_formdata()
|
||||
|
||||
class QQOfficialMessageEvent(AstrMessageEvent):
|
||||
MARKDOWN_NOT_ALLOWED_ERROR = "不允许发送原生 markdown"
|
||||
IMAGE_FILE_TYPE = 1
|
||||
VIDEO_FILE_TYPE = 2
|
||||
VOICE_FILE_TYPE = 3
|
||||
FILE_FILE_TYPE = 4
|
||||
STREAM_MARKDOWN_NEWLINE_ERROR = "流式消息md分片需要\\n结束"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -70,71 +65,35 @@ class QQOfficialMessageEvent(AstrMessageEvent):
|
||||
await self._post_send()
|
||||
|
||||
async def send_streaming(self, generator, use_fallback: bool = False):
|
||||
"""流式输出仅支持消息列表私聊(C2C),其他消息源退化为普通发送"""
|
||||
# 先标记事件层“已执行发送操作”,避免异常路径遗漏
|
||||
await super().send_streaming(generator, use_fallback)
|
||||
# QQ C2C 流式协议:开始/中间分片使用 state=1,结束分片使用 state=10
|
||||
"""流式输出仅支持消息列表私聊"""
|
||||
stream_payload = {"state": 1, "id": None, "index": 0, "reset": False}
|
||||
last_edit_time = 0 # 上次发送分片的时间
|
||||
throttle_interval = 1 # 分片间最短间隔 (秒)
|
||||
last_edit_time = 0 # 上次编辑消息的时间
|
||||
throttle_interval = 1 # 编辑消息的间隔时间 (秒)
|
||||
ret = None
|
||||
source = (
|
||||
self.message_obj.raw_message
|
||||
) # 提前获取,避免 generator 为空时 NameError
|
||||
try:
|
||||
async for chain in generator:
|
||||
source = self.message_obj.raw_message
|
||||
|
||||
if not isinstance(source, botpy.message.C2CMessage):
|
||||
# 非 C2C 场景:直接累积,最后统一发
|
||||
if not self.send_buffer:
|
||||
self.send_buffer = chain
|
||||
else:
|
||||
self.send_buffer.chain.extend(chain.chain)
|
||||
continue
|
||||
|
||||
# ---- C2C 流式场景 ----
|
||||
|
||||
# tool_call break 信号:工具开始执行,先把已有 buffer 以 state=10 结束当前流式段
|
||||
if chain.type == "break":
|
||||
if self.send_buffer:
|
||||
stream_payload["state"] = 10
|
||||
ret = await self._post_send(stream=stream_payload)
|
||||
ret_id = self._extract_response_message_id(ret)
|
||||
if ret_id is not None:
|
||||
stream_payload["id"] = ret_id
|
||||
# 重置 stream_payload,为下一段流式做准备
|
||||
stream_payload = {
|
||||
"state": 1,
|
||||
"id": None,
|
||||
"index": 0,
|
||||
"reset": False,
|
||||
}
|
||||
last_edit_time = 0
|
||||
continue
|
||||
|
||||
# 累积内容
|
||||
if not self.send_buffer:
|
||||
self.send_buffer = chain
|
||||
else:
|
||||
self.send_buffer.chain.extend(chain.chain)
|
||||
|
||||
# 节流:按时间间隔发送中间分片
|
||||
current_time = asyncio.get_running_loop().time()
|
||||
if current_time - last_edit_time >= throttle_interval:
|
||||
ret = cast(
|
||||
message.Message,
|
||||
await self._post_send(stream=stream_payload),
|
||||
)
|
||||
stream_payload["index"] += 1
|
||||
ret_id = self._extract_response_message_id(ret)
|
||||
if ret_id is not None:
|
||||
stream_payload["id"] = ret_id
|
||||
last_edit_time = asyncio.get_running_loop().time()
|
||||
self.send_buffer = None # 清空已发送的分片,避免下次重复发送旧内容
|
||||
if isinstance(source, botpy.message.C2CMessage):
|
||||
# 真流式传输
|
||||
current_time = asyncio.get_running_loop().time()
|
||||
time_since_last_edit = current_time - last_edit_time
|
||||
|
||||
if time_since_last_edit >= throttle_interval:
|
||||
ret = cast(
|
||||
message.Message,
|
||||
await self._post_send(stream=stream_payload),
|
||||
)
|
||||
stream_payload["index"] += 1
|
||||
stream_payload["id"] = ret["id"]
|
||||
last_edit_time = asyncio.get_running_loop().time()
|
||||
|
||||
if isinstance(source, botpy.message.C2CMessage):
|
||||
# 结束流式对话,发送 buffer 中剩余内容
|
||||
# 结束流式对话,并且传输 buffer 中剩余的消息
|
||||
stream_payload["state"] = 10
|
||||
ret = await self._post_send(stream=stream_payload)
|
||||
else:
|
||||
@@ -142,22 +101,9 @@ class QQOfficialMessageEvent(AstrMessageEvent):
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"发送流式消息时出错: {e}", exc_info=True)
|
||||
# 避免累计内容在异常后被整包重复发送:仅清理缓存,不做非流式整包兜底
|
||||
# 如需兜底,应该只发送未发送 delta(后续可继续优化)
|
||||
self.send_buffer = None
|
||||
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _extract_response_message_id(ret) -> str | None:
|
||||
"""兼容 qq-botpy 返回 Message 对象或 dict 两种形态。"""
|
||||
if ret is None:
|
||||
return None
|
||||
if isinstance(ret, dict):
|
||||
ret_id = ret.get("id")
|
||||
return str(ret_id) if ret_id is not None else None
|
||||
ret_id = getattr(ret, "id", None)
|
||||
return str(ret_id) if ret_id is not None else None
|
||||
return await super().send_streaming(generator, use_fallback)
|
||||
|
||||
async def _post_send(self, stream: dict | None = None):
|
||||
if not self.send_buffer:
|
||||
@@ -180,37 +126,16 @@ class QQOfficialMessageEvent(AstrMessageEvent):
|
||||
image_base64,
|
||||
image_path,
|
||||
record_file_path,
|
||||
video_file_source,
|
||||
file_source,
|
||||
file_name,
|
||||
) = await QQOfficialMessageEvent._parse_to_qqofficial(self.send_buffer)
|
||||
|
||||
# C2C 流式仅用于文本分片,富媒体时降级为普通发送,避免平台侧流式校验报错。
|
||||
if stream and (image_base64 or record_file_path):
|
||||
logger.debug("[QQOfficial] 检测到富媒体,降级为非流式发送。")
|
||||
stream = None
|
||||
|
||||
if (
|
||||
not plain_text
|
||||
and not image_base64
|
||||
and not image_path
|
||||
and not record_file_path
|
||||
and not video_file_source
|
||||
and not file_source
|
||||
):
|
||||
return None
|
||||
|
||||
# QQ C2C 流式 API 说明:
|
||||
# - 开始/中间分片(state=1):增量追加内容,不需要 \n(加了会导致强制换行)
|
||||
# - 最终分片(state=10):结束流,content 必须以 \n 结尾(QQ API 要求)
|
||||
if (
|
||||
stream
|
||||
and stream.get("state") == 10
|
||||
and plain_text
|
||||
and not plain_text.endswith("\n")
|
||||
):
|
||||
plain_text = plain_text + "\n"
|
||||
|
||||
payload: dict = {
|
||||
# "content": plain_text,
|
||||
"markdown": MarkdownPayload(content=plain_text) if plain_text else None,
|
||||
@@ -232,7 +157,7 @@ class QQOfficialMessageEvent(AstrMessageEvent):
|
||||
if image_base64:
|
||||
media = await self.upload_group_and_c2c_image(
|
||||
image_base64,
|
||||
self.IMAGE_FILE_TYPE,
|
||||
1,
|
||||
group_openid=source.group_openid,
|
||||
)
|
||||
payload["media"] = media
|
||||
@@ -240,39 +165,15 @@ class QQOfficialMessageEvent(AstrMessageEvent):
|
||||
payload.pop("markdown", None)
|
||||
payload["content"] = plain_text or None
|
||||
if record_file_path: # group record msg
|
||||
media = await self.upload_group_and_c2c_media(
|
||||
media = await self.upload_group_and_c2c_record(
|
||||
record_file_path,
|
||||
self.VOICE_FILE_TYPE,
|
||||
3,
|
||||
group_openid=source.group_openid,
|
||||
)
|
||||
if media:
|
||||
payload["media"] = media
|
||||
payload["msg_type"] = 7
|
||||
payload.pop("markdown", None)
|
||||
payload["content"] = plain_text or None
|
||||
if video_file_source:
|
||||
media = await self.upload_group_and_c2c_media(
|
||||
video_file_source,
|
||||
self.VIDEO_FILE_TYPE,
|
||||
group_openid=source.group_openid,
|
||||
)
|
||||
if media:
|
||||
payload["media"] = media
|
||||
payload["msg_type"] = 7
|
||||
payload.pop("markdown", None)
|
||||
payload["content"] = plain_text or None
|
||||
if file_source:
|
||||
media = await self.upload_group_and_c2c_media(
|
||||
file_source,
|
||||
self.FILE_FILE_TYPE,
|
||||
file_name=file_name,
|
||||
group_openid=source.group_openid,
|
||||
)
|
||||
if media:
|
||||
payload["media"] = media
|
||||
payload["msg_type"] = 7
|
||||
payload.pop("markdown", None)
|
||||
payload["content"] = plain_text or None
|
||||
payload["media"] = media
|
||||
payload["msg_type"] = 7
|
||||
payload.pop("markdown", None)
|
||||
payload["content"] = plain_text or None
|
||||
ret = await self._send_with_markdown_fallback(
|
||||
send_func=lambda retry_payload: self.bot.api.post_group_message(
|
||||
group_openid=source.group_openid, # type: ignore
|
||||
@@ -280,14 +181,13 @@ class QQOfficialMessageEvent(AstrMessageEvent):
|
||||
),
|
||||
payload=payload,
|
||||
plain_text=plain_text,
|
||||
stream=stream,
|
||||
)
|
||||
|
||||
case botpy.message.C2CMessage():
|
||||
if image_base64:
|
||||
media = await self.upload_group_and_c2c_image(
|
||||
image_base64,
|
||||
self.IMAGE_FILE_TYPE,
|
||||
1,
|
||||
openid=source.author.user_openid,
|
||||
)
|
||||
payload["media"] = media
|
||||
@@ -295,39 +195,15 @@ class QQOfficialMessageEvent(AstrMessageEvent):
|
||||
payload.pop("markdown", None)
|
||||
payload["content"] = plain_text or None
|
||||
if record_file_path: # c2c record
|
||||
media = await self.upload_group_and_c2c_media(
|
||||
media = await self.upload_group_and_c2c_record(
|
||||
record_file_path,
|
||||
self.VOICE_FILE_TYPE,
|
||||
3,
|
||||
openid=source.author.user_openid,
|
||||
)
|
||||
if media:
|
||||
payload["media"] = media
|
||||
payload["msg_type"] = 7
|
||||
payload.pop("markdown", None)
|
||||
payload["content"] = plain_text or None
|
||||
if video_file_source:
|
||||
media = await self.upload_group_and_c2c_media(
|
||||
video_file_source,
|
||||
self.VIDEO_FILE_TYPE,
|
||||
openid=source.author.user_openid,
|
||||
)
|
||||
if media:
|
||||
payload["media"] = media
|
||||
payload["msg_type"] = 7
|
||||
payload.pop("markdown", None)
|
||||
payload["content"] = plain_text or None
|
||||
if file_source:
|
||||
media = await self.upload_group_and_c2c_media(
|
||||
file_source,
|
||||
self.FILE_FILE_TYPE,
|
||||
file_name=file_name,
|
||||
openid=source.author.user_openid,
|
||||
)
|
||||
if media:
|
||||
payload["media"] = media
|
||||
payload["msg_type"] = 7
|
||||
payload.pop("markdown", None)
|
||||
payload["content"] = plain_text or None
|
||||
payload["media"] = media
|
||||
payload["msg_type"] = 7
|
||||
payload.pop("markdown", None)
|
||||
payload["content"] = plain_text or None
|
||||
if stream:
|
||||
ret = await self._send_with_markdown_fallback(
|
||||
send_func=lambda retry_payload: self.post_c2c_message(
|
||||
@@ -337,7 +213,6 @@ class QQOfficialMessageEvent(AstrMessageEvent):
|
||||
),
|
||||
payload=payload,
|
||||
plain_text=plain_text,
|
||||
stream=stream,
|
||||
)
|
||||
else:
|
||||
ret = await self._send_with_markdown_fallback(
|
||||
@@ -347,7 +222,6 @@ class QQOfficialMessageEvent(AstrMessageEvent):
|
||||
),
|
||||
payload=payload,
|
||||
plain_text=plain_text,
|
||||
stream=stream,
|
||||
)
|
||||
logger.debug(f"Message sent to C2C: {ret}")
|
||||
|
||||
@@ -363,7 +237,6 @@ class QQOfficialMessageEvent(AstrMessageEvent):
|
||||
),
|
||||
payload=payload,
|
||||
plain_text=plain_text,
|
||||
stream=stream,
|
||||
)
|
||||
|
||||
case botpy.message.DirectMessage():
|
||||
@@ -378,7 +251,6 @@ class QQOfficialMessageEvent(AstrMessageEvent):
|
||||
),
|
||||
payload=payload,
|
||||
plain_text=plain_text,
|
||||
stream=stream,
|
||||
)
|
||||
|
||||
case _:
|
||||
@@ -395,31 +267,10 @@ class QQOfficialMessageEvent(AstrMessageEvent):
|
||||
send_func,
|
||||
payload: dict,
|
||||
plain_text: str,
|
||||
stream: dict | None = None,
|
||||
):
|
||||
try:
|
||||
return await send_func(payload)
|
||||
except botpy.errors.ServerError as err:
|
||||
# QQ 流式 markdown 分片校验:内容必须以换行结尾。
|
||||
# 某些边界场景服务端仍可能判定失败,这里做一次修正重试。
|
||||
if stream and self.STREAM_MARKDOWN_NEWLINE_ERROR in str(err):
|
||||
retry_payload = payload.copy()
|
||||
|
||||
markdown_payload = retry_payload.get("markdown")
|
||||
if isinstance(markdown_payload, dict):
|
||||
md_content = cast(str, markdown_payload.get("content", "") or "")
|
||||
if md_content and not md_content.endswith("\n"):
|
||||
retry_payload["markdown"] = {"content": md_content + "\n"}
|
||||
|
||||
content = cast(str | None, retry_payload.get("content"))
|
||||
if content and not content.endswith("\n"):
|
||||
retry_payload["content"] = content + "\n"
|
||||
|
||||
logger.warning(
|
||||
"[QQOfficial] 流式 markdown 分片换行校验失败,已修正后重试一次。"
|
||||
)
|
||||
return await send_func(retry_payload)
|
||||
|
||||
if (
|
||||
self.MARKDOWN_NOT_ALLOWED_ERROR not in str(err)
|
||||
or not payload.get("markdown")
|
||||
@@ -431,14 +282,10 @@ class QQOfficialMessageEvent(AstrMessageEvent):
|
||||
"[QQOfficial] markdown 发送被拒绝,回退到 content 模式重试。"
|
||||
)
|
||||
fallback_payload = payload.copy()
|
||||
fallback_payload.pop("markdown", None)
|
||||
fallback_payload["markdown"] = None
|
||||
fallback_payload["content"] = plain_text
|
||||
if fallback_payload.get("msg_type") == 2:
|
||||
fallback_payload["msg_type"] = 0
|
||||
if stream:
|
||||
fallback_content = cast(str, fallback_payload.get("content") or "")
|
||||
if fallback_content and not fallback_content.endswith("\n"):
|
||||
fallback_payload["content"] = fallback_content + "\n"
|
||||
return await send_func(fallback_payload)
|
||||
|
||||
async def upload_group_and_c2c_image(
|
||||
@@ -480,19 +327,16 @@ class QQOfficialMessageEvent(AstrMessageEvent):
|
||||
ttl=result.get("ttl", 0),
|
||||
)
|
||||
|
||||
async def upload_group_and_c2c_media(
|
||||
async def upload_group_and_c2c_record(
|
||||
self,
|
||||
file_source: str,
|
||||
file_type: int,
|
||||
srv_send_msg: bool = False,
|
||||
file_name: str | None = None,
|
||||
**kwargs,
|
||||
) -> Media | None:
|
||||
"""上传媒体文件"""
|
||||
# 构建基础payload
|
||||
payload = {"file_type": file_type, "srv_send_msg": srv_send_msg}
|
||||
if file_name:
|
||||
payload["file_name"] = file_name
|
||||
|
||||
# 处理文件数据
|
||||
if os.path.exists(file_source):
|
||||
@@ -556,21 +400,13 @@ class QQOfficialMessageEvent(AstrMessageEvent):
|
||||
) -> message.Message:
|
||||
payload = locals()
|
||||
payload.pop("self", None)
|
||||
# QQ API does not accept stream.id=None; remove it when not yet assigned
|
||||
if "stream" in payload and payload["stream"] is not None:
|
||||
stream_data = dict(payload["stream"])
|
||||
if stream_data.get("id") is None:
|
||||
stream_data.pop("id", None)
|
||||
payload["stream"] = stream_data
|
||||
route = Route("POST", "/v2/users/{openid}/messages", openid=openid)
|
||||
result = await self.bot.api._http.request(route, json=payload)
|
||||
|
||||
if result is None:
|
||||
logger.warning("[QQOfficial] post_c2c_message: API 返回 None,跳过本次发送")
|
||||
return None
|
||||
if not isinstance(result, dict):
|
||||
logger.error(f"[QQOfficial] post_c2c_message: 响应不是 dict: {result}")
|
||||
return None
|
||||
raise RuntimeError(
|
||||
f"Failed to post c2c message, response is not dict: {result}"
|
||||
)
|
||||
|
||||
return message.Message(**result)
|
||||
|
||||
@@ -580,9 +416,6 @@ class QQOfficialMessageEvent(AstrMessageEvent):
|
||||
image_base64 = None # only one img supported
|
||||
image_file_path = None
|
||||
record_file_path = None
|
||||
video_file_source = None
|
||||
file_source = None
|
||||
file_name = None
|
||||
for i in message.chain:
|
||||
if isinstance(i, Plain):
|
||||
plain_text += i.text
|
||||
@@ -621,30 +454,6 @@ class QQOfficialMessageEvent(AstrMessageEvent):
|
||||
except Exception as e:
|
||||
logger.error(f"处理语音时出错: {e}")
|
||||
record_file_path = None
|
||||
elif isinstance(i, Video) and not video_file_source:
|
||||
if i.file.startswith("file:///"):
|
||||
video_file_source = i.file[8:]
|
||||
else:
|
||||
video_file_source = i.file
|
||||
elif isinstance(i, File) and not file_source:
|
||||
file_name = i.name
|
||||
if i.file_:
|
||||
file_path = i.file_
|
||||
if file_path.startswith("file:///"):
|
||||
file_path = file_path[8:]
|
||||
elif file_path.startswith("file://"):
|
||||
file_path = file_path[7:]
|
||||
file_source = file_path
|
||||
elif i.url:
|
||||
file_source = i.url
|
||||
else:
|
||||
logger.debug(f"qq_official 忽略 {i.type}")
|
||||
return (
|
||||
plain_text,
|
||||
image_base64,
|
||||
image_file_path,
|
||||
record_file_path,
|
||||
video_file_source,
|
||||
file_source,
|
||||
file_name,
|
||||
)
|
||||
return plain_text, image_base64, image_file_path, record_file_path
|
||||
|
||||
@@ -3,10 +3,8 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
import random
|
||||
import time
|
||||
from types import SimpleNamespace
|
||||
from typing import Any, cast
|
||||
from typing import cast
|
||||
|
||||
import botpy
|
||||
import botpy.message
|
||||
@@ -14,7 +12,7 @@ from botpy import Client
|
||||
|
||||
from astrbot import logger
|
||||
from astrbot.api.event import MessageChain
|
||||
from astrbot.api.message_components import At, File, Image, Plain, Record, Video
|
||||
from astrbot.api.message_components import At, File, Image, Plain
|
||||
from astrbot.api.platform import (
|
||||
AstrBotMessage,
|
||||
MessageMember,
|
||||
@@ -48,7 +46,6 @@ class botClient(Client):
|
||||
)
|
||||
abm.group_id = cast(str, message.group_openid)
|
||||
abm.session_id = abm.group_id
|
||||
self.platform.remember_session_scene(abm.session_id, "group")
|
||||
self._commit(abm)
|
||||
|
||||
# 收到频道消息
|
||||
@@ -59,7 +56,6 @@ class botClient(Client):
|
||||
)
|
||||
abm.group_id = message.channel_id
|
||||
abm.session_id = abm.group_id
|
||||
self.platform.remember_session_scene(abm.session_id, "channel")
|
||||
self._commit(abm)
|
||||
|
||||
# 收到私聊消息
|
||||
@@ -71,7 +67,6 @@ class botClient(Client):
|
||||
MessageType.FRIEND_MESSAGE,
|
||||
)
|
||||
abm.session_id = abm.sender.user_id
|
||||
self.platform.remember_session_scene(abm.session_id, "friend")
|
||||
self._commit(abm)
|
||||
|
||||
# 收到 C2C 消息
|
||||
@@ -81,11 +76,9 @@ class botClient(Client):
|
||||
MessageType.FRIEND_MESSAGE,
|
||||
)
|
||||
abm.session_id = abm.sender.user_id
|
||||
self.platform.remember_session_scene(abm.session_id, "friend")
|
||||
self._commit(abm)
|
||||
|
||||
def _commit(self, abm: AstrBotMessage) -> None:
|
||||
self.platform.remember_session_message_id(abm.session_id, abm.message_id)
|
||||
self.platform.commit_event(
|
||||
QQOfficialMessageEvent(
|
||||
abm.message_str,
|
||||
@@ -131,9 +124,6 @@ class QQOfficialPlatformAdapter(Platform):
|
||||
|
||||
self.client.set_platform(self)
|
||||
|
||||
self._session_last_message_id: dict[str, str] = {}
|
||||
self._session_scene: dict[str, str] = {}
|
||||
|
||||
self.test_mode = os.environ.get("TEST_MODE", "off") == "on"
|
||||
|
||||
async def send_by_session(
|
||||
@@ -141,191 +131,14 @@ class QQOfficialPlatformAdapter(Platform):
|
||||
session: MessageSesion,
|
||||
message_chain: MessageChain,
|
||||
) -> None:
|
||||
await self._send_by_session_common(session, message_chain)
|
||||
|
||||
async def _send_by_session_common(
|
||||
self,
|
||||
session: MessageSesion,
|
||||
message_chain: MessageChain,
|
||||
) -> None:
|
||||
(
|
||||
plain_text,
|
||||
image_base64,
|
||||
image_path,
|
||||
record_file_path,
|
||||
video_file_source,
|
||||
file_source,
|
||||
file_name,
|
||||
) = await QQOfficialMessageEvent._parse_to_qqofficial(message_chain)
|
||||
if (
|
||||
not plain_text
|
||||
and not image_path
|
||||
and not image_base64
|
||||
and not record_file_path
|
||||
and not video_file_source
|
||||
and not file_source
|
||||
):
|
||||
return
|
||||
|
||||
msg_id = self._session_last_message_id.get(session.session_id)
|
||||
if not msg_id:
|
||||
logger.warning(
|
||||
"[QQOfficial] No cached msg_id for session: %s, skip send_by_session",
|
||||
session.session_id,
|
||||
)
|
||||
return
|
||||
|
||||
payload: dict[str, Any] = {"content": plain_text, "msg_id": msg_id}
|
||||
ret: Any = None
|
||||
send_helper = SimpleNamespace(bot=self.client)
|
||||
|
||||
if session.message_type == MessageType.GROUP_MESSAGE:
|
||||
scene = self._session_scene.get(session.session_id)
|
||||
if scene == "group":
|
||||
payload["msg_seq"] = random.randint(1, 10000)
|
||||
if image_base64:
|
||||
media = await QQOfficialMessageEvent.upload_group_and_c2c_image(
|
||||
send_helper, # type: ignore
|
||||
image_base64,
|
||||
QQOfficialMessageEvent.IMAGE_FILE_TYPE,
|
||||
group_openid=session.session_id,
|
||||
)
|
||||
payload["media"] = media
|
||||
payload["msg_type"] = 7
|
||||
if record_file_path:
|
||||
media = await QQOfficialMessageEvent.upload_group_and_c2c_media(
|
||||
send_helper, # type: ignore
|
||||
record_file_path,
|
||||
QQOfficialMessageEvent.VOICE_FILE_TYPE,
|
||||
group_openid=session.session_id,
|
||||
)
|
||||
if media:
|
||||
payload["media"] = media
|
||||
payload["msg_type"] = 7
|
||||
if video_file_source:
|
||||
media = await QQOfficialMessageEvent.upload_group_and_c2c_media(
|
||||
send_helper, # type: ignore
|
||||
video_file_source,
|
||||
QQOfficialMessageEvent.VIDEO_FILE_TYPE,
|
||||
group_openid=session.session_id,
|
||||
)
|
||||
if media:
|
||||
payload["media"] = media
|
||||
payload["msg_type"] = 7
|
||||
payload.pop("msg_id", None)
|
||||
if file_source:
|
||||
media = await QQOfficialMessageEvent.upload_group_and_c2c_media(
|
||||
send_helper, # type: ignore
|
||||
file_source,
|
||||
QQOfficialMessageEvent.FILE_FILE_TYPE,
|
||||
file_name=file_name,
|
||||
group_openid=session.session_id,
|
||||
)
|
||||
if media:
|
||||
payload["media"] = media
|
||||
payload["msg_type"] = 7
|
||||
payload.pop("msg_id", None)
|
||||
ret = await self.client.api.post_group_message(
|
||||
group_openid=session.session_id,
|
||||
**payload,
|
||||
)
|
||||
else:
|
||||
if image_path:
|
||||
payload["file_image"] = image_path
|
||||
ret = await self.client.api.post_message(
|
||||
channel_id=session.session_id,
|
||||
**payload,
|
||||
)
|
||||
|
||||
elif session.message_type == MessageType.FRIEND_MESSAGE:
|
||||
payload["msg_seq"] = random.randint(1, 10000)
|
||||
if image_base64:
|
||||
media = await QQOfficialMessageEvent.upload_group_and_c2c_image(
|
||||
send_helper, # type: ignore
|
||||
image_base64,
|
||||
QQOfficialMessageEvent.IMAGE_FILE_TYPE,
|
||||
openid=session.session_id,
|
||||
)
|
||||
payload["media"] = media
|
||||
payload["msg_type"] = 7
|
||||
if record_file_path:
|
||||
media = await QQOfficialMessageEvent.upload_group_and_c2c_media(
|
||||
send_helper, # type: ignore
|
||||
record_file_path,
|
||||
QQOfficialMessageEvent.VOICE_FILE_TYPE,
|
||||
openid=session.session_id,
|
||||
)
|
||||
if media:
|
||||
payload["media"] = media
|
||||
payload["msg_type"] = 7
|
||||
if video_file_source:
|
||||
media = await QQOfficialMessageEvent.upload_group_and_c2c_media(
|
||||
send_helper, # type: ignore
|
||||
video_file_source,
|
||||
QQOfficialMessageEvent.VIDEO_FILE_TYPE,
|
||||
openid=session.session_id,
|
||||
)
|
||||
if media:
|
||||
payload["media"] = media
|
||||
payload["msg_type"] = 7
|
||||
# QQ API rejects msg_id for media (video/file) messages sent
|
||||
# via the proactive tool-call path; remove it to avoid 越权 error.
|
||||
payload.pop("msg_id", None)
|
||||
if file_source:
|
||||
media = await QQOfficialMessageEvent.upload_group_and_c2c_media(
|
||||
send_helper, # type: ignore
|
||||
file_source,
|
||||
QQOfficialMessageEvent.FILE_FILE_TYPE,
|
||||
file_name=file_name,
|
||||
openid=session.session_id,
|
||||
)
|
||||
if media:
|
||||
payload["media"] = media
|
||||
payload["msg_type"] = 7
|
||||
payload.pop("msg_id", None)
|
||||
|
||||
ret = await QQOfficialMessageEvent.post_c2c_message(
|
||||
send_helper, # type: ignore
|
||||
openid=session.session_id,
|
||||
**payload,
|
||||
)
|
||||
else:
|
||||
logger.warning(
|
||||
"[QQOfficial] Unsupported message type for send_by_session: %s",
|
||||
session.message_type,
|
||||
)
|
||||
return
|
||||
|
||||
sent_message_id = self._extract_message_id(ret)
|
||||
if sent_message_id:
|
||||
self.remember_session_message_id(session.session_id, sent_message_id)
|
||||
await super().send_by_session(session, message_chain)
|
||||
|
||||
def remember_session_message_id(self, session_id: str, message_id: str) -> None:
|
||||
if not session_id or not message_id:
|
||||
return
|
||||
self._session_last_message_id[session_id] = message_id
|
||||
|
||||
def remember_session_scene(self, session_id: str, scene: str) -> None:
|
||||
if not session_id or not scene:
|
||||
return
|
||||
self._session_scene[session_id] = scene
|
||||
|
||||
def _extract_message_id(self, ret: Any) -> str | None:
|
||||
if isinstance(ret, dict):
|
||||
message_id = ret.get("id")
|
||||
return str(message_id) if message_id else None
|
||||
message_id = getattr(ret, "id", None)
|
||||
if message_id:
|
||||
return str(message_id)
|
||||
return None
|
||||
raise NotImplementedError("QQ 机器人官方 API 适配器不支持 send_by_session")
|
||||
|
||||
def meta(self) -> PlatformMetadata:
|
||||
return PlatformMetadata(
|
||||
name="qq_official",
|
||||
description="QQ 机器人官方 API 适配器",
|
||||
id=cast(str, self.config.get("id")),
|
||||
support_proactive_message=True,
|
||||
support_proactive_message=False,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
@@ -345,10 +158,7 @@ class QQOfficialPlatformAdapter(Platform):
|
||||
return
|
||||
|
||||
for attachment in attachments:
|
||||
content_type = cast(
|
||||
str,
|
||||
getattr(attachment, "content_type", "") or "",
|
||||
).lower()
|
||||
content_type = cast(str, getattr(attachment, "content_type", "") or "")
|
||||
url = QQOfficialPlatformAdapter._normalize_attachment_url(
|
||||
cast(str | None, getattr(attachment, "url", None))
|
||||
)
|
||||
@@ -364,32 +174,7 @@ class QQOfficialPlatformAdapter(Platform):
|
||||
or getattr(attachment, "name", None)
|
||||
or "attachment",
|
||||
)
|
||||
ext = os.path.splitext(filename)[1].lower()
|
||||
image_exts = {".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp"}
|
||||
audio_exts = {
|
||||
".mp3",
|
||||
".wav",
|
||||
".ogg",
|
||||
".m4a",
|
||||
".amr",
|
||||
".silk",
|
||||
}
|
||||
video_exts = {
|
||||
".mp4",
|
||||
".mov",
|
||||
".avi",
|
||||
".mkv",
|
||||
".webm",
|
||||
}
|
||||
|
||||
if content_type.startswith("audio") or ext in audio_exts:
|
||||
msg.append(Record.fromURL(url))
|
||||
elif content_type.startswith("video") or ext in video_exts:
|
||||
msg.append(Video.fromURL(url))
|
||||
elif content_type.startswith("image") or ext in image_exts:
|
||||
msg.append(Image.fromURL(url))
|
||||
else:
|
||||
msg.append(File(name=filename, file=url, url=url))
|
||||
msg.append(File(name=filename, file=url, url=url))
|
||||
|
||||
@staticmethod
|
||||
def _parse_from_qqofficial(
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import asyncio
|
||||
import logging
|
||||
import random
|
||||
from types import SimpleNamespace
|
||||
from typing import Any, cast
|
||||
|
||||
import botpy
|
||||
@@ -13,6 +15,7 @@ from astrbot.core.platform.astr_message_event import MessageSesion
|
||||
from astrbot.core.utils.webhook_utils import log_webhook_info
|
||||
|
||||
from ...register import register_platform_adapter
|
||||
from ..qqofficial.qqofficial_message_event import QQOfficialMessageEvent
|
||||
from ..qqofficial.qqofficial_platform_adapter import QQOfficialPlatformAdapter
|
||||
from .qo_webhook_event import QQOfficialWebhookMessageEvent
|
||||
from .qo_webhook_server import QQOfficialWebhook
|
||||
@@ -120,11 +123,95 @@ class QQOfficialWebhookPlatformAdapter(Platform):
|
||||
session: MessageSesion,
|
||||
message_chain: MessageChain,
|
||||
) -> None:
|
||||
await QQOfficialPlatformAdapter._send_by_session_common(
|
||||
cast(Any, self),
|
||||
session,
|
||||
message_chain,
|
||||
)
|
||||
(
|
||||
plain_text,
|
||||
image_base64,
|
||||
image_path,
|
||||
record_file_path,
|
||||
) = await QQOfficialMessageEvent._parse_to_qqofficial(message_chain)
|
||||
if not plain_text and not image_path:
|
||||
return
|
||||
|
||||
msg_id = self._session_last_message_id.get(session.session_id)
|
||||
if not msg_id:
|
||||
logger.warning(
|
||||
"[QQOfficialWebhook] No cached msg_id for session: %s, skip send_by_session",
|
||||
session.session_id,
|
||||
)
|
||||
return
|
||||
|
||||
payload: dict[str, Any] = {"content": plain_text, "msg_id": msg_id}
|
||||
ret: Any = None
|
||||
send_helper = SimpleNamespace(bot=self.client)
|
||||
if session.message_type == MessageType.GROUP_MESSAGE:
|
||||
scene = self._session_scene.get(session.session_id)
|
||||
if scene == "group":
|
||||
payload["msg_seq"] = random.randint(1, 10000)
|
||||
if image_base64:
|
||||
media = await QQOfficialMessageEvent.upload_group_and_c2c_image(
|
||||
send_helper, # type: ignore
|
||||
image_base64,
|
||||
1,
|
||||
group_openid=session.session_id,
|
||||
)
|
||||
payload["media"] = media
|
||||
payload["msg_type"] = 7
|
||||
if record_file_path:
|
||||
media = await QQOfficialMessageEvent.upload_group_and_c2c_record(
|
||||
send_helper, # type: ignore
|
||||
record_file_path,
|
||||
3,
|
||||
group_openid=session.session_id,
|
||||
)
|
||||
payload["media"] = media
|
||||
payload["msg_type"] = 7
|
||||
ret = await self.client.api.post_group_message(
|
||||
group_openid=session.session_id,
|
||||
**payload,
|
||||
)
|
||||
else:
|
||||
if image_path:
|
||||
payload["file_image"] = image_path
|
||||
ret = await self.client.api.post_message(
|
||||
channel_id=session.session_id,
|
||||
**payload,
|
||||
)
|
||||
elif session.message_type == MessageType.FRIEND_MESSAGE:
|
||||
payload["msg_seq"] = random.randint(1, 10000)
|
||||
if image_base64:
|
||||
media = await QQOfficialMessageEvent.upload_group_and_c2c_image(
|
||||
send_helper, # type: ignore
|
||||
image_base64,
|
||||
1,
|
||||
openid=session.session_id,
|
||||
)
|
||||
payload["media"] = media
|
||||
payload["msg_type"] = 7
|
||||
if record_file_path:
|
||||
media = await QQOfficialMessageEvent.upload_group_and_c2c_record(
|
||||
send_helper, # type: ignore
|
||||
record_file_path,
|
||||
3,
|
||||
openid=session.session_id,
|
||||
)
|
||||
payload["media"] = media
|
||||
payload["msg_type"] = 7
|
||||
ret = await QQOfficialMessageEvent.post_c2c_message(
|
||||
send_helper, # type: ignore
|
||||
openid=session.session_id,
|
||||
**payload,
|
||||
)
|
||||
else:
|
||||
logger.warning(
|
||||
"[QQOfficialWebhook] Unsupported message type for send_by_session: %s",
|
||||
session.message_type,
|
||||
)
|
||||
return
|
||||
|
||||
sent_message_id = self._extract_message_id(ret)
|
||||
if sent_message_id:
|
||||
self.remember_session_message_id(session.session_id, sent_message_id)
|
||||
await super().send_by_session(session, message_chain)
|
||||
|
||||
def remember_session_message_id(self, session_id: str, message_id: str) -> None:
|
||||
if not session_id or not message_id:
|
||||
|
||||
@@ -278,6 +278,7 @@ class TelegramPlatformEvent(AstrMessageEvent):
|
||||
try:
|
||||
md_text = telegramify_markdown.markdownify(
|
||||
chunk,
|
||||
normalize_whitespace=False,
|
||||
)
|
||||
await client.send_message(
|
||||
text=md_text,
|
||||
@@ -455,6 +456,7 @@ class TelegramPlatformEvent(AstrMessageEvent):
|
||||
try:
|
||||
markdown_text = telegramify_markdown.markdownify(
|
||||
delta,
|
||||
normalize_whitespace=False,
|
||||
)
|
||||
await self.client.send_message(
|
||||
text=markdown_text,
|
||||
@@ -535,6 +537,7 @@ class TelegramPlatformEvent(AstrMessageEvent):
|
||||
try:
|
||||
md = telegramify_markdown.markdownify(
|
||||
draft_text,
|
||||
normalize_whitespace=False,
|
||||
)
|
||||
await self._send_message_draft(
|
||||
user_name,
|
||||
@@ -692,6 +695,7 @@ class TelegramPlatformEvent(AstrMessageEvent):
|
||||
try:
|
||||
markdown_text = telegramify_markdown.markdownify(
|
||||
delta,
|
||||
normalize_whitespace=False,
|
||||
)
|
||||
await self.client.edit_message_text(
|
||||
text=markdown_text,
|
||||
|
||||
@@ -3,7 +3,6 @@ from __future__ import annotations
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shlex
|
||||
import shutil
|
||||
import tempfile
|
||||
import zipfile
|
||||
@@ -80,59 +79,7 @@ def _parse_frontmatter_description(text: str) -> str:
|
||||
|
||||
# Regex for sanitizing paths used in prompt examples — only allow
|
||||
# safe path characters to prevent prompt injection via crafted skill paths.
|
||||
_SAFE_PATH_RE = re.compile(r"[^\w./ ,()'\-]", re.UNICODE)
|
||||
_WINDOWS_DRIVE_PATH_RE = re.compile(r"^[A-Za-z]:(?:/|\\)")
|
||||
_WINDOWS_UNC_PATH_RE = re.compile(r"^(//|\\\\)[^/\\]+[/\\][^/\\]+")
|
||||
_CONTROL_CHARS_RE = re.compile(r"[\x00-\x1F\x7F]")
|
||||
|
||||
|
||||
def _is_windows_prompt_path(path: str) -> bool:
|
||||
if os.name != "nt":
|
||||
return False
|
||||
return bool(_WINDOWS_DRIVE_PATH_RE.match(path) or _WINDOWS_UNC_PATH_RE.match(path))
|
||||
|
||||
|
||||
def _sanitize_prompt_path_for_prompt(path: str) -> str:
|
||||
if not path:
|
||||
return ""
|
||||
|
||||
if _WINDOWS_DRIVE_PATH_RE.match(path) or _WINDOWS_UNC_PATH_RE.match(path):
|
||||
path = path.replace("\\", "/")
|
||||
|
||||
drive_prefix = ""
|
||||
if _WINDOWS_DRIVE_PATH_RE.match(path):
|
||||
drive_prefix = path[:2]
|
||||
path = path[2:]
|
||||
|
||||
path = path.replace("`", "")
|
||||
path = _CONTROL_CHARS_RE.sub("", path)
|
||||
sanitized = _SAFE_PATH_RE.sub("", path)
|
||||
return f"{drive_prefix}{sanitized}"
|
||||
|
||||
|
||||
def _sanitize_prompt_description(description: str) -> str:
|
||||
description = description.replace("`", "")
|
||||
description = _CONTROL_CHARS_RE.sub(" ", description)
|
||||
description = " ".join(description.split())
|
||||
return description
|
||||
|
||||
|
||||
def _sanitize_skill_display_name(name: str) -> str:
|
||||
if _SKILL_NAME_RE.fullmatch(name):
|
||||
return name
|
||||
return "<invalid_skill_name>"
|
||||
|
||||
|
||||
def _build_skill_read_command_example(path: str) -> str:
|
||||
if path == "<skills_root>/<skill_name>/SKILL.md":
|
||||
return f"cat {path}"
|
||||
if _is_windows_prompt_path(path):
|
||||
command = "type"
|
||||
path_arg = f'"{path}"'
|
||||
else:
|
||||
command = "cat"
|
||||
path_arg = shlex.quote(path)
|
||||
return f"{command} {path_arg}"
|
||||
_SAFE_PATH_RE = re.compile(r"[^A-Za-z0-9_./ -]")
|
||||
|
||||
|
||||
def build_skills_prompt(skills: list[SkillInfo]) -> str:
|
||||
@@ -145,37 +92,16 @@ def build_skills_prompt(skills: list[SkillInfo]) -> str:
|
||||
skills_lines: list[str] = []
|
||||
example_path = ""
|
||||
for skill in skills:
|
||||
display_name = _sanitize_skill_display_name(skill.name)
|
||||
|
||||
description = skill.description or "No description"
|
||||
if skill.source_type == "sandbox_only":
|
||||
description = _sanitize_prompt_description(description)
|
||||
if not description:
|
||||
description = "Read SKILL.md for details."
|
||||
|
||||
if skill.source_type == "sandbox_only":
|
||||
rendered_path = (
|
||||
f"{str(SANDBOX_WORKSPACE_ROOT)}/{str(SANDBOX_SKILLS_ROOT)}/"
|
||||
f"{display_name}/SKILL.md"
|
||||
)
|
||||
else:
|
||||
rendered_path = _sanitize_prompt_path_for_prompt(skill.path)
|
||||
if not rendered_path:
|
||||
rendered_path = "<skills_root>/<skill_name>/SKILL.md"
|
||||
|
||||
skills_lines.append(
|
||||
f"- **{display_name}**: {description}\n File: `{rendered_path}`"
|
||||
f"- **{skill.name}**: {description}\n File: `{skill.path}`"
|
||||
)
|
||||
if not example_path:
|
||||
example_path = rendered_path
|
||||
example_path = skill.path
|
||||
skills_block = "\n".join(skills_lines)
|
||||
# Sanitize example_path — it may originate from sandbox cache (untrusted)
|
||||
if example_path == "<skills_root>/<skill_name>/SKILL.md":
|
||||
example_path = "<skills_root>/<skill_name>/SKILL.md"
|
||||
else:
|
||||
example_path = _sanitize_prompt_path_for_prompt(example_path)
|
||||
example_path = example_path or "<skills_root>/<skill_name>/SKILL.md"
|
||||
example_command = _build_skill_read_command_example(example_path)
|
||||
example_path = _SAFE_PATH_RE.sub("", example_path) if example_path else ""
|
||||
example_path = example_path or "<skills_root>/<skill_name>/SKILL.md"
|
||||
|
||||
return (
|
||||
"## Skills\n\n"
|
||||
@@ -193,9 +119,8 @@ def build_skills_prompt(skills: list[SkillInfo]) -> str:
|
||||
"*Never silently skip a matching skill* — either use it or briefly "
|
||||
"explain why you chose not to.\n"
|
||||
"3. **Mandatory grounding** — Before executing any skill you MUST "
|
||||
"first read its `SKILL.md` by running a shell command compatible "
|
||||
"with the current runtime shell and using the **absolute path** "
|
||||
f"shown above (e.g. `{example_command}`). "
|
||||
"first read its `SKILL.md` by running a shell command with the "
|
||||
f"**absolute path** shown above (e.g. `cat {example_path}`). "
|
||||
"Never rely on memory or assumptions about a skill's content.\n"
|
||||
"4. **Progressive disclosure** — Load only what is directly "
|
||||
"referenced from `SKILL.md`:\n"
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
"""插件的重载、启停、安装、卸载等操作。"""
|
||||
|
||||
import asyncio
|
||||
import contextlib
|
||||
import functools
|
||||
import inspect
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import traceback
|
||||
from types import ModuleType
|
||||
|
||||
@@ -31,12 +29,12 @@ from astrbot.core.utils.astrbot_path import (
|
||||
get_astrbot_config_path,
|
||||
get_astrbot_path,
|
||||
get_astrbot_plugin_path,
|
||||
get_astrbot_temp_path,
|
||||
)
|
||||
from astrbot.core.utils.io import remove_dir
|
||||
from astrbot.core.utils.metrics import Metric
|
||||
from astrbot.core.utils.requirements_utils import (
|
||||
plan_missing_requirements_install,
|
||||
RequirementsPrecheckFailed,
|
||||
find_missing_requirements_or_raise,
|
||||
)
|
||||
|
||||
from . import StarMetadata
|
||||
@@ -76,78 +74,30 @@ class PluginDependencyInstallError(Exception):
|
||||
self.error = error
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _temporary_filtered_requirements_file(
|
||||
*,
|
||||
install_lines: tuple[str, ...],
|
||||
):
|
||||
filtered_requirements_path: str | None = None
|
||||
temp_dir = get_astrbot_temp_path()
|
||||
|
||||
try:
|
||||
os.makedirs(temp_dir, exist_ok=True)
|
||||
with tempfile.NamedTemporaryFile(
|
||||
mode="w",
|
||||
suffix="_plugin_requirements.txt",
|
||||
delete=False,
|
||||
dir=temp_dir,
|
||||
encoding="utf-8",
|
||||
) as filtered_requirements_file:
|
||||
filtered_requirements_file.write("\n".join(install_lines) + "\n")
|
||||
filtered_requirements_path = filtered_requirements_file.name
|
||||
|
||||
yield filtered_requirements_path
|
||||
finally:
|
||||
if filtered_requirements_path and os.path.exists(filtered_requirements_path):
|
||||
try:
|
||||
os.remove(filtered_requirements_path)
|
||||
except OSError as exc:
|
||||
logger.warning(
|
||||
"删除临时插件依赖文件失败:%s(路径:%s)",
|
||||
exc,
|
||||
filtered_requirements_path,
|
||||
)
|
||||
|
||||
|
||||
async def _install_requirements_with_precheck(
|
||||
*,
|
||||
plugin_label: str,
|
||||
requirements_path: str,
|
||||
) -> None:
|
||||
install_plan = plan_missing_requirements_install(requirements_path)
|
||||
|
||||
if install_plan is None:
|
||||
try:
|
||||
missing = find_missing_requirements_or_raise(requirements_path)
|
||||
except RequirementsPrecheckFailed:
|
||||
logger.info(
|
||||
f"正在安装插件 {plugin_label} 的依赖库(缺失依赖预检查不可裁剪,回退到完整安装): "
|
||||
f"正在安装插件 {plugin_label} 的依赖库(预检查失败,回退到完整安装): "
|
||||
f"{requirements_path}"
|
||||
)
|
||||
await pip_installer.install(requirements_path=requirements_path)
|
||||
return
|
||||
|
||||
if not install_plan.missing_names:
|
||||
if not missing:
|
||||
logger.info(f"插件 {plugin_label} 的依赖已满足,跳过安装。")
|
||||
return
|
||||
|
||||
if not install_plan.install_lines:
|
||||
fallback_reason = install_plan.fallback_reason or "unknown reason"
|
||||
logger.info(
|
||||
"检测到插件 %s 缺失依赖,但无法安全裁剪 requirements,回退到完整安装: %s (%s)",
|
||||
plugin_label,
|
||||
requirements_path,
|
||||
fallback_reason,
|
||||
)
|
||||
await pip_installer.install(requirements_path=requirements_path)
|
||||
return
|
||||
|
||||
logger.info(
|
||||
f"检测到插件 {plugin_label} 缺失依赖,正在按 requirements.txt 安装: "
|
||||
f"{requirements_path} -> {sorted(install_plan.missing_names)}"
|
||||
f"{requirements_path} -> {sorted(missing)}"
|
||||
)
|
||||
|
||||
with _temporary_filtered_requirements_file(
|
||||
install_lines=install_plan.install_lines,
|
||||
) as filtered_requirements_path:
|
||||
await pip_installer.install(requirements_path=filtered_requirements_path)
|
||||
await pip_installer.install(requirements_path=requirements_path)
|
||||
|
||||
|
||||
class PluginManager:
|
||||
|
||||
@@ -30,7 +30,7 @@ class CreateActiveCronTool(FunctionTool[AstrAgentContext]):
|
||||
"properties": {
|
||||
"cron_expression": {
|
||||
"type": "string",
|
||||
"description": "Cron expression defining recurring schedule (e.g., '0 8 * * *' or '0 23 * * mon-fri'). Prefer named weekdays like 'mon-fri' or 'sat,sun' instead of numeric day-of-week ranges such as '1-5' to avoid ambiguity across cron implementations.",
|
||||
"description": "Cron expression defining recurring schedule (e.g., '0 8 * * *').",
|
||||
},
|
||||
"run_at": {
|
||||
"type": "string",
|
||||
|
||||
@@ -4,7 +4,7 @@ import os
|
||||
import re
|
||||
import shlex
|
||||
import sys
|
||||
from collections.abc import Iterable, Iterator, Sequence
|
||||
from collections.abc import Iterable, Iterator
|
||||
from dataclasses import dataclass
|
||||
|
||||
from packaging.requirements import InvalidRequirement, Requirement
|
||||
@@ -29,13 +29,6 @@ class ParsedPackageInput:
|
||||
requirement_names: frozenset[str]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class MissingRequirementsPlan:
|
||||
missing_names: frozenset[str]
|
||||
install_lines: tuple[str, ...]
|
||||
fallback_reason: str | None = None
|
||||
|
||||
|
||||
def canonicalize_distribution_name(name: str) -> str:
|
||||
return re.sub(r"[-_.]+", "-", name).strip("-").lower()
|
||||
|
||||
@@ -371,8 +364,8 @@ def _load_requirement_lines_for_precheck(
|
||||
None,
|
||||
)
|
||||
if fallback_line is not None:
|
||||
logger.info(
|
||||
"缺失依赖预检查发现无法安全裁剪的 option/direct-reference 行,将回退到完整安装: %s (%s)",
|
||||
logger.warning(
|
||||
"预检查缺失依赖失败,将回退到完整安装: unresolved direct reference in %s: %s",
|
||||
requirements_path,
|
||||
fallback_line,
|
||||
)
|
||||
@@ -388,13 +381,6 @@ def find_missing_requirements(requirements_path: str) -> set[str] | None:
|
||||
if not can_precheck or requirement_lines is None:
|
||||
return None
|
||||
|
||||
return find_missing_requirements_from_lines(requirement_lines)
|
||||
|
||||
|
||||
def find_missing_requirements_from_lines(
|
||||
requirement_lines: Sequence[str],
|
||||
) -> set[str] | None:
|
||||
|
||||
required = list(iter_requirements(lines=requirement_lines))
|
||||
if not required:
|
||||
return set()
|
||||
@@ -415,70 +401,6 @@ def find_missing_requirements_from_lines(
|
||||
return missing
|
||||
|
||||
|
||||
def build_missing_requirements_install_lines(
|
||||
requirements_path: str,
|
||||
requirement_lines: Sequence[str],
|
||||
missing_names: set[str] | frozenset[str],
|
||||
) -> tuple[str, ...] | None:
|
||||
wanted_names = set(missing_names)
|
||||
install_lines: list[str] = []
|
||||
for line in requirement_lines:
|
||||
parsed = _parse_requirement_line(line)
|
||||
if parsed is None:
|
||||
if looks_like_direct_reference(line) or line.startswith(("-", "--")):
|
||||
logger.debug(
|
||||
"缺失依赖行筛选回退到完整安装:requirements 中包含无法安全裁剪的 option/direct-reference 行: %s (%s)",
|
||||
requirements_path,
|
||||
line,
|
||||
)
|
||||
return None
|
||||
continue
|
||||
|
||||
name, _specifier = parsed
|
||||
if name in wanted_names:
|
||||
install_lines.append(line)
|
||||
|
||||
return tuple(install_lines)
|
||||
|
||||
|
||||
def plan_missing_requirements_install(
|
||||
requirements_path: str,
|
||||
) -> MissingRequirementsPlan | None:
|
||||
can_precheck, requirement_lines = _load_requirement_lines_for_precheck(
|
||||
requirements_path
|
||||
)
|
||||
if not can_precheck or requirement_lines is None:
|
||||
return None
|
||||
|
||||
missing = find_missing_requirements_from_lines(requirement_lines)
|
||||
if missing is None:
|
||||
return None
|
||||
|
||||
install_lines = build_missing_requirements_install_lines(
|
||||
requirements_path,
|
||||
requirement_lines,
|
||||
missing,
|
||||
)
|
||||
if install_lines is None:
|
||||
return None
|
||||
if missing and not install_lines:
|
||||
logger.warning(
|
||||
"预检查缺失依赖成功,但无法映射到可安装 requirement 行,将回退到完整安装: %s -> %s",
|
||||
requirements_path,
|
||||
sorted(missing),
|
||||
)
|
||||
return MissingRequirementsPlan(
|
||||
missing_names=frozenset(missing),
|
||||
install_lines=(),
|
||||
fallback_reason="unmapped missing requirement names",
|
||||
)
|
||||
|
||||
return MissingRequirementsPlan(
|
||||
missing_names=frozenset(missing),
|
||||
install_lines=install_lines,
|
||||
)
|
||||
|
||||
|
||||
def find_missing_requirements_or_raise(requirements_path: str) -> set[str]:
|
||||
missing = find_missing_requirements(requirements_path)
|
||||
if missing is None:
|
||||
|
||||
@@ -977,17 +977,7 @@ class BackupRoute(Route):
|
||||
if not jwt_secret:
|
||||
return Response().error("服务器配置错误").__dict__
|
||||
|
||||
# Verify JWT token with strict security options
|
||||
jwt.decode(
|
||||
token,
|
||||
jwt_secret,
|
||||
algorithms=["HS256"],
|
||||
options={
|
||||
"require": ["exp"], # Require expiration claim
|
||||
"verify_signature": True, # Explicitly verify signature
|
||||
"verify_exp": True, # Verify expiration
|
||||
},
|
||||
)
|
||||
jwt.decode(token, jwt_secret, algorithms=["HS256"])
|
||||
except jwt.ExpiredSignatureError:
|
||||
return Response().error("Token 已过期,请刷新页面后重试").__dict__
|
||||
except jwt.InvalidTokenError:
|
||||
|
||||
@@ -43,6 +43,7 @@ class SessionManagementRoute(Route):
|
||||
"/session/group/create": ("POST", self.create_group),
|
||||
"/session/group/update": ("POST", self.update_group),
|
||||
"/session/group/delete": ("POST", self.delete_group),
|
||||
"/session/group/update-config": ("POST", self.update_group_config),
|
||||
}
|
||||
self.conv_mgr = core_lifecycle.conversation_manager
|
||||
self.core_lifecycle = core_lifecycle
|
||||
@@ -145,9 +146,20 @@ class SessionManagementRoute(Route):
|
||||
page=page, page_size=page_size, search=search
|
||||
)
|
||||
|
||||
# 构建规则列表
|
||||
# 收集属于有配置分组的 UMO,避免重复显示
|
||||
grouped_umos = set()
|
||||
groups = self._get_groups()
|
||||
for group_data in groups.values():
|
||||
if group_data.get("config"):
|
||||
grouped_umos.update(group_data.get("umos", []))
|
||||
|
||||
# 构建规则列表(排除已被分组管理的 UMO)
|
||||
rules_list = []
|
||||
filtered_count = 0
|
||||
for umo, rules in umo_rules.items():
|
||||
if umo in grouped_umos:
|
||||
filtered_count += 1
|
||||
continue
|
||||
rule_info = {
|
||||
"umo": umo,
|
||||
"rules": rules,
|
||||
@@ -159,6 +171,7 @@ class SessionManagementRoute(Route):
|
||||
rule_info["message_type"] = parts[1]
|
||||
rule_info["session_id"] = parts[2]
|
||||
rules_list.append(rule_info)
|
||||
total -= filtered_count
|
||||
|
||||
# 获取可用的 providers 和 personas
|
||||
provider_manager = self.core_lifecycle.provider_manager
|
||||
@@ -240,6 +253,7 @@ class SessionManagementRoute(Route):
|
||||
"available_plugins": available_plugins,
|
||||
"available_kbs": available_kbs,
|
||||
"available_rule_keys": AVAILABLE_SESSION_RULE_KEYS,
|
||||
"group_rules": self._get_group_rules(),
|
||||
}
|
||||
)
|
||||
.__dict__
|
||||
@@ -793,6 +807,51 @@ class SessionManagementRoute(Route):
|
||||
"""保存分组"""
|
||||
sp.put("session_groups", groups)
|
||||
|
||||
def _get_group_rules(self) -> list:
|
||||
"""获取有配置的分组列表,用于在规则列表中显示"""
|
||||
groups = self._get_groups()
|
||||
group_rules = []
|
||||
for group_id, group_data in groups.items():
|
||||
config = group_data.get("config", {})
|
||||
if config: # 只返回有配置的分组
|
||||
group_rules.append(
|
||||
{
|
||||
"group_id": group_id,
|
||||
"name": group_data.get("name", ""),
|
||||
"umo_count": len(group_data.get("umos", [])),
|
||||
"config": config,
|
||||
}
|
||||
)
|
||||
return group_rules
|
||||
|
||||
async def _sync_group_config_to_umos(
|
||||
self, config: dict, umos: list[str]
|
||||
) -> tuple[int, list[str]]:
|
||||
"""将分组配置同步到指定的 UMO 列表
|
||||
|
||||
Returns:
|
||||
(success_count, failed_umos)
|
||||
"""
|
||||
success_count = 0
|
||||
failed_umos = []
|
||||
for umo in umos:
|
||||
try:
|
||||
for rule_key, rule_value in config.items():
|
||||
if rule_key not in AVAILABLE_SESSION_RULE_KEYS:
|
||||
continue
|
||||
if rule_value is None:
|
||||
continue
|
||||
if rule_key == "session_plugin_config":
|
||||
# session_plugin_config 需要包裹 umo key
|
||||
await sp.session_put(umo, rule_key, {umo: rule_value})
|
||||
else:
|
||||
await sp.session_put(umo, rule_key, rule_value)
|
||||
success_count += 1
|
||||
except Exception as e:
|
||||
logger.error(f"同步配置到 {umo} 失败: {e!s}")
|
||||
failed_umos.append(umo)
|
||||
return success_count, failed_umos
|
||||
|
||||
async def list_groups(self):
|
||||
"""获取所有分组列表"""
|
||||
try:
|
||||
@@ -806,6 +865,7 @@ class SessionManagementRoute(Route):
|
||||
"name": group_data.get("name", ""),
|
||||
"umos": group_data.get("umos", []),
|
||||
"umo_count": len(group_data.get("umos", [])),
|
||||
"config": group_data.get("config", {}),
|
||||
}
|
||||
)
|
||||
return Response().ok({"groups": groups_list}).__dict__
|
||||
@@ -875,6 +935,7 @@ class SessionManagementRoute(Route):
|
||||
return Response().error(f"分组 '{group_id}' 不存在").__dict__
|
||||
|
||||
group = groups[group_id]
|
||||
old_umos = set(group.get("umos", []))
|
||||
|
||||
# 更新名称
|
||||
if name is not None:
|
||||
@@ -883,6 +944,7 @@ class SessionManagementRoute(Route):
|
||||
# 直接设置 umos 列表
|
||||
if umos is not None:
|
||||
group["umos"] = umos
|
||||
new_umos = set(umos)
|
||||
else:
|
||||
# 增量更新
|
||||
current_umos = set(group.get("umos", []))
|
||||
@@ -891,9 +953,21 @@ class SessionManagementRoute(Route):
|
||||
if remove_umos:
|
||||
current_umos.difference_update(remove_umos)
|
||||
group["umos"] = list(current_umos)
|
||||
new_umos = current_umos
|
||||
|
||||
self._save_groups(groups)
|
||||
|
||||
# 自动同步分组配置给新加入的成员
|
||||
group_config = group.get("config", {})
|
||||
newly_added = new_umos - old_umos
|
||||
if group_config and newly_added:
|
||||
sync_count, _ = await self._sync_group_config_to_umos(
|
||||
group_config, list(newly_added)
|
||||
)
|
||||
logger.info(
|
||||
f"自动同步分组 '{group['name']}' 配置到 {sync_count} 个新成员"
|
||||
)
|
||||
|
||||
return (
|
||||
Response()
|
||||
.ok(
|
||||
@@ -936,3 +1010,81 @@ class SessionManagementRoute(Route):
|
||||
except Exception as e:
|
||||
logger.error(f"删除分组失败: {e!s}")
|
||||
return Response().error(f"删除分组失败: {e!s}").__dict__
|
||||
|
||||
async def update_group_config(self):
|
||||
"""更新分组的配置,并同步到所有成员 UMO
|
||||
|
||||
请求体:
|
||||
{
|
||||
"group_id": "分组ID",
|
||||
"config": {
|
||||
"session_service_config": {...},
|
||||
"session_plugin_config": {...},
|
||||
"kb_config": {...},
|
||||
"provider_perf_chat_completion": ...,
|
||||
"provider_perf_speech_to_text": ...,
|
||||
"provider_perf_text_to_speech": ...
|
||||
}
|
||||
}
|
||||
"""
|
||||
try:
|
||||
data = await request.get_json()
|
||||
group_id = data.get("group_id")
|
||||
config = data.get("config", {})
|
||||
|
||||
if not group_id:
|
||||
return Response().error("缺少必要参数: group_id").__dict__
|
||||
|
||||
groups = self._get_groups()
|
||||
|
||||
if group_id not in groups:
|
||||
return Response().error(f"分组 '{group_id}' 不存在").__dict__
|
||||
|
||||
group = groups[group_id]
|
||||
|
||||
# 保存配置到分组
|
||||
group["config"] = config
|
||||
self._save_groups(groups)
|
||||
|
||||
# 同步到所有成员 UMO
|
||||
umos = group.get("umos", [])
|
||||
|
||||
if not config:
|
||||
# 空配置 → 清除成员上的所有分组下发规则
|
||||
success_count = 0
|
||||
failed_umos = []
|
||||
for umo in umos:
|
||||
try:
|
||||
for rule_key in AVAILABLE_SESSION_RULE_KEYS:
|
||||
try:
|
||||
await sp.session_remove(umo, rule_key)
|
||||
except Exception:
|
||||
pass
|
||||
success_count += 1
|
||||
except Exception as e:
|
||||
logger.error(f"清除 {umo} 规则失败: {e!s}")
|
||||
failed_umos.append(umo)
|
||||
else:
|
||||
success_count, failed_umos = await self._sync_group_config_to_umos(
|
||||
config, umos
|
||||
)
|
||||
|
||||
msg = f"分组 '{group['name']}' 配置已保存并同步到 {success_count}/{len(umos)} 个会话"
|
||||
if failed_umos:
|
||||
msg += f",{len(failed_umos)} 个失败"
|
||||
|
||||
return (
|
||||
Response()
|
||||
.ok(
|
||||
{
|
||||
"message": msg,
|
||||
"success_count": success_count,
|
||||
"failed_count": len(failed_umos),
|
||||
"failed_umos": failed_umos,
|
||||
}
|
||||
)
|
||||
.__dict__
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"更新分组配置失败: {e!s}")
|
||||
return Response().error(f"更新分组配置失败: {e!s}").__dict__
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
## What's Changed
|
||||
|
||||
### 新增
|
||||
|
||||
- 新增俄语翻译([#6081](https://github.com/AstrBotDevs/AstrBot/pull/6081))。
|
||||
- QQ 官方 Bot 新增文件、语音、视频消息支持(含 WebSocket 模式)([#6063](https://github.com/AstrBotDevs/AstrBot/pull/6063))。
|
||||
|
||||
### 优化
|
||||
|
||||
- 优化 QQ 官方 Bot 的流式消息投递可靠性与主动媒体发送能力([#6131](https://github.com/AstrBotDevs/AstrBot/pull/6131))。
|
||||
- 优化边界场景下 booter 选择逻辑与消息发送工具([#6064](https://github.com/AstrBotDevs/AstrBot/pull/6064))。
|
||||
|
||||
### 修复
|
||||
|
||||
- 修复 Dashboard README 对话框锚点导航失效([#6083](https://github.com/AstrBotDevs/AstrBot/pull/6083))。
|
||||
- 优先使用具名 weekday 的 cron 示例,避免歧义([#6091](https://github.com/AstrBotDevs/AstrBot/pull/6091))。
|
||||
- 修复插件市场安装后状态未及时刷新的问题([#6124](https://github.com/AstrBotDevs/AstrBot/pull/6124))。
|
||||
- 修复插件依赖安装逻辑:仅安装缺失依赖([#6088](https://github.com/AstrBotDevs/AstrBot/pull/6088))。
|
||||
- 移除 Telegram 适配器中已废弃的 `normalize_whitespace` 参数([#6044](https://github.com/AstrBotDevs/AstrBot/pull/6044))。
|
||||
- 修复 Windows 本地 skill 文件读取问题([#6028](https://github.com/AstrBotDevs/AstrBot/pull/6028))。
|
||||
- 修复 Discord pre-ack emoji 配置重启后不持久化的问题([#6031](https://github.com/AstrBotDevs/AstrBot/pull/6031))。
|
||||
- 统一 WebUI 搜索框清空行为([#6017](https://github.com/AstrBotDevs/AstrBot/pull/6017))。
|
||||
- 优化插件依赖自动安装流程与 Dashboard 安装体验([#5954](https://github.com/AstrBotDevs/AstrBot/pull/5954))。
|
||||
|
||||
|
||||
### 文档
|
||||
|
||||
- 新增 Astrbook 和玖帕喵社区链接([#6135](https://github.com/AstrBotDevs/AstrBot/pull/6135))。
|
||||
- 修正文档 `docker.md` 与 `napcat.md` 中的拼写错误([#6048](https://github.com/AstrBotDevs/AstrBot/pull/6048))。
|
||||
- 在多语言 README 中补充官方开发群号,并改进配置元数据中的正则说明。
|
||||
- 更新编辑链接模式并移除过时仓库引用。
|
||||
|
||||
---
|
||||
|
||||
## What's Changed (EN)
|
||||
|
||||
### New Features
|
||||
|
||||
- Added Russian translation support ([#6081](https://github.com/AstrBotDevs/AstrBot/pull/6081)).
|
||||
- Added file, voice, and video message support for QQ Official Bot (including WebSocket mode) ([#6063](https://github.com/AstrBotDevs/AstrBot/pull/6063)).
|
||||
|
||||
### Improvements
|
||||
|
||||
- Improved streaming message delivery reliability and proactive media sending for QQ Official API ([#6131](https://github.com/AstrBotDevs/AstrBot/pull/6131)).
|
||||
- Optimized booter selection logic in edge cases and message sending tooling ([#6064](https://github.com/AstrBotDevs/AstrBot/pull/6064)).
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fixed broken README dialog anchor navigation in the Dashboard ([#6083](https://github.com/AstrBotDevs/AstrBot/pull/6083)).
|
||||
- Preferred named weekday cron examples to reduce ambiguity ([#6091](https://github.com/AstrBotDevs/AstrBot/pull/6091)).
|
||||
- Fixed plugin market install-state refresh after installation ([#6124](https://github.com/AstrBotDevs/AstrBot/pull/6124)).
|
||||
- Fixed plugin dependency installation logic to install only missing packages ([#6088](https://github.com/AstrBotDevs/AstrBot/pull/6088)).
|
||||
- Removed deprecated `normalize_whitespace` parameter in the Telegram adapter ([#6044](https://github.com/AstrBotDevs/AstrBot/pull/6044)).
|
||||
- Fixed local skill file reading issues on Windows ([#6028](https://github.com/AstrBotDevs/AstrBot/pull/6028)).
|
||||
- Fixed Discord pre-ack emoji config not being persisted across restarts ([#6031](https://github.com/AstrBotDevs/AstrBot/pull/6031)).
|
||||
- Unified WebUI search input clear behavior ([#6017](https://github.com/AstrBotDevs/AstrBot/pull/6017)).
|
||||
- Improved plugin dependency auto-install flow and Dashboard installation experience ([#5954](https://github.com/AstrBotDevs/AstrBot/pull/5954)).
|
||||
|
||||
### Documentation
|
||||
|
||||
- Added Astrbook and Jiupa Miao community links ([#6135](https://github.com/AstrBotDevs/AstrBot/pull/6135)).
|
||||
- Fixed typos in `docker.md` and `napcat.md` ([#6048](https://github.com/AstrBotDevs/AstrBot/pull/6048)).
|
||||
- Added official developer group IDs to multilingual READMEs and improved regex description in config metadata.
|
||||
- Updated edit-link patterns and removed obsolete repository references.
|
||||
+7
-13
@@ -17,17 +17,17 @@
|
||||
"@tiptap/starter-kit": "2.1.7",
|
||||
"@tiptap/vue-3": "2.1.7",
|
||||
"apexcharts": "3.42.0",
|
||||
"axios": "1.13.5",
|
||||
"axios": ">=1.6.2 <1.10.0 || >1.10.0 <2.0.0",
|
||||
"axios-mock-adapter": "^1.22.0",
|
||||
"chance": "1.1.11",
|
||||
"date-fns": "2.30.0",
|
||||
"dompurify": "^3.3.2",
|
||||
"dompurify": "^3.3.1",
|
||||
"event-source-polyfill": "^1.0.31",
|
||||
"highlight.js": "^11.11.1",
|
||||
"js-md5": "^0.8.3",
|
||||
"katex": "^0.16.27",
|
||||
"lodash": "4.17.23",
|
||||
"markdown-it": "^14.1.1",
|
||||
"lodash": "4.17.21",
|
||||
"markdown-it": "^14.1.0",
|
||||
"markstream-vue": "^0.0.6",
|
||||
"mermaid": "^11.12.2",
|
||||
"monaco-editor": "^0.52.2",
|
||||
@@ -38,7 +38,7 @@
|
||||
"stream-markdown": "^0.0.13",
|
||||
"stream-monaco": "^0.0.17",
|
||||
"vee-validate": "4.11.3",
|
||||
"vite-plugin-vuetify": "2.1.3",
|
||||
"vite-plugin-vuetify": "1.0.2",
|
||||
"vue": "3.3.4",
|
||||
"vue-i18n": "^11.1.5",
|
||||
"vue-router": "4.2.4",
|
||||
@@ -54,7 +54,7 @@
|
||||
"@types/dompurify": "^3.0.5",
|
||||
"@types/markdown-it": "^14.1.2",
|
||||
"@types/node": "^20.5.7",
|
||||
"@vitejs/plugin-vue": "5.2.4",
|
||||
"@vitejs/plugin-vue": "4.3.3",
|
||||
"@vue/eslint-config-prettier": "8.0.0",
|
||||
"@vue/eslint-config-typescript": "11.0.3",
|
||||
"@vue/tsconfig": "^0.4.0",
|
||||
@@ -64,15 +64,9 @@
|
||||
"sass": "1.66.1",
|
||||
"sass-loader": "13.3.2",
|
||||
"typescript": "5.1.6",
|
||||
"vite": "6.4.1",
|
||||
"vite": "4.4.9",
|
||||
"vue-cli-plugin-vuetify": "2.5.8",
|
||||
"vue-tsc": "1.8.8",
|
||||
"vuetify-loader": "^2.0.0-alpha.9"
|
||||
},
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"immutable": "4.3.8",
|
||||
"lodash-es": "4.17.23"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+267
-597
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { useModuleI18n } from '@/i18n/composables';
|
||||
import { normalizeTextInput } from '@/utils/inputValue';
|
||||
|
||||
const { tm } = useModuleI18n('features/command');
|
||||
|
||||
@@ -53,7 +52,6 @@ const statusItems = [
|
||||
{ title: tm('filters.disabled'), value: 'disabled' },
|
||||
{ title: tm('filters.conflict'), value: 'conflict' }
|
||||
];
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -110,11 +108,10 @@ const statusItems = [
|
||||
<div style="min-width: 200px; max-width: 350px; flex: 1; border: 1px solid #B9B9B9; border-radius: 16px;">
|
||||
<v-text-field
|
||||
:model-value="searchQuery"
|
||||
@update:model-value="emit('update:searchQuery', normalizeTextInput($event))"
|
||||
@update:model-value="emit('update:searchQuery', $event)"
|
||||
density="compact"
|
||||
:label="tm('search.placeholder')"
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
clearable
|
||||
variant="solo-filled"
|
||||
flat
|
||||
hide-details
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
*/
|
||||
import { ref, computed, type Ref } from 'vue';
|
||||
import type { CommandItem, FilterState } from '../types';
|
||||
import { normalizeTextInput } from '@/utils/inputValue';
|
||||
|
||||
export function useCommandFilters(commands: Ref<CommandItem[]>) {
|
||||
// 过滤状态
|
||||
@@ -96,7 +95,7 @@ export function useCommandFilters(commands: Ref<CommandItem[]>) {
|
||||
* 过滤后的指令列表(支持层级结构)
|
||||
*/
|
||||
const filteredCommands = computed(() => {
|
||||
const query = normalizeTextInput(searchQuery.value).toLowerCase();
|
||||
const query = searchQuery.value.toLowerCase();
|
||||
const conflictCmds: CommandItem[] = [];
|
||||
const normalCmds: CommandItem[] = [];
|
||||
|
||||
@@ -185,3 +184,4 @@ export function useCommandFilters(commands: Ref<CommandItem[]>) {
|
||||
isGroupExpanded
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
import { computed, onActivated, onMounted, ref, watch} from 'vue';
|
||||
import axios from 'axios';
|
||||
import { useModuleI18n } from '@/i18n/composables';
|
||||
import { normalizeTextInput } from '@/utils/inputValue';
|
||||
|
||||
// Composables
|
||||
import { useComponentData } from './composables/useComponentData';
|
||||
@@ -84,7 +83,7 @@ const {
|
||||
} = useCommandActions(toast, () => fetchCommands(tm('messages.loadFailed')));
|
||||
|
||||
const filteredTools = computed(() => {
|
||||
const query = normalizeTextInput(toolSearch.value).trim().toLowerCase();
|
||||
const query = toolSearch.value.trim().toLowerCase();
|
||||
if (!query) return tools.value;
|
||||
return tools.value.filter(tool =>
|
||||
tool.name?.toLowerCase().includes(query) ||
|
||||
@@ -254,8 +253,7 @@ watch(viewMode, async (mode) => {
|
||||
<div class="d-flex flex-wrap align-center ga-3 mb-4">
|
||||
<div style="min-width: 240px; max-width: 380px; flex: 1;">
|
||||
<v-text-field
|
||||
:model-value="toolSearch"
|
||||
@update:model-value="toolSearch = normalizeTextInput($event)"
|
||||
v-model="toolSearch"
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
:label="tmTool('functionTools.search')"
|
||||
variant="outlined"
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
v-model="modelSearchProxy"
|
||||
density="compact"
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
clearable
|
||||
hide-details
|
||||
variant="solo-filled"
|
||||
flat
|
||||
@@ -162,7 +161,6 @@
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { normalizeTextInput } from '@/utils/inputValue'
|
||||
|
||||
const props = defineProps({
|
||||
entries: {
|
||||
@@ -224,7 +222,7 @@ const emit = defineEmits([
|
||||
|
||||
const modelSearchProxy = computed({
|
||||
get: () => props.modelSearch,
|
||||
set: (val) => emit('update:modelSearch', normalizeTextInput(val))
|
||||
set: (val) => emit('update:modelSearch', val)
|
||||
})
|
||||
|
||||
const isProviderTesting = (providerId) => props.testingProviders.includes(providerId)
|
||||
|
||||
@@ -48,24 +48,6 @@ const loading = ref(false);
|
||||
const isEmpty = ref(false);
|
||||
const copyFeedbackTimer = ref(null);
|
||||
const lastRequestId = ref(0);
|
||||
const scrollContainer = ref(null);
|
||||
|
||||
function slugifyHeading(text, slugCounts) {
|
||||
const base = (text || "")
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.normalize("NFKD")
|
||||
.replace(/[\u0300-\u036f]/g, "")
|
||||
.replace(/[^\p{Letter}\p{Number}\s-]/gu, "")
|
||||
.replace(/\s+/g, "-")
|
||||
.replace(/-+/g, "-");
|
||||
|
||||
if (!base) return "";
|
||||
|
||||
const count = slugCounts.get(base) || 0;
|
||||
slugCounts.set(base, count + 1);
|
||||
return count === 0 ? base : `${base}-${count}`;
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
if (copyFeedbackTimer.value) clearTimeout(copyFeedbackTimer.value);
|
||||
@@ -171,18 +153,6 @@ const renderedHtml = computed(() => {
|
||||
// 3. 后处理方案:完全隔离,安全性最高
|
||||
const tempDiv = document.createElement("div");
|
||||
tempDiv.innerHTML = cleanHtml;
|
||||
|
||||
const slugCounts = new Map();
|
||||
tempDiv.querySelectorAll("h1, h2, h3, h4, h5, h6").forEach((heading) => {
|
||||
if (heading.id) {
|
||||
slugCounts.set(heading.id, (slugCounts.get(heading.id) || 0) + 1);
|
||||
return;
|
||||
}
|
||||
|
||||
const slug = slugifyHeading(heading.textContent, slugCounts);
|
||||
if (slug) heading.id = slug;
|
||||
});
|
||||
|
||||
tempDiv.querySelectorAll("a").forEach((link) => {
|
||||
const href = link.getAttribute("href");
|
||||
// 强制所有外部链接使用安全的 _blank 策略
|
||||
@@ -281,35 +251,18 @@ watch(
|
||||
|
||||
function handleContainerClick(event) {
|
||||
const btn = event.target.closest(".copy-code-btn");
|
||||
if (btn) {
|
||||
const code = btn.closest(".code-block-wrapper")?.querySelector("code");
|
||||
if (code) {
|
||||
if (navigator.clipboard?.writeText) {
|
||||
navigator.clipboard
|
||||
.writeText(code.textContent)
|
||||
.then(() => showCopyFeedback(btn, true))
|
||||
.catch(() => tryFallbackCopy(code.textContent, btn));
|
||||
} else {
|
||||
tryFallbackCopy(code.textContent, btn);
|
||||
}
|
||||
if (!btn) return;
|
||||
const code = btn.closest(".code-block-wrapper")?.querySelector("code");
|
||||
if (code) {
|
||||
if (navigator.clipboard?.writeText) {
|
||||
navigator.clipboard
|
||||
.writeText(code.textContent)
|
||||
.then(() => showCopyFeedback(btn, true))
|
||||
.catch(() => tryFallbackCopy(code.textContent, btn));
|
||||
} else {
|
||||
tryFallbackCopy(code.textContent, btn);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const anchor = event.target.closest('a[href^="#"]');
|
||||
if (!anchor) return;
|
||||
|
||||
const rawHref = anchor.getAttribute("href");
|
||||
const targetId = rawHref ? decodeURIComponent(rawHref.slice(1)) : "";
|
||||
if (!targetId) return;
|
||||
|
||||
const target = scrollContainer.value?.querySelector(
|
||||
`#${CSS.escape(targetId)}`,
|
||||
);
|
||||
if (!target) return;
|
||||
|
||||
event.preventDefault();
|
||||
target.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
}
|
||||
|
||||
function tryFallbackCopy(text, btn) {
|
||||
@@ -373,7 +326,7 @@ const showActionArea = computed(() => {
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
<v-card-text ref="scrollContainer" style="overflow-y: auto">
|
||||
<v-card-text style="overflow-y: auto">
|
||||
<div v-if="showActionArea" class="d-flex justify-space-between mb-4">
|
||||
<v-btn
|
||||
v-if="modeConfig.showGithubButton && repoUrl"
|
||||
@@ -483,7 +436,6 @@ const showActionArea = computed(() => {
|
||||
margin-bottom: 16px;
|
||||
font-weight: 600;
|
||||
line-height: 1.25;
|
||||
scroll-margin-top: 12px;
|
||||
}
|
||||
|
||||
:deep(.markdown-body h1) {
|
||||
|
||||
@@ -2,7 +2,6 @@ import { ref, computed, onMounted, nextTick, watch } from 'vue'
|
||||
import axios from 'axios'
|
||||
import { getProviderIcon } from '@/utils/providerUtils'
|
||||
import { askForConfirmation as askForConfirmationDialog, useConfirmDialog } from '@/utils/confirmDialog'
|
||||
import { normalizeTextInput } from '@/utils/inputValue'
|
||||
|
||||
export interface UseProviderSourcesOptions {
|
||||
defaultTab?: string
|
||||
@@ -158,7 +157,7 @@ export function useProviderSources(options: UseProviderSourcesOptions) {
|
||||
})
|
||||
|
||||
const filteredMergedModelEntries = computed(() => {
|
||||
const term = normalizeTextInput(modelSearch.value).trim().toLowerCase()
|
||||
const term = modelSearch.value.trim().toLowerCase()
|
||||
if (!term) return mergedModelEntries.value
|
||||
|
||||
return mergedModelEntries.value.filter((entry: any) => {
|
||||
|
||||
@@ -11,7 +11,7 @@ const translations = ref<Record<string, any>>({});
|
||||
*/
|
||||
export async function initI18n(locale: Locale = 'zh-CN') {
|
||||
currentLocale.value = locale;
|
||||
|
||||
|
||||
// 加载静态翻译数据
|
||||
loadTranslations(locale);
|
||||
}
|
||||
@@ -50,7 +50,7 @@ export function useI18n() {
|
||||
const t = (key: string, params?: Record<string, string | number>): string => {
|
||||
const keys = key.split('.');
|
||||
let value: any = translations.value;
|
||||
|
||||
|
||||
// 遍历键路径
|
||||
for (const k of keys) {
|
||||
if (value && typeof value === 'object' && k in value) {
|
||||
@@ -61,35 +61,35 @@ export function useI18n() {
|
||||
return `[MISSING: ${key}]`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (typeof value !== 'string') {
|
||||
console.warn(`Translation value is not string: ${key}`, value);
|
||||
// 返回带括号的键名,便于在开发时识别类型错误的翻译
|
||||
return `[INVALID: ${key}]`;
|
||||
}
|
||||
|
||||
|
||||
// 此时value确定是string类型
|
||||
let result: string = value;
|
||||
|
||||
|
||||
// 处理参数插值
|
||||
if (params) {
|
||||
result = result.replace(/\{(\w+)\}/g, (match: string, paramKey: string) => {
|
||||
return params[paramKey]?.toString() || match;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
|
||||
// 切换语言
|
||||
const setLocale = async (newLocale: Locale) => {
|
||||
if (newLocale !== currentLocale.value) {
|
||||
currentLocale.value = newLocale;
|
||||
loadTranslations(newLocale);
|
||||
|
||||
|
||||
// 保存到localStorage
|
||||
localStorage.setItem('astrbot-locale', newLocale);
|
||||
|
||||
|
||||
// 触发自定义事件,通知相关页面重新加载配置数据
|
||||
// 这是因为插件适配器的 i18n 数据是通过后端 API 注入的,
|
||||
// 需要根据 Accept-Language 头重新获取
|
||||
@@ -98,16 +98,16 @@ export function useI18n() {
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// 获取当前语言
|
||||
const locale = computed(() => currentLocale.value);
|
||||
|
||||
|
||||
// 获取可用语言列表
|
||||
const availableLocales: Locale[] = ['zh-CN', 'en-US', 'ru-RU'];
|
||||
|
||||
const availableLocales: Locale[] = ['zh-CN', 'en-US'];
|
||||
|
||||
// 检查是否已加载
|
||||
const isLoaded = computed(() => Object.keys(translations.value).length > 0);
|
||||
|
||||
|
||||
return {
|
||||
t,
|
||||
locale,
|
||||
@@ -122,13 +122,13 @@ export function useI18n() {
|
||||
*/
|
||||
export function useModuleI18n(moduleName: string) {
|
||||
const { t } = useI18n();
|
||||
|
||||
|
||||
const tm = (key: string, params?: Record<string, string | number>): string => {
|
||||
// 将斜杠转换为点号以匹配嵌套对象结构
|
||||
const normalizedModuleName = moduleName.replace(/\//g, '.');
|
||||
return t(`${normalizedModuleName}.${key}`, params);
|
||||
};
|
||||
|
||||
|
||||
// 获取原始翻译值(可能是字符串、数组或对象)
|
||||
const getRaw = (key: string): any => {
|
||||
const normalizedModuleName = moduleName.replace(/\//g, '.');
|
||||
@@ -143,10 +143,10 @@ export function useModuleI18n(moduleName: string) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
|
||||
return { tm, getRaw };
|
||||
}
|
||||
|
||||
@@ -155,21 +155,20 @@ export function useModuleI18n(moduleName: string) {
|
||||
*/
|
||||
export function useLanguageSwitcher() {
|
||||
const { locale, setLocale, availableLocales } = useI18n();
|
||||
|
||||
|
||||
const languageOptions = computed(() => [
|
||||
{ value: 'zh-CN', label: '简体中文', flag: '🇨🇳' },
|
||||
{ value: 'en-US', label: 'English', flag: '🇺🇸' },
|
||||
{ value: 'ru-RU', label: 'Русский', flag: '🇷🇺' }
|
||||
{ value: 'en-US', label: 'English', flag: '🇺🇸' }
|
||||
]);
|
||||
|
||||
|
||||
const currentLanguage = computed(() => {
|
||||
return languageOptions.value.find(lang => lang.value === locale.value);
|
||||
});
|
||||
|
||||
|
||||
const switchLanguage = async (newLocale: Locale) => {
|
||||
await setLocale(newLocale);
|
||||
};
|
||||
|
||||
|
||||
return {
|
||||
locale,
|
||||
languageOptions,
|
||||
@@ -221,9 +220,9 @@ function deepMerge(target: Record<string, any>, source: Record<string, any>) {
|
||||
export async function setupI18n() {
|
||||
// 从localStorage获取保存的语言设置
|
||||
const savedLocale = localStorage.getItem('astrbot-locale') as Locale;
|
||||
const initialLocale = savedLocale && ['zh-CN', 'en-US', 'ru-RU'].includes(savedLocale)
|
||||
? savedLocale
|
||||
const initialLocale = savedLocale && ['zh-CN', 'en-US'].includes(savedLocale)
|
||||
? savedLocale
|
||||
: 'zh-CN';
|
||||
|
||||
|
||||
await initI18n(initialLocale);
|
||||
}
|
||||
@@ -78,7 +78,6 @@
|
||||
},
|
||||
"persona": {
|
||||
"description": "Persona",
|
||||
"hint": "Set the default persona for AI conversations. Personas can be managed in the Persona tab.",
|
||||
"provider_settings": {
|
||||
"default_personality": {
|
||||
"description": "Default Persona"
|
||||
@@ -874,8 +873,7 @@
|
||||
]
|
||||
},
|
||||
"regex": {
|
||||
"description": "Segmentation Regular Expression",
|
||||
"hint": "Used to identify split points with a regular expression. Prefer patterns that match separators."
|
||||
"description": "Segmentation Regular Expression"
|
||||
},
|
||||
"split_words": {
|
||||
"description": "Split Word List",
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
{
|
||||
"create": "Создать",
|
||||
"read": "Чтение",
|
||||
"update": "Обновить",
|
||||
"delete": "Удалить",
|
||||
"search": "Поиск",
|
||||
"filter": "Фильтр",
|
||||
"sort": "Сортировка",
|
||||
"export": "Экспорт",
|
||||
"import": "Импорт",
|
||||
"backup": "Резервное копирование",
|
||||
"restore": "Восстановление",
|
||||
"copy": "Копировать",
|
||||
"paste": "Вставить",
|
||||
"cut": "Вырезать",
|
||||
"undo": "Отменить",
|
||||
"redo": "Повторить",
|
||||
"refresh": "Обновить",
|
||||
"submit": "Отправить",
|
||||
"reset": "Сбросить",
|
||||
"clear": "Очистить",
|
||||
"save": "Сохранить",
|
||||
"close": "Закрыть"
|
||||
}
|
||||
@@ -1,133 +0,0 @@
|
||||
{
|
||||
"save": "Сохранить",
|
||||
"cancel": "Отмена",
|
||||
"close": "Закрыть",
|
||||
"copy": "Копировать",
|
||||
"copied": "Скопировано",
|
||||
"copyFailed": "Ошибка копирования",
|
||||
"delete": "Удалить",
|
||||
"edit": "Редактировать",
|
||||
"add": "Добавить",
|
||||
"confirm": "Подтвердить",
|
||||
"loading": "Загрузка...",
|
||||
"success": "Успешно",
|
||||
"error": "Ошибка",
|
||||
"warning": "Внимание",
|
||||
"info": "Информация",
|
||||
"name": "Имя",
|
||||
"description": "Описание",
|
||||
"author": "Автор",
|
||||
"status": "Статус",
|
||||
"actions": "Действия",
|
||||
"enable": "Включить",
|
||||
"disable": "Выключить",
|
||||
"enabled": "Включено",
|
||||
"disabled": "Выключено",
|
||||
"reload": "Перезагрузить",
|
||||
"configure": "Настроить",
|
||||
"install": "Установить",
|
||||
"uninstall": "Удалить",
|
||||
"update": "Обновить",
|
||||
"language": "Язык",
|
||||
"settings": "Настройки",
|
||||
"locale": "JSON",
|
||||
"type": "Тип",
|
||||
"press": "Нажмите",
|
||||
"longPress": "Долгое нажатие",
|
||||
"yes": "Да",
|
||||
"no": "Нет",
|
||||
"imagePreview": "Предпросмотр изображения",
|
||||
"autoDetect": "Автоопределение",
|
||||
"dialog": {
|
||||
"confirmTitle": "Подтверждение",
|
||||
"confirmMessage": "Вы уверены, что хотите выполнить это действие?",
|
||||
"confirmButton": "ОК",
|
||||
"cancelButton": "Отмена"
|
||||
},
|
||||
"restart": {
|
||||
"waiting": "Ожидание перезагрузки AstrBot...",
|
||||
"maxRetriesReached": "Превышено количество попыток проверки статуса. Пожалуйста, проверьте вручную."
|
||||
},
|
||||
"readme": {
|
||||
"title": "Документация плагина",
|
||||
"buttons": {
|
||||
"viewOnGithub": "Открыть репозиторий на GitHub",
|
||||
"refresh": "Обновить"
|
||||
},
|
||||
"loading": "Загрузка README...",
|
||||
"errors": {
|
||||
"fetchFailed": "Не удалось загрузить README",
|
||||
"fetchError": "Произошла ошибка при загрузке README"
|
||||
},
|
||||
"empty": {
|
||||
"title": "У этого плагина нет ссылки на документацию или репозиторий GitHub.",
|
||||
"subtitle": "Пожалуйста, посетите магазин плагинов или свяжитесь с автором для получения дополнительной информации."
|
||||
}
|
||||
},
|
||||
"changelog": {
|
||||
"title": "Журнал изменений",
|
||||
"loading": "Загрузка журнала изменений...",
|
||||
"empty": {
|
||||
"title": "У этого плагина нет журнала изменений",
|
||||
"subtitle": "Разработчики могут добавить файл CHANGELOG.md в директорию плагина"
|
||||
}
|
||||
},
|
||||
"editor": {
|
||||
"fullscreen": "На весь экран",
|
||||
"editingTitle": "Редактирование содержимого"
|
||||
},
|
||||
"templateList": {
|
||||
"addEntry": "Добавить запись",
|
||||
"empty": "Записей нет, выберите шаблон для добавления",
|
||||
"missingTemplate": "Шаблон не найден, пожалуйста, удалите и добавьте заново.",
|
||||
"unknownTemplate": "Неизвестный шаблон"
|
||||
},
|
||||
"list": {
|
||||
"addItemPlaceholder": "Добавьте новый элемент и нажмите Enter",
|
||||
"addButton": "Добавить",
|
||||
"addMore": "Добавить еще",
|
||||
"batchImport": "Массовый импорт",
|
||||
"batchImportTitle": "Массовый импорт",
|
||||
"batchImportLabel": "Один элемент на строку",
|
||||
"batchImportPlaceholder": "Например:\nЭлемент 1\nЭлемент 2\nЭлемент 3",
|
||||
"batchImportHint": "Каждая строка будет считаться отдельным элементом. Пустые строки игнорируются.",
|
||||
"batchImportButton": "Импортировать {count} эл.",
|
||||
"noItems": "Список пуст",
|
||||
"noItemsHint": "Элементов нет. Напишите что-нибудь выше и нажмите Enter.",
|
||||
"inputPlaceholder": "Введите текст и нажмите Enter",
|
||||
"editTitle": "Изменить элемент",
|
||||
"modifyButton": "Изменить"
|
||||
},
|
||||
"itemCard": {
|
||||
"enabled": "Включено",
|
||||
"disabled": "Выключено",
|
||||
"delete": "Удалить",
|
||||
"edit": "Изменить",
|
||||
"copy": "Копировать",
|
||||
"noData": "Нет данных"
|
||||
},
|
||||
"objectEditor": {
|
||||
"dialogTitle": "Изменение пар ключ-значение",
|
||||
"noItems": "Нет элементов",
|
||||
"noParams": "Нет параметров",
|
||||
"presets": "Пресеты",
|
||||
"newKeyLabel": "Имя ключа",
|
||||
"valueTypeLabel": "Тип значения",
|
||||
"keyExists": "Ключ уже существует",
|
||||
"invalidJson": "Некорректный формат JSON",
|
||||
"placeholders": {
|
||||
"keyName": "Ключ",
|
||||
"stringValue": "Строка",
|
||||
"numberValue": "Число",
|
||||
"jsonValue": "JSON"
|
||||
}
|
||||
},
|
||||
"firstNotice": {
|
||||
"title": "Первичная информация",
|
||||
"loading": "Загрузка информации...",
|
||||
"empty": {
|
||||
"title": "Нет информации для отображения",
|
||||
"subtitle": "Файл FIRST_NOTICE.md не найден или пуст."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
{
|
||||
"logoTitle": "Панель управления AstrBot",
|
||||
"version": {
|
||||
"hasNewVersion": "Доступна новая версия AstrBot!",
|
||||
"dashboardHasNewVersion": "Доступна новая версия WebUI!"
|
||||
},
|
||||
"buttons": {
|
||||
"update": "Обновить",
|
||||
"account": "Аккаунт",
|
||||
"theme": {
|
||||
"light": "Светлая тема",
|
||||
"dark": "Темная тема"
|
||||
}
|
||||
},
|
||||
"updateDialog": {
|
||||
"title": "Обновить AstrBot",
|
||||
"currentVersion": "Текущая версия",
|
||||
"status": {
|
||||
"checking": "Проверка обновлений...",
|
||||
"switching": "Переключение версии...",
|
||||
"updating": "Обновление..."
|
||||
},
|
||||
"tabs": {
|
||||
"release": "😊 Релиз"
|
||||
},
|
||||
"updateToLatest": "Обновить до последней версии",
|
||||
"preRelease": "Предварительная версия",
|
||||
"preReleaseWarning": {
|
||||
"title": "Внимание: предварительная версия",
|
||||
"description": "Версии с меткой Pre-release могут содержать неизвестные ошибки. Не рекомендуется использовать в рабочих средах. Если вы обнаружили ошибку, пожалуйста, сообщите о ней в ",
|
||||
"issueLink": "GitHub Issues"
|
||||
},
|
||||
"tip": "💡 ПОДСКАЗКА: ",
|
||||
"tipContinue": "По умолчанию при переключении версии загружаются соответствующие файлы WebUI. Код WebUI находится в директории dashboard, вы можете собрать его самостоятельно с помощью npm.",
|
||||
"dockerTip": "При переключении версии будет предпринята попытка обновить как основной процесс бота, так и панель управления. Если вы используете Docker, вы также можете обновить образ или использовать",
|
||||
"dockerTipLink": "watchtower",
|
||||
"dockerTipContinue": "для автоматического мониторинга и обновления.",
|
||||
"table": {
|
||||
"tag": "Тег",
|
||||
"publishDate": "Дата публикации",
|
||||
"content": "Содержание",
|
||||
"sourceUrl": "Исходный код",
|
||||
"actions": "Действия",
|
||||
"view": "Просмотр",
|
||||
"switch": "Переключить"
|
||||
},
|
||||
"releaseNotes": {
|
||||
"title": "Журнал изменений"
|
||||
},
|
||||
"redirectConfirm": {
|
||||
"title": "Переход по ссылке",
|
||||
"message": "Вы будете перенаправлены на страницу GitHub Releases. Продолжить?",
|
||||
"latestLabel": "Последняя версия",
|
||||
"targetVersion": "Целевая версия:",
|
||||
"currentVersion": "Текущая версия:",
|
||||
"guideTitle": "Рекомендации после перехода:",
|
||||
"guideStep1": "Загрузите пакет, соответствующий архитектуре вашей системы.",
|
||||
"guideStep2": "После завершения установки перезапустите AstrBot.",
|
||||
"guideStep3": "Если вы используете Docker, отдайте приоритет обновлению через образ."
|
||||
},
|
||||
"desktopApp": {
|
||||
"title": "Обновить десктопное приложение",
|
||||
"message": "Проверка и обновление десктопной версии AstrBot.",
|
||||
"currentVersion": "Текущая версия:",
|
||||
"latestVersion": "Последняя версия:",
|
||||
"checking": "Проверка обновлений десктопного приложения...",
|
||||
"hasNewVersion": "Найдена новая версия. Нажмите для подтверждения обновления.",
|
||||
"isLatest": "Установлена последняя версия",
|
||||
"installing": "Загрузка и установка обновления... Приложение будет перезапущено автоматически.",
|
||||
"checkFailed": "Ошибка проверки обновлений. Попробуйте позже.",
|
||||
"installFailed": "Ошибка обновления. Попробуйте позже."
|
||||
},
|
||||
"dashboardUpdate": {
|
||||
"title": "Обновить только панель управления",
|
||||
"currentVersion": "Текущая версия",
|
||||
"hasNewVersion": "Доступна новая версия!",
|
||||
"isLatest": "Установлена последняя версия.",
|
||||
"downloadAndUpdate": "Скачать и обновить"
|
||||
}
|
||||
},
|
||||
"accountDialog": {
|
||||
"title": "Изменить аккаунт",
|
||||
"securityWarning": "Безопасность: Пожалуйста, смените пароль по умолчанию для защиты аккаунта",
|
||||
"form": {
|
||||
"currentPassword": "Текущий пароль",
|
||||
"newPassword": "Новый пароль",
|
||||
"confirmPassword": "Подтвердите новый пароль",
|
||||
"newUsername": "Новое имя пользователя (опционально)",
|
||||
"passwordHint": "Пароль должен быть не менее 8 символов",
|
||||
"confirmPasswordHint": "Введите новый пароль еще раз",
|
||||
"usernameHint": "Оставьте пустым, если не хотите менять имя пользователя",
|
||||
"defaultCredentials": "Логин и пароль по умолчанию: astrbot"
|
||||
},
|
||||
"validation": {
|
||||
"passwordRequired": "Введите пароль",
|
||||
"passwordMinLength": "Пароль должен быть не менее 8 символов",
|
||||
"passwordMatch": "Паролы не совпадают",
|
||||
"usernameMinLength": "Имя пользователя должно быть не менее 3 символов"
|
||||
},
|
||||
"actions": {
|
||||
"save": "Сохранить изменения",
|
||||
"cancel": "Отмена"
|
||||
},
|
||||
"messages": {
|
||||
"updateFailed": "Ошибка обновления, попробуйте еще раз"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
{
|
||||
"welcome": "Добро пожаловать",
|
||||
"dashboard": "Статистика",
|
||||
"platforms": "Боты",
|
||||
"providers": "Провайдеры моделей",
|
||||
"commands": "Команды",
|
||||
"persona": "Персонажи",
|
||||
"subagent": "Субагенты",
|
||||
"toolUse": "Инструменты MCP",
|
||||
"extension": "Плагины",
|
||||
"extensionTabs": {
|
||||
"installed": "Плагины AstrBot",
|
||||
"market": "Магазин плагинов",
|
||||
"mcp": "Серверы MCP",
|
||||
"skills": "Навыки",
|
||||
"components": "Управление поведением"
|
||||
},
|
||||
"config": "Конфигурация",
|
||||
"chat": "Чат",
|
||||
"cron": "Запланированные задачи",
|
||||
"conversation": "Данные диалогов",
|
||||
"sessionManagement": "Пользовательские правила",
|
||||
"console": "Логи платформы",
|
||||
"trace": "Трассировка",
|
||||
"alkaid": "Alkaid Lab",
|
||||
"knowledgeBase": "База знаний",
|
||||
"about": "О программе",
|
||||
"settings": "Настройки",
|
||||
"changelog": "Журнал изменений",
|
||||
"documentation": "Документация",
|
||||
"faq": "FAQ",
|
||||
"github": "GitHub",
|
||||
"drag": "Перетащить",
|
||||
"groups": {
|
||||
"more": "Дополнительно"
|
||||
},
|
||||
"changelogDialog": {
|
||||
"title": "Журнал изменений",
|
||||
"loading": "Загрузка...",
|
||||
"error": "Ошибка загрузки",
|
||||
"notFound": "Журнал изменений для этой версии не найден",
|
||||
"selectVersion": "Выберите версию",
|
||||
"current": "Текущая"
|
||||
},
|
||||
"configTabs": {
|
||||
"normal": "Обычная конфигурация",
|
||||
"system": "Системная конфигурация"
|
||||
}
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
{
|
||||
"knowledgeBaseSelector": {
|
||||
"notSelected": "Не выбрано",
|
||||
"buttonText": "Выбрать базу знаний...",
|
||||
"dialogTitle": "Выбор базы знаний",
|
||||
"loading": "Загрузка...",
|
||||
"noKnowledgeBases": "Базы знаний не найдены",
|
||||
"createKnowledgeBase": "Создать базу знаний",
|
||||
"selectedCount": "Выбрано баз знаний: {count}",
|
||||
"confirmSelection": "ОК",
|
||||
"cancelSelection": "Отмена",
|
||||
"noDescription": "Нет описания",
|
||||
"documentCount": "Документов: {count}",
|
||||
"chunkCount": "Фрагментов: {count}"
|
||||
},
|
||||
"pluginSetSelector": {
|
||||
"notSelected": "Плагины не включены",
|
||||
"allPlugins": "Включить все плагины (*)",
|
||||
"selectedCount": "Выбрано плагинов: {count}",
|
||||
"buttonText": "Выбрать набор плагинов...",
|
||||
"dialogTitle": "Выбор набора плагинов",
|
||||
"loading": "Загрузка...",
|
||||
"enableAll": "Включить все",
|
||||
"enableNone": "Ничего не включать",
|
||||
"customSelect": "Настроить выбор",
|
||||
"noPlugins": "Доступных плагинов нет",
|
||||
"confirmSelection": "ОК",
|
||||
"cancelSelection": "Отмена",
|
||||
"noDescription": "Нет описания",
|
||||
"notActivated": "Не активирован",
|
||||
"note": "*Системные и уже выключенные в настройках плагины не отображаются.",
|
||||
"selectedPluginsLabel": "Выбранные плагины:",
|
||||
"allPluginsLabel": "Все плагины"
|
||||
},
|
||||
"providerSelector": {
|
||||
"notSelected": "Не выбрано",
|
||||
"buttonText": "Выбрать провайдера...",
|
||||
"dialogTitle": "Выбор провайдера",
|
||||
"loading": "Загрузка...",
|
||||
"noProviders": "Доступных провайдеров нет",
|
||||
"confirmSelection": "ОК",
|
||||
"cancelSelection": "Отмена",
|
||||
"clearSelection": "Сбросить выбор",
|
||||
"clearSelectionSubtitle": "Очистить текущий выбор",
|
||||
"unknownType": "Неизвестный тип",
|
||||
"createProvider": "Создать провайдера",
|
||||
"manageProviders": "Управление провайдерами",
|
||||
"selectProviderPool": "Выбрать пул провайдеров...",
|
||||
"selectedCount": "Выбрано провайдеров: {count}"
|
||||
},
|
||||
"personaSelector": {
|
||||
"notSelected": "Не выбрано",
|
||||
"defaultPersona": "Персонаж по умолчанию",
|
||||
"buttonText": "Выбрать персонажа...",
|
||||
"editPersona": "Изменить текущего персонажа",
|
||||
"dialogTitle": "Выбор персонажа",
|
||||
"noDescription": "Нет описания",
|
||||
"noPersonas": "Доступных персонажей нет",
|
||||
"createPersona": "Создать персонажа",
|
||||
"cancelSelection": "Отмена",
|
||||
"confirmSelection": "ОК",
|
||||
"selectPersonaPool": "Выбрать пул персонажей...",
|
||||
"rootFolder": "Все персонажи",
|
||||
"emptyFolder": "Папка пуста"
|
||||
},
|
||||
"personaQuickPreview": {
|
||||
"title": "Быстрый просмотр",
|
||||
"loading": "Загрузка...",
|
||||
"noPersonaSelected": "Персонаж не выбран",
|
||||
"personaNotFound": "Информация о персонаже не найдена",
|
||||
"systemPromptLabel": "Системный промпт",
|
||||
"toolsLabel": "Инструменты",
|
||||
"skillsLabel": "Навыки (Skills)",
|
||||
"originLabel": "Источник",
|
||||
"originNameLabel": "Имя источника",
|
||||
"toolInactive": "Выключено",
|
||||
"toolInactiveTooltip": "Этот инструмент выключен. Включите его в Плагины -> Управление поведением -> Функции.",
|
||||
"allTools": "Доступны все инструменты",
|
||||
"allToolsWithCount": "Доступны все инструменты ({count})",
|
||||
"noTools": "Инструменты не настроены",
|
||||
"allSkills": "Доступны все навыки (Skills)",
|
||||
"allSkillsWithCount": "Доступны все навыки ({count})",
|
||||
"noSkills": "Навыки (Skills) не настроены"
|
||||
},
|
||||
"t2iTemplateEditor": {
|
||||
"buttonText": "Настроить T2I шаблон",
|
||||
"dialogTitle": "Настройка HTML шаблона Text-to-Image",
|
||||
"newTemplateNameLabel": "Введите имя нового шаблона",
|
||||
"nameRequired": "Имя обязательно для заполнения",
|
||||
"selectTemplateLabel": "Выбрать шаблон",
|
||||
"applied": "Применено",
|
||||
"apply": "Применить",
|
||||
"templateEditor": "Редактор шаблона",
|
||||
"new": "Создать",
|
||||
"resetBase": "Сбросить 'base'",
|
||||
"delete": "Удалить",
|
||||
"save": "Сохранить",
|
||||
"livePreview": "Предпросмотр (может отличаться)",
|
||||
"refreshPreview": "Обновить",
|
||||
"syntaxHint": "Поддерживается синтаксис jinja2. Переменные: text | safe (текст для рендеринга), version (версия AstrBot)",
|
||||
"saveAndApply": "Сохранить и применить текущий шаблон",
|
||||
"confirmReset": "Подтверждение сброса",
|
||||
"confirmResetMessage": "Вы уверены, что хотите сбросить шаблон 'base' до значений по умолчанию? Все несохраненные изменения будут потеряны. Это действие необратимо.",
|
||||
"confirmResetButton": "Сбросить",
|
||||
"confirmDelete": "Подтверждение удаления",
|
||||
"confirmDeleteMessage": "Вы уверены, что хотите удалить шаблон '{name}'? Это действие необратимо.",
|
||||
"confirmDeleteButton": "Удалить",
|
||||
"confirmAction": "Подтверждение действия",
|
||||
"confirmApplyMessage": "Вы уверены, что хотите сохранить изменения в '{name}' и сделать его активным шаблоном?"
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"loading": "Загрузка",
|
||||
"success": "Успешно",
|
||||
"error": "Ошибка",
|
||||
"warning": "Внимание",
|
||||
"info": "Информация",
|
||||
"pending": "В ожидании",
|
||||
"processing": "В процессе",
|
||||
"completed": "Завершено",
|
||||
"failed": "Ошибка",
|
||||
"cancelled": "Отменено",
|
||||
"timeout": "Тайм-аут",
|
||||
"connecting": "Подключение",
|
||||
"connected": "Подключено",
|
||||
"disconnected": "Отключено",
|
||||
"online": "В сети",
|
||||
"offline": "Не в сети",
|
||||
"active": "Активен",
|
||||
"inactive": "Неактивен",
|
||||
"ready": "Готов",
|
||||
"busy": "Занят"
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"hero": {
|
||||
"title": "AstrBot",
|
||||
"subtitle": "Проект, рожденный из интереса и любви ❤️",
|
||||
"starButton": "Star этот проект! 🌟",
|
||||
"issueButton": "Сообщить об ошибке"
|
||||
},
|
||||
"contributors": {
|
||||
"title": "Контрибьюторы",
|
||||
"description": "Этот проект поддерживается участниками open-source сообщества. Спасибо каждому за вклад!",
|
||||
"viewLink": "Посмотреть всех участников"
|
||||
},
|
||||
"stats": {
|
||||
"title": "Глобальное развертывание",
|
||||
"license": "AstrBot распространяется по лицензии AGPL v3"
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
{
|
||||
"title": "Лаборатория Alkaid",
|
||||
"subtitle": "Исследуйте передовые возможности AI",
|
||||
"comingSoon": "Этот мир еще впереди, заходите позже!",
|
||||
"page": {
|
||||
"title": "Проект Alkaid.",
|
||||
"subtitle": "AstrBot Alpha Project",
|
||||
"navigation": {
|
||||
"knowledgeBase": "База знаний (Плагин)",
|
||||
"longTermMemory": "Долгосрочная память",
|
||||
"other": "..."
|
||||
}
|
||||
},
|
||||
"features": {
|
||||
"knowledgeBase": "База знаний",
|
||||
"longTermMemory": "Долгосрочная память",
|
||||
"advancedChat": "Продвинутый чат",
|
||||
"multiModal": "Мультимодальность"
|
||||
},
|
||||
"status": {
|
||||
"experimental": "Экспериментально",
|
||||
"beta": "Бета",
|
||||
"stable": "Стабильно",
|
||||
"deprecated": "Устарело"
|
||||
},
|
||||
"sigma": {
|
||||
"subtitle": "Экспериментальный проект AstrBot",
|
||||
"visualization": "Визуализация",
|
||||
"filterUserId": "Фильтр по User ID",
|
||||
"filter": "Фильтр",
|
||||
"resetFilter": "Сброс",
|
||||
"refreshGraph": "Обновить граф",
|
||||
"nodeDetails": "Детали узла",
|
||||
"id": "ID",
|
||||
"type": "Тип",
|
||||
"name": "Имя",
|
||||
"userId": "ID пользователя",
|
||||
"timestamp": "Метка времени",
|
||||
"graphStats": "Статистика графа",
|
||||
"nodeCount": "Узлов",
|
||||
"edgeCount": "Связей",
|
||||
"inDevelopment": "В разработке"
|
||||
}
|
||||
}
|
||||
@@ -1,155 +0,0 @@
|
||||
{
|
||||
"title": "База знаний",
|
||||
"subtitle": "Управление контентом базы знаний и поиск",
|
||||
"documents": {
|
||||
"title": "Список документов",
|
||||
"name": "Имя файла",
|
||||
"size": "Размер",
|
||||
"uploadTime": "Дата загрузки",
|
||||
"status": "Статус",
|
||||
"actions": "Действия"
|
||||
},
|
||||
"management": {
|
||||
"delete": "Удалить",
|
||||
"preview": "Предпросмотр",
|
||||
"download": "Скачать",
|
||||
"reindex": "Переиндексировать"
|
||||
},
|
||||
"notInstalled": {
|
||||
"title": "Плагин базы знаний не установлен",
|
||||
"install": "Установить сейчас"
|
||||
},
|
||||
"empty": {
|
||||
"title": "База знаний пуста. Создайте свою первую базу! 🙂",
|
||||
"create": "Создать базу знаний"
|
||||
},
|
||||
"list": {
|
||||
"title": "Список баз знаний",
|
||||
"create": "Создать базу знаний",
|
||||
"config": "Настройка",
|
||||
"checkUpdate": "Проверить обновления плагина",
|
||||
"updatePlugin": "Обновить плагин до версии {version}",
|
||||
"knowledgeCount": "записей",
|
||||
"tips": "Совет: используйте команду /kb в чате, чтобы узнать, как пользоваться базой!"
|
||||
},
|
||||
"createDialog": {
|
||||
"title": "Создание базы знаний",
|
||||
"nameLabel": "Название",
|
||||
"descriptionLabel": "Описание",
|
||||
"descriptionPlaceholder": "Краткое описание...",
|
||||
"embeddingModelLabel": "Embedding модель",
|
||||
"rerankModelLabel": "Rerank модель",
|
||||
"providerInfo": "Провайдер: {id} | Размерность: {dimensions}",
|
||||
"rerankProviderInfo": "Провайдер: {id}",
|
||||
"tips": "Совет: после выбора Embedding модели не рекомендуется менять провайдера или размерность векторов, так как это сделает текущий индекс нечитаемым.",
|
||||
"cancel": "Отмена",
|
||||
"create": "Создать"
|
||||
},
|
||||
"emojiPicker": {
|
||||
"title": "Выберите иконку",
|
||||
"close": "Закрыть",
|
||||
"categories": {
|
||||
"emotions": "Смайлы",
|
||||
"animals": "Животные и природа",
|
||||
"food": "Еда и напитки",
|
||||
"activities": "Занятия и вещи",
|
||||
"travel": "Места и путешествия",
|
||||
"symbols": "Символы и флаги"
|
||||
}
|
||||
},
|
||||
"contentDialog": {
|
||||
"title": "Управление базой знаний",
|
||||
"embeddingModel": "Embedding модель",
|
||||
"vectorDimension": "Размерность",
|
||||
"usage": "Использование: введите «/kb use {name}» в чате",
|
||||
"tabs": {
|
||||
"upload": "Загрузка файлов",
|
||||
"search": "Поиск",
|
||||
"fromURL": "Импорт из URL"
|
||||
}
|
||||
},
|
||||
"upload": {
|
||||
"title": "Загрузка файлов",
|
||||
"subtitle": "Поддерживаются форматы txt, pdf, word, excel и др.",
|
||||
"dropzone": "Перетащите файлы сюда или нажмите для выбора",
|
||||
"chunkSettings": {
|
||||
"title": "Настройка фрагментации (Chunking)",
|
||||
"tooltip": "Размер фрагмента определяет объем текста в одном блоке. Перекрытие позволяет сохранить контекст между соседними блоками.\nМаленькие фрагменты точнее, но увеличивают объем базы.",
|
||||
"chunkSizeLabel": "Размер фрагмента",
|
||||
"chunkSizeHint": "Длина текста в одном блоке (пусто = по умолчанию)",
|
||||
"overlapLabel": "Перекрытие",
|
||||
"overlapHint": "Нахлест между соседними блоками (пусто = по умолчанию)"
|
||||
},
|
||||
"upload": "Начать загрузку",
|
||||
"uploading": "Загрузка..."
|
||||
},
|
||||
"search": {
|
||||
"queryLabel": "Поиск по базе знаний",
|
||||
"queryPlaceholder": "Введите ключевые слова...",
|
||||
"resultCountLabel": "Количество результатов",
|
||||
"searching": "Поиск...",
|
||||
"resultsTitle": "Результаты поиска",
|
||||
"relevance": "Релевантность",
|
||||
"noResults": "Совпадений не найдено"
|
||||
},
|
||||
"deleteDialog": {
|
||||
"title": "Подтверждение удаления",
|
||||
"confirmText": "Вы уверены, что хотите удалить базу знаний «{name}»?",
|
||||
"warning": "Это действие необратимо. Весь контент базы знаний будет навсегда удален.",
|
||||
"cancel": "Отмена",
|
||||
"delete": "Удалить"
|
||||
},
|
||||
"messages": {
|
||||
"pluginNotAvailable": "Плагин не установлен или недоступен",
|
||||
"pluginNotActivated": "Плагин astrbot_plugin_knowledge_base не включен. Пожалуйста, активируйте его в разделе плагинов и перезапустите AstrBot.",
|
||||
"checkPluginFailed": "Не удалось проверить плагин",
|
||||
"installFailed": "Ошибка установки",
|
||||
"installPluginFailed": "Не удалось установить плагин",
|
||||
"getKnowledgeBaseListFailed": "Ошибка получения списка баз знаний",
|
||||
"knowledgeBaseCreated": "База знаний создана",
|
||||
"createFailed": "Ошибка создания",
|
||||
"createKnowledgeBaseFailed": "Не удалось создать базу знаний",
|
||||
"pleaseEnterKnowledgeBaseName": "Укажите название базы знаний",
|
||||
"pleaseSelectFile": "Пожалуйста, сначала выберите файл",
|
||||
"operationSuccess": "Успешно: {message}",
|
||||
"uploadFailed": "Ошибка загрузки",
|
||||
"fileUploadFailed": "Не удалось загрузить файл",
|
||||
"pleaseEnterSearchContent": "Введите текст для поиска",
|
||||
"noMatchingContent": "Ничего не найдено",
|
||||
"searchFailed": "Ошибка поиска",
|
||||
"searchKnowledgeBaseFailed": "Не удалось выполнить поиск",
|
||||
"deleteTargetNotExists": "Объект для удаления не найден",
|
||||
"knowledgeBaseDeleted": "База знаний удалена",
|
||||
"deleteFailed": "Ошибка удаления",
|
||||
"deleteKnowledgeBaseFailed": "Не удалось удалить базу знаний",
|
||||
"getEmbeddingModelListFailed": "Не удалось загрузить список Embedding моделей",
|
||||
"updateAvailable": "Доступна новая версия: {current} -> {latest}",
|
||||
"pluginUpToDate": "У вас последняя версия плагина",
|
||||
"pluginNotFoundInMarket": "Плагин не найден в магазине",
|
||||
"checkUpdateFailed": "Ошибка проверки обновлений",
|
||||
"updateSuccess": "Плагин успешно обновлен",
|
||||
"updateFailed": "Ошибка обновления",
|
||||
"updatePluginFailed": "Не удалось обновить плагин"
|
||||
},
|
||||
"importFromUrl": {
|
||||
"title": "Импорт из URL",
|
||||
"urlLabel": "Адрес страницы",
|
||||
"urlPlaceholder": "Введите URL для извлечения знаний",
|
||||
"optionsTitle": "Настройки импорта",
|
||||
"tooltip": "Эти параметры управляют извлечением текста из URL.\nЕсли оставить пустыми, будут использованы настройки по умолчанию.\nТекстовая очистка через LLM может занять время.",
|
||||
"useLlmRepairLabel": "Исправление текста через LLM",
|
||||
"useClusteringSummaryLabel": "Кластеризация и суммаризация",
|
||||
"repairLlmProviderIdLabel": "Модель для очистки",
|
||||
"summarizeLlmProviderIdLabel": "Модель для суммаризации",
|
||||
"embeddingProviderIdLabel": "Embedding модель",
|
||||
"chunkSizeLabel": "Размер фрагмента",
|
||||
"chunkOverlapLabel": "Перекрытие",
|
||||
"startImport": "Начать импорт",
|
||||
"importing": "Импорт...",
|
||||
"importSuccess": "Импортировано успешно",
|
||||
"importFailed": "Ошибка импорта",
|
||||
"uploadingChunks": "Текст извлечен, загрузка фрагментов...",
|
||||
"preRequisite": "Примечание: сначала установите плагин astrbot_plugin_url_2_knowledge_base и выполните установку playwright согласно документации.",
|
||||
"allChunksUploaded": "Все фрагменты успешно загружены"
|
||||
}
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
{
|
||||
"title": "Долгосрочная память",
|
||||
"subtitle": "Управление памятью вашего AI-помощника",
|
||||
"memories": {
|
||||
"title": "Список воспоминаний",
|
||||
"content": "Содержание",
|
||||
"importance": "Важность",
|
||||
"createTime": "Дата создания",
|
||||
"lastAccess": "Последнее обращение",
|
||||
"category": "Категория"
|
||||
},
|
||||
"categories": {
|
||||
"personal": "Личное",
|
||||
"preferences": "Предпочтения",
|
||||
"conversations": "История диалогов",
|
||||
"facts": "Факты",
|
||||
"skills": "Навыки"
|
||||
},
|
||||
"importance": {
|
||||
"high": "Высокая",
|
||||
"medium": "Средняя",
|
||||
"low": "Низкая"
|
||||
},
|
||||
"actions": {
|
||||
"view": "Детали",
|
||||
"edit": "Изменить",
|
||||
"delete": "Удалить",
|
||||
"pin": "Закрепить",
|
||||
"unpin": "Открепить"
|
||||
},
|
||||
"filters": {
|
||||
"all": "Все",
|
||||
"category": "По категории",
|
||||
"importance": "По важности",
|
||||
"dateRange": "По периоду",
|
||||
"title": "Фильтр",
|
||||
"userIdLabel": "Фильтр по User ID",
|
||||
"filterButton": "Применить",
|
||||
"resetButton": "Сбросить",
|
||||
"refreshButton": "Обновить граф"
|
||||
},
|
||||
"search": {
|
||||
"title": "Поиск по памяти",
|
||||
"userIdLabel": "ID пользователя",
|
||||
"queryLabel": "Ключевое слово",
|
||||
"searchButton": "Поиск",
|
||||
"resultsTitle": "Результаты поиска",
|
||||
"noResults": "Ничего не найдено",
|
||||
"similarity": "Сходство",
|
||||
"noTextContent": "Нет текста"
|
||||
},
|
||||
"addMemory": {
|
||||
"title": "Добавить данные в память",
|
||||
"textLabel": "Текст воспоминания",
|
||||
"userIdLabel": "ID пользователя",
|
||||
"summarizeLabel": "Нужна суммаризация",
|
||||
"addButton": "Добавить"
|
||||
},
|
||||
"nodeDetails": {
|
||||
"title": "Детали узла",
|
||||
"id": "ID",
|
||||
"type": "Тип",
|
||||
"name": "Имя",
|
||||
"userId": "ID пользователя",
|
||||
"timestamp": "Метка времени"
|
||||
},
|
||||
"graphStats": {
|
||||
"title": "Статистика графа",
|
||||
"nodeCount": "Узлов",
|
||||
"edgeCount": "Связей"
|
||||
},
|
||||
"factDialog": {
|
||||
"title": "Факт из памяти",
|
||||
"id": "ID",
|
||||
"docId": "ID документа",
|
||||
"createdAt": "Создано",
|
||||
"updatedAt": "Обновлено",
|
||||
"metadata": "Метаданные",
|
||||
"metadataKey": "Ключ",
|
||||
"metadataValue": "Значение",
|
||||
"loading": "Загрузка...",
|
||||
"close": "Закрыть",
|
||||
"noValue": "нет",
|
||||
"unknown": "неизвестно"
|
||||
},
|
||||
"messages": {
|
||||
"searchQueryRequired": "Пожалуйста, введите запрос",
|
||||
"searchSuccess": "Найдено записей: {count}",
|
||||
"searchNoResults": "В памяти ничего не найдено",
|
||||
"searchError": "Ошибка поиска",
|
||||
"addSuccess": "Данные успешно добавлены в память!",
|
||||
"addError": "Не удалось добавить данные",
|
||||
"factDetailsError": "Ошибка загрузки деталей",
|
||||
"metadataParseError": "Не удалось разобрать метаданные",
|
||||
"relationNoMemoryData": "У этой связи нет ассоциированных данных"
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"login": "Вход",
|
||||
"username": "Имя пользователя",
|
||||
"password": "Пароль",
|
||||
"defaultHint": "Логин и пароль по умолчанию: astrbot",
|
||||
"logo": {
|
||||
"title": "Панель управления AstrBot",
|
||||
"subtitle": "Добро пожаловать"
|
||||
},
|
||||
"theme": {
|
||||
"switchToDark": "Перейти на темную тему",
|
||||
"switchToLight": "Перейти на светлую тему"
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"messageCount": "Количество сообщений",
|
||||
"time": "Время"
|
||||
}
|
||||
@@ -1,146 +0,0 @@
|
||||
{
|
||||
"title": "Давай пообщаемся!",
|
||||
"subtitle": "Общение с AI-помощником",
|
||||
"input": {
|
||||
"placeholder": "Введите сообщение...",
|
||||
"send": "Отправить",
|
||||
"clear": "Очистить",
|
||||
"upload": "Загрузить файл",
|
||||
"voice": "Голосовой ввод",
|
||||
"recordingPrompt": "Запись... говорите",
|
||||
"chatPrompt": "Давай пообщаемся!",
|
||||
"dropToUpload": "Отпустите, чтобы загрузить файл",
|
||||
"stopGenerating": "Остановить генерацию"
|
||||
},
|
||||
"message": {
|
||||
"user": "Вы",
|
||||
"assistant": "Ассистент",
|
||||
"system": "Система",
|
||||
"error": "Ошибка в сообщении",
|
||||
"loading": "Думаю..."
|
||||
},
|
||||
"voice": {
|
||||
"start": "Начать запись",
|
||||
"stop": "Стоп",
|
||||
"recording": "Запись",
|
||||
"processing": "Обработка...",
|
||||
"error": "Ошибка записи",
|
||||
"listening": "Слушаю...",
|
||||
"speaking": "Говорю",
|
||||
"startRecording": "Начать голосовой ввод",
|
||||
"liveMode": "Общение в реальном времени"
|
||||
},
|
||||
"welcome": {
|
||||
"title": "Добро пожаловать в AstrBot",
|
||||
"subtitle": "Ваш умный помощник",
|
||||
"quickActions": "Быстрые действия",
|
||||
"examples": "Примеры вопросов"
|
||||
},
|
||||
"actions": {
|
||||
"copy": "Копировать",
|
||||
"regenerate": "Перегенерировать",
|
||||
"like": "Нравится",
|
||||
"dislike": "Не нравится",
|
||||
"share": "Поделиться",
|
||||
"newChat": "Новый чат",
|
||||
"deleteChat": "Удалить чат",
|
||||
"editTitle": "Изменить заголовок",
|
||||
"fullscreen": "На весь экран",
|
||||
"exitFullscreen": "Выход из полноэкранного режима",
|
||||
"reply": "Ответить",
|
||||
"providerConfig": "Настройки AI",
|
||||
"toolsUsed": "Использованные инструменты",
|
||||
"toolCallUsed": "Использован инструмент {name}",
|
||||
"pythonCodeAnalysis": "Использован анализ кода Python"
|
||||
},
|
||||
"ipython": {
|
||||
"output": "Вывод"
|
||||
},
|
||||
"conversation": {
|
||||
"newConversation": "Новый чат",
|
||||
"noHistory": "История диалогов пуста",
|
||||
"systemStatus": "Статус системы",
|
||||
"llmService": "Сервис LLM",
|
||||
"speechToText": "Преобразование речи",
|
||||
"editDisplayName": "Изменить имя чата",
|
||||
"displayName": "Имя чата",
|
||||
"displayNameUpdated": "Имя чата обновлено",
|
||||
"displayNameUpdateFailed": "Не удалось обновить имя чата",
|
||||
"confirmDelete": "Вы уверены, что хотите удалить «{name}»? Это действие необратимо."
|
||||
},
|
||||
"modes": {
|
||||
"darkMode": "Темная тема",
|
||||
"lightMode": "Светлая тема"
|
||||
},
|
||||
"shortcuts": {
|
||||
"help": "Справка",
|
||||
"voiceRecord": "Запись голоса",
|
||||
"pasteImage": "Вставить изображение"
|
||||
},
|
||||
"streaming": {
|
||||
"enabled": "Потоковый ответ включен",
|
||||
"disabled": "Потоковый ответ выключен",
|
||||
"on": "Поток",
|
||||
"off": "Обычный"
|
||||
},
|
||||
"transport": {
|
||||
"title": "Протокол передачи",
|
||||
"sse": "SSE",
|
||||
"websocket": "WebSocket"
|
||||
},
|
||||
"config": {
|
||||
"title": "Конфигурация"
|
||||
},
|
||||
"reasoning": {
|
||||
"thinking": "Рассуждение"
|
||||
},
|
||||
"reply": {
|
||||
"replyTo": "В ответ на",
|
||||
"notFound": "Сообщение не найдено"
|
||||
},
|
||||
"project": {
|
||||
"title": "Проект",
|
||||
"create": "Создать проект",
|
||||
"edit": "Изменить проект",
|
||||
"name": "Имя проекта",
|
||||
"emoji": "Иконка (Emoji)",
|
||||
"description": "Описание проекта (опционально)",
|
||||
"noSessions": "В этом проекте пока нет диалогов",
|
||||
"confirmDelete": "Вы уверены, что хотите удалить проект «{title}»? Диалоги внутри проекта не будут удалены."
|
||||
},
|
||||
"time": {
|
||||
"today": "Сегодня",
|
||||
"yesterday": "Вчера"
|
||||
},
|
||||
"stats": {
|
||||
"tokens": "Токены",
|
||||
"inputTokens": "Входящие",
|
||||
"outputTokens": "Исходящие",
|
||||
"cachedTokens": "Кэшированные",
|
||||
"duration": "Время",
|
||||
"ttft": "Время до первого токена"
|
||||
},
|
||||
"refs": {
|
||||
"title": "Ссылки",
|
||||
"sources": "Источники"
|
||||
},
|
||||
"connection": {
|
||||
"title": "Статус подключения",
|
||||
"message": "Системе необходимо переустановить соединение с чатом.",
|
||||
"reasons": "Это может быть вызвано следующими причинами:",
|
||||
"reasonWindowResize": "Изменение размера окна (нормально)",
|
||||
"reasonMultipleTabs": "Страница чата открыта в другой вкладке",
|
||||
"reasonNetworkIssue": "Временная проблема с сетью",
|
||||
"notice": "Примечание: для стабильной работы допускается только одно активное соединение. Если вы используете чат в нескольких вкладках, рекомендуем оставить только одну.",
|
||||
"understand": "Понятно",
|
||||
"status": {
|
||||
"reconnecting": "Переподключение...",
|
||||
"reconnected": "Соединение восстановлено",
|
||||
"failed": "Ошибка подключения, обновите страницу"
|
||||
}
|
||||
},
|
||||
"errors": {
|
||||
"sendMessageFailed": "Ошибка отправки сообщения, попробуйте еще раз",
|
||||
"createSessionFailed": "Ошибка создания сессии, обновите страницу"
|
||||
}
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
{
|
||||
"title": "Управление командами",
|
||||
"summary": {
|
||||
"total": "Всего команд",
|
||||
"disabled": "Отключено",
|
||||
"conflicts": "Конфликты"
|
||||
},
|
||||
"conflictAlert": {
|
||||
"title": "Обнаружены конфликты команд",
|
||||
"description": "Сейчас конфликтуют {count} пары команд. Это может привести к одновременному срабатыванию нескольких плагинов и непредсказуемому поведению.",
|
||||
"hint": "Нажмите «Переименовать», чтобы изменить название конфликтующей команды."
|
||||
},
|
||||
"table": {
|
||||
"headers": {
|
||||
"command": "Команда",
|
||||
"type": "Тип",
|
||||
"plugin": "Плагин",
|
||||
"description": "Описание",
|
||||
"permission": "Доступ",
|
||||
"status": "Статус",
|
||||
"actions": "Действия"
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
"command": "Команда",
|
||||
"group": "Группа команд",
|
||||
"subCommand": "Под-команда"
|
||||
},
|
||||
"status": {
|
||||
"enabled": "Активна",
|
||||
"disabled": "Отключена",
|
||||
"conflict": "Конфликт"
|
||||
},
|
||||
"permission": {
|
||||
"everyone": "Все",
|
||||
"admin": "Админ"
|
||||
},
|
||||
"tooltips": {
|
||||
"enable": "Включить",
|
||||
"disable": "Выключить",
|
||||
"rename": "Переименовать",
|
||||
"viewDetails": "Подробности"
|
||||
},
|
||||
"dialogs": {
|
||||
"rename": {
|
||||
"title": "Переименование команды",
|
||||
"newName": "Новое название",
|
||||
"aliases": "Управление алиасами",
|
||||
"addAlias": "Добавить алиас",
|
||||
"cancel": "Отмена",
|
||||
"confirm": "Подтвердить"
|
||||
},
|
||||
"details": {
|
||||
"title": "Детали команды",
|
||||
"type": "Тип команды",
|
||||
"handler": "Обработчик (Handler)",
|
||||
"module": "Путь к модулю",
|
||||
"originalCommand": "Исходная команда",
|
||||
"effectiveCommand": "Действующая команда",
|
||||
"parentGroup": "Родительская группа",
|
||||
"subCommands": "Под-команды",
|
||||
"aliases": "Алиасы (Синонимы)",
|
||||
"permission": "Требования прав",
|
||||
"conflictStatus": "Статус конфликта"
|
||||
}
|
||||
},
|
||||
"messages": {
|
||||
"toggleSuccess": "Статус команды обновлен",
|
||||
"toggleFailed": "Не удалось изменить статус команды",
|
||||
"renameSuccess": "Команда переименована",
|
||||
"renameFailed": "Ошибка переименования",
|
||||
"loadFailed": "Ошибка загрузки списка команд",
|
||||
"updateSuccess": "Обновлено успешно",
|
||||
"updateFailed": "Ошибка обновления"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "Поиск команд..."
|
||||
},
|
||||
"empty": {
|
||||
"noCommands": "Команд не найдено",
|
||||
"noCommandsDesc": "По вашему запросу не найдено ни одной команды"
|
||||
},
|
||||
"filters": {
|
||||
"all": "Все",
|
||||
"enabled": "Активные",
|
||||
"disabled": "Отключенные",
|
||||
"conflict": "Конфликтующие",
|
||||
"byPlugin": "По плагину",
|
||||
"byType": "По типу",
|
||||
"byPermission": "По правам",
|
||||
"byStatus": "По статусу",
|
||||
"showSystemPlugins": "Показывать системные плагины",
|
||||
"systemPluginConflictHint": "Конфликт затрагивает системный плагин, его нельзя скрыть до разрешения конфликта"
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,129 +0,0 @@
|
||||
{
|
||||
"title": "Конфигурация",
|
||||
"subtitle": "Управление системными настройками",
|
||||
"editor": {
|
||||
"visual": "Визуальный редактор",
|
||||
"code": "Редактор кода",
|
||||
"revertCode": "Отменить изменения",
|
||||
"applyConfig": "Применить",
|
||||
"applyTip": "Кнопка «Применить» временно фиксирует изменения в визуальном редакторе. Чтобы сохранить их на постоянной основе, нажмите кнопку «Сохранить» в правом нижнем углу."
|
||||
},
|
||||
"actions": {
|
||||
"save": "Сохранить",
|
||||
"delete": "Удалить",
|
||||
"add": "Добавить",
|
||||
"reset": "Сбросить настройки",
|
||||
"export": "Экспорт",
|
||||
"import": "Импорт",
|
||||
"validate": "Проверить"
|
||||
},
|
||||
"help": {
|
||||
"documentation": "Документация",
|
||||
"support": "Поддержка",
|
||||
"helpText": "Нужна помощь? См. {documentation} или обратитесь в {support}.",
|
||||
"helpPrefix": "Нужна помощь? См.",
|
||||
"helpMiddle": "или обратитесь в",
|
||||
"helpSuffix": "."
|
||||
},
|
||||
"messages": {
|
||||
"configApplied": "Настройки применены образно. Нажмите «Сохранить» для окончательной записи.",
|
||||
"configApplyError": "Ошибка применения: некорректный формат JSON.",
|
||||
"unsavedChangesNotice": "Есть несохраненные изменения. Пожалуйста, нажмите «Сохранить», чтобы они вступили в силу.",
|
||||
"saveSuccess": "Настройки успешно сохранены",
|
||||
"saveError": "Ошибка при сохранении",
|
||||
"loadError": "Ошибка при загрузке настроек",
|
||||
"deleteSuccess": "Удалено",
|
||||
"deleteError": "Ошибка удаления",
|
||||
"updateSuccess": "Обновлено",
|
||||
"updateError": "Ошибка обновления"
|
||||
},
|
||||
"sections": {
|
||||
"general": "Основные",
|
||||
"advanced": "Расширенные",
|
||||
"security": "Безопасность",
|
||||
"appearance": "Внешний вид",
|
||||
"notification": "Уведомления"
|
||||
},
|
||||
"general": {
|
||||
"botName": "Имя бота",
|
||||
"language": "Язык интерфейса",
|
||||
"timezone": "Часовой пояс",
|
||||
"autoSave": "Автосохранение",
|
||||
"debugMode": "Режим отладки"
|
||||
},
|
||||
"advanced": {
|
||||
"logLevel": "Уровень логирования",
|
||||
"maxConnections": "Макс. соединений",
|
||||
"timeout": "Тайм-аут",
|
||||
"retryAttempts": "Попытки повтора",
|
||||
"cacheSize": "Размер кэша"
|
||||
},
|
||||
"security": {
|
||||
"apiKey": "Ключ API",
|
||||
"allowedHosts": "Разрешенные хосты",
|
||||
"rateLimit": "Лимит запросов",
|
||||
"encryption": "Шифрование"
|
||||
},
|
||||
"configSelection": {
|
||||
"selectConfig": "Выбор конфигурации",
|
||||
"normalConfig": "Обычная",
|
||||
"systemConfig": "Системная"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "Поиск по настройкам (поле/описание/подсказка)",
|
||||
"noResult": "Совпадений не найдено"
|
||||
},
|
||||
"configManagement": {
|
||||
"title": "Управление конфигурациями",
|
||||
"description": "AstrBot поддерживает несколько конфигураций для разных ботов. По умолчанию используется «default».",
|
||||
"newConfig": "Новая конфигурация",
|
||||
"editConfig": "Изменить конфигурацию",
|
||||
"manageConfigs": "Управление файлами...",
|
||||
"configName": "Имя",
|
||||
"fillConfigName": "Введите имя конфигурации",
|
||||
"confirmDelete": "Вы уверены, что хотите удалить конфигурацию «{name}»? Это действие необратимо.",
|
||||
"pleaseEnterName": "Пожалуйста, введите имя",
|
||||
"createFailed": "Ошибка создания конфигурации",
|
||||
"deleteFailed": "Ошибка удаления",
|
||||
"updateFailed": "Ошибка обновления"
|
||||
},
|
||||
"buttons": {
|
||||
"cancel": "Отмена",
|
||||
"create": "Создать",
|
||||
"update": "Обновить"
|
||||
},
|
||||
"codeEditor": {
|
||||
"title": "Редактирование файла"
|
||||
},
|
||||
"fileUpload": {
|
||||
"button": "Файлы",
|
||||
"dialogTitle": "Загруженные файлы",
|
||||
"dropzone": "Загрузить файлы",
|
||||
"allowedTypes": "Разрешенные типы: {types}",
|
||||
"empty": "Файлов нет",
|
||||
"statusMissing": "Файл отсутствует",
|
||||
"statusUnconfigured": "Не в конфиге",
|
||||
"uploadSuccess": "Загружено файлов: {count}",
|
||||
"uploadFailed": "Ошибка загрузки",
|
||||
"loadFailed": "Ошибка получения списка файлов",
|
||||
"fileTooLarge": "Файл слишком велик (макс. {max} МБ): {name}",
|
||||
"deleteSuccess": "Файл удален",
|
||||
"deleteFailed": "Ошибка удаления",
|
||||
"addToConfig": "Добавлено в конфигурацию",
|
||||
"fileCount": "Файлов: {count}",
|
||||
"done": "Готово"
|
||||
},
|
||||
"unsavedChangesWarning": {
|
||||
"dialogTitle": "Несохраненные изменения",
|
||||
"leavePage": "У вас есть несохраненные изменения. Сохранить перед уходом?",
|
||||
"switchConfig": "Переключение конфигурации приведет к потере несохраненных изменений. Сохранить?",
|
||||
"options": {
|
||||
"save": "Сохранить",
|
||||
"saveAndSwitch": "Сохранить и переключить",
|
||||
"discardAndSwitch": "Сбросить и переключить",
|
||||
"closeCard": "Закрыть",
|
||||
"confirm": "ОК",
|
||||
"cancel": "Отмена"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
{
|
||||
"title": "Логи платформы",
|
||||
"autoScroll": {
|
||||
"enabled": "Автопрокрутка включена",
|
||||
"disabled": "Автопрокрутка выключена"
|
||||
},
|
||||
"pipInstall": {
|
||||
"button": "Установить pip-пакет",
|
||||
"dialogTitle": "Установка Pip-пакета",
|
||||
"packageLabel": "*Имя пакета, например: llmtuner",
|
||||
"mirrorLabel": "Использовать зеркало PyPI (опционально)",
|
||||
"mirrorHint": "Приоритет зеркала PyPI > настройки «Зеркало репозитория PyPI»",
|
||||
"installButton": "Установить"
|
||||
},
|
||||
"debugHint": {
|
||||
"text": "Для отображения Debug-логов необходимо установить соответствующий уровень в «Конфигурация → Система → Уровень логирования»"
|
||||
}
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
{
|
||||
"title": "Управление диалогами",
|
||||
"subtitle": "Просмотр и управление историей сообщений",
|
||||
"filters": {
|
||||
"title": "Фильтры",
|
||||
"platform": "ID бота",
|
||||
"type": "Тип",
|
||||
"search": "Поиск по ключевым словам",
|
||||
"reset": "Сбросить"
|
||||
},
|
||||
"history": {
|
||||
"title": "История",
|
||||
"refresh": "Обновить"
|
||||
},
|
||||
"batch": {
|
||||
"deleteSelected": "Удалить выбранные ({count})",
|
||||
"exportSelected": "Экспорт выбранных ({count})"
|
||||
},
|
||||
"pagination": {
|
||||
"itemsPerPage": "на странице",
|
||||
"showingItems": "Показано {start}-{end} из {total}"
|
||||
},
|
||||
"table": {
|
||||
"headers": {
|
||||
"title": "Заголовок диалога",
|
||||
"platform": "ID бота",
|
||||
"type": "Тип сообщения",
|
||||
"cid": "ID диалога",
|
||||
"umo": "Источник сообщения",
|
||||
"sessionId": "ID сессии",
|
||||
"createdAt": "Создан",
|
||||
"updatedAt": "Обновлен",
|
||||
"actions": "Действия"
|
||||
}
|
||||
},
|
||||
"actions": {
|
||||
"view": "Просмотр",
|
||||
"edit": "Редактировать",
|
||||
"delete": "Удалить"
|
||||
},
|
||||
"messageTypes": {
|
||||
"group": "Группа",
|
||||
"friend": "ЛС",
|
||||
"unknown": "Неизвестно"
|
||||
},
|
||||
"status": {
|
||||
"noTitle": "Без заголовка",
|
||||
"unknown": "Неизвестно",
|
||||
"noData": "История диалогов пуста",
|
||||
"emptyContent": "Содержимое диалога пусто",
|
||||
"audioNotSupported": "Ваш браузер не поддерживает воспроизведение аудио."
|
||||
},
|
||||
"dialogs": {
|
||||
"view": {
|
||||
"title": "Детали диалога",
|
||||
"editMode": "Режим редактирования",
|
||||
"previewMode": "Режим просмотра",
|
||||
"saveChanges": "Сохранить изменения",
|
||||
"close": "Закрыть",
|
||||
"confirmClose": "У вас есть несохраненные изменения. Вы уверены, что хотите закрыть?"
|
||||
},
|
||||
"edit": {
|
||||
"title": "Изменить информацию",
|
||||
"titleLabel": "Заголовок диалога",
|
||||
"titlePlaceholder": "Введите заголовок",
|
||||
"cancel": "Отмена",
|
||||
"save": "Сохранить"
|
||||
},
|
||||
"delete": {
|
||||
"title": "Подтверждение удаления",
|
||||
"message": "Вы уверены, что хотите удалить диалог «{title}»? Это действие необратимо.",
|
||||
"cancel": "Отмена",
|
||||
"confirm": "Удалить"
|
||||
},
|
||||
"batchDelete": {
|
||||
"title": "Массовое удаление",
|
||||
"message": "Вы уверены, что хотите удалить {count} выбранных диалогов? Это действие необратимо!",
|
||||
"andMore": "и еще {count}",
|
||||
"cancel": "Отмена",
|
||||
"confirm": "Удалить всё",
|
||||
"warning": "Внимание: удаление нельзя будет отменить!"
|
||||
}
|
||||
},
|
||||
"messages": {
|
||||
"fetchError": "Не удалось загрузить список диалогов",
|
||||
"saveSuccess": "Сохранено",
|
||||
"saveError": "Ошибка сохранения",
|
||||
"deleteSuccess": "Удалено",
|
||||
"deleteError": "Ошибка удаления",
|
||||
"historyError": "Не удалось загрузить историю диалога",
|
||||
"historySaveSuccess": "История сохранена",
|
||||
"historySaveError": "Ошибка сохранения истории",
|
||||
"invalidJson": "Некорректный формат JSON",
|
||||
"noItemSelected": "Сначала выберите диалоги для удаления",
|
||||
"batchDeleteSuccess": "Успешно удалено {count} диалогов",
|
||||
"batchDeleteError": "Ошибка массового удаления",
|
||||
"batchDeletePartial": "Удаление завершено: успешно {deleted}, ошибок {failed}",
|
||||
"exportSuccess": "Экспорт завершен",
|
||||
"exportError": "Ошибка экспорта",
|
||||
"noItemSelectedForExport": "Сначала выберите диалоги для экспорта"
|
||||
}
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
{
|
||||
"page": {
|
||||
"title": "Запланированные задачи",
|
||||
"beta": "Экспериментальные функции",
|
||||
"subtitle": "Управление будущими задачами AstrBot. Бот автоматически проснется, выполнит задачу и отправит результат. Требуется включить «Проактивные способности» в конфигурации.",
|
||||
"proactive": {
|
||||
"supported": "Отправка результатов поддерживается на платформах: {platforms}",
|
||||
"unsupported": "Нет платформ, поддерживающих проактивные сообщения. Включите их в настройках платформ."
|
||||
}
|
||||
},
|
||||
"actions": {
|
||||
"create": "Новая задача",
|
||||
"refresh": "Обновить",
|
||||
"delete": "Удалить",
|
||||
"cancel": "Отмена",
|
||||
"submit": "Создать"
|
||||
},
|
||||
"table": {
|
||||
"title": "Список задач",
|
||||
"empty": "Задач пока нет.",
|
||||
"headers": {
|
||||
"name": "Имя",
|
||||
"type": "Тип",
|
||||
"cron": "Cron",
|
||||
"session": "ID сессии",
|
||||
"nextRun": "Следующий запуск",
|
||||
"lastRun": "Последний запуск",
|
||||
"note": "Описание",
|
||||
"actions": "Действия"
|
||||
},
|
||||
"type": {
|
||||
"once": "Разовая",
|
||||
"recurring": "Повторяющаяся",
|
||||
"activeAgent": "Активный агент",
|
||||
"workflow": "Рабочий процесс",
|
||||
"unknown": "{type}"
|
||||
},
|
||||
"timezoneLocal": "Местное время",
|
||||
"notAvailable": "—"
|
||||
},
|
||||
"form": {
|
||||
"title": "Создать задачу",
|
||||
"chatHint": "Вы можете ставить задачи прямо в чате, AstrBot создаст их автоматически без заполнения этой формы.",
|
||||
"runOnce": "Разовая задача",
|
||||
"name": "Имя задачи",
|
||||
"note": "Описание",
|
||||
"cron": "Cron-выражения",
|
||||
"cronPlaceholder": "0 9 * * *",
|
||||
"runAt": "Время запуска",
|
||||
"session": "Целевая сессия (platform_id:message_type:session_id)",
|
||||
"timezone": "Часовой пояс (опционально, напр. Europe/Moscow)",
|
||||
"enabled": "Включено"
|
||||
},
|
||||
"messages": {
|
||||
"loadFailed": "Ошибка загрузки задач",
|
||||
"updateFailed": "Ошибка обновления",
|
||||
"deleteSuccess": "Удалено",
|
||||
"deleteFailed": "Ошибка удаления",
|
||||
"sessionRequired": "Укажите сессию",
|
||||
"noteRequired": "Заполните описание",
|
||||
"cronRequired": "Укажите Cron-выражение",
|
||||
"runAtRequired": "Выберите время запуска",
|
||||
"createSuccess": "Задача создана",
|
||||
"createFailed": "Ошибка создания"
|
||||
}
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
{
|
||||
"title": "Логи платформы",
|
||||
"subtitle": "Мониторинг и статистика в реальном времени",
|
||||
"lastUpdate": "Последнее обновление",
|
||||
"status": {
|
||||
"loading": "Загрузка...",
|
||||
"dataError": "Ошибка получения данных",
|
||||
"noticeError": "Ошибка получения объявлений",
|
||||
"online": "В сети",
|
||||
"uptime": "Время работы",
|
||||
"memoryUsage": "Память"
|
||||
},
|
||||
"stats": {
|
||||
"totalMessage": {
|
||||
"title": "Всего сообщений",
|
||||
"subtitle": "Все сообщения со всех платформ"
|
||||
},
|
||||
"onlinePlatform": {
|
||||
"title": "Платформы",
|
||||
"subtitle": "Количество подключенных платформ"
|
||||
},
|
||||
"runningTime": {
|
||||
"title": "Время работы",
|
||||
"subtitle": "Общее время работы системы",
|
||||
"format": "{hours} ч. {minutes} мин. {seconds} сек."
|
||||
},
|
||||
"memoryUsage": {
|
||||
"title": "Память",
|
||||
"subtitle": "Использование оперативной памяти",
|
||||
"cpuLoad": "Загрузка CPU",
|
||||
"status": {
|
||||
"good": "Отлично",
|
||||
"normal": "Нормально",
|
||||
"high": "Высокая"
|
||||
}
|
||||
}
|
||||
},
|
||||
"charts": {
|
||||
"messageTrend": {
|
||||
"title": "Тренды сообщений",
|
||||
"subtitle": "Изменение количества сообщений во времени",
|
||||
"totalMessages": "Всего сообщений",
|
||||
"dailyAverage": "В среднем за день",
|
||||
"growthRate": "Скорость роста",
|
||||
"timeLabel": "Время",
|
||||
"messageCount": "Кол-во сообщений",
|
||||
"timeRanges": {
|
||||
"1day": "За 1 день",
|
||||
"3days": "За 3 дня",
|
||||
"1week": "За 7 дней",
|
||||
"1month": "За 30 дней"
|
||||
}
|
||||
},
|
||||
"platformStat": {
|
||||
"title": "Статистика по платформам",
|
||||
"subtitle": "Распределение сообщений по платформам",
|
||||
"total": "Всего",
|
||||
"noData": "Нет данных по платформам",
|
||||
"messageUnit": "шт.",
|
||||
"platformCount": "Кол-во платформ",
|
||||
"mostActive": "Самый активный",
|
||||
"totalPercentage": "Доля от общего числа"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,358 +0,0 @@
|
||||
{
|
||||
"title": "Плагины",
|
||||
"subtitle": "Управление и настройка расширений системы",
|
||||
"tabs": {
|
||||
"installedPlugins": "Плагины AstrBot",
|
||||
"market": "Магазин плагинов",
|
||||
"installedMcpServers": "MCP",
|
||||
"skills": "Навыки",
|
||||
"handlersOperation": "Управление поведением"
|
||||
},
|
||||
"titles": {
|
||||
"installedAstrBotPlugins": "Установленные плагины AstrBot"
|
||||
},
|
||||
"failedPlugins": {
|
||||
"title": "Ошибка загрузки ({count})",
|
||||
"hint": "Эти плагины не удалось загрузить. Вы можете попробовать перезагрузить их или удалить.",
|
||||
"columns": {
|
||||
"plugin": "Плагин",
|
||||
"error": "Ошибка"
|
||||
}
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "Поиск плагинов...",
|
||||
"marketPlaceholder": "Поиск в магазине..."
|
||||
},
|
||||
"filters": {
|
||||
"all": "Все"
|
||||
},
|
||||
"views": {
|
||||
"card": "Плитка",
|
||||
"list": "Список"
|
||||
},
|
||||
"buttons": {
|
||||
"showSystemPlugins": "Показать системные",
|
||||
"hideSystemPlugins": "Скрыть системные",
|
||||
"install": "Установить",
|
||||
"uninstall": "Удалить",
|
||||
"update": "Обновить",
|
||||
"reload": "Перезагрузить",
|
||||
"enable": "Включить",
|
||||
"disable": "Выключить",
|
||||
"configure": "Настроить",
|
||||
"viewInfo": "Детали",
|
||||
"viewDocs": "Документация",
|
||||
"viewRepo": "Репозиторий",
|
||||
"close": "Закрыть",
|
||||
"save": "Сохранить",
|
||||
"saveAndClose": "Сохранить и закрыть",
|
||||
"cancel": "Отмена",
|
||||
"actions": "Действия",
|
||||
"back": "Назад",
|
||||
"selectFile": "Выбрать файл",
|
||||
"refresh": "Обновить",
|
||||
"updateAll": "Обновить все",
|
||||
"deleteSource": "Удалить источник",
|
||||
"reshuffle": "Мне повезет!"
|
||||
},
|
||||
"status": {
|
||||
"enabled": "Включен",
|
||||
"disabled": "Выключен",
|
||||
"system": "Системный",
|
||||
"loading": "Загрузка...",
|
||||
"installed": "Установлен",
|
||||
"unknown": "Неизвестно"
|
||||
},
|
||||
"tooltips": {
|
||||
"enable": "Включить",
|
||||
"disable": "Выключить",
|
||||
"reload": "Перезагрузить",
|
||||
"configure": "Настроить",
|
||||
"viewInfo": "Просмотр поведения",
|
||||
"viewDocs": "Документация",
|
||||
"update": "Обновить",
|
||||
"uninstall": "Удалить"
|
||||
},
|
||||
"table": {
|
||||
"headers": {
|
||||
"name": "Имя",
|
||||
"description": "Описание",
|
||||
"version": "Версия",
|
||||
"author": "Автор",
|
||||
"status": "Статус",
|
||||
"actions": "Действия",
|
||||
"stars": "Звезды",
|
||||
"lastUpdate": "Обновлен",
|
||||
"tags": "Теги",
|
||||
"eventType": "Тип события",
|
||||
"specificType": "Тип",
|
||||
"trigger": "Триггер"
|
||||
}
|
||||
},
|
||||
"empty": {
|
||||
"noPlugins": "Плагины не найдены",
|
||||
"noPluginsDesc": "Попробуйте установить новые плагины или включите отображение системных."
|
||||
},
|
||||
"market": {
|
||||
"recommended": "🥳 Рекомендуем",
|
||||
"allPlugins": "📦 Все плагины",
|
||||
"showFullName": "Полное имя",
|
||||
"devDocs": "Документация для разработчиков",
|
||||
"submitRepo": "Добавить репозиторий",
|
||||
"customSource": "Свои источники",
|
||||
"source": "Источник",
|
||||
"availableSources": "Доступные источники",
|
||||
"sourceManagement": "Управление источниками",
|
||||
"addSource": "Добавить источник",
|
||||
"sourceName": "Имя",
|
||||
"sourceUrl": "Исходный URL",
|
||||
"defaultSource": "Источник по умолчанию",
|
||||
"removeSource": "Удалить источник",
|
||||
"confirmRemoveSource": "Вы уверены, что хотите удалить этот источник плагинов?",
|
||||
"sourceAdded": "Источник успешно добавлен",
|
||||
"sourceRemoved": "Источник удален",
|
||||
"sourceError": "Ошибка операции",
|
||||
"selectSource": "Выбрать источник",
|
||||
"currentSource": "Текущий источник",
|
||||
"editSource": "Изменить источник",
|
||||
"sourceUpdated": "Источник обновлен",
|
||||
"defaultOfficialSource": "Официальный источник",
|
||||
"sourceExists": "Этот источник уже есть в списке",
|
||||
"installPlugin": "Установить плагин",
|
||||
"randomPlugins": "🎲 Случайные плагины",
|
||||
"showRandomPlugins": "Показать случайные",
|
||||
"hideRandomPlugins": "Скрыть случайные",
|
||||
"sourceSafetyWarning": "Даже при использовании источников по умолчанию мы не можем гарантировать 100% безопасность и стабильность сторонних плагинов. Пожалуйста, будьте внимательны."
|
||||
},
|
||||
"sort": {
|
||||
"by": "Сортировать по",
|
||||
"default": "По умолчанию",
|
||||
"installTime": "Дате установки",
|
||||
"name": "Имени",
|
||||
"stars": "Звездам",
|
||||
"author": "Автору",
|
||||
"updated": "Дате обновления",
|
||||
"updateStatus": "Статусу обновления",
|
||||
"ascending": "По возрастанию",
|
||||
"descending": "По убыванию"
|
||||
},
|
||||
"tags": {
|
||||
"danger": "Опасно"
|
||||
},
|
||||
"dialogs": {
|
||||
"error": {
|
||||
"title": "Ошибка",
|
||||
"checkConsole": "Подробности смотрите в логах платформы"
|
||||
},
|
||||
"config": {
|
||||
"title": "Настройка плагина",
|
||||
"noConfig": "У этого плагина нет настраиваемых параметров"
|
||||
},
|
||||
"loading": {
|
||||
"title": "Загрузка...",
|
||||
"logs": "Логи"
|
||||
},
|
||||
"uninstall": {
|
||||
"title": "Подтверждение удаления",
|
||||
"message": "Вы уверены, что хотите удалить этот плагин?",
|
||||
"deleteConfig": "Удалить файл конфигурации плагина",
|
||||
"deleteData": "Удалить сохраненные данные плагина",
|
||||
"configHint": "Конфиг находится в data/config",
|
||||
"dataHint": "Данные находятся в data/plugin_data и data/plugins_data"
|
||||
},
|
||||
"install": {
|
||||
"title": "Установка плагина",
|
||||
"fromFile": "Из файла",
|
||||
"fromUrl": "По ссылке",
|
||||
"supportPlatformsCount": "Поддерживает платформ: {count}"
|
||||
},
|
||||
"danger_warning": {
|
||||
"title": "Внимание!",
|
||||
"message": "Этот плагин может содержать небезопасный код или функции, которые могут привести к нестабильности системы или потере данных. Вы уверены, что хотите продолжить установку?",
|
||||
"confirm": "Продолжить",
|
||||
"cancel": "Отмена"
|
||||
},
|
||||
"versionCompatibility": {
|
||||
"title": "Предупреждение о версии",
|
||||
"message": "Требуемая плагином версия AstrBot не совпадает с вашей текущей версией. Вы можете продолжить установку на свой страх и риск.",
|
||||
"confirm": "Игнорировать и установить",
|
||||
"cancel": "Отмена"
|
||||
},
|
||||
"forceUpdate": {
|
||||
"title": "Новых версий не найдено",
|
||||
"message": "Новых версий не обнаружено. Выполнить принудительную переустановку из удаленного репозитория?",
|
||||
"confirm": "Принудительно"
|
||||
},
|
||||
"updateAllConfirm": {
|
||||
"title": "Обновить всё",
|
||||
"message": "Обновить все плагины ({count} шт.)? Это может занять некоторое время.",
|
||||
"confirm": "Подтвердить"
|
||||
}
|
||||
},
|
||||
"messages": {
|
||||
"uninstalling": "Удаление",
|
||||
"refreshing": "Обновление списка плагинов...",
|
||||
"refreshSuccess": "Список плагинов обновлен",
|
||||
"refreshFailed": "Ошибка при обновлении списка",
|
||||
"operationFailed": "Ошибка операции",
|
||||
"reloadSuccess": "Перезагрузка завершена",
|
||||
"reloadFailed": "Ошибка перезагрузки",
|
||||
"updateSuccess": "Обновление завершено",
|
||||
"addSuccess": "Успешно добавлено",
|
||||
"saveSuccess": "Сохранено",
|
||||
"deleteSuccess": "Удалено",
|
||||
"installing": "Установка из файла...",
|
||||
"installingFromUrl": "Установка по ссылке...",
|
||||
"installFailed": "Ошибка установки:",
|
||||
"getMarketDataFailed": "Ошибка получения данных магазина:",
|
||||
"hasUpdate": "Доступно обновление:",
|
||||
"confirmDelete": "Вы уверены, что хотите удалить плагин?",
|
||||
"fillUrlOrFile": "Укажите ссылку или выберите файл",
|
||||
"dontFillBoth": "Пожалуйста, используйте либо ссылку, либо файл, но не оба сразу",
|
||||
"supportedFormats": "Поддерживаются файлы плагинов в формате .zip",
|
||||
"updateAllSuccess": "Все плагины успешно обновлены",
|
||||
"updateAllFailed": "Ошибок при обновлении: {failed} из {total}:",
|
||||
"fillSourceNameAndUrl": "Пожалуйста, введите имя и адрес источника",
|
||||
"invalidUrl": "Введите корректный URL",
|
||||
"enterJsonUrl": "Введите URL, возвращающий список плагинов в формате JSON"
|
||||
},
|
||||
"upload": {
|
||||
"fromFile": "Загрузить файл",
|
||||
"fromUrl": "Указать ссылку",
|
||||
"selectFile": "Выбрать файл",
|
||||
"enterUrl": "Ссылка на репозиторий"
|
||||
},
|
||||
"skills": {
|
||||
"modeLocal": "Локальные навыки",
|
||||
"modeNeo": "Навыки Neo",
|
||||
"actions": "Действия",
|
||||
"upload": "Загрузить навыки",
|
||||
"refresh": "Обновить",
|
||||
"empty": "Навыки не найдены",
|
||||
"emptyHint": "Пожалуйста, загрузите архив с навыками",
|
||||
"uploadDialogTitle": "Загрузка навыков",
|
||||
"uploadHint": "Поддерживается массовая загрузка zip-архивов. Вы также можете перетащить файлы в это окно. Система автоматически проверит структуру каждого архива.",
|
||||
"structureRequirement": "Архив должен содержать одну корневую папку (например, `skillname/`), внутри которой обязательно должен находиться файл `SKILL.md`.",
|
||||
"abilityMultiple": "Поддержка массовой загрузки",
|
||||
"abilityValidate": "Автопроверка `SKILL.md`",
|
||||
"abilitySkip": "Пропуск дубликатов",
|
||||
"selectFile": "Выбрать файл",
|
||||
"selectFiles": "Выбрать файлы",
|
||||
"dropzoneTitle": "Перетащите zip-файлы сюда",
|
||||
"dropzoneAction": "или нажмите, чтобы выбрать файлы на компьютере",
|
||||
"dropzoneHint": "Система проверит структуру архивов перед загрузкой",
|
||||
"fileListTitle": "Очередь загрузки",
|
||||
"fileListEmpty": "Здесь будет отображаться статус проверки и загрузки файлов",
|
||||
"uploading": "Загрузка...",
|
||||
"batchResultTitle": "Результаты загрузки",
|
||||
"batchResultSummary": "Всего: {total}, успешно: {success}",
|
||||
"batchSuccessList": "Успешно загружено",
|
||||
"batchFailedList": "Ошибка загрузки",
|
||||
"confirm": "ОК",
|
||||
"confirmUpload": "Начать загрузку",
|
||||
"cancel": "Отмена",
|
||||
"statusWaiting": "В очереди",
|
||||
"statusUploading": "Загрузка...",
|
||||
"statusSuccess": "Готово",
|
||||
"statusError": "Ошибка структуры",
|
||||
"statusSkipped": "Пропущено",
|
||||
"summaryTotal": "Всего: {count}",
|
||||
"summaryReady": "Готовы: {count}",
|
||||
"summarySuccess": "Успешно: {count}",
|
||||
"summaryFailed": "Ошибок: {count}",
|
||||
"summarySkipped": "Дубликатов: {count}",
|
||||
"validationReady": "Ожидает загрузки (проверка структуры будет выполнена автоматически)",
|
||||
"validationZipOnly": "Допускаются только zip-архивы",
|
||||
"validationDuplicate": "Файл уже есть в списке, пропуск",
|
||||
"validationUploading": "Проверка и загрузка...",
|
||||
"validationUploadFailed": "Ошибка загрузки, попробуйте еще раз",
|
||||
"validationUploadedAs": "Установлено как {name}",
|
||||
"validationNoResult": "Результат не получен, проверьте логи платформы",
|
||||
"noDescription": "Нет описания",
|
||||
"path": "Путь",
|
||||
"uploadSuccess": "Успешно загружено",
|
||||
"uploadFailed": "Ошибка загрузки",
|
||||
"download": "Скачать",
|
||||
"downloadSuccess": "Скачивание начато",
|
||||
"downloadFailed": "Ошибка скачивания",
|
||||
"loadFailed": "Не удалось загрузить навыки",
|
||||
"updateSuccess": "Обновлено",
|
||||
"updateFailed": "Ошибка обновления",
|
||||
"deleteTitle": "Подтверждение удаления",
|
||||
"deleteMessage": "Вы уверены, что хотите удалить этот навык?",
|
||||
"deleteSuccess": "Удалено",
|
||||
"deleteFailed": "Ошибка удаления",
|
||||
"neoSkillKey": "Фильтр по ключу",
|
||||
"neoStatus": "Статус кандидата",
|
||||
"neoStage": "Этап публикации",
|
||||
"neoFilterHint": "Фильтрация записей о публикации",
|
||||
"neoAll": "Все",
|
||||
"neoCandidates": "Кандидаты Neo",
|
||||
"neoReleases": "Релизы Neo",
|
||||
"neoLoadFailed": "Ошибка загрузки данных Neo Skills",
|
||||
"neoPass": "Одобрить",
|
||||
"neoReject": "Отклонить",
|
||||
"neoEvaluateSuccess": "Оценка обновлена",
|
||||
"neoEvaluateFailed": "Ошибка обновления оценки",
|
||||
"neoPromoteSuccess": "Опубликовано",
|
||||
"neoPromoteFailed": "Ошибка публикации",
|
||||
"neoRollback": "Откат",
|
||||
"neoRollbackSuccess": "Откат выполнен",
|
||||
"neoRollbackFailed": "Ошибка отката",
|
||||
"neoDeactivate": "Деактивация",
|
||||
"neoDeactivateSuccess": "Деактивировано",
|
||||
"neoDeactivateFailed": "Ошибка деактивации",
|
||||
"neoSync": "Синхронизация",
|
||||
"neoSyncSuccess": "Синхронизировано",
|
||||
"neoSyncFailed": "Ошибка синхронизации",
|
||||
"neoDelete": "Удалить",
|
||||
"neoDeleteSuccess": "Удалено",
|
||||
"neoDeleteFailed": "Ошибка удаления",
|
||||
"neoPayloadTitle": "Детали Neo Payload",
|
||||
"neoPayloadFailed": "Ошибка чтения Payload",
|
||||
"runtimeNoneWarning": "Среда выполнения Computer Use не задана. Навыки могут не работать, так как нет активного окружения.",
|
||||
"runtimeHint": "Установите среду выполнения в «local» или «sandbox» в настройках способностей использования компьютера.",
|
||||
"neoRuntimeRequired": "Neo Skills доступны только в среде sandbox с драйвером shipyard_neo.",
|
||||
"sourceLocalOnly": "Локальный навык",
|
||||
"sourceSandboxOnly": "Предустановленный Sandbox навык",
|
||||
"sourceBoth": "Локальный + Sandbox",
|
||||
"sandboxDiscoveryPending": "Предустановленные Sandbox навыки не найдены. Запустите сессию Sandbox хотя бы один раз.",
|
||||
"sandboxPresetReadonly": "Предустановленные навыки Sandbox доступны только для чтения и не могут быть удалены здесь."
|
||||
},
|
||||
"card": {
|
||||
"actions": {
|
||||
"pluginConfig": "Настройки",
|
||||
"uninstallPlugin": "Удалить",
|
||||
"reloadPlugin": "Перезагрузить",
|
||||
"togglePlugin": "Плагин",
|
||||
"viewHandlers": "Действия",
|
||||
"updateTo": "Обновить до",
|
||||
"reinstall": "Переустановить"
|
||||
},
|
||||
"status": {
|
||||
"hasUpdate": "Доступно обновление",
|
||||
"disabled": "Плагин выключен",
|
||||
"handlersCount": "действий",
|
||||
"supportPlatform": "Платформы",
|
||||
"supportPlatformsCount": "Платформ: {count}",
|
||||
"astrbotVersion": "Требуемая версия AstrBot"
|
||||
},
|
||||
"alt": {
|
||||
"logo": "логотип",
|
||||
"extensionIcon": "иконка расширения"
|
||||
},
|
||||
"errors": {
|
||||
"confirmNotRegistered": "$confirm не зарегистрирован"
|
||||
}
|
||||
},
|
||||
"conflicts": {
|
||||
"title": "Конфликт команд",
|
||||
"message": "Обнаружены конфликтующие команды. Это может привести к некорректной работе. Рекомендуется разрешить конфликты в панели «Управление командами».",
|
||||
"pairs": "конфликтующих пар",
|
||||
"goToManage": "Управление",
|
||||
"later": "Позже"
|
||||
},
|
||||
"pluginChangelog": {
|
||||
"menuTitle": "Журнал изменений"
|
||||
}
|
||||
}
|
||||
@@ -1,118 +0,0 @@
|
||||
{
|
||||
"title": "Детали базы знаний",
|
||||
"backToList": "К списку",
|
||||
"tabs": {
|
||||
"overview": "Обзор",
|
||||
"documents": "Документы",
|
||||
"retrieval": "Поиск",
|
||||
"sessions": "Сессии",
|
||||
"settings": "Настройки"
|
||||
},
|
||||
"overview": {
|
||||
"title": "Информация",
|
||||
"name": "Название",
|
||||
"description": "Описание",
|
||||
"emoji": "Иконка",
|
||||
"createdAt": "Создана",
|
||||
"updatedAt": "Обновлена",
|
||||
"stats": "Статистика",
|
||||
"docCount": "Количество документов",
|
||||
"chunkCount": "Количество фрагментов",
|
||||
"embeddingModel": "Embedding модель",
|
||||
"rerankModel": "Rerank модель",
|
||||
"notSet": "не выбрано"
|
||||
},
|
||||
"documents": {
|
||||
"title": "Список документов",
|
||||
"upload": "Загрузить",
|
||||
"empty": "Документов нет",
|
||||
"name": "Имя файла",
|
||||
"type": "Тип",
|
||||
"size": "Размер",
|
||||
"chunks": "Фрагменты",
|
||||
"createdAt": "Дата загрузки",
|
||||
"actions": "Действия",
|
||||
"view": "Смотреть",
|
||||
"delete": "Удалить",
|
||||
"deleteConfirm": "Вы уверены, что хотите удалить «{name}»?",
|
||||
"deleteWarning": "Это удалит файл и все его фрагменты из индекса.",
|
||||
"uploading": "Загрузка...",
|
||||
"uploadSuccess": "Файл успешно загружен",
|
||||
"uploadFailed": "Ошибка загрузки",
|
||||
"deleteSuccess": "Файл удален",
|
||||
"deleteFailed": "Ошибка удаления"
|
||||
},
|
||||
"upload": {
|
||||
"title": "Добавление контента",
|
||||
"selectFile": "Файл",
|
||||
"dropzone": "Нажмите или перетащите файл сюда",
|
||||
"supportedFormats": "Форматы: ",
|
||||
"maxSize": "Максимум: 128MB",
|
||||
"chunkSettings": "Фрагментация",
|
||||
"batchSettings": "Пакетная обработка",
|
||||
"cleaningSettings": "Очистка данных",
|
||||
"enableCleaning": "Включить очистку контента",
|
||||
"cleaningProvider": "Сервис для очистки",
|
||||
"cleaningProviderHint": "LLM провайдер для суммаризации и извлечения смыслов из веб-страниц",
|
||||
"chunkSize": "Размер чанка",
|
||||
"chunkSizeHint": "Символов в блоке (по умолчанию: 512)",
|
||||
"chunkOverlap": "Перекрытие",
|
||||
"chunkOverlapHint": "Перекрытие между блоками (по умолчанию: 50)",
|
||||
"batchSize": "Размер пакета",
|
||||
"batchSizeHint": "Блоков за один запрос (по умолчанию: 32)",
|
||||
"tasksLimit": "Лимит задач",
|
||||
"tasksLimitHint": "Макс. параллельных потоков (по умолчанию: 3)",
|
||||
"maxRetries": "Попытки",
|
||||
"maxRetriesHint": "Повторов при сбое (по умолчанию: 3)",
|
||||
"cancel": "Отмена",
|
||||
"submit": "Загрузить",
|
||||
"fileRequired": "Пожалуйста, выберите файл",
|
||||
"fileUpload": "Загрузка файла",
|
||||
"fromUrl": "Из URL",
|
||||
"urlPlaceholder": "Ссылка на веб-страницу",
|
||||
"urlRequired": "Введите URL",
|
||||
"urlHint": "Контент будет автоматически извлечен со страницы. Убедитесь, что сайт разрешает доступ роботам.",
|
||||
"beta": "Бета-версия"
|
||||
},
|
||||
"retrieval": {
|
||||
"title": "Поиск и проверка",
|
||||
"subtitle": "Проверьте качество поиска (Dense & Sparse) по вашей базе знаний",
|
||||
"query": "Тестовый запрос",
|
||||
"queryPlaceholder": "Что вы хотите найти?",
|
||||
"search": "Найти",
|
||||
"searching": "Ищем...",
|
||||
"results": "Результаты поиска",
|
||||
"noResults": "Релевантный контент не найден",
|
||||
"tryDifferentQuery": "Попробуйте изменить формулировку запроса",
|
||||
"settings": "Параметры поиска",
|
||||
"topK": "Количество результатов",
|
||||
"topKHint": "Сколько фрагментов возвращать",
|
||||
"enableRerank": "Включить Rerank",
|
||||
"enableRerankHint": "Применить переранжирование для повышения точности",
|
||||
"score": "Вес (Score)",
|
||||
"document": "Документ",
|
||||
"chunk": "Фрагмент #{index}",
|
||||
"content": "Текст",
|
||||
"charCount": "{count} симв.",
|
||||
"searchSuccess": "Поиск завершен, найдено: {count}",
|
||||
"searchFailed": "Ошибка выполнения поиска",
|
||||
"queryRequired": "Введите поисковый запрос"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Общие настройки базы",
|
||||
"basic": "Основные",
|
||||
"retrieval": "Поиск",
|
||||
"chunkSize": "Размер чанка",
|
||||
"chunkOverlap": "Перекрытие",
|
||||
"topKDense": "Вернуть (Dense)",
|
||||
"topKSparse": "Вернуть (Sparse)",
|
||||
"topMFinal": "Итоговый результат",
|
||||
"enableRerank": "Включить Rerank",
|
||||
"embeddingProvider": "Провайдер Embedding",
|
||||
"rerankProvider": "Провайдер Rerank",
|
||||
"save": "Сохранить",
|
||||
"saveSuccess": "Настройки сохранены",
|
||||
"saveFailed": "Ошибка сохранения",
|
||||
"tips": "Внимание! Изменение этих параметров повлияет на будущую выдачу базы знаний."
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
{
|
||||
"title": "Просмотр документа",
|
||||
"backToKB": "К базе знаний",
|
||||
"info": {
|
||||
"title": "Информация о документе",
|
||||
"name": "Имя файла",
|
||||
"type": "Формат",
|
||||
"size": "Размер",
|
||||
"chunkCount": "Количество фрагментов",
|
||||
"createdAt": "Загружен"
|
||||
},
|
||||
"chunks": {
|
||||
"title": "Фрагменты текста",
|
||||
"empty": "Фрагменты не найдены",
|
||||
"index": "Индекс",
|
||||
"content": "Текст",
|
||||
"charCount": "Символов",
|
||||
"actions": "Действия",
|
||||
"view": "Детали",
|
||||
"edit": "Изменить",
|
||||
"delete": "Удалить",
|
||||
"preview": "Обзор",
|
||||
"search": "Поиск по документу",
|
||||
"searchPlaceholder": "Найти во фрагментах...",
|
||||
"showing": "Показано",
|
||||
"deleteConfirm": "Удалить этот фрагмент?",
|
||||
"deleteSuccess": "Фрагмент удален",
|
||||
"deleteFailed": "Ошибка удаления"
|
||||
},
|
||||
"edit": {
|
||||
"title": "Редактирование фрагмента",
|
||||
"content": "Текст",
|
||||
"cancel": "Отмена",
|
||||
"save": "Сохранить",
|
||||
"saveSuccess": "Фрагмент обновлен",
|
||||
"saveFailed": "Ошибка сохранения"
|
||||
},
|
||||
"delete": {
|
||||
"title": "Удаление",
|
||||
"confirmText": "Вы уверены?",
|
||||
"warning": "Удаление фрагмента может ухудшить качество ответов AI по этой теме.",
|
||||
"cancel": "Отмена",
|
||||
"confirm": "Удалить",
|
||||
"deleteSuccess": "Удаление выполнено",
|
||||
"deleteFailed": "Ошибка удаления"
|
||||
},
|
||||
"view": {
|
||||
"title": "Детальный просмотр",
|
||||
"index": "Индекс",
|
||||
"content": "Текст",
|
||||
"charCount": "Символов",
|
||||
"vecDocId": "ID вектора",
|
||||
"close": "Закрыть"
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
{
|
||||
"title": "Управление базами знаний",
|
||||
"subtitle": "Централизованное управление всеми знаниями AstrBot",
|
||||
"list": {
|
||||
"title": "Мои базы знаний",
|
||||
"subtitle": "Все доступные коллекции знаний",
|
||||
"create": "Создать базу",
|
||||
"refresh": "Обновить",
|
||||
"empty": "Баз знаний пока нет",
|
||||
"loading": "Загрузка...",
|
||||
"documents": "док.",
|
||||
"chunks": "фрагм.",
|
||||
"sessionConfig": "Профиль"
|
||||
},
|
||||
"card": {
|
||||
"edit": "Изменить",
|
||||
"delete": "Удалить",
|
||||
"open": "Открыть",
|
||||
"docCount": "Документов: {count}",
|
||||
"chunkCount": "Фрагментов: {count}"
|
||||
},
|
||||
"create": {
|
||||
"title": "Создание базы знаний",
|
||||
"nameLabel": "Название",
|
||||
"namePlaceholder": "Придумайте имя для базы",
|
||||
"descriptionLabel": "Описание",
|
||||
"descriptionPlaceholder": "Для чего нужна эта база?",
|
||||
"emojiLabel": "Иконка",
|
||||
"embeddingModelLabel": "Embedding модель",
|
||||
"rerankModelLabel": "Rerank модель (опционально)",
|
||||
"providerInfo": "Провайдер: {id} | Размерность: {dimensions}",
|
||||
"rerankProviderInfo": "Провайдер: {id}",
|
||||
"cancel": "Отмена",
|
||||
"submit": "Создать",
|
||||
"nameRequired": "Введите название базы знаний"
|
||||
},
|
||||
"edit": {
|
||||
"title": "Редактирование",
|
||||
"submit": "Сохранить"
|
||||
},
|
||||
"delete": {
|
||||
"title": "Удаление",
|
||||
"confirmText": "Вы уверены, что хотите удалить базу знаний «{name}»?",
|
||||
"warning": "Это действие необратимо. Все документы, фрагменты и настройки будут навсегда удалены.",
|
||||
"cancel": "Отмена",
|
||||
"confirm": "Удалить"
|
||||
},
|
||||
"emoji": {
|
||||
"title": "Выберите иконку",
|
||||
"close": "Закрыть",
|
||||
"categories": {
|
||||
"books": "Книги и документы",
|
||||
"emotions": "Эмоции",
|
||||
"objects": "Вещи",
|
||||
"symbols": "Символы"
|
||||
}
|
||||
},
|
||||
"messages": {
|
||||
"createSuccess": "База знаний создана",
|
||||
"createFailed": "Ошибка создания",
|
||||
"updateSuccess": "Обновлено успешно",
|
||||
"updateFailed": "Ошибка обновления",
|
||||
"deleteSuccess": "Удалено успешно",
|
||||
"deleteFailed": "Ошибка удаления",
|
||||
"loadError": "Не удалось загрузить список"
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
{
|
||||
"dialog": {
|
||||
"title": "Помощник по миграции",
|
||||
"warning": "👋 Добро пожаловать в v4.0.0! В этой версии мы оптимизировали формат хранения данных. Обнаружена необходимость миграции базы данных.",
|
||||
"loading": "Загрузка списка платформ...",
|
||||
"loadError": "Ошибка загрузки, попробуйте еще раз",
|
||||
"noPlatforms": "Конфигурации платформ не найдены",
|
||||
"retry": "Повторить",
|
||||
"startMigration": "Начать миграцию",
|
||||
"migrating": "Выполняется миграция...",
|
||||
"migratingSubtitle": "Пожалуйста, подождите. Не закрывайте это окно до завершения процесса.",
|
||||
"migrationError": "Ошибка миграции",
|
||||
"success": "Миграция успешно завершена!",
|
||||
"completed": "Миграция выполнена",
|
||||
"restartRecommended": "Рекомендуется перезапустить приложение, чтобы все изменения вступили в силу.",
|
||||
"restartNow": "Перезапустить сейчас"
|
||||
}
|
||||
}
|
||||
@@ -1,146 +0,0 @@
|
||||
{
|
||||
"page": {
|
||||
"description": "Управление настройками и поведением персонажей"
|
||||
},
|
||||
"buttons": {
|
||||
"create": "Создать персонажа",
|
||||
"createFirst": "Создать первого персонажа",
|
||||
"edit": "Изменить",
|
||||
"delete": "Удалить",
|
||||
"cancel": "Отмена",
|
||||
"save": "Сохранить",
|
||||
"move": "Переместить",
|
||||
"addDialogPair": "Добавить пример диалога"
|
||||
},
|
||||
"labels": {
|
||||
"presetDialogs": "Примеры диалогов ({count})",
|
||||
"createdAt": "Создан",
|
||||
"updatedAt": "Обновлен"
|
||||
},
|
||||
"form": {
|
||||
"personaId": "ID персонажа",
|
||||
"systemPrompt": "Системный промпт",
|
||||
"customErrorMessage": "Свое сообщение об ошибке (опционально)",
|
||||
"customErrorMessageHelp": "Это сообщение будет отправлено пользователю при сбое запроса к LLM. Если оставить пустым, будет использовано системное сообщение по умолчанию.",
|
||||
"presetDialogs": "Примеры диалогов",
|
||||
"presetDialogsHelp": "Добавьте примеры взаимодействия, чтобы помочь AI лучше понять свою роль и стиль общения.",
|
||||
"userMessage": "Сообщение пользователя",
|
||||
"assistantMessage": "Ответ AI",
|
||||
"tools": "Инструменты / MCP серверы",
|
||||
"toolsHelp": "Выберите инструменты, доступные этому персонажу. Инструменты позволяют AI взаимодействовать с внешним миром: искать в интернете, выполнять расчеты и т.д.",
|
||||
"toolsSelection": "Выбор инструментов",
|
||||
"selectAllTools": "Выбрать все",
|
||||
"clearAllTools": "Очистить всё",
|
||||
"allSelected": "Выбрано всё",
|
||||
"mcpServersQuickSelect": "Быстрый выбор MCP серверов",
|
||||
"searchTools": "Поиск инструментов",
|
||||
"selectedTools": "Выбранные инструменты",
|
||||
"noToolsAvailable": "Нет доступных инструментов",
|
||||
"noToolsFound": "Инструменты не найдены",
|
||||
"loadingTools": "Загрузка инструментов...",
|
||||
"allToolsAvailable": "Использовать все доступные инструменты",
|
||||
"noToolsSelected": "Инструменты не выбраны",
|
||||
"skills": "Навыки (Skills)",
|
||||
"skillsHelp": "Выберите навыки, доступные этому персонажу. Навыки предоставляют AI готовые сценарии и правила работы.",
|
||||
"skillsAllAvailable": "По умолчанию использовать все навыки",
|
||||
"skillsSelectSpecific": "Выбрать определенные навыки",
|
||||
"searchSkills": "Поиск навыков",
|
||||
"selectedSkills": "Выбранные навыки",
|
||||
"noSkillsAvailable": "Нет доступных навыков",
|
||||
"noSkillsFound": "Навыки не найдены",
|
||||
"loadingSkills": "Загрузка навыков...",
|
||||
"allSkillsAvailable": "Использовать все доступные навыки",
|
||||
"noSkillsSelected": "Навыки не выбраны",
|
||||
"skillsRuntimeNoneWarning": "Среда выполнения Computer Use не задана. Навыки могут не работать, так как нет активного окружения.",
|
||||
"createInFolder": "Будет создан в папке «{folder}»",
|
||||
"rootFolder": "Все персонажи"
|
||||
},
|
||||
"dialog": {
|
||||
"create": {
|
||||
"title": "Создание персонажа"
|
||||
},
|
||||
"edit": {
|
||||
"title": "Редактирование персонажа"
|
||||
}
|
||||
},
|
||||
"empty": {
|
||||
"title": "Персонажи не настроены",
|
||||
"description": "Самое время создать одного!",
|
||||
"folderEmpty": "Папка пуста",
|
||||
"folderEmptyDescription": "Создайте нового персонажа или папку, чтобы начать"
|
||||
},
|
||||
"validation": {
|
||||
"required": "Это поле обязательно для заполнения",
|
||||
"minLength": "Минимум {min} символов",
|
||||
"alphanumeric": "Разрешены только латинские буквы, цифры, подчёркивания и дефисы",
|
||||
"dialogRequired": "{type} не может быть пустым",
|
||||
"personaIdExists": "Персонаж с таким ID уже существует"
|
||||
},
|
||||
"messages": {
|
||||
"loadError": "Не удалось загрузить список персонажей",
|
||||
"saveSuccess": "Сохранено",
|
||||
"saveError": "Ошибка сохранения",
|
||||
"deleteConfirm": "Вы уверены, что хотите удалить персонажа «{id}»? Это действие необратимо.",
|
||||
"deleteSuccess": "Удалено",
|
||||
"deleteError": "Ошибка удаления"
|
||||
},
|
||||
"persona": {
|
||||
"personasTitle": "Персонаж",
|
||||
"toolsCount": "инстр.",
|
||||
"skillsCount": "навыков",
|
||||
"contextMenu": {
|
||||
"moveTo": "Переместить в..."
|
||||
},
|
||||
"messages": {
|
||||
"moveSuccess": "Персонаж перемещен",
|
||||
"moveError": "Не удалось переместить персонажа"
|
||||
}
|
||||
},
|
||||
"folder": {
|
||||
"sidebarTitle": "Папки",
|
||||
"rootFolder": "Корень",
|
||||
"foldersTitle": "Папки",
|
||||
"noFolders": "Папок нет",
|
||||
"createButton": "Новая папка",
|
||||
"searchPlaceholder": "Поиск папок...",
|
||||
"form": {
|
||||
"name": "Имя папки",
|
||||
"description": "Описание (опционально)"
|
||||
},
|
||||
"validation": {
|
||||
"nameRequired": "Имя папки не может быть пустым"
|
||||
},
|
||||
"contextMenu": {
|
||||
"open": "Открыть",
|
||||
"rename": "Переименовать",
|
||||
"moveTo": "Переместить в...",
|
||||
"delete": "Удалить"
|
||||
},
|
||||
"createDialog": {
|
||||
"title": "Создать папку",
|
||||
"createButton": "Создать"
|
||||
},
|
||||
"renameDialog": {
|
||||
"title": "Переименовать папку"
|
||||
},
|
||||
"deleteDialog": {
|
||||
"title": "Удаление папки",
|
||||
"message": "Вы уверены, что хотите удалить папку «{name}»?",
|
||||
"warning": "Все персонажи из этой папки будут перемещены в корневой каталог."
|
||||
},
|
||||
"messages": {
|
||||
"createSuccess": "Папка создана",
|
||||
"createError": "Ошибка создания папки",
|
||||
"renameSuccess": "Папка переименована",
|
||||
"renameError": "Ошибка переименования папки",
|
||||
"deleteSuccess": "Папка удалена",
|
||||
"deleteError": "Ошибка удаления папки"
|
||||
}
|
||||
},
|
||||
"moveDialog": {
|
||||
"title": "Перемещение",
|
||||
"description": "Выберите папку для «{name}»",
|
||||
"success": "Объект перемещен",
|
||||
"error": "Ошибка перемещения"
|
||||
}
|
||||
}
|
||||
@@ -1,135 +0,0 @@
|
||||
{
|
||||
"title": "Боты",
|
||||
"subtitle": "Управление адаптерами платформ для подключения к мессенджерам",
|
||||
"adapters": "Адаптеры платформ",
|
||||
"addAdapter": "Создать бота",
|
||||
"emptyText": "Боты не настроены. Нажмите «Создать бота», чтобы начать.",
|
||||
"viewWebhook": "Показать Webhook",
|
||||
"webhookCopied": "URL скопирован в буфер обмена",
|
||||
"webhookCopyFailed": "Не удалось скопировать, сделайте это вручную",
|
||||
"webhookDialog": {
|
||||
"title": "Адрес Webhook",
|
||||
"description": "Используйте этот адрес для обратных вызовов. Убедитесь, что ваш AstrBot доступен из интернета. Рекомендуется указать «Внешний URL для Webhook» в Конфигурация -> Система.",
|
||||
"close": "Закрыть"
|
||||
},
|
||||
"details": {
|
||||
"adapterType": "Тип адаптера",
|
||||
"token": "Токен",
|
||||
"description": "Описание"
|
||||
},
|
||||
"logs": {
|
||||
"title": "Логи платформы",
|
||||
"expand": "Развернуть",
|
||||
"collapse": "Свернуть"
|
||||
},
|
||||
"dialog": {
|
||||
"add": "Добавить",
|
||||
"edit": "Изменить",
|
||||
"adapter": "Бот",
|
||||
"refresh": "Обновить",
|
||||
"cancel": "Отмена",
|
||||
"save": "Сохранить",
|
||||
"addPlatform": "Создать бота",
|
||||
"connectTitle": "Подключение к {name}",
|
||||
"viewTutorial": "Открыть руководство",
|
||||
"noTemplates": "Шаблоны не найдены",
|
||||
"idConflict": {
|
||||
"title": "Конфликт ID",
|
||||
"message": "Бот с ID «{id}» уже существует. Пожалуйста, используйте уникальный ID.",
|
||||
"confirm": "Понятно"
|
||||
},
|
||||
"securityWarning": {
|
||||
"title": "Безопасность",
|
||||
"aiocqhttpTokenMissing": "Для защиты соединения крайне рекомендуется установить ws_reverse_token. Работа без токена небезопасна.",
|
||||
"learnMore": "Подробнее"
|
||||
},
|
||||
"invalidPlatformId": "ID платформы не может содержать символы ':' или '!'."
|
||||
},
|
||||
"createDialog": {
|
||||
"step1Title": "Выберите мессенджер",
|
||||
"step1Hint": "Куда вы хотите подключить бота? (QQ, Telegram, Discord, WeChat и др.)",
|
||||
"platformTypeLabel": "Платформа",
|
||||
"configFileTitle": "Файл конфигурации",
|
||||
"optional": "опционально",
|
||||
"configHint": "Как настроить бота? Конфиг содержит модель, персонажа, базу знаний и набор плагинов.",
|
||||
"configDefaultHint": "По умолчанию используется профиль «default». Вы сможете изменить его позже.",
|
||||
"useExistingConfig": "Использовать существующий конфиг",
|
||||
"selectConfigLabel": "Выберите профиль",
|
||||
"createNewConfig": "Создать новый профиль",
|
||||
"newConfigNameLabel": "Имя нового профиля",
|
||||
"newConfigTitle": "Создание нового профиля",
|
||||
"newConfigLoadFailed": "Не удалось загрузить шаблон конфигурации",
|
||||
"addRouteRule": "Добавить правило маршрутизации",
|
||||
"viewMode": "Просмотр",
|
||||
"editMode": "Редактирование",
|
||||
"noRouteRules": "Правила маршрутизации не заданы, будет использоваться профиль по умолчанию",
|
||||
"sessionIdPlaceholder": "ID сессии или *",
|
||||
"allSessions": "Все сессии",
|
||||
"configMissing": "Файл конфигурации не найден",
|
||||
"routeHint": "* При получении сообщения AstrBot ищет первое совпадение в списке сверху вниз. Используйте слэш-команду /sid, чтобы узнать ID текущей сессии. Если совпадений нет, используется профиль по умолчанию.",
|
||||
"warningContinue": "Игнорировать и создать",
|
||||
"warningEditAgain": "Вернуться к редактированию",
|
||||
"configDrawerTitle": "Управление профилями",
|
||||
"configDrawerIdLabel": "ID",
|
||||
"configTableHeaders": {
|
||||
"configId": "ID связанного профиля",
|
||||
"scope": "Область применения"
|
||||
},
|
||||
"routeTableHeaders": {
|
||||
"source": "Источник (тип:ID)",
|
||||
"config": "Файл конфига",
|
||||
"actions": "Действия"
|
||||
},
|
||||
"messageTypeOptions": {
|
||||
"all": "Все сообщения",
|
||||
"group": "Групповые (GroupMessage)",
|
||||
"friend": "Личные (FriendMessage)"
|
||||
},
|
||||
"messageTypeLabels": {
|
||||
"all": "Все",
|
||||
"group": "Группа",
|
||||
"friend": "ЛС"
|
||||
}
|
||||
},
|
||||
"messages": {
|
||||
"updateSuccess": "Обновлено!",
|
||||
"addSuccess": "Добавлено!",
|
||||
"deleteSuccess": "Удалено!",
|
||||
"statusUpdateSuccess": "Статус обновлен!",
|
||||
"deleteConfirm": "Вы уверены, что хотите удалить этого бота?",
|
||||
"configNotFoundOpenConfig": "Целевой конфиг не найден. Открыта страница настроек для проверки.",
|
||||
"updateMissingPlatformId": "Ошибка обновления: отсутствует ID платформы.",
|
||||
"platformUpdateFailed": "Не удалось обновить платформу.",
|
||||
"addSuccessWithConfig": "Бот успешно добавлен, профиль обновлен",
|
||||
"configIdMissing": "Не удалось получить ID конфигурации.",
|
||||
"routingUpdateFailed": "Ошибка обновления маршрутов: {message}",
|
||||
"createConfigFailed": "Ошибка создания профиля: {message}",
|
||||
"platformIdMissing": "Не удалось получить ID платформы.",
|
||||
"routingSaveFailed": "Ошибка сохранения маршрутов: {message}"
|
||||
},
|
||||
"status": {
|
||||
"enabled": "Включен",
|
||||
"disabled": "Выключен",
|
||||
"connecting": "Подключение",
|
||||
"connected": "Подключен",
|
||||
"disconnected": "Отключен",
|
||||
"error": "Ошибка"
|
||||
},
|
||||
"runtimeStatus": {
|
||||
"running": "Работает",
|
||||
"error": "Ошибка",
|
||||
"pending": "Ожидание",
|
||||
"stopped": "Остановлен",
|
||||
"unknown": "Неизвестно",
|
||||
"errors": "ошибок"
|
||||
},
|
||||
"errorDialog": {
|
||||
"title": "Детали ошибки",
|
||||
"platformId": "ID платформы",
|
||||
"errorCount": "Кол-во ошибок",
|
||||
"lastError": "Последняя ошибка",
|
||||
"occurredAt": "Время",
|
||||
"traceback": "Стек вызовов",
|
||||
"close": "Закрыть"
|
||||
}
|
||||
}
|
||||
@@ -1,151 +0,0 @@
|
||||
{
|
||||
"title": "Провайдеры моделей",
|
||||
"subtitle": "Настройка AI моделей для диалогов. Также поддерживает Dify, Coze, а также внешние Agent-сервисы.",
|
||||
"providers": {
|
||||
"title": "Сервис-провайдеры",
|
||||
"settings": "Настройки",
|
||||
"addProvider": "Добавить провайдера",
|
||||
"providerType": "Тип провайдера",
|
||||
"tabs": {
|
||||
"all": "Все",
|
||||
"chatCompletion": "Диалоги",
|
||||
"agentRunner": "Агенты",
|
||||
"speechToText": "STT (Речь -> Текст)",
|
||||
"textToSpeech": "TTS (Текст -> Речь)",
|
||||
"embedding": "Эмбеддинги",
|
||||
"rerank": "Rerank (Ранжирование)"
|
||||
},
|
||||
"empty": {
|
||||
"all": "Провайдеры не добавлены. Нажмите «Добавить провайдера», чтобы начать.",
|
||||
"typed": "Провайдеры типа «{type}» не найдены."
|
||||
},
|
||||
"description": {
|
||||
"openai": "Поддерживаются все провайдеры, совместимые с OpenAI API.",
|
||||
"vllm_rerank": "Также поддерживает Jina AI, Cohere, PPIO и другие.",
|
||||
"default": "Преобразование речи в текст"
|
||||
}
|
||||
},
|
||||
"availability": {
|
||||
"title": "Доступность провайдеров",
|
||||
"subtitle": "Статус определяется путем выполнения тестового запроса. Может взиматься плата согласно тарифу API.",
|
||||
"refresh": "Проверить статус",
|
||||
"noData": "Нажмите «Проверить статус», чтобы узнать доступность моделей",
|
||||
"available": "Доступен",
|
||||
"unavailable": "Недоступен",
|
||||
"pending": "Проверка...",
|
||||
"errorMessage": "Ошибка",
|
||||
"test": "Тест"
|
||||
},
|
||||
"logs": {
|
||||
"title": "Логи сервиса",
|
||||
"expand": "Развернуть",
|
||||
"collapse": "Свернуть"
|
||||
},
|
||||
"dialogs": {
|
||||
"addProvider": {
|
||||
"title": "Новый провайдер",
|
||||
"tabs": {
|
||||
"basic": "Диалоги",
|
||||
"agentRunner": "Агенты",
|
||||
"speechToText": "Преобразование текста в речь",
|
||||
"textToSpeech": "Переранжирование",
|
||||
"embedding": "Эмбеддинги",
|
||||
"rerank": "API Key"
|
||||
},
|
||||
"noTemplates": "Шаблоны для этого типа не найдены"
|
||||
},
|
||||
"config": {
|
||||
"addTitle": "Добавить",
|
||||
"editTitle": "Изменить",
|
||||
"provider": "Провайдер",
|
||||
"cancel": "Отмена",
|
||||
"save": "Сохранить"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Общие настройки провайдеров",
|
||||
"sessionSeparation": {
|
||||
"title": "Изоляция провайдеров по сессиям",
|
||||
"description": "Позволяет выбирать независимых провайдеров для генерации текста, TTS и STT в каждой конкретной сессии."
|
||||
},
|
||||
"close": "Закрыть"
|
||||
}
|
||||
},
|
||||
"messages": {
|
||||
"success": {
|
||||
"update": "Обновлено!",
|
||||
"add": "Добавлено!",
|
||||
"delete": "Удалено!",
|
||||
"statusUpdate": "Статус обновлен!",
|
||||
"sessionSeparation": "Настройки изоляции сохранены"
|
||||
},
|
||||
"error": {
|
||||
"sessionSeparation": "Не удалось загрузить настройки изоляции",
|
||||
"fetchStatus": "Не удалось получить статус провайдеров",
|
||||
"testError": "Тест {id} провален: {error}"
|
||||
},
|
||||
"confirm": {
|
||||
"delete": "Вы уверены, что хотите удалить провайдера «{id}»?"
|
||||
}
|
||||
},
|
||||
"providerTypes": {
|
||||
"title": "Тип провайдера"
|
||||
},
|
||||
"providerSources": {
|
||||
"title": "Источник провайдера",
|
||||
"add": "Добавить",
|
||||
"empty": "Источники не найдены",
|
||||
"selectHint": "Пожалуйста, выберите источник провайдера",
|
||||
"selectCreated": "Выбрать существующий источник",
|
||||
"save": "Сохранить конфиг",
|
||||
"saveAndFetchModels": "Сохранить и загрузить модели",
|
||||
"fetchModels": "Загрузить список моделей",
|
||||
"saveSuccess": "Источник успешно сохранен",
|
||||
"saveError": "Ошибка сохранения источника",
|
||||
"deleteConfirm": "Вы уверены, что хотите удалить источник «{id}»? Все связанные конфигурации моделей будут удалены.",
|
||||
"deleteSuccess": "Источник удален",
|
||||
"deleteError": "Ошибка удаления",
|
||||
"enabled": "Включен",
|
||||
"disabled": "Выключен",
|
||||
"advancedConfig": "Расширенные настройки...",
|
||||
"fields": {
|
||||
"name": "Имя",
|
||||
"apiKey": "Base URL",
|
||||
"baseUrl": "Base URL"
|
||||
},
|
||||
"hints": {
|
||||
"id": "Уникальный ID источника",
|
||||
"key": "Ваш серетный API-ключ",
|
||||
"apiBase": "Адрес API точки входа (Endpoint URL)",
|
||||
"proxy": "Прокси сервер (HTTP/HTTPS), напр. http://127.0.0.1:7890. Используется только для запросов к этому провайдеру."
|
||||
},
|
||||
"labels": {
|
||||
"proxy": "Прокси"
|
||||
}
|
||||
},
|
||||
"models": {
|
||||
"available": "Доступные модели",
|
||||
"configured": "Настроенные модели",
|
||||
"empty": "Модели не настроены. Нажмите «Загрузить список моделей» выше.",
|
||||
"noModelsFound": "Модели не найдены",
|
||||
"fetchError": "Не удалось получить список моделей",
|
||||
"addSuccess": "Модель {model} успешно добавлена",
|
||||
"deleteConfirm": "Вы уверены, что хотите удалить модель «{id}»?",
|
||||
"deleteSuccess": "Модель удалена",
|
||||
"deleteError": "Ошибка удаления модели",
|
||||
"testSuccess": "Тест модели «{id}» пройден успешно",
|
||||
"testError": "Тест модели провален",
|
||||
"searchPlaceholder": "Поиск по имени или ID",
|
||||
"manualAddButton": "Добавить вручную",
|
||||
"manualDialogTitle": "Произвольная модель",
|
||||
"manualDialogModelLabel": "Код модели (напр. gpt-4o-mini)",
|
||||
"manualDialogPreviewLabel": "Отображаемый ID (авто)",
|
||||
"manualDialogPreviewHint": "Будет выглядеть как: SourceID/ModelID",
|
||||
"manualModelRequired": "Укажите ID модели",
|
||||
"manualModelExists": "Эта модель уже добавлена",
|
||||
"configure": "Настроить",
|
||||
"tooltips": {
|
||||
"providerId": "ID провайдера",
|
||||
"modelId": "ID модели"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,130 +0,0 @@
|
||||
{
|
||||
"title": "Управление сессиями",
|
||||
"subtitle": "Настройка индивидуальных правил для конкретных диалогов. Эти правила имеют приоритет над глобальной конфигурацией.",
|
||||
"buttons": {
|
||||
"refresh": "Обновить",
|
||||
"edit": "Изменить",
|
||||
"editRule": "Редактировать правило",
|
||||
"deleteAllRules": "Удалить все правила",
|
||||
"addRule": "Добавить правило",
|
||||
"save": "Сохранить",
|
||||
"cancel": "Отмена",
|
||||
"delete": "Удалить",
|
||||
"clear": "Очистить",
|
||||
"next": "Далее",
|
||||
"editCustomName": "Изменить заметку",
|
||||
"batchDelete": "Массовое удаление"
|
||||
},
|
||||
"customRules": {
|
||||
"title": "Пользовательские правила",
|
||||
"rulesCount": "правил",
|
||||
"hasRules": "Настроено",
|
||||
"noRules": "Индивидуальных правил нет",
|
||||
"noRulesDesc": "Нажмите «Добавить правило», чтобы задать настройки для конкретного диалога",
|
||||
"serviceConfig": "Сервис",
|
||||
"pluginConfig": "Плагины",
|
||||
"kbConfig": "База знаний",
|
||||
"providerConfig": "Модель",
|
||||
"configured": "Настроено",
|
||||
"noCustomName": "Без заметки"
|
||||
},
|
||||
"quickEditName": {
|
||||
"title": "Редактирование заметки"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "Поиск сессии..."
|
||||
},
|
||||
"table": {
|
||||
"headers": {
|
||||
"umoInfo": "Источник (UMO)",
|
||||
"rulesOverview": "Обзор правил",
|
||||
"actions": "Действия"
|
||||
}
|
||||
},
|
||||
"persona": {
|
||||
"none": "Из конфигурации"
|
||||
},
|
||||
"provider": {
|
||||
"followConfig": "Из конфигурации"
|
||||
},
|
||||
"addRule": {
|
||||
"title": "Добавление правила",
|
||||
"description": "Выберите источник сообщения (UMO) для настройки. Индивидуальные правила приоритетнее глобальных. Используйте команду /sid в чате, чтобы узнать информацию об источнике.",
|
||||
"selectUmo": "Выберите сессию",
|
||||
"noUmos": "Нет доступных сессий"
|
||||
},
|
||||
"ruleEditor": {
|
||||
"title": "Редактор правил",
|
||||
"description": "Настройте поведение для этой сессии. Настройки ниже перекроют глобальный конфиг.",
|
||||
"serviceConfig": {
|
||||
"title": "Сервисные настройки",
|
||||
"sessionEnabled": "Обрабатывать сообщения",
|
||||
"llmEnabled": "Использовать LLM",
|
||||
"ttsEnabled": "Использовать TTS",
|
||||
"customName": "Заметка для сессии"
|
||||
},
|
||||
"providerConfig": {
|
||||
"title": "Выбор моделей",
|
||||
"chatProvider": "Чат-модель",
|
||||
"sttProvider": "STT (Распознавание)",
|
||||
"ttsProvider": "TTS (Озвучка)"
|
||||
},
|
||||
"personaConfig": {
|
||||
"title": "Персона",
|
||||
"selectPersona": "Выберите Persona",
|
||||
"hint": "При выборе Persona все диалоги из этого источника будут использовать именно её."
|
||||
},
|
||||
"pluginConfig": {
|
||||
"title": "Плагины",
|
||||
"disabledPlugins": "Отключенные плагины",
|
||||
"hint": "Выберите плагины, которые нужно ОТКЛЮЧИТЬ в этой сессии. Остальные останутся активными."
|
||||
},
|
||||
"kbConfig": {
|
||||
"title": "База знаний",
|
||||
"selectKbs": "Выбор баз знаний",
|
||||
"topK": "Количество результатов (Top K)",
|
||||
"enableRerank": "Использовать Rerank"
|
||||
}
|
||||
},
|
||||
"deleteConfirm": {
|
||||
"title": "Подтверждение",
|
||||
"message": "Удалить все настройки для этой сессии? Будут применены глобальные настройки."
|
||||
},
|
||||
"batchDeleteConfirm": {
|
||||
"title": "Массовое удаление",
|
||||
"message": "Удалить {count} выбранных правил? Будут применены глобальные настройки."
|
||||
},
|
||||
"batchOperations": {
|
||||
"title": "Массовые операции",
|
||||
"hint": "Быстрое изменение настроек для группы сессий",
|
||||
"scope": "Область применения",
|
||||
"scopeSelected": "Выбранные",
|
||||
"scopeAll": "Все сессии",
|
||||
"scopeGroup": "Все группы",
|
||||
"scopePrivate": "Личные диалоги",
|
||||
"llmStatus": "Статус LLM",
|
||||
"ttsStatus": "Статус TTS",
|
||||
"chatProvider": "Чат-модель",
|
||||
"ttsProvider": "TTS-модель",
|
||||
"apply": "Применить"
|
||||
},
|
||||
"status": {
|
||||
"enabled": "Включено",
|
||||
"disabled": "Выключено"
|
||||
},
|
||||
"messages": {
|
||||
"refreshSuccess": "Данные обновлены",
|
||||
"loadError": "Ошибка загрузки",
|
||||
"saveSuccess": "Настройки сохранены",
|
||||
"saveError": "Ошибка сохранения",
|
||||
"clearSuccess": "Очищено",
|
||||
"clearError": "Ошибка очистки",
|
||||
"deleteSuccess": "Удалено",
|
||||
"deleteError": "Ошибка удаления",
|
||||
"noChanges": "Изменений не обнаружено",
|
||||
"batchDeleteSuccess": "Массовое удаление выполнено",
|
||||
"batchDeleteError": "Ошибка массового удаления",
|
||||
"batchUpdateError": "Ошибка пакетного обновления",
|
||||
"batchUpdateSuccess": "Пакетное обновление успешно выполнено"
|
||||
}
|
||||
}
|
||||
@@ -1,180 +0,0 @@
|
||||
{
|
||||
"network": {
|
||||
"title": "Сеть",
|
||||
"githubProxy": {
|
||||
"title": "Зеркало GitHub",
|
||||
"subtitle": "Адрес для ускорения загрузки плагинов и обновлений AstrBot. Особенно актуально для пользователей из Китая. Все адреса предоставляются как есть, если обновление не удается — проверьте доступность выбранного зеркала.",
|
||||
"label": "Выбрать ускоритель GitHub"
|
||||
},
|
||||
"proxySelector": {
|
||||
"title": "Ускорение GitHub",
|
||||
"noProxy": "Не использовать",
|
||||
"useProxy": "Включить",
|
||||
"testConnection": "Проверить соединение",
|
||||
"available": "Доступен",
|
||||
"unavailable": "Недоступен",
|
||||
"custom": "Свой вариант"
|
||||
}
|
||||
},
|
||||
"theme": {
|
||||
"title": "Тема оформления",
|
||||
"subtitle": "Настройка основных и дополнительных цветов. Изменения вступают в силу немедленно и сохраняются в браузере.",
|
||||
"customize": {
|
||||
"title": "Цвета темы",
|
||||
"primary": "Основной",
|
||||
"secondary": "Дополнительный",
|
||||
"reset": "Сбросить"
|
||||
}
|
||||
},
|
||||
"system": {
|
||||
"title": "Система",
|
||||
"restart": {
|
||||
"title": "Перезапуск",
|
||||
"subtitle": "Выполнить мягкий перезапуск AstrBot",
|
||||
"button": "Перезагрузить"
|
||||
},
|
||||
"migration": {
|
||||
"title": "Миграция данных в v4.0.0",
|
||||
"subtitle": "Если у вас возникли проблемы с совместимостью данных после обновления, запустите помощник вручную.",
|
||||
"button": "Запустить миграцию"
|
||||
},
|
||||
"backup": {
|
||||
"title": "Резервное копирование",
|
||||
"subtitle": "Важнейший инструмент для безопасного переноса данных между серверами.",
|
||||
"button": "Управление бэкапами"
|
||||
}
|
||||
},
|
||||
"sidebar": {
|
||||
"title": "Боковая панель",
|
||||
"customize": {
|
||||
"title": "Настройка меню",
|
||||
"subtitle": "Перетаскивайте элементы, чтобы изменить их порядок или скрыть в группе «Дополнительно». Настройки сохраняются локально в браузере.",
|
||||
"reset": "Сбросить порядок",
|
||||
"mainItems": "Основные разделы",
|
||||
"moreItems": "Дополнительно"
|
||||
}
|
||||
},
|
||||
"backup": {
|
||||
"dialog": {
|
||||
"title": "Резервное копирование"
|
||||
},
|
||||
"tabs": {
|
||||
"export": "Экспорт",
|
||||
"import": "Импорт",
|
||||
"list": "Список копий"
|
||||
},
|
||||
"export": {
|
||||
"title": "Создать резервную копию",
|
||||
"description": "Экспорт всех данных в ZIP-архив, включая базы данных, базу знаний, конфигурации и вложения.",
|
||||
"includes": "Включает: основную БД, векторные индексы знаний, файлы конфигурации, медиа-вложения.",
|
||||
"button": "Начать экспорт",
|
||||
"processing": "Экспорт...",
|
||||
"wait": "Пожалуйста, подождите, мы упаковываем данные...",
|
||||
"completed": "Готово!",
|
||||
"download": "Скачать архив",
|
||||
"another": "Создать новый",
|
||||
"failed": "Ошибка экспорта",
|
||||
"retry": "Повторить"
|
||||
},
|
||||
"import": {
|
||||
"title": "Восстановление из копии",
|
||||
"warning": "⚠️ Внимание! Импорт полностью удалит и перезапишет текущие данные! Убедитесь, что у вас есть копия текущего состояния.",
|
||||
"selectFile": "Выберите ZIP-архив",
|
||||
"uploadAndCheck": "Загрузить и проверить",
|
||||
"uploading": "Загрузка...",
|
||||
"uploadWait": "Файл передается на сервер...",
|
||||
"uploadInit": "Инициализация...",
|
||||
"uploadingChunks": "Передача фрагментов...",
|
||||
"uploadComplete": "Загружено, идет сборка...",
|
||||
"checking": "Проверка структуры...",
|
||||
"invalidBackup": "Некорректный файл резервной копии",
|
||||
"backupContents": "Состав архива",
|
||||
"tables": "таблиц БД",
|
||||
"knowledgeBases": "баз знаний",
|
||||
"configFiles": "конфигов",
|
||||
"confirmImport": "Подтвердите импорт",
|
||||
"button": "Начать восстановление",
|
||||
"processing": "Восстановление...",
|
||||
"wait": "Идет процесс развертывания данных...",
|
||||
"completed": "Восстановление успешно завершено!",
|
||||
"restartRequired": "Данные восстановлены. Необходимо немедленно перезапустить AstrBot для вступления изменений в силу.",
|
||||
"restartNow": "Перезапустить сейчас",
|
||||
"failed": "Ошибка импорта",
|
||||
"retry": "Повторить",
|
||||
"version": {
|
||||
"backupVersion": "Версия бэкапа",
|
||||
"currentVersion": "Текущая версия",
|
||||
"backupTime": "Дата создания",
|
||||
"matchTitle": "✅ Версии совпадают",
|
||||
"matchMessage": "Импорт перезапишет все текущие данные, включая:\n• Основную БД (чаты, настройки)\n• Базы знаний\n• Плагины и их данные\n• Файлы конфигурации\n\nЭто действие необратимо! Продолжить?",
|
||||
"minorDiffTitle": "⚠️ Разница в минорной версии",
|
||||
"minorDiffMessage": "Разница в минорных версиях обычно допустима, но структура данных могла немного измениться. Все текущие данные будут удалены!\n\nПродолжить импорт?",
|
||||
"majorDiffTitle": "⛔ Импорт невозможен",
|
||||
"majorDiffMessage": "Версии основного выпуска различаются. Импорт между мажорными версиями может привести к фатальному повреждению данных.\nИспользуйте AstrBot той же основной версии."
|
||||
}
|
||||
},
|
||||
"list": {
|
||||
"empty": "Резервные копии не найдены",
|
||||
"refresh": "Обновить список",
|
||||
"confirmDelete": "Вы уверены, что хотите безвозвратно удалить эту копию?",
|
||||
"uploaded": "Загружено",
|
||||
"restore": "Восстановить из этого файла",
|
||||
"rename": "Переименовать",
|
||||
"renameTitle": "Переименование файла",
|
||||
"newName": "Новое имя",
|
||||
"renameHint": "Разрешены буквы, цифры, точки, дефисы и подчеркивания",
|
||||
"renameRequired": "Введите имя файла",
|
||||
"renameInvalidChars": "Имя содержит недопустимые символы",
|
||||
"renameFailed": "Ошибка переименования",
|
||||
"ftpHint": "Для больших архивов вы можете загружать их напрямую в папку data/backups через FTP/SFTP."
|
||||
}
|
||||
},
|
||||
"apiKey": {
|
||||
"title": "API Keys",
|
||||
"manageTitle": "Ключи доступа разработчика",
|
||||
"subtitle": "Управление токенами для доступа к открытому HTTP API AstrBot.",
|
||||
"name": "Имя ключа",
|
||||
"expiresInDays": "Срок действия",
|
||||
"expiryOptions": {
|
||||
"day1": "1 день",
|
||||
"day7": "7 дней",
|
||||
"day30": "30 дней",
|
||||
"day90": "90 дней",
|
||||
"permanent": "Бессрочно"
|
||||
},
|
||||
"permanentWarning": "Бессрочные ключи менее безопасны. Пожалуйста, храните их в надежном месте.",
|
||||
"scopes": "Область доступа (Scopes)",
|
||||
"create": "Создать API Key",
|
||||
"revoke": "Отозвать",
|
||||
"delete": "Удалить",
|
||||
"copy": "Копировать",
|
||||
"docsLink": "Документация API",
|
||||
"plaintextHint": "Обязательно сохраните ключ сейчас. После закрытия окна вы больше не сможете увидеть его значение.",
|
||||
"empty": "Ключи не созданы",
|
||||
"status": {
|
||||
"active": "Активен",
|
||||
"inactive": "Неактивен"
|
||||
},
|
||||
"table": {
|
||||
"name": "Имя",
|
||||
"prefix": "Префикс",
|
||||
"scopes": "Права",
|
||||
"status": "Статус",
|
||||
"lastUsed": "Использован",
|
||||
"createdAt": "Создан",
|
||||
"actions": "Действия"
|
||||
},
|
||||
"messages": {
|
||||
"loadFailed": "Не удалось загрузить ключи",
|
||||
"scopeRequired": "Выберите хотя бы одну область доступа",
|
||||
"createSuccess": "API Key создан",
|
||||
"createFailed": "Ошибка создания ключа",
|
||||
"revokeSuccess": "Ключ отозван",
|
||||
"revokeFailed": "Ошибка отзыва ключа",
|
||||
"deleteSuccess": "Ключ удален",
|
||||
"deleteFailed": "Ошибка удаления ключа",
|
||||
"copySuccess": "Ключ скопирован",
|
||||
"copyFailed": "Ошибка копирования"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
{
|
||||
"page": {
|
||||
"title": "Оркестрация SubAgent",
|
||||
"beta": "Экспериментально",
|
||||
"subtitle": "Основной LLM может напрямую использовать свои инструменты или делегировать задачи SubAgent через handoff."
|
||||
},
|
||||
"actions": {
|
||||
"refresh": "Обновить",
|
||||
"save": "Сохранить",
|
||||
"add": "Добавить SubAgent",
|
||||
"delete": "Удалить",
|
||||
"close": "Закрыть"
|
||||
},
|
||||
"switches": {
|
||||
"enable": "Включить оркестрацию SubAgent",
|
||||
"enableHint": "Включить функциональность под-агентов",
|
||||
"dedupe": "Дедупликация инструментов основного LLM (скрывать инструменты, дублируемые SubAgent)",
|
||||
"dedupeHint": "Удалить дублирующиеся инструменты из основного агента"
|
||||
},
|
||||
"description": {
|
||||
"disabled": "Выключено: SubAgent отключен; основной LLM подключает инструменты согласно правилам персонажа (все по умолчанию) и вызывает их напрямую.",
|
||||
"enabled": "Включено: основной LLM сохраняет свои инструменты и подключает инструменты делегирования transfer_to_*. При дедупликации инструменты, пересекающиеся с SubAgent, удаляются из основного набора."
|
||||
},
|
||||
"section": {
|
||||
"title": "Субагенты",
|
||||
"globalSettings": "Глобальные настройки"
|
||||
},
|
||||
"cards": {
|
||||
"statusEnabled": "Включено",
|
||||
"statusDisabled": "Отключено",
|
||||
"unnamed": "Безымянный SubAgent",
|
||||
"transferPrefix": "передать_{name}",
|
||||
"switchLabel": "Включить",
|
||||
"previewTitle": "Предпросмотр: инструмент handoff, видимый основному LLM",
|
||||
"personaChip": "Персонаж: {id}",
|
||||
"personaPreview": "ПРЕДПРОСМОТР ПЕРСОНАЖА"
|
||||
},
|
||||
"form": {
|
||||
"nameLabel": "Имя агента (используется для transfer_to_{name})",
|
||||
"nameHint": "Используйте строчные латинские буквы и подчеркивания; имя должно быть глобально уникальным.",
|
||||
"providerLabel": "Chat Provider (опционально)",
|
||||
"providerHint": "Оставьте пустым, чтобы использовать глобальный провайдер по умолчанию.",
|
||||
"personaLabel": "Выберите персонажа",
|
||||
"personaHint": "SubAgent наследует системные настройки и инструменты выбранного персонажа.",
|
||||
"descriptionLabel": "Описание для основного LLM (используется для принятия решения о handoff)",
|
||||
"descriptionHint": "Отображается как описание инструмента transfer_to_* — будьте кратки и ясны."
|
||||
},
|
||||
"messages": {
|
||||
"loadConfigFailed": "Не удалось загрузить конфигурацию",
|
||||
"loadPersonaFailed": "Не удалось загрузить список персонажей",
|
||||
"nameMissing": "У SubAgent отсутствует имя",
|
||||
"nameInvalid": "Недопустимое имя SubAgent: только строчные латинские буквы/цифры/подчеркивания, должно начинаться с буквы",
|
||||
"nameDuplicate": "Дублирующееся имя SubAgent: {name}",
|
||||
"personaMissing": "У SubAgent {name} не выбран персонаж",
|
||||
"saveSuccess": "Успешно сохранено",
|
||||
"saveFailed": "Ошибка сохранения",
|
||||
"nameRequired": "Имя обязательно",
|
||||
"namePattern": "Только строчные буквы, цифры и подчеркивание"
|
||||
},
|
||||
"empty": {
|
||||
"title": "Агенты не настроены",
|
||||
"subtitle": "Добавьте первого под-агента, чтобы начать",
|
||||
"action": "Создать первого агента"
|
||||
}
|
||||
}
|
||||
@@ -1,195 +0,0 @@
|
||||
{
|
||||
"title": "Инструменты и функции",
|
||||
"subtitle": "Управление MCP-серверами и доступными функциями",
|
||||
"tooltip": {
|
||||
"info": "Что такое Function Calling и MCP?",
|
||||
"marketplace": "Обзор и установка MCP-серверов от сообщества",
|
||||
"serverConfig": "Конфигурация MCP-серверов (stdio) поддерживает следующие поля:\ncommand: имя команды (например, python или uv)\nargs: массив аргументов (например, [\"run\", \"server.py\"])\nenv: объект переменных окружения (например, {\"api_key\": \"abc\"})\ncwd: рабочий каталог (например, /path/to/server)\nencoding: кодировка вывода (по умолчанию utf-8)\nПодробности см. в документации MCP.\n⚠️ Если вы используете Docker, устанавливайте сервера в смонтированную директорию data."
|
||||
},
|
||||
"tabs": {
|
||||
"local": "Локальные сервера",
|
||||
"marketplace": "Магазин MCP"
|
||||
},
|
||||
"mcpServers": {
|
||||
"title": "MCP Сервера",
|
||||
"buttons": {
|
||||
"refresh": "Обновить",
|
||||
"add": "Добавить сервер",
|
||||
"useTemplateStdio": "Шаблон Stdio",
|
||||
"useTemplateStreamableHttp": "Шаблон Streamable HTTP",
|
||||
"useTemplateSse": "Шаблон SSE",
|
||||
"sync": "Синхронизировать"
|
||||
},
|
||||
"empty": "MCP-сервера не найдены. Нажмите «Добавить сервер».",
|
||||
"status": {
|
||||
"noTools": "Нет доступных инструментов",
|
||||
"availableTools": "Доступные инструменты",
|
||||
"configSummary": "Конфигурация: {keys}",
|
||||
"noConfig": "Конфигурация не задана"
|
||||
}
|
||||
},
|
||||
"functionTools": {
|
||||
"title": "Функции (Tools)",
|
||||
"buttons": {
|
||||
"view": "Показать инструменты"
|
||||
},
|
||||
"search": "Поиск по функциям",
|
||||
"empty": "Доступные инструменты не найдены",
|
||||
"description": "Описание функции",
|
||||
"parameters": "Параметры",
|
||||
"noParameters": "У этого инструмента нет параметров",
|
||||
"table": {
|
||||
"paramName": "Параметр",
|
||||
"type": "Тип",
|
||||
"description": "Описание",
|
||||
"required": "Обяз.",
|
||||
"origin": "Источник",
|
||||
"originName": "Имя источника",
|
||||
"actions": "Действия"
|
||||
}
|
||||
},
|
||||
"marketplace": {
|
||||
"title": "Магазин MCP-серверов",
|
||||
"search": "Поиск по магазину",
|
||||
"buttons": {
|
||||
"refresh": "Обновить",
|
||||
"detail": "Инфо",
|
||||
"import": "Импорт"
|
||||
},
|
||||
"loading": "Загрузка списка серверов...",
|
||||
"empty": "Доступных MCP-серверов не найдено",
|
||||
"status": {
|
||||
"availableTools": "Инструментов: {count}",
|
||||
"noToolsInfo": "Нет данных об инструментах"
|
||||
}
|
||||
},
|
||||
"dialogs": {
|
||||
"addServer": {
|
||||
"title": "Добавление MCP-сервера",
|
||||
"editTitle": "Редактирование MCP-сервера",
|
||||
"fields": {
|
||||
"name": "Название сервера",
|
||||
"nameRequired": "Название обязательно",
|
||||
"enable": "Включить сервер",
|
||||
"config": "Конфигурация сервера"
|
||||
},
|
||||
"errors": {
|
||||
"configEmpty": "Конфигурация не может быть пустой",
|
||||
"jsonFormat": "Ошибка формата JSON: {error}",
|
||||
"jsonParse": "Ошибка разбора JSON: {error}"
|
||||
},
|
||||
"buttons": {
|
||||
"cancel": "Отмена",
|
||||
"save": "Сохранить",
|
||||
"testConnection": "Тест связи",
|
||||
"sync": "Синхронизировать"
|
||||
},
|
||||
"tips": {
|
||||
"timeoutConfig": "Тайм-аут вызова инструментов настраивается отдельно на странице конфигурации"
|
||||
}
|
||||
},
|
||||
"serverDetail": {
|
||||
"title": "Детали сервера",
|
||||
"installConfig": "Конфигурация установки",
|
||||
"availableTools": "Список инструментов",
|
||||
"buttons": {
|
||||
"close": "Закрыть",
|
||||
"importConfig": "Импортировать конфиг"
|
||||
}
|
||||
},
|
||||
"confirmDelete": "Вы уверены, что хотите удалить сервер «{name}»?",
|
||||
"syncProvider": {
|
||||
"title": "Синхронизация MCP",
|
||||
"subtitle": "Загрузка конфигурации MCP-серверов от провайдера",
|
||||
"steps": {
|
||||
"selectProvider": "Шаг 1: Провайдер",
|
||||
"configureAuth": "Шаг 2: Авторизация",
|
||||
"syncServers": "Шаг 3: Синхронизация"
|
||||
},
|
||||
"providers": {
|
||||
"modelscope": "ModelScope",
|
||||
"description": "ModelScope — это сообщество моделей с открытым исходным кодом, предоставляющее различные MCP-сервера для AI-сервисов"
|
||||
},
|
||||
"fields": {
|
||||
"provider": "Выберите провайдера",
|
||||
"accessToken": "Токен доступа",
|
||||
"tokenRequired": "Токен обязателен",
|
||||
"tokenHint": "Введите ваш токен доступа ModelScope"
|
||||
},
|
||||
"buttons": {
|
||||
"cancel": "Отмена",
|
||||
"previous": "Назад",
|
||||
"next": "Далее",
|
||||
"sync": "Начать",
|
||||
"getToken": "Получить токен"
|
||||
},
|
||||
"status": {
|
||||
"selectProvider": "Пожалуйста, выберите провайдера MCP-серверов",
|
||||
"enterToken": "Введите токен для продолжения",
|
||||
"readyToSync": "Готов к синхронизации"
|
||||
},
|
||||
"messages": {
|
||||
"syncSuccess": "MCP-сервера успешно синхронизированы!",
|
||||
"syncError": "Ошибка синхронизации: {error}",
|
||||
"tokenHelp": "Как получить токен ModelScope? Нажмите кнопку справа для инструкции"
|
||||
}
|
||||
}
|
||||
},
|
||||
"messages": {
|
||||
"getServersError": "Ошибка получения списка серверов: {error}",
|
||||
"getToolsError": "Ошибка получения списка инструментов: {error}",
|
||||
"saveSuccess": "Настройки сохранены!",
|
||||
"saveError": "Ошибка сохранения: {error}",
|
||||
"deleteSuccess": "Сервер удален успешно!",
|
||||
"deleteError": "Ошибка удаления: {error}",
|
||||
"updateSuccess": "Обновлено успешно!",
|
||||
"updateError": "Ошибка обновления: {error}",
|
||||
"getMarketError": "Не удалось загрузить магазин MCP: {error}",
|
||||
"importError": {
|
||||
"noConfig": "У этого сервера нет доступной конфигурации",
|
||||
"invalidFormat": "Неверный формат конфигурации",
|
||||
"failed": "Импорт не удался: {error}"
|
||||
},
|
||||
"configParseError": "Ошибка разбора конфигурации: {error}",
|
||||
"noAvailableConfig": "Конфигурация отсутствует",
|
||||
"toggleToolSuccess": "Статус инструмента изменен!",
|
||||
"toggleToolError": "Не удалось изменить статус: {error}",
|
||||
"testError": "Ошибка теста связи: {error}"
|
||||
},
|
||||
"syncProvider": {
|
||||
"title": "Синхронизация серверов MCP",
|
||||
"subtitle": "Синхронизировать конфигурации серверов MCP от провайдеров с локальными",
|
||||
"steps": {
|
||||
"selectProvider": "Шаг 1: Выберите провайдер",
|
||||
"configureAuth": "Шаг 2: Настройте аутентификацию",
|
||||
"syncServers": "Шаг 3: Синхронизируйте серверы"
|
||||
},
|
||||
"providers": {
|
||||
"modelscope": "ModelScope",
|
||||
"description": "ModelScope — это сообщество открытых моделей, предоставляющее серверы MCP для различных сервисов машинного обучения и ИИ"
|
||||
},
|
||||
"fields": {
|
||||
"provider": "Выберите провайдер",
|
||||
"accessToken": "Токен доступа",
|
||||
"tokenRequired": "Требуется токен доступа",
|
||||
"tokenHint": "Введите ваш токен доступа ModelScope"
|
||||
},
|
||||
"buttons": {
|
||||
"cancel": "Отмена",
|
||||
"previous": "Назад",
|
||||
"next": "Далее",
|
||||
"sync": "Начать синхронизацию",
|
||||
"getToken": "Получить токен"
|
||||
},
|
||||
"status": {
|
||||
"selectProvider": "Пожалуйста, выберите провайдер сервера MCP",
|
||||
"enterToken": "Введите токен доступа для продолжения",
|
||||
"readyToSync": "Готово к синхронизации конфигураций серверов"
|
||||
},
|
||||
"messages": {
|
||||
"syncSuccess": "Серверы MCP успешно синхронизированы!",
|
||||
"syncError": "Ошибка синхронизации: {error}",
|
||||
"tokenHelp": "Как получить токен доступа ModelScope? Нажмите кнопку справа для получения инструкций"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"title": "Трассировка (Trace)",
|
||||
"autoScroll": {
|
||||
"enabled": "Автопрокрутка: ВКЛ",
|
||||
"disabled": "Автопрокрутка: ВЫКЛ"
|
||||
},
|
||||
"hint": "В данный момент записываются только вызовы моделей основного агента AstrBot. Система будет совершенствоваться.",
|
||||
"recording": "Запись...",
|
||||
"paused": "Пауза"
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
{
|
||||
"greeting": {
|
||||
"morning": "Доброе утро, добро пожаловать в AstrBot",
|
||||
"afternoon": "Добрый день, добро пожаловать в AstrBot",
|
||||
"evening": "Добрый вечер, добро пожаловать в AstrBot",
|
||||
"newYear": "С Новым Годом!"
|
||||
},
|
||||
"subtitle": "Сначала пройдите базовое руководство. Настройку платформ и провайдеров моделей можно завершить позже.",
|
||||
"announcement": {
|
||||
"title": "Объявление"
|
||||
},
|
||||
"onboard": {
|
||||
"title": "Быстрый старт",
|
||||
"subtitle": "Вы можете выполнить первичную настройку прямо здесь.",
|
||||
"step1Title": "Настройка платформ",
|
||||
"step1Desc": "Подключите AstrBot к QQ, Lark, WeChat, Telegram и другим мессенджерам.",
|
||||
"step2Title": "Настройка AI моделей",
|
||||
"step2Desc": "Выберите и настройте AI провайдеров для AstrBot.",
|
||||
"configure": "Настроить",
|
||||
"skip": "Пропустить",
|
||||
"pending": "Ожидает",
|
||||
"completed": "Готово",
|
||||
"skipped": "Пропущено",
|
||||
"platformLoadFailed": "Ошибка загрузки конфигурации платформ",
|
||||
"providerLoadFailed": "Ошибка загрузки конфигурации провайдеров",
|
||||
"providerUpdateFailed": "Ошибка обновления провайдера по умолчанию в файле default",
|
||||
"providerDefaultUpdated": "Провайдер {id} установлен по умолчанию в файле default"
|
||||
},
|
||||
"resources": {
|
||||
"title": "Ресурсы",
|
||||
"githubDesc": "Поставьте нам звезду на GitHub!",
|
||||
"docsTitle": "Документация",
|
||||
"docsDesc": "Официальная документация AstrBot.",
|
||||
"afdianTitle": "Afdian",
|
||||
"afdianDesc": "Поддержите команду AstrBot через Afdian."
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
{
|
||||
"network": {
|
||||
"timeout": "Время ожидания запроса истекло, попробуйте позже",
|
||||
"connection": "Ошибка сетевого соединения. Проверьте интернет",
|
||||
"server": "Внутренняя ошибка сервера. Обратитесь в поддержку",
|
||||
"unavailable": "Сервис временно недоступен",
|
||||
"forbidden": "Доступ запрещен"
|
||||
},
|
||||
"validation": {
|
||||
"required": "Это поле обязательно для заполнения",
|
||||
"invalid": "Неверный формат ввода",
|
||||
"tooLong": "Введено слишком много символов",
|
||||
"tooShort": "Введено слишком мало символов",
|
||||
"email": "Укажите корректный email",
|
||||
"url": "Укажите корректный URL",
|
||||
"number": "Введите числовое значение"
|
||||
},
|
||||
"auth": {
|
||||
"unauthorized": "Авторизация не выполнена, войдите снова",
|
||||
"forbidden": "Недостаточно прав для выполнения операции",
|
||||
"tokenExpired": "Сессия истекла, пожалуйста, войдите заново",
|
||||
"invalidCredentials": "Неверное имя пользователя или пароль"
|
||||
},
|
||||
"file": {
|
||||
"uploadFailed": "Загрузка файла не удалась",
|
||||
"invalidFormat": "Неподдерживаемый формат файла",
|
||||
"tooLarge": "Файл слишком большой",
|
||||
"notFound": "Файл не найден"
|
||||
},
|
||||
"operation": {
|
||||
"failed": "Операция не удалась",
|
||||
"cancelled": "Операция отменена",
|
||||
"notSupported": "Действие не поддерживается",
|
||||
"conflict": "Конфликт операций, попробуйте позже"
|
||||
},
|
||||
"browser": {
|
||||
"audioNotSupported": "Ваш браузер не поддерживает воспроизведение аудио."
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"operation": {
|
||||
"saved": "Сохранено",
|
||||
"created": "Создано",
|
||||
"updated": "Обновлено успешно",
|
||||
"deleted": "Удалено",
|
||||
"uploaded": "Загружено",
|
||||
"downloaded": "Скачано",
|
||||
"imported": "Импорт завершен",
|
||||
"exported": "Экспорт завершен",
|
||||
"copied": "Скопировано в буфер",
|
||||
"sent": "Отправлено"
|
||||
},
|
||||
"connection": {
|
||||
"connected": "Подключено",
|
||||
"authenticated": "Вход выполнен",
|
||||
"synchronized": "Синхронизация завершена"
|
||||
},
|
||||
"validation": {
|
||||
"valid": "Проверка пройдена",
|
||||
"completed": "Готово"
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
{
|
||||
"required": "Это поле обязательно",
|
||||
"email": "Введите корректный email",
|
||||
"url": "Введите корректный URL",
|
||||
"number": "Введите число",
|
||||
"min": "Минимальное значение: {min}",
|
||||
"max": "Максимальное значение: {max}",
|
||||
"minLength": "Минимум {length} симв.",
|
||||
"maxLength": "Максимум {length} симв.",
|
||||
"pattern": "Неверный формат",
|
||||
"unique": "Такое значение уже существует",
|
||||
"confirm": "Значения не совпадают",
|
||||
"fileSize": "Размер файла не должен превышать {size}MB",
|
||||
"fileType": "Неподдерживаемый тип файла",
|
||||
"required_field": "Заполните обязательные поля",
|
||||
"invalid_format": "Некорректный формат",
|
||||
"password_too_short": "Пароль должен быть не менее 8 символов",
|
||||
"password_too_weak": "Пароль слишком слабый",
|
||||
"invalid_phone": "Некорректный номер телефона",
|
||||
"invalid_date": "Некорректная дата",
|
||||
"date_range": "Неверный диапазон дат",
|
||||
"upload_failed": "Загрузка не удалась",
|
||||
"network_error": "Ошибка сети, попробуйте снова",
|
||||
"operation_cannot_be_undone": "⚠️ Это действие нельзя отменить, будьте осторожны!"
|
||||
}
|
||||
@@ -876,8 +876,7 @@
|
||||
]
|
||||
},
|
||||
"regex": {
|
||||
"description": "分段正则表达式",
|
||||
"hint": "用于按正则规则识别分段点。建议使用能匹配分隔符的表达式。"
|
||||
"description": "分段正则表达式"
|
||||
},
|
||||
"split_words": {
|
||||
"description": "分段词列表",
|
||||
|
||||
@@ -42,7 +42,7 @@ import zhCNErrors from './locales/zh-CN/messages/errors.json';
|
||||
import zhCNSuccess from './locales/zh-CN/messages/success.json';
|
||||
import zhCNValidation from './locales/zh-CN/messages/validation.json';
|
||||
|
||||
// English translation
|
||||
// 英文翻译
|
||||
import enUSCommon from './locales/en-US/core/common.json';
|
||||
import enUSActions from './locales/en-US/core/actions.json';
|
||||
import enUSStatus from './locales/en-US/core/status.json';
|
||||
@@ -83,47 +83,6 @@ import enUSErrors from './locales/en-US/messages/errors.json';
|
||||
import enUSSuccess from './locales/en-US/messages/success.json';
|
||||
import enUSValidation from './locales/en-US/messages/validation.json';
|
||||
|
||||
// Russian translation
|
||||
import ruRUCommon from './locales/ru-RU/core/common.json';
|
||||
import ruRUActions from './locales/ru-RU/core/actions.json';
|
||||
import ruRUStatus from './locales/ru-RU/core/status.json';
|
||||
import ruRUNavigation from './locales/ru-RU/core/navigation.json';
|
||||
import ruRUHeader from './locales/ru-RU/core/header.json';
|
||||
import ruRUShared from './locales/ru-RU/core/shared.json';
|
||||
|
||||
import ruRUChat from './locales/ru-RU/features/chat.json';
|
||||
import ruRUExtension from './locales/ru-RU/features/extension.json';
|
||||
import ruRUConversation from './locales/ru-RU/features/conversation.json';
|
||||
import ruRUSessionManagement from './locales/ru-RU/features/session-management.json';
|
||||
import ruRUToolUse from './locales/ru-RU/features/tool-use.json';
|
||||
import ruRUProvider from './locales/ru-RU/features/provider.json';
|
||||
import ruRUPlatform from './locales/ru-RU/features/platform.json';
|
||||
import ruRUConfig from './locales/ru-RU/features/config.json';
|
||||
import ruRUConfigMetadata from './locales/ru-RU/features/config-metadata.json';
|
||||
import ruRUConsole from './locales/ru-RU/features/console.json';
|
||||
import ruRUTrace from './locales/ru-RU/features/trace.json';
|
||||
import ruRUAbout from './locales/ru-RU/features/about.json';
|
||||
import ruRUSettings from './locales/ru-RU/features/settings.json';
|
||||
import ruRUAuth from './locales/ru-RU/features/auth.json';
|
||||
import ruRUChart from './locales/ru-RU/features/chart.json';
|
||||
import ruRUDashboard from './locales/ru-RU/features/dashboard.json';
|
||||
import ruRUCron from './locales/ru-RU/features/cron.json';
|
||||
import ruRUAlkaidIndex from './locales/ru-RU/features/alkaid/index.json';
|
||||
import ruRUAlkaidKnowledgeBase from './locales/ru-RU/features/alkaid/knowledge-base.json';
|
||||
import ruRUAlkaidMemory from './locales/ru-RU/features/alkaid/memory.json';
|
||||
import ruRUKnowledgeBaseIndex from './locales/ru-RU/features/knowledge-base/index.json';
|
||||
import ruRUKnowledgeBaseDetail from './locales/ru-RU/features/knowledge-base/detail.json';
|
||||
import ruRUKnowledgeBaseDocument from './locales/ru-RU/features/knowledge-base/document.json';
|
||||
import ruRUPersona from './locales/ru-RU/features/persona.json';
|
||||
import ruRUMigration from './locales/ru-RU/features/migration.json';
|
||||
import ruRUCommand from './locales/ru-RU/features/command.json';
|
||||
import ruRUSubagent from './locales/ru-RU/features/subagent.json';
|
||||
import ruRUWelcome from './locales/ru-RU/features/welcome.json';
|
||||
|
||||
import ruRUErrors from './locales/ru-RU/messages/errors.json';
|
||||
import ruRUSuccess from './locales/ru-RU/messages/success.json';
|
||||
import ruRUValidation from './locales/ru-RU/messages/validation.json';
|
||||
|
||||
// 组装翻译对象
|
||||
export const translations = {
|
||||
'zh-CN': {
|
||||
@@ -223,55 +182,6 @@ export const translations = {
|
||||
success: enUSSuccess,
|
||||
validation: enUSValidation
|
||||
}
|
||||
},
|
||||
'ru-RU': {
|
||||
core: {
|
||||
common: ruRUCommon,
|
||||
actions: ruRUActions,
|
||||
status: ruRUStatus,
|
||||
navigation: ruRUNavigation,
|
||||
header: ruRUHeader,
|
||||
shared: ruRUShared
|
||||
},
|
||||
features: {
|
||||
chat: ruRUChat,
|
||||
extension: ruRUExtension,
|
||||
conversation: ruRUConversation,
|
||||
'session-management': ruRUSessionManagement,
|
||||
tooluse: ruRUToolUse,
|
||||
provider: ruRUProvider,
|
||||
platform: ruRUPlatform,
|
||||
config: ruRUConfig,
|
||||
'config-metadata': ruRUConfigMetadata,
|
||||
console: ruRUConsole,
|
||||
trace: ruRUTrace,
|
||||
about: ruRUAbout,
|
||||
settings: ruRUSettings,
|
||||
auth: ruRUAuth,
|
||||
chart: ruRUChart,
|
||||
dashboard: ruRUDashboard,
|
||||
cron: ruRUCron,
|
||||
alkaid: {
|
||||
index: ruRUAlkaidIndex,
|
||||
'knowledge-base': ruRUAlkaidKnowledgeBase,
|
||||
memory: ruRUAlkaidMemory
|
||||
},
|
||||
'knowledge-base': {
|
||||
index: ruRUKnowledgeBaseIndex,
|
||||
detail: ruRUKnowledgeBaseDetail,
|
||||
document: ruRUKnowledgeBaseDocument
|
||||
},
|
||||
persona: ruRUPersona,
|
||||
migration: ruRUMigration,
|
||||
command: ruRUCommand,
|
||||
subagent: ruRUSubagent,
|
||||
welcome: ruRUWelcome
|
||||
},
|
||||
messages: {
|
||||
errors: ruRUErrors,
|
||||
success: ruRUSuccess,
|
||||
validation: ruRUValidation
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
export const normalizeTextInput = (value: unknown): string =>
|
||||
typeof value === 'string' ? value : '';
|
||||
@@ -13,11 +13,9 @@
|
||||
</v-select>
|
||||
<v-text-field
|
||||
class="config-search-input"
|
||||
:model-value="configSearchKeyword"
|
||||
@update:model-value="onConfigSearchInput"
|
||||
v-model="configSearchKeyword"
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
:label="tm('search.placeholder')"
|
||||
clearable
|
||||
hide-details
|
||||
density="compact"
|
||||
rounded="md"
|
||||
@@ -213,7 +211,6 @@ import {
|
||||
useConfirmDialog
|
||||
} from '@/utils/confirmDialog';
|
||||
import UnsavedChangesConfirmDialog from '@/components/config/UnsavedChangesConfirmDialog.vue';
|
||||
import { normalizeTextInput } from '@/utils/inputValue';
|
||||
|
||||
export default {
|
||||
name: 'ConfigPage',
|
||||
@@ -422,9 +419,6 @@ export default {
|
||||
|
||||
},
|
||||
methods: {
|
||||
onConfigSearchInput(value) {
|
||||
this.configSearchKeyword = normalizeTextInput(value);
|
||||
},
|
||||
extractConfigTypeFromHash(hash) {
|
||||
const rawHash = String(hash || '');
|
||||
const lastHashIndex = rawHash.lastIndexOf('#');
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<template>
|
||||
<template>
|
||||
<div class="session-management-page">
|
||||
<v-container fluid class="pa-0">
|
||||
<v-card flat>
|
||||
@@ -35,7 +35,16 @@
|
||||
<!-- UMO 信息 -->
|
||||
<template v-slot:item.umo_info="{ item }">
|
||||
<div>
|
||||
<div class="d-flex align-center">
|
||||
<div class="d-flex align-center" v-if="item.isGroup">
|
||||
<v-chip size="x-small" color="deep-purple" variant="flat" class="mr-2">
|
||||
分组
|
||||
</v-chip>
|
||||
<span class="font-weight-medium">{{ item.groupName }}</span>
|
||||
<v-chip size="x-small" variant="outlined" class="ml-2">
|
||||
{{ item.umo_count }} 个会话
|
||||
</v-chip>
|
||||
</div>
|
||||
<div class="d-flex align-center" v-else>
|
||||
<v-chip size="x-small" :color="getPlatformColor(item.platform)" class="mr-2">
|
||||
{{ item.platform || 'unknown' }}
|
||||
</v-chip>
|
||||
@@ -282,14 +291,24 @@
|
||||
{{ tm('addRule.description') }}
|
||||
</v-alert>
|
||||
|
||||
<v-autocomplete v-model="selectedNewUmo" :items="availableUmos" :loading="loadingUmos"
|
||||
<v-radio-group v-model="addRuleTargetType" inline hide-details class="mb-4">
|
||||
<v-radio label="单个会话" value="session"></v-radio>
|
||||
<v-radio label="分组" value="group" :disabled="groups.length === 0"></v-radio>
|
||||
</v-radio-group>
|
||||
|
||||
<v-autocomplete v-if="addRuleTargetType === 'session'" v-model="selectedNewUmo" :items="availableUmos" :loading="loadingUmos"
|
||||
:label="tm('addRule.selectUmo')" variant="outlined" clearable :no-data-text="tm('addRule.noUmos')" />
|
||||
|
||||
<v-select v-if="addRuleTargetType === 'group'" v-model="selectedGroup" :items="groupSelectOptions"
|
||||
item-title="label" item-value="value" return-object
|
||||
label="选择分组" variant="outlined" clearable
|
||||
:no-data-text="'暂无分组,请先创建分组'" />
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions class="px-4 pb-4">
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn variant="text" @click="addRuleDialog = false">{{ tm('buttons.cancel') }}</v-btn>
|
||||
<v-btn color="primary" variant="tonal" @click="createNewRule" :disabled="!selectedNewUmo">
|
||||
<v-btn color="primary" variant="tonal" @click="createNewRule" :disabled="addRuleTargetType === 'session' ? !selectedNewUmo : !selectedGroup">
|
||||
{{ tm('buttons.next') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
@@ -334,12 +353,7 @@
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<div class="d-flex justify-end mt-4">
|
||||
<v-btn color="primary" variant="tonal" size="small" @click="saveServiceConfig" :loading="saving"
|
||||
prepend-icon="mdi-content-save">
|
||||
{{ tm('buttons.save') }}
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Provider Config Section -->
|
||||
<div class="d-flex align-center mb-4 mt-4">
|
||||
@@ -364,12 +378,7 @@
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<div class="d-flex justify-end mt-4">
|
||||
<v-btn color="primary" variant="tonal" size="small" @click="saveProviderConfig" :loading="saving"
|
||||
prepend-icon="mdi-content-save">
|
||||
{{ tm('buttons.save') }}
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Persona Config Section -->
|
||||
<div class="d-flex align-center mb-4 mt-4">
|
||||
@@ -389,12 +398,7 @@
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<div class="d-flex justify-end mt-4">
|
||||
<v-btn color="primary" variant="tonal" size="small" @click="saveServiceConfig" :loading="saving"
|
||||
prepend-icon="mdi-content-save">
|
||||
{{ tm('buttons.save') }}
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Plugin Config Section -->
|
||||
<div class="d-flex align-center mb-4 mt-4">
|
||||
@@ -414,12 +418,7 @@
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<div class="d-flex justify-end mt-4">
|
||||
<v-btn color="primary" variant="tonal" size="small" @click="savePluginConfig" :loading="saving"
|
||||
prepend-icon="mdi-content-save">
|
||||
{{ tm('buttons.save') }}
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- KB Config Section -->
|
||||
<div class="d-flex align-center mb-4 mt-4">
|
||||
@@ -442,14 +441,17 @@
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<div class="d-flex justify-end mt-4">
|
||||
<v-btn color="primary" variant="tonal" size="small" @click="saveKbConfig" :loading="saving"
|
||||
prepend-icon="mdi-content-save">
|
||||
{{ tm('buttons.save') }}
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</v-card-text>
|
||||
<v-card-actions class="px-6 pb-4">
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn variant="text" @click="closeRuleEditor">{{ tm('buttons.cancel') }}</v-btn>
|
||||
<v-btn color="primary" variant="tonal" @click="saveAllConfigs" :loading="saving"
|
||||
prepend-icon="mdi-content-save">
|
||||
{{ tm('buttons.save') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
@@ -567,6 +569,8 @@ export default {
|
||||
addRuleDialog: false,
|
||||
availableUmos: [],
|
||||
selectedNewUmo: null,
|
||||
addRuleTargetType: 'session',
|
||||
selectedGroup: null,
|
||||
|
||||
// 规则编辑
|
||||
ruleDialog: false,
|
||||
@@ -729,6 +733,13 @@ export default {
|
||||
return options
|
||||
},
|
||||
|
||||
groupSelectOptions() {
|
||||
return this.groups.map(g => ({
|
||||
label: `${g.name} (${g.umo_count} 个会话)`,
|
||||
value: g,
|
||||
}))
|
||||
},
|
||||
|
||||
groupOptions() {
|
||||
return this.groups.map(g => ({
|
||||
label: `${g.name} (${g.umo_count} 个会话)`,
|
||||
@@ -811,7 +822,7 @@ export default {
|
||||
})
|
||||
if (response.data.status === 'ok') {
|
||||
const data = response.data.data
|
||||
this.rulesList = data.rules
|
||||
this.rulesList = data.rules || []
|
||||
this.totalItems = data.total
|
||||
this.availablePersonas = data.available_personas
|
||||
this.availableChatProviders = data.available_chat_providers
|
||||
@@ -819,6 +830,20 @@ export default {
|
||||
this.availableTtsProviders = data.available_tts_providers
|
||||
this.availablePlugins = data.available_plugins || []
|
||||
this.availableKbs = data.available_kbs || []
|
||||
|
||||
// 合并分组规则到列表中
|
||||
const groupRules = data.group_rules || []
|
||||
for (const gr of groupRules) {
|
||||
this.rulesList.unshift({
|
||||
umo: `[\u5206\u7ec4] ${gr.name}`,
|
||||
isGroup: true,
|
||||
groupId: gr.group_id,
|
||||
groupName: gr.name,
|
||||
umo_count: gr.umo_count,
|
||||
rules: gr.config || {},
|
||||
})
|
||||
}
|
||||
this.totalItems += groupRules.length
|
||||
} else {
|
||||
this.showError(response.data.message || this.tm('messages.loadError'))
|
||||
}
|
||||
@@ -872,10 +897,89 @@ export default {
|
||||
async openAddRuleDialog() {
|
||||
this.addRuleDialog = true
|
||||
this.selectedNewUmo = null
|
||||
this.addRuleTargetType = 'session'
|
||||
this.selectedGroup = null
|
||||
await this.loadUmos()
|
||||
},
|
||||
|
||||
async saveAllConfigs() {
|
||||
if (!this.selectedUmo) return
|
||||
|
||||
// 分组模式:调用分组配置 API
|
||||
if (this.selectedUmo.isGroup) {
|
||||
this.saving = true
|
||||
try {
|
||||
const config = {
|
||||
session_service_config: { ...this.serviceConfig },
|
||||
provider_perf_chat_completion: this.providerConfig.chat_completion || null,
|
||||
provider_perf_speech_to_text: this.providerConfig.speech_to_text || null,
|
||||
provider_perf_text_to_speech: this.providerConfig.text_to_speech || null,
|
||||
session_plugin_config: { ...this.pluginConfig },
|
||||
kb_config: { ...this.kbConfig },
|
||||
}
|
||||
// 清理空值
|
||||
if (!config.session_service_config.custom_name) delete config.session_service_config.custom_name
|
||||
if (config.session_service_config.persona_id === null) delete config.session_service_config.persona_id
|
||||
|
||||
const response = await axios.post('/api/session/group/update-config', {
|
||||
group_id: this.selectedUmo.groupId,
|
||||
config: config
|
||||
})
|
||||
if (response.data.status === 'ok') {
|
||||
this.showSuccess(response.data.data?.message || '分组配置已保存并同步')
|
||||
await this.loadData()
|
||||
} else {
|
||||
this.showError(response.data.message || this.tm('messages.saveError'))
|
||||
}
|
||||
} catch (error) {
|
||||
this.showError(error.response?.data?.message || this.tm('messages.saveError'))
|
||||
} finally {
|
||||
this.saving = false
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 单个会话模式
|
||||
this.saving = true
|
||||
this._batchSaving = true
|
||||
try {
|
||||
await this.saveServiceConfig()
|
||||
await this.saveProviderConfig()
|
||||
await this.savePluginConfig()
|
||||
await this.saveKbConfig()
|
||||
this.showSuccess(this.tm('messages.saveSuccess'))
|
||||
} catch (error) {
|
||||
this.showError(error.response?.data?.message || this.tm('messages.saveError'))
|
||||
} finally {
|
||||
this._batchSaving = false
|
||||
this.saving = false
|
||||
}
|
||||
},
|
||||
|
||||
createNewRule() {
|
||||
if (this.addRuleTargetType === 'group') {
|
||||
// 分组模式
|
||||
if (!this.selectedGroup) return
|
||||
const group = this.selectedGroup.value || this.selectedGroup
|
||||
if (!group.umos || group.umos.length === 0) {
|
||||
this.showError('该分组没有成员会话')
|
||||
return
|
||||
}
|
||||
// 创建一个特殊的规则项,标记为分组
|
||||
const newItem = {
|
||||
umo: `[分组] ${group.name}`,
|
||||
isGroup: true,
|
||||
groupId: group.id,
|
||||
groupName: group.name,
|
||||
groupUmos: group.umos,
|
||||
rules: {},
|
||||
}
|
||||
this.addRuleDialog = false
|
||||
this.openRuleEditor(newItem)
|
||||
return
|
||||
}
|
||||
|
||||
// 单个会话模式(原逻辑)
|
||||
if (!this.selectedNewUmo) return
|
||||
|
||||
// 创建一个新的规则项并打开编辑器
|
||||
@@ -943,13 +1047,37 @@ export default {
|
||||
async saveServiceConfig() {
|
||||
if (!this.selectedUmo) return
|
||||
|
||||
this.saving = true
|
||||
if (!this._batchSaving) this.saving = true
|
||||
try {
|
||||
const config = { ...this.serviceConfig }
|
||||
// 清理空值
|
||||
if (!config.custom_name) delete config.custom_name
|
||||
if (config.persona_id === null) delete config.persona_id
|
||||
|
||||
// 分组模式:批量下发给所有成员
|
||||
if (this.selectedUmo.isGroup) {
|
||||
const umos = this.selectedUmo.groupUmos
|
||||
let successCount = 0
|
||||
for (const umo of umos) {
|
||||
try {
|
||||
await axios.post('/api/session/update-rule', {
|
||||
umo: umo,
|
||||
rule_key: 'session_service_config',
|
||||
rule_value: config
|
||||
})
|
||||
successCount++
|
||||
} catch (e) {
|
||||
console.error(`更新 ${umo} 失败:`, e)
|
||||
}
|
||||
}
|
||||
if (!this._batchSaving) {
|
||||
this.showSuccess(`已更新 ${successCount}/${umos.length} 个会话的服务配置`)
|
||||
await this.loadData()
|
||||
this.saving = false
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const response = await axios.post('/api/session/update-rule', {
|
||||
umo: this.selectedUmo.umo,
|
||||
rule_key: 'session_service_config',
|
||||
@@ -957,7 +1085,7 @@ export default {
|
||||
})
|
||||
|
||||
if (response.data.status === 'ok') {
|
||||
this.showSuccess(this.tm('messages.saveSuccess'))
|
||||
if (!this._batchSaving) this.showSuccess(this.tm('messages.saveSuccess'))
|
||||
this.editingRules.session_service_config = config
|
||||
|
||||
// 更新或添加到列表
|
||||
@@ -980,17 +1108,45 @@ export default {
|
||||
} catch (error) {
|
||||
this.showError(error.response?.data?.message || this.tm('messages.saveError'))
|
||||
}
|
||||
this.saving = false
|
||||
if (!this._batchSaving) this.saving = false
|
||||
},
|
||||
|
||||
async saveProviderConfig() {
|
||||
if (!this.selectedUmo) return
|
||||
|
||||
this.saving = true
|
||||
if (!this._batchSaving) this.saving = true
|
||||
try {
|
||||
const providerTypes = ['chat_completion', 'speech_to_text', 'text_to_speech']
|
||||
|
||||
// 分组模式:批量下发给所有成员
|
||||
if (this.selectedUmo.isGroup) {
|
||||
const umos = this.selectedUmo.groupUmos
|
||||
let successCount = 0
|
||||
for (const umo of umos) {
|
||||
try {
|
||||
const tasks = []
|
||||
for (const type of providerTypes) {
|
||||
const value = this.providerConfig[type]
|
||||
if (value) {
|
||||
tasks.push(axios.post('/api/session/update-rule', { umo, rule_key: `provider_perf_${type}`, rule_value: value }))
|
||||
}
|
||||
}
|
||||
if (tasks.length > 0) await Promise.all(tasks)
|
||||
successCount++
|
||||
} catch (e) {
|
||||
console.error(`更新 ${umo} Provider 失败:`, e)
|
||||
}
|
||||
}
|
||||
if (!this._batchSaving) {
|
||||
this.showSuccess(`已更新 ${successCount}/${umos.length} 个会话的 Provider 配置`)
|
||||
await this.loadData()
|
||||
this.saving = false
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const updateTasks = []
|
||||
const deleteTasks = []
|
||||
const providerTypes = ['chat_completion', 'speech_to_text', 'text_to_speech']
|
||||
|
||||
for (const type of providerTypes) {
|
||||
const value = this.providerConfig[type]
|
||||
@@ -1017,7 +1173,7 @@ export default {
|
||||
const allTasks = [...updateTasks, ...deleteTasks]
|
||||
if (allTasks.length > 0) {
|
||||
await Promise.all(allTasks)
|
||||
this.showSuccess(this.tm('messages.saveSuccess'))
|
||||
if (!this._batchSaving) this.showSuccess(this.tm('messages.saveSuccess'))
|
||||
|
||||
// 更新或添加到列表
|
||||
let item = this.rulesList.find(u => u.umo === this.selectedUmo.umo)
|
||||
@@ -1042,24 +1198,48 @@ export default {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.showSuccess(this.tm('messages.noChanges'))
|
||||
if (!this._batchSaving) this.showSuccess(this.tm('messages.noChanges'))
|
||||
}
|
||||
} catch (error) {
|
||||
this.showError(error.response?.data?.message || this.tm('messages.saveError'))
|
||||
}
|
||||
this.saving = false
|
||||
if (!this._batchSaving) this.saving = false
|
||||
},
|
||||
|
||||
async savePluginConfig() {
|
||||
if (!this.selectedUmo) return
|
||||
|
||||
this.saving = true
|
||||
if (!this._batchSaving) this.saving = true
|
||||
try {
|
||||
const config = {
|
||||
enabled_plugins: this.pluginConfig.enabled_plugins,
|
||||
disabled_plugins: this.pluginConfig.disabled_plugins,
|
||||
}
|
||||
|
||||
// 分组模式:批量下发给所有成员
|
||||
if (this.selectedUmo.isGroup) {
|
||||
const umos = this.selectedUmo.groupUmos
|
||||
let successCount = 0
|
||||
for (const umo of umos) {
|
||||
try {
|
||||
if (config.enabled_plugins.length === 0 && config.disabled_plugins.length === 0) {
|
||||
await axios.post('/api/session/delete-rule', { umo, rule_key: 'session_plugin_config' })
|
||||
} else {
|
||||
await axios.post('/api/session/update-rule', { umo, rule_key: 'session_plugin_config', rule_value: config })
|
||||
}
|
||||
successCount++
|
||||
} catch (e) {
|
||||
console.error(`更新 ${umo} 插件配置失败:`, e)
|
||||
}
|
||||
}
|
||||
if (!this._batchSaving) {
|
||||
this.showSuccess(`已更新 ${successCount}/${umos.length} 个会话的插件配置`)
|
||||
await this.loadData()
|
||||
this.saving = false
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 如果两个列表都为空,删除配置
|
||||
if (config.enabled_plugins.length === 0 && config.disabled_plugins.length === 0) {
|
||||
if (this.editingRules.session_plugin_config) {
|
||||
@@ -1071,7 +1251,7 @@ export default {
|
||||
let item = this.rulesList.find(u => u.umo === this.selectedUmo.umo)
|
||||
if (item) delete item.rules.session_plugin_config
|
||||
}
|
||||
this.showSuccess(this.tm('messages.saveSuccess'))
|
||||
if (!this._batchSaving) this.showSuccess(this.tm('messages.saveSuccess'))
|
||||
} else {
|
||||
const response = await axios.post('/api/session/update-rule', {
|
||||
umo: this.selectedUmo.umo,
|
||||
@@ -1080,7 +1260,7 @@ export default {
|
||||
})
|
||||
|
||||
if (response.data.status === 'ok') {
|
||||
this.showSuccess(this.tm('messages.saveSuccess'))
|
||||
if (!this._batchSaving) this.showSuccess(this.tm('messages.saveSuccess'))
|
||||
this.editingRules.session_plugin_config = config
|
||||
|
||||
let item = this.rulesList.find(u => u.umo === this.selectedUmo.umo)
|
||||
@@ -1102,13 +1282,13 @@ export default {
|
||||
} catch (error) {
|
||||
this.showError(error.response?.data?.message || this.tm('messages.saveError'))
|
||||
}
|
||||
this.saving = false
|
||||
if (!this._batchSaving) this.saving = false
|
||||
},
|
||||
|
||||
async saveKbConfig() {
|
||||
if (!this.selectedUmo) return
|
||||
|
||||
this.saving = true
|
||||
if (!this._batchSaving) this.saving = true
|
||||
try {
|
||||
const config = {
|
||||
kb_ids: this.kbConfig.kb_ids,
|
||||
@@ -1116,6 +1296,30 @@ export default {
|
||||
enable_rerank: this.kbConfig.enable_rerank,
|
||||
}
|
||||
|
||||
// 分组模式:批量下发给所有成员
|
||||
if (this.selectedUmo.isGroup) {
|
||||
const umos = this.selectedUmo.groupUmos
|
||||
let successCount = 0
|
||||
for (const umo of umos) {
|
||||
try {
|
||||
if (config.kb_ids.length === 0) {
|
||||
await axios.post('/api/session/delete-rule', { umo, rule_key: 'kb_config' })
|
||||
} else {
|
||||
await axios.post('/api/session/update-rule', { umo, rule_key: 'kb_config', rule_value: config })
|
||||
}
|
||||
successCount++
|
||||
} catch (e) {
|
||||
console.error(`更新 ${umo} 知识库配置失败:`, e)
|
||||
}
|
||||
}
|
||||
if (!this._batchSaving) {
|
||||
this.showSuccess(`已更新 ${successCount}/${umos.length} 个会话的知识库配置`)
|
||||
await this.loadData()
|
||||
this.saving = false
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 如果 kb_ids 为空,删除配置
|
||||
if (config.kb_ids.length === 0) {
|
||||
if (this.editingRules.kb_config) {
|
||||
@@ -1127,7 +1331,7 @@ export default {
|
||||
let item = this.rulesList.find(u => u.umo === this.selectedUmo.umo)
|
||||
if (item) delete item.rules.kb_config
|
||||
}
|
||||
this.showSuccess(this.tm('messages.saveSuccess'))
|
||||
if (!this._batchSaving) this.showSuccess(this.tm('messages.saveSuccess'))
|
||||
} else {
|
||||
const response = await axios.post('/api/session/update-rule', {
|
||||
umo: this.selectedUmo.umo,
|
||||
@@ -1136,7 +1340,7 @@ export default {
|
||||
})
|
||||
|
||||
if (response.data.status === 'ok') {
|
||||
this.showSuccess(this.tm('messages.saveSuccess'))
|
||||
if (!this._batchSaving) this.showSuccess(this.tm('messages.saveSuccess'))
|
||||
this.editingRules.kb_config = config
|
||||
|
||||
let item = this.rulesList.find(u => u.umo === this.selectedUmo.umo)
|
||||
@@ -1158,7 +1362,7 @@ export default {
|
||||
} catch (error) {
|
||||
this.showError(error.response?.data?.message || this.tm('messages.saveError'))
|
||||
}
|
||||
this.saving = false
|
||||
if (!this._batchSaving) this.saving = false
|
||||
},
|
||||
|
||||
confirmDeleteRules(item) {
|
||||
@@ -1171,6 +1375,24 @@ export default {
|
||||
|
||||
this.deleting = true
|
||||
try {
|
||||
// 分组规则:清空分组配置
|
||||
if (this.deleteTarget.isGroup) {
|
||||
const response = await axios.post('/api/session/group/update-config', {
|
||||
group_id: this.deleteTarget.groupId,
|
||||
config: {}
|
||||
})
|
||||
if (response.data.status === 'ok') {
|
||||
this.showSuccess('分组配置已清除')
|
||||
this.deleteDialog = false
|
||||
this.deleteTarget = null
|
||||
await this.loadData()
|
||||
} else {
|
||||
this.showError(response.data.message || this.tm('messages.deleteError'))
|
||||
}
|
||||
this.deleting = false
|
||||
return
|
||||
}
|
||||
|
||||
const response = await axios.post('/api/session/delete-rule', {
|
||||
umo: this.deleteTarget.umo
|
||||
})
|
||||
|
||||
@@ -353,11 +353,10 @@
|
||||
<v-window-item value="search">
|
||||
<div class="search-container pa-4">
|
||||
<v-form @submit.prevent="searchKnowledgeBase" class="d-flex align-center">
|
||||
<v-text-field :model-value="searchQuery"
|
||||
@update:model-value="onSearchQueryInput" :label="tm('search.queryLabel')"
|
||||
<v-text-field v-model="searchQuery" :label="tm('search.queryLabel')"
|
||||
append-icon="mdi-magnify" variant="outlined" class="flex-grow-1 me-2"
|
||||
@click:append="searchKnowledgeBase" @keyup.enter="searchKnowledgeBase"
|
||||
:placeholder="tm('search.queryPlaceholder')" hide-details clearable></v-text-field>
|
||||
:placeholder="tm('search.queryPlaceholder')" hide-details></v-text-field>
|
||||
|
||||
<v-select v-model="topK" :items="[3, 5, 10, 20]"
|
||||
:label="tm('search.resultCountLabel')" variant="outlined"
|
||||
@@ -435,7 +434,6 @@
|
||||
import axios from 'axios';
|
||||
import ConsoleDisplayer from '@/components/shared/ConsoleDisplayer.vue';
|
||||
import { useModuleI18n } from '@/i18n/composables';
|
||||
import { normalizeTextInput } from '@/utils/inputValue';
|
||||
|
||||
export default {
|
||||
name: 'KnowledgeBase',
|
||||
@@ -582,9 +580,6 @@ export default {
|
||||
this.getProviderList();
|
||||
},
|
||||
methods: {
|
||||
onSearchQueryInput(value) {
|
||||
this.searchQuery = normalizeTextInput(value);
|
||||
},
|
||||
getSelectedGitHubProxy() {
|
||||
if (typeof window === "undefined" || !window.localStorage) return "";
|
||||
return localStorage.getItem("githubProxyRadioValue") === "1"
|
||||
@@ -908,8 +903,7 @@ export default {
|
||||
},
|
||||
|
||||
searchKnowledgeBase() {
|
||||
const query = normalizeTextInput(this.searchQuery).trim();
|
||||
if (!query) {
|
||||
if (!this.searchQuery.trim()) {
|
||||
this.showSnackbar(this.tm('messages.pleaseEnterSearchContent'), 'warning');
|
||||
return;
|
||||
}
|
||||
@@ -920,7 +914,7 @@ export default {
|
||||
axios.get(`/api/plug/alkaid/kb/collection/search`, {
|
||||
params: {
|
||||
collection_name: this.currentKB.collection_name,
|
||||
query,
|
||||
query: this.searchQuery,
|
||||
top_k: this.topK
|
||||
}
|
||||
})
|
||||
|
||||
@@ -37,12 +37,10 @@
|
||||
<h3>{{ tm('search.title') }}</h3>
|
||||
<v-card variant="outlined" class="mt-2 pa-3">
|
||||
<div>
|
||||
<v-text-field :model-value="searchMemoryUserId"
|
||||
@update:model-value="onSearchMemoryUserIdInput" :label="tm('search.userIdLabel')" variant="outlined" density="compact" hide-details
|
||||
class="mb-2" clearable></v-text-field>
|
||||
<v-text-field :model-value="searchQuery"
|
||||
@update:model-value="onSearchQueryInput" :label="tm('search.queryLabel')" variant="outlined" density="compact" hide-details
|
||||
@keyup.enter="searchMemory" class="mb-2" clearable></v-text-field>
|
||||
<v-text-field v-model="searchMemoryUserId" :label="tm('search.userIdLabel')" variant="outlined" density="compact" hide-details
|
||||
class="mb-2"></v-text-field>
|
||||
<v-text-field v-model="searchQuery" :label="tm('search.queryLabel')" variant="outlined" density="compact" hide-details
|
||||
@keyup.enter="searchMemory" class="mb-2"></v-text-field>
|
||||
<v-btn color="info" @click="searchMemory" :loading="isSearching" variant="tonal">
|
||||
<v-icon start>mdi-text-search</v-icon>
|
||||
{{ tm('search.searchButton') }}
|
||||
@@ -256,7 +254,6 @@
|
||||
import axios from 'axios';
|
||||
// import * as d3 from "d3"; // npm install d3
|
||||
import { useModuleI18n } from '@/i18n/composables';
|
||||
import { normalizeTextInput } from '@/utils/inputValue';
|
||||
|
||||
export default {
|
||||
name: 'LongTermMemory',
|
||||
@@ -339,16 +336,9 @@ export default {
|
||||
this.searchResults = [];
|
||||
},
|
||||
methods: {
|
||||
onSearchMemoryUserIdInput(value) {
|
||||
this.searchMemoryUserId = normalizeTextInput(value);
|
||||
},
|
||||
onSearchQueryInput(value) {
|
||||
this.searchQuery = normalizeTextInput(value);
|
||||
},
|
||||
// 添加搜索记忆方法
|
||||
searchMemory() {
|
||||
const query = normalizeTextInput(this.searchQuery).trim();
|
||||
if (!query) {
|
||||
if (!this.searchQuery.trim()) {
|
||||
this.$toast.warning(this.tm('messages.searchQueryRequired'));
|
||||
return;
|
||||
}
|
||||
@@ -359,13 +349,12 @@ export default {
|
||||
|
||||
// 构建查询参数
|
||||
const params = {
|
||||
query
|
||||
query: this.searchQuery
|
||||
};
|
||||
|
||||
// 如果有选择用户ID,也加入查询参数
|
||||
const normalizedUserId = normalizeTextInput(this.searchMemoryUserId).trim();
|
||||
if (normalizedUserId) {
|
||||
params.user_id = normalizedUserId;
|
||||
if (this.searchMemoryUserId) {
|
||||
params.user_id = this.searchMemoryUserId;
|
||||
}
|
||||
|
||||
axios.get('/api/plug/alkaid/ltm/graph/search', { params })
|
||||
|
||||
@@ -3,7 +3,6 @@ import PluginSortControl from "@/components/extension/PluginSortControl.vue";
|
||||
import ExtensionCard from "@/components/shared/ExtensionCard.vue";
|
||||
import StyledMenu from "@/components/shared/StyledMenu.vue";
|
||||
import defaultPluginIcon from "@/assets/images/plugin_icon.png";
|
||||
import { normalizeTextInput } from "@/utils/inputValue";
|
||||
|
||||
const props = defineProps({
|
||||
state: {
|
||||
@@ -165,12 +164,10 @@ const {
|
||||
|
||||
<div class="d-flex align-center flex-wrap ml-auto" style="gap: 8px">
|
||||
<v-text-field
|
||||
:model-value="pluginSearch"
|
||||
@update:model-value="pluginSearch = normalizeTextInput($event)"
|
||||
v-model="pluginSearch"
|
||||
density="compact"
|
||||
:label="tm('search.placeholder')"
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
clearable
|
||||
variant="solo-filled"
|
||||
flat
|
||||
hide-details
|
||||
|
||||
@@ -3,7 +3,6 @@ import MarketPluginCard from "@/components/extension/MarketPluginCard.vue";
|
||||
import PluginSortControl from "@/components/extension/PluginSortControl.vue";
|
||||
import defaultPluginIcon from "@/assets/images/plugin_icon.png";
|
||||
import { computed } from "vue";
|
||||
import { normalizeTextInput } from "@/utils/inputValue";
|
||||
|
||||
const props = defineProps({
|
||||
state: {
|
||||
@@ -213,13 +212,11 @@ const marketSortItems = computed(() => [
|
||||
</div>
|
||||
|
||||
<v-text-field
|
||||
:model-value="marketSearch"
|
||||
@update:model-value="marketSearch = normalizeTextInput($event)"
|
||||
v-model="marketSearch"
|
||||
class="ml-auto"
|
||||
density="compact"
|
||||
:label="tm('search.marketPlaceholder')"
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
clearable
|
||||
variant="solo-filled"
|
||||
flat
|
||||
hide-details
|
||||
|
||||
@@ -1309,7 +1309,6 @@ export const useExtensionPage = () => {
|
||||
onLoadingDialogResult(1, resData.message);
|
||||
dialog.value = false;
|
||||
await getExtensions();
|
||||
checkAlreadyInstalled();
|
||||
|
||||
viewReadme({
|
||||
name: resData.data.name,
|
||||
|
||||
@@ -245,7 +245,7 @@ export default defineConfig({
|
||||
next: '下一篇'
|
||||
},
|
||||
editLink: {
|
||||
pattern: 'https://github.com/AstrBotdevs/AstrBot/edit/master/docs/:path',
|
||||
pattern: 'https://github.com/AstrBotdevs/AstrBot-docs/edit/v4/:path',
|
||||
text: '发现文档有问题?在 GitHub 上编辑此页',
|
||||
},
|
||||
logo: '/logo_prod.png',
|
||||
@@ -484,7 +484,7 @@ export default defineConfig({
|
||||
next: 'Next'
|
||||
},
|
||||
editLink: {
|
||||
pattern: 'https://github.com/AstrBotdevs/AstrBot/edit/master/docs/:path',
|
||||
pattern: 'https://github.com/AstrBotdevs/AstrBot-docs/edit/v4/:path',
|
||||
text: 'Edit this page on GitHub',
|
||||
},
|
||||
logo: '/logo_prod.png',
|
||||
|
||||
@@ -14,6 +14,8 @@ Welcome to submit Issues or Pull Requests:
|
||||
|
||||
- [AstrBotDevs/AstrBot](https://github.com/AstrBotDevs/AstrBot)
|
||||
|
||||
- [AstrBotDevs/AstrBot-Docs](https://github.com/AstrBotDevs/AstrBot-docs)
|
||||
|
||||
### Tencent QQ Groups
|
||||
|
||||
> - All groups are available to join. If you find that the group size is below the limit, please feel free to join.
|
||||
|
||||
@@ -128,9 +128,6 @@ The default AstrBot configuration is as follows:
|
||||
"telegram": {
|
||||
"pre_ack_emoji": {"enable": False, "emojis": ["✍️"]},
|
||||
},
|
||||
"discord": {
|
||||
"pre_ack_emoji": {"enable": False, "emojis": ["🤔"]},
|
||||
},
|
||||
},
|
||||
"wake_prefix": ["/"],
|
||||
"log_level": "INFO",
|
||||
@@ -514,11 +511,6 @@ When enabled, AstrBot sends a pre-reply emoji before requesting the LLM to infor
|
||||
- `enable`: Whether to enable pre-reply emojis for Telegram messages. Default is `false`.
|
||||
- `emojis`: List of pre-reply emojis. Default is `["✍️"]`. Telegram only supports a fixed set of reactions; refer to [reactions.txt](https://gist.github.com/Soulter/3f22c8e5f9c7e152e967e8bc28c97fc9).
|
||||
|
||||
##### discord
|
||||
|
||||
- `enable`: Whether to enable pre-reply emojis for Discord messages. Default is `false`.
|
||||
- `emojis`: List of pre-reply emojis. Default is `["🤔"]`. Refer to [Discord Reaction FAQ](https://support.discord.com/hc/en-us/articles/12102061808663-Reactions-and-Super-Reactions-FAQ).
|
||||
|
||||
### `wake_prefix`
|
||||
|
||||
Wake prefix. Default is `/`. When a message starts with `/`, AstrBot is awakened.
|
||||
|
||||
@@ -7,17 +7,17 @@
|
||||
|
||||
## Supported Basic Message Types
|
||||
|
||||
> Version v4.19.6.
|
||||
> Version v4.15.0.
|
||||
|
||||
| Message Type | Receive | Send | Notes |
|
||||
| --- | --- | --- | --- |
|
||||
| Text | Yes | Yes | |
|
||||
| Image | Yes | Yes | |
|
||||
| Voice | Yes | Yes | |
|
||||
| Video | Yes | Yes | |
|
||||
| File | Yes | Yes | |
|
||||
| Voice | No | No | |
|
||||
| Video | No | No | |
|
||||
| File | No | No | |
|
||||
|
||||
Proactive message push: Supported.
|
||||
Proactive message push: Not supported.
|
||||
|
||||
## Apply for a Bot
|
||||
|
||||
|
||||
@@ -2,17 +2,17 @@
|
||||
|
||||
## Supported Basic Message Types
|
||||
|
||||
> Version v4.19.6.
|
||||
> Version v4.15.0.
|
||||
|
||||
| Message Type | Receive | Send | Notes |
|
||||
| --- | --- | --- | --- |
|
||||
| Text | Yes | Yes | |
|
||||
| Image | Yes | Yes | |
|
||||
| Voice | Yes | Yes | |
|
||||
| Video | Yes | Yes | |
|
||||
| File | Yes | Yes | |
|
||||
| Voice | No | No | |
|
||||
| Video | No | No | |
|
||||
| File | No | No | |
|
||||
|
||||
Proactive message push: Supported.
|
||||
Proactive message push: Not supported.
|
||||
|
||||
## Quick Deployment Steps
|
||||
|
||||
|
||||
@@ -23,20 +23,14 @@
|
||||
|
||||
https://discord.gg/PxgzhmxJ
|
||||
|
||||
### Astrbook
|
||||
|
||||
- [Astrbook](https://book.astrbot.app/) - 专为 AI Agent 打造的社交社区,你可以在这里看到机器人们的日常动态,也可以将你的 Bot 接入其中。
|
||||
|
||||
### 玖帕喵 Prompt Market
|
||||
|
||||
- [玖帕喵](https://jiupamiao.asia/) - AI 人设与 Prompt 分享市场,在这里发现和分享高质量的 Prompts。玖帕喵,喵喵喵喵,喵!
|
||||
|
||||
### GitHub
|
||||
|
||||
欢迎提交 Issue 或 Pull Request:
|
||||
|
||||
- [AstrBotDevs/AstrBot](https://github.com/AstrBotDevs/AstrBot)
|
||||
|
||||
- [AstrBotDevs/AstrBot-Docs](https://github.com/AstrBotDevs/AstrBot-docs)
|
||||
|
||||
## 成为 AstrBot 组织成员
|
||||
|
||||
欢迎加入我们!
|
||||
|
||||
@@ -73,10 +73,9 @@ sudo docker run -itd -p 6185:6185 -p 6199:6199 -v $PWD/data:/AstrBot/data -v /et
|
||||
> ```
|
||||
>
|
||||
> (感谢 DaoCloud ❤️)
|
||||
>
|
||||
|
||||
> Windows 下不需要加 sudo,下同
|
||||
>
|
||||
Windows 同步 Host Time(需要WSL2)
|
||||
> Windows 同步 Host Time(需要WSL2)
|
||||
|
||||
```
|
||||
-v \\wsl.localhost\(your-wsl-os)\etc\timezone:/etc/timezone:ro
|
||||
|
||||
@@ -128,9 +128,6 @@ AstrBot 默认配置如下:
|
||||
"telegram": {
|
||||
"pre_ack_emoji": {"enable": False, "emojis": ["✍️"]},
|
||||
},
|
||||
"discord": {
|
||||
"pre_ack_emoji": {"enable": False, "emojis": ["🤔"]},
|
||||
},
|
||||
},
|
||||
"wake_prefix": ["/"],
|
||||
"log_level": "INFO",
|
||||
@@ -509,16 +506,11 @@ AstrBot WebUI 配置。
|
||||
- `enable`: 是否启用飞书消息预回复表情。默认为 `false`。
|
||||
- `emojis`: 预回复的表情列表。默认为 `["Typing"]`。表情枚举名参考:[表情文案说明](https://open.feishu.cn/document/server-docs/im-v1/message-reaction/emojis-introduce)
|
||||
|
||||
##### telegram
|
||||
#### telegram
|
||||
|
||||
- `enable`: 是否启用 Telegram 消息预回复表情。默认为 `false`。
|
||||
- `emojis`: 预回复的表情列表。默认为 `["✍️"]`。Telegram 仅支持固定反应集合,参考:[reactions.txt](https://gist.github.com/Soulter/3f22c8e5f9c7e152e967e8bc28c97fc9)
|
||||
|
||||
##### discord
|
||||
|
||||
- `enable`: 是否启用 Discord 消息预回复表情。默认为 `false`。
|
||||
- `emojis`: 预回复的表情列表。默认为 `["🤔"]`。Discord反应支持参考:[Discord Reaction FAQ](https://support.discord.com/hc/en-us/articles/12102061808663-Reactions-and-Super-Reactions-FAQ)
|
||||
|
||||
### `wake_prefix`
|
||||
|
||||
唤醒前缀。默认为 `/`。当消息以 `/` 开头时,AstrBot 会被唤醒。
|
||||
|
||||
@@ -96,7 +96,7 @@ docker logs napcat
|
||||
|
||||
### 配置管理员
|
||||
|
||||
填写完毕后,进入 `配置文件` 页,点击 `平台配置` 选项卡,找到 `管理员 ID`,填写你的账号(不是机器人的账号)。
|
||||
填写完毕后,进入 `配置文件` 页,点击 `平台配置` 选项卡,找到 `管理员 ID`,填写你的账号号(不是机器人的账号)。
|
||||
|
||||
切记点击右下角 `保存`,AstrBot 重启并会应用配置。
|
||||
|
||||
|
||||
@@ -10,17 +10,17 @@
|
||||
|
||||
## 支持的基本消息类型
|
||||
|
||||
> 版本 v4.19.6。
|
||||
> 版本 v4.15.0。
|
||||
|
||||
| 消息类型 | 是否支持接收 | 是否支持发送 | 备注 |
|
||||
| --- | --- | --- | --- |
|
||||
| 文本 | 是 | 是 | |
|
||||
| 图片 | 是 | 是 | |
|
||||
| 语音 | 是 | 是 | |
|
||||
| 视频 | 是 | 是 | |
|
||||
| 文件 | 是 | 是 | |
|
||||
| 语音 | 否 | 否 | |
|
||||
| 视频 | 否 | 否 | |
|
||||
| 文件 | 否 | 否 | |
|
||||
|
||||
主动消息推送:支持。
|
||||
主动消息推送:不支持。
|
||||
|
||||
## 申请一个机器人
|
||||
|
||||
|
||||
@@ -3,17 +3,17 @@
|
||||
|
||||
## 支持的基本消息类型
|
||||
|
||||
> 版本 v4.19.6。
|
||||
> 版本 v4.15.0。
|
||||
|
||||
| 消息类型 | 是否支持接收 | 是否支持发送 | 备注 |
|
||||
| --- | --- | --- | --- |
|
||||
| 文本 | 是 | 是 | |
|
||||
| 图片 | 是 | 是 | |
|
||||
| 语音 | 是 | 是 | |
|
||||
| 视频 | 是 | 是 | |
|
||||
| 文件 | 是 | 是 | |
|
||||
| 语音 | 否 | 否 | |
|
||||
| 视频 | 否 | 否 | |
|
||||
| 文件 | 否 | 否 | |
|
||||
|
||||
主动消息推送:支持。
|
||||
主动消息推送:不支持。
|
||||
|
||||
## 快速部署通道
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ spec:
|
||||
# 初始化容器:首次生成随机 machine-id,后续复用
|
||||
initContainers:
|
||||
- name: init-machine-id
|
||||
image: busybox:1.37.0
|
||||
image: busybox:latest
|
||||
command:
|
||||
- /bin/sh
|
||||
- -c
|
||||
@@ -123,4 +123,4 @@ spec:
|
||||
- name: localtime
|
||||
hostPath:
|
||||
path: /etc/localtime
|
||||
type: File
|
||||
type: File
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "AstrBot"
|
||||
version = "4.20.0"
|
||||
version = "4.19.5"
|
||||
description = "Easy-to-use multi-platform LLM chatbot and development framework"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
@@ -47,7 +47,7 @@ dependencies = [
|
||||
"slack-sdk>=3.35.0",
|
||||
"sqlalchemy[asyncio]>=2.0.41",
|
||||
"sqlmodel>=0.0.24",
|
||||
"telegramify-markdown>=1.0.0",
|
||||
"telegramify-markdown>=0.5.1",
|
||||
"watchfiles>=1.0.5",
|
||||
"websockets>=15.0.1",
|
||||
"wechatpy>=1.8.18",
|
||||
|
||||
+1
-1
@@ -40,7 +40,7 @@ silk-python>=0.2.6
|
||||
slack-sdk>=3.35.0
|
||||
sqlalchemy[asyncio]>=2.0.41
|
||||
sqlmodel>=0.0.24
|
||||
telegramify-markdown>=1.0.0
|
||||
telegramify-markdown>=0.5.1
|
||||
watchfiles>=1.0.5
|
||||
websockets>=15.0.1
|
||||
wechatpy>=1.8.18
|
||||
|
||||
@@ -1,88 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import subprocess
|
||||
|
||||
from astrbot.core.computer.booters import local as local_booter
|
||||
from astrbot.core.computer.booters.local import LocalShellComponent
|
||||
|
||||
|
||||
class _FakeCompletedProcess:
|
||||
def __init__(self, stdout: bytes, stderr: bytes = b"", returncode: int = 0):
|
||||
self.stdout = stdout
|
||||
self.stderr = stderr
|
||||
self.returncode = returncode
|
||||
|
||||
|
||||
def test_local_shell_component_decodes_utf8_output(monkeypatch):
|
||||
def fake_run(*args, **kwargs):
|
||||
_ = args, kwargs
|
||||
return _FakeCompletedProcess(stdout="技能内容".encode())
|
||||
|
||||
monkeypatch.setattr(subprocess, "run", fake_run)
|
||||
|
||||
result = asyncio.run(LocalShellComponent().exec("dummy"))
|
||||
|
||||
assert result["stdout"] == "技能内容"
|
||||
assert result["stderr"] == ""
|
||||
assert result["exit_code"] == 0
|
||||
|
||||
|
||||
def test_local_shell_component_prefers_utf8_before_windows_locale(
|
||||
monkeypatch,
|
||||
):
|
||||
def fake_run(*args, **kwargs):
|
||||
_ = args, kwargs
|
||||
return _FakeCompletedProcess(stdout="技能内容".encode())
|
||||
|
||||
monkeypatch.setattr(subprocess, "run", fake_run)
|
||||
monkeypatch.setattr(local_booter.os, "name", "nt", raising=False)
|
||||
monkeypatch.setattr(
|
||||
local_booter.locale,
|
||||
"getpreferredencoding",
|
||||
lambda _do_setlocale=False: "cp936",
|
||||
)
|
||||
|
||||
result = asyncio.run(LocalShellComponent().exec("dummy"))
|
||||
|
||||
assert result["stdout"] == "技能内容"
|
||||
assert result["stderr"] == ""
|
||||
assert result["exit_code"] == 0
|
||||
|
||||
|
||||
def test_local_shell_component_falls_back_to_gbk_on_windows(monkeypatch):
|
||||
def fake_run(*args, **kwargs):
|
||||
_ = args, kwargs
|
||||
return _FakeCompletedProcess(stdout="微博热搜".encode("gbk"))
|
||||
|
||||
monkeypatch.setattr(subprocess, "run", fake_run)
|
||||
monkeypatch.setattr(local_booter.os, "name", "nt", raising=False)
|
||||
monkeypatch.setattr(
|
||||
local_booter.locale,
|
||||
"getpreferredencoding",
|
||||
lambda _do_setlocale=False: "cp1252",
|
||||
)
|
||||
|
||||
result = asyncio.run(LocalShellComponent().exec("dummy"))
|
||||
|
||||
assert result["stdout"] == "微博热搜"
|
||||
assert result["stderr"] == ""
|
||||
assert result["exit_code"] == 0
|
||||
|
||||
|
||||
def test_local_shell_component_falls_back_to_utf8_replace(monkeypatch):
|
||||
def fake_run(*args, **kwargs):
|
||||
_ = args, kwargs
|
||||
return _FakeCompletedProcess(stdout=b"\xffabc")
|
||||
|
||||
monkeypatch.setattr(subprocess, "run", fake_run)
|
||||
monkeypatch.setattr(local_booter.os, "name", "posix", raising=False)
|
||||
monkeypatch.setattr(
|
||||
local_booter.locale,
|
||||
"getpreferredencoding",
|
||||
lambda _do_setlocale=False: "utf-8",
|
||||
)
|
||||
|
||||
result = asyncio.run(LocalShellComponent().exec("dummy"))
|
||||
|
||||
assert result["stdout"] == "\ufffdabc"
|
||||
@@ -145,182 +145,24 @@ def test_find_missing_requirements_or_raise_uses_requirements_exception(tmp_path
|
||||
requirements_utils.find_missing_requirements_or_raise(str(requirements_path))
|
||||
|
||||
|
||||
def test_build_missing_requirements_install_lines_keeps_only_missing_lines(tmp_path):
|
||||
requirements_path = tmp_path / "requirements.txt"
|
||||
requirements_path.write_text(
|
||||
'aiohttp>=3.0\nboto3==1.2; python_version >= "3.0"\nbotocore\n',
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
install_lines = requirements_utils.build_missing_requirements_install_lines(
|
||||
str(requirements_path),
|
||||
[
|
||||
"aiohttp>=3.0",
|
||||
'boto3==1.2; python_version >= "3.0"',
|
||||
"botocore",
|
||||
],
|
||||
{"boto3", "botocore"},
|
||||
)
|
||||
|
||||
assert install_lines == (
|
||||
'boto3==1.2; python_version >= "3.0"',
|
||||
"botocore",
|
||||
)
|
||||
|
||||
|
||||
def test_build_missing_requirements_install_lines_returns_empty_tuple_when_all_satisfied(
|
||||
tmp_path,
|
||||
):
|
||||
requirements_path = tmp_path / "requirements.txt"
|
||||
requirements_path.write_text("aiohttp>=3.0\nboto3\n", encoding="utf-8")
|
||||
|
||||
install_lines = requirements_utils.build_missing_requirements_install_lines(
|
||||
str(requirements_path), ["aiohttp>=3.0", "boto3"], set()
|
||||
)
|
||||
|
||||
assert install_lines == ()
|
||||
|
||||
|
||||
def test_build_missing_requirements_install_lines_returns_none_for_option_lines(
|
||||
tmp_path,
|
||||
):
|
||||
requirements_path = tmp_path / "requirements.txt"
|
||||
requirements_path.write_text(
|
||||
"--extra-index-url https://example.com/simple\nboto3\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
install_lines = requirements_utils.build_missing_requirements_install_lines(
|
||||
str(requirements_path),
|
||||
["--extra-index-url https://example.com/simple", "boto3"],
|
||||
{"boto3"},
|
||||
)
|
||||
|
||||
assert install_lines is None
|
||||
|
||||
|
||||
def test_build_missing_requirements_install_lines_skips_inactive_marker_lines(
|
||||
tmp_path,
|
||||
):
|
||||
requirements_path = tmp_path / "requirements.txt"
|
||||
requirements_path.write_text(
|
||||
'boto3\nother-package; sys_platform == "win32"\n',
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
install_lines = requirements_utils.build_missing_requirements_install_lines(
|
||||
str(requirements_path),
|
||||
["boto3", 'other-package; sys_platform == "win32"'],
|
||||
{"boto3"},
|
||||
)
|
||||
|
||||
assert install_lines == ("boto3",)
|
||||
|
||||
|
||||
def test_plan_missing_requirements_install_returns_none_when_missing_names_cannot_map_to_lines(
|
||||
monkeypatch,
|
||||
tmp_path,
|
||||
):
|
||||
requirements_path = tmp_path / "requirements.txt"
|
||||
requirements_path.write_text("boto3\n", encoding="utf-8")
|
||||
|
||||
monkeypatch.setattr(
|
||||
requirements_utils,
|
||||
"find_missing_requirements_from_lines",
|
||||
lambda lines: {"botocore"},
|
||||
)
|
||||
|
||||
plan = requirements_utils.plan_missing_requirements_install(str(requirements_path))
|
||||
|
||||
assert plan is not None
|
||||
assert plan.missing_names == frozenset({"botocore"})
|
||||
assert plan.install_lines == ()
|
||||
assert plan.fallback_reason == "unmapped missing requirement names"
|
||||
|
||||
|
||||
def test_plan_missing_requirements_install_loads_requirement_lines_once(
|
||||
monkeypatch,
|
||||
tmp_path,
|
||||
):
|
||||
requirements_path = tmp_path / "requirements.txt"
|
||||
requirements_path.write_text("boto3\nbotocore\n", encoding="utf-8")
|
||||
calls = []
|
||||
|
||||
def mock_load(path):
|
||||
calls.append(path)
|
||||
return True, ["boto3", "botocore"]
|
||||
|
||||
monkeypatch.setattr(
|
||||
requirements_utils,
|
||||
"_load_requirement_lines_for_precheck",
|
||||
mock_load,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
requirements_utils,
|
||||
"collect_installed_distribution_versions",
|
||||
lambda paths: {},
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
requirements_utils,
|
||||
"get_requirement_check_paths",
|
||||
lambda: ["/tmp/site-packages"],
|
||||
)
|
||||
|
||||
plan = requirements_utils.plan_missing_requirements_install(str(requirements_path))
|
||||
|
||||
assert plan is not None
|
||||
assert plan.missing_names == frozenset({"boto3", "botocore"})
|
||||
assert plan.install_lines == ("boto3", "botocore")
|
||||
assert calls == [str(requirements_path)]
|
||||
|
||||
|
||||
def test_build_missing_requirements_install_lines_logs_why_option_lines_fall_back(
|
||||
monkeypatch,
|
||||
tmp_path,
|
||||
):
|
||||
requirements_path = tmp_path / "requirements.txt"
|
||||
requirements_path.write_text(
|
||||
"--extra-index-url https://example.com/simple\nboto3\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
debug_logs = []
|
||||
|
||||
monkeypatch.setattr(
|
||||
"astrbot.core.utils.requirements_utils.logger.debug",
|
||||
lambda line, *args: debug_logs.append(line % args if args else line),
|
||||
)
|
||||
|
||||
install_lines = requirements_utils.build_missing_requirements_install_lines(
|
||||
str(requirements_path),
|
||||
["--extra-index-url https://example.com/simple", "boto3"],
|
||||
{"boto3"},
|
||||
)
|
||||
|
||||
assert install_lines is None
|
||||
assert any(str(requirements_path) in log for log in debug_logs)
|
||||
assert any("option/direct-reference" in log for log in debug_logs)
|
||||
|
||||
|
||||
def test_find_missing_requirements_logs_path_and_reason_on_precheck_fallback(
|
||||
monkeypatch,
|
||||
tmp_path,
|
||||
):
|
||||
requirements_path = tmp_path / "requirements.txt"
|
||||
requirements_path.write_text("git+https://example.com/demo.git\n", encoding="utf-8")
|
||||
|
||||
info_logs = []
|
||||
warning_logs = []
|
||||
|
||||
monkeypatch.setattr(
|
||||
"astrbot.core.utils.requirements_utils.logger.info",
|
||||
lambda line, *args: info_logs.append(line % args if args else line),
|
||||
"astrbot.core.utils.requirements_utils.logger.warning",
|
||||
lambda line, *args: warning_logs.append(line % args if args else line),
|
||||
)
|
||||
|
||||
missing = requirements_utils.find_missing_requirements(str(requirements_path))
|
||||
|
||||
assert missing is None
|
||||
assert any(str(requirements_path) in log for log in info_logs)
|
||||
assert any("option/direct-reference" in log for log in info_logs)
|
||||
assert any(str(requirements_path) in log for log in warning_logs)
|
||||
assert any("direct reference" in log for log in warning_logs)
|
||||
|
||||
|
||||
def test_load_requirement_lines_for_precheck_uses_parse_requirement_line_result(
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user