Merge branch 'master' into deprecate/register_star-decorator
This commit is contained in:
@@ -17,6 +17,7 @@ assignees: ''
|
||||
{
|
||||
"name": "插件名",
|
||||
"desc": "插件介绍",
|
||||
"author": "作者名",
|
||||
"repo": "插件仓库链接",
|
||||
"tags": [],
|
||||
"social_link": ""
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
# Keep GitHub Actions up to date with GitHub's Dependabot...
|
||||
# https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot
|
||||
# https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#package-ecosystem
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: github-actions
|
||||
directory: /
|
||||
groups:
|
||||
github-actions:
|
||||
patterns:
|
||||
- "*" # Group all Actions updates into a single larger pull request
|
||||
schedule:
|
||||
interval: weekly
|
||||
@@ -73,7 +73,7 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.10'
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
name: Run tests and upload coverage
|
||||
|
||||
on:
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
@@ -8,6 +8,7 @@ on:
|
||||
- 'README.md'
|
||||
- 'changelogs/**'
|
||||
- 'dashboard/**'
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
@@ -21,25 +22,24 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
uses: actions/setup-python@v5
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
pip install pytest pytest-cov pytest-asyncio
|
||||
pip install pytest pytest-asyncio pytest-cov
|
||||
pip install --editable .
|
||||
|
||||
- name: Run tests
|
||||
run: |
|
||||
mkdir data
|
||||
mkdir data/plugins
|
||||
mkdir data/config
|
||||
mkdir data/temp
|
||||
mkdir -p data/plugins
|
||||
mkdir -p data/config
|
||||
mkdir -p data/temp
|
||||
export TESTING=true
|
||||
export ZHIPU_API_KEY=${{ secrets.OPENAI_API_KEY }}
|
||||
PYTHONPATH=./ pytest --cov=. tests/ -v -o log_cli=true -o log_level=DEBUG
|
||||
pytest --cov=. -v -o log_cli=true -o log_level=DEBUG
|
||||
|
||||
- name: Upload results to Codecov
|
||||
uses: codecov/codecov-action@v4
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
@@ -12,7 +12,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Pull The Codes
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 # Must be 0 so we can fetch tags
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- uses: actions/stale@v5
|
||||
- uses: actions/stale@v9
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
stale-issue-message: 'Stale issue message'
|
||||
|
||||
@@ -6,7 +6,7 @@ import os
|
||||
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
||||
|
||||
VERSION = "3.5.18"
|
||||
VERSION = "3.5.19"
|
||||
DB_PATH = os.path.join(get_astrbot_data_path(), "data_v3.db")
|
||||
|
||||
# 默认配置
|
||||
@@ -64,7 +64,7 @@ DEFAULT_CONFIG = {
|
||||
"streaming_response": False,
|
||||
"show_tool_use_status": False,
|
||||
"streaming_segmented": False,
|
||||
"separate_provider": False,
|
||||
"separate_provider": True,
|
||||
},
|
||||
"provider_stt_settings": {
|
||||
"enable": False,
|
||||
@@ -724,16 +724,16 @@ CONFIG_METADATA_2 = {
|
||||
"model": "deepseek-chat",
|
||||
},
|
||||
},
|
||||
"智谱 AI": {
|
||||
"id": "zhipu_default",
|
||||
"type": "zhipu_chat_completion",
|
||||
"302.AI": {
|
||||
"id": "302ai",
|
||||
"type": "openai_chat_completion",
|
||||
"provider_type": "chat_completion",
|
||||
"enable": True,
|
||||
"key": [],
|
||||
"api_base": "https://api.302.ai/v1",
|
||||
"timeout": 120,
|
||||
"api_base": "https://open.bigmodel.cn/api/paas/v4/",
|
||||
"model_config": {
|
||||
"model": "glm-4-flash",
|
||||
"model": "gpt-4.1-mini",
|
||||
},
|
||||
},
|
||||
"硅基流动": {
|
||||
@@ -748,6 +748,18 @@ CONFIG_METADATA_2 = {
|
||||
"model": "deepseek-ai/DeepSeek-V3",
|
||||
},
|
||||
},
|
||||
"PPIO派欧云": {
|
||||
"id": "ppio",
|
||||
"type": "openai_chat_completion",
|
||||
"provider_type": "chat_completion",
|
||||
"enable": True,
|
||||
"key": [],
|
||||
"api_base": "https://api.ppinfra.com/v3/openai",
|
||||
"timeout": 120,
|
||||
"model_config": {
|
||||
"model": "deepseek/deepseek-r1",
|
||||
},
|
||||
},
|
||||
"Kimi": {
|
||||
"id": "moonshot",
|
||||
"type": "openai_chat_completion",
|
||||
@@ -760,16 +772,16 @@ CONFIG_METADATA_2 = {
|
||||
"model": "moonshot-v1-8k",
|
||||
},
|
||||
},
|
||||
"PPIO派欧云": {
|
||||
"id": "ppio",
|
||||
"type": "openai_chat_completion",
|
||||
"智谱 AI": {
|
||||
"id": "zhipu_default",
|
||||
"type": "zhipu_chat_completion",
|
||||
"provider_type": "chat_completion",
|
||||
"enable": True,
|
||||
"key": [],
|
||||
"api_base": "https://api.ppinfra.com/v3/openai",
|
||||
"timeout": 120,
|
||||
"api_base": "https://open.bigmodel.cn/api/paas/v4/",
|
||||
"model_config": {
|
||||
"model": "deepseek/deepseek-r1",
|
||||
"model": "glm-4-flash",
|
||||
},
|
||||
},
|
||||
"Dify": {
|
||||
|
||||
@@ -106,7 +106,6 @@ class ToolLoopAgent(BaseAgentRunner):
|
||||
|
||||
# 处理 LLM 响应
|
||||
llm_resp = llm_resp_result
|
||||
logger.debug(f"LLMResp: {llm_resp}")
|
||||
|
||||
if llm_resp.role == "err":
|
||||
# 如果 LLM 响应错误,转换到错误状态
|
||||
@@ -219,7 +218,9 @@ class ToolLoopAgent(BaseAgentRunner):
|
||||
content="返回了图片(已直接发送给用户)",
|
||||
)
|
||||
)
|
||||
yield MessageChain().base64_image(res.content[0].data)
|
||||
yield MessageChain(type="tool_direct_result").base64_image(
|
||||
res.content[0].data
|
||||
)
|
||||
elif isinstance(res.content[0], EmbeddedResource):
|
||||
resource = res.content[0].resource
|
||||
if isinstance(resource, TextResourceContents):
|
||||
@@ -243,7 +244,9 @@ class ToolLoopAgent(BaseAgentRunner):
|
||||
content="返回了图片(已直接发送给用户)",
|
||||
)
|
||||
)
|
||||
yield MessageChain().base64_image(res.content[0].data)
|
||||
yield MessageChain(type="tool_direct_result").base64_image(
|
||||
res.content[0].data
|
||||
)
|
||||
else:
|
||||
tool_call_result_blocks.append(
|
||||
ToolCallMessageSegment(
|
||||
@@ -276,7 +279,9 @@ class ToolLoopAgent(BaseAgentRunner):
|
||||
self._transition_state(AgentState.DONE)
|
||||
if res := self.event.get_result():
|
||||
if res.chain:
|
||||
yield MessageChain(chain=res.chain)
|
||||
yield MessageChain(
|
||||
chain=res.chain, type="tool_direct_result"
|
||||
)
|
||||
|
||||
self.event.clear_result()
|
||||
except Exception as e:
|
||||
|
||||
@@ -177,7 +177,13 @@ class LLMRequestSubStage(Stage):
|
||||
if event.is_stopped():
|
||||
return
|
||||
if resp.type == "tool_call_result":
|
||||
continue # 跳过工具调用结果
|
||||
msg_chain = resp.data["chain"]
|
||||
if msg_chain.type == "tool_direct_result":
|
||||
# tool_direct_result 用于标记 llm tool 需要直接发送给用户的内容
|
||||
resp.data["chain"].type = "tool_call_result"
|
||||
await event.send(resp.data["chain"])
|
||||
continue
|
||||
# 对于其他情况,暂时先不处理
|
||||
if resp.type == "tool_call":
|
||||
if self.streaming_response:
|
||||
# 用来标记流式响应需要分节
|
||||
@@ -254,11 +260,13 @@ class LLMRequestSubStage(Stage):
|
||||
|
||||
# 异步处理 WebChat 特殊情况
|
||||
if event.get_platform_name() == "webchat":
|
||||
asyncio.create_task(self._handle_webchat(event, req))
|
||||
asyncio.create_task(self._handle_webchat(event, req, provider))
|
||||
|
||||
await self._save_to_history(event, req, tool_loop_agent.get_final_llm_resp())
|
||||
|
||||
async def _handle_webchat(self, event: AstrMessageEvent, req: ProviderRequest):
|
||||
async def _handle_webchat(
|
||||
self, event: AstrMessageEvent, req: ProviderRequest, prov: Provider
|
||||
):
|
||||
"""处理 WebChat 平台的特殊情况,包括第一次 LLM 对话时总结对话内容生成 title"""
|
||||
conversation = await self.conv_manager.get_conversation(
|
||||
event.unified_msg_origin, req.conversation.cid
|
||||
@@ -268,10 +276,9 @@ class LLMRequestSubStage(Stage):
|
||||
latest_pair = messages[-2:]
|
||||
if not latest_pair:
|
||||
return
|
||||
provider = self.ctx.plugin_manager.context.get_using_provider()
|
||||
cleaned_text = "User: " + latest_pair[0].get("content", "").strip()
|
||||
logger.debug(f"WebChat 对话标题生成请求,清理后的文本: {cleaned_text}")
|
||||
llm_resp = await provider.text_chat(
|
||||
llm_resp = await prov.text_chat(
|
||||
system_prompt="You are expert in summarizing user's query.",
|
||||
prompt=(
|
||||
f"Please summarize the following query of user:\n"
|
||||
@@ -333,7 +340,6 @@ class LLMRequestSubStage(Stage):
|
||||
await self.conv_manager.update_conversation(
|
||||
event.unified_msg_origin, req.conversation.cid, history=messages
|
||||
)
|
||||
logger.debug(f"messages persisted: {messages}")
|
||||
|
||||
def fix_messages(self, messages: list[dict]) -> list[dict]:
|
||||
"""验证并且修复上下文"""
|
||||
|
||||
@@ -210,6 +210,16 @@ class WeChatPadProAdapter(Platform):
|
||||
logger.error(traceback.format_exc())
|
||||
return False
|
||||
|
||||
def _extract_auth_key(self, data):
|
||||
"""Helper method to extract auth_key from response data."""
|
||||
if isinstance(data, dict):
|
||||
auth_keys = data.get("authKeys") # 新接口
|
||||
if isinstance(auth_keys, list) and auth_keys:
|
||||
return auth_keys[0]
|
||||
elif isinstance(data, list) and data: # 旧接口
|
||||
return data[0]
|
||||
return None
|
||||
|
||||
async def generate_auth_key(self):
|
||||
"""
|
||||
生成授权码。
|
||||
@@ -218,28 +228,26 @@ class WeChatPadProAdapter(Platform):
|
||||
params = {"key": self.admin_key}
|
||||
payload = {"Count": 1, "Days": 365} # 生成一个有效期365天的授权码
|
||||
|
||||
self.auth_key = None # Reset auth_key before generating a new one
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
try:
|
||||
async with session.post(url, params=params, json=payload) as response:
|
||||
if response.status != 200:
|
||||
logger.error(f"生成授权码失败: {response.status}, {await response.text()}")
|
||||
return
|
||||
|
||||
response_data = await response.json()
|
||||
# 修正成功判断条件和授权码提取路径
|
||||
if response.status == 200 and response_data.get("Code") == 200:
|
||||
# 授权码在 Data 字段的列表中
|
||||
if (
|
||||
response_data.get("Data")
|
||||
and isinstance(response_data["Data"], list)
|
||||
and len(response_data["Data"]) > 0
|
||||
):
|
||||
self.auth_key = response_data["Data"][0]
|
||||
logger.info(f"成功获取授权码 {self.auth_key[:8]}...")
|
||||
if response_data.get("Code") == 200:
|
||||
if data := response_data.get("Data"):
|
||||
self.auth_key = self._extract_auth_key(data)
|
||||
|
||||
if self.auth_key:
|
||||
logger.info("成功获取授权码")
|
||||
else:
|
||||
logger.error(
|
||||
f"生成授权码成功但未找到授权码: {response_data}"
|
||||
)
|
||||
logger.error(f"生成授权码成功但未找到授权码: {response_data}")
|
||||
else:
|
||||
logger.error(
|
||||
f"生成授权码失败: {response.status}, {response_data}"
|
||||
)
|
||||
logger.error(f"生成授权码失败: {response_data}")
|
||||
except aiohttp.ClientConnectorError as e:
|
||||
logger.error(f"连接到 WeChatPadPro 服务失败: {e}")
|
||||
except Exception as e:
|
||||
|
||||
@@ -40,11 +40,13 @@ class ProviderManager:
|
||||
begin_dialogs = []
|
||||
user_turn = True
|
||||
for dialog in begin_dialogs:
|
||||
bd_processed.append({
|
||||
"role": "user" if user_turn else "assistant",
|
||||
"content": dialog,
|
||||
"_no_save": None, # 不持久化到 db
|
||||
})
|
||||
bd_processed.append(
|
||||
{
|
||||
"role": "user" if user_turn else "assistant",
|
||||
"content": dialog,
|
||||
"_no_save": None, # 不持久化到 db
|
||||
}
|
||||
)
|
||||
user_turn = not user_turn
|
||||
if mood_imitation_dialogs:
|
||||
if len(mood_imitation_dialogs) % 2 != 0:
|
||||
@@ -93,15 +95,15 @@ class ProviderManager:
|
||||
"""加载的 Text To Speech Provider 的实例"""
|
||||
self.embedding_provider_insts: List[Provider] = []
|
||||
"""加载的 Embedding Provider 的实例"""
|
||||
self.inst_map = {}
|
||||
self.inst_map: dict[str, Provider] = {}
|
||||
"""Provider 实例映射. key: provider_id, value: Provider 实例"""
|
||||
self.llm_tools = llm_tools
|
||||
|
||||
self.curr_provider_inst: Provider = None
|
||||
self.curr_provider_inst: Provider | None = None
|
||||
"""默认的 Provider 实例"""
|
||||
self.curr_stt_provider_inst: STTProvider = None
|
||||
self.curr_stt_provider_inst: STTProvider | None = None
|
||||
"""默认的 Speech To Text Provider 实例"""
|
||||
self.curr_tts_provider_inst: TTSProvider = None
|
||||
self.curr_tts_provider_inst: TTSProvider | None = None
|
||||
"""默认的 Text To Speech Provider 实例"""
|
||||
self.db_helper = db_helper
|
||||
|
||||
@@ -145,21 +147,24 @@ class ProviderManager:
|
||||
await self.load_provider(provider_config)
|
||||
|
||||
# 设置默认提供商
|
||||
self.curr_provider_inst = self.inst_map.get(
|
||||
self.provider_settings.get("default_provider_id")
|
||||
selected_provider_id = sp.get(
|
||||
"curr_provider", self.provider_settings.get("default_provider_id")
|
||||
)
|
||||
selected_stt_provider_id = sp.get(
|
||||
"curr_provider_stt", self.provider_stt_settings.get("provider_id")
|
||||
)
|
||||
selected_tts_provider_id = sp.get(
|
||||
"curr_provider_tts", self.provider_tts_settings.get("provider_id")
|
||||
)
|
||||
self.curr_provider_inst = self.inst_map.get(selected_provider_id)
|
||||
if not self.curr_provider_inst and self.provider_insts:
|
||||
self.curr_provider_inst = self.provider_insts[0]
|
||||
|
||||
self.curr_stt_provider_inst = self.inst_map.get(
|
||||
self.provider_stt_settings.get("provider_id")
|
||||
)
|
||||
self.curr_stt_provider_inst = self.inst_map.get(selected_stt_provider_id)
|
||||
if not self.curr_stt_provider_inst and self.stt_provider_insts:
|
||||
self.curr_stt_provider_inst = self.stt_provider_insts[0]
|
||||
|
||||
self.curr_tts_provider_inst = self.inst_map.get(
|
||||
self.provider_tts_settings.get("provider_id")
|
||||
)
|
||||
self.curr_tts_provider_inst = self.inst_map.get(selected_tts_provider_id)
|
||||
if not self.curr_tts_provider_inst and self.tts_provider_insts:
|
||||
self.curr_tts_provider_inst = self.tts_provider_insts[0]
|
||||
|
||||
@@ -417,7 +422,7 @@ class ProviderManager:
|
||||
self.curr_tts_provider_inst = None
|
||||
|
||||
if getattr(self.inst_map[provider_id], "terminate", None):
|
||||
await self.inst_map[provider_id].terminate()
|
||||
await self.inst_map[provider_id].terminate() # type: ignore
|
||||
|
||||
logger.info(
|
||||
f"{provider_id} 提供商适配器已终止({len(self.provider_insts)}, {len(self.stt_provider_insts)}, {len(self.tts_provider_insts)})"
|
||||
@@ -427,6 +432,6 @@ class ProviderManager:
|
||||
async def terminate(self):
|
||||
for provider_inst in self.provider_insts:
|
||||
if hasattr(provider_inst, "terminate"):
|
||||
await provider_inst.terminate()
|
||||
await provider_inst.terminate() # type: ignore
|
||||
# 清理 MCP Client 连接
|
||||
await self.llm_tools.mcp_service_queue.put({"type": "terminate"})
|
||||
|
||||
@@ -487,7 +487,7 @@ class ProviderOpenAIOfficial(Provider):
|
||||
if flag:
|
||||
flag = False # 删除 image 后,下一条(LLM 响应)也要删除
|
||||
continue
|
||||
if isinstance(context["content"], list):
|
||||
if "content" in context and isinstance(context["content"], list):
|
||||
flag = True
|
||||
# continue
|
||||
new_content = []
|
||||
|
||||
@@ -8,22 +8,48 @@ from typing import Union
|
||||
class PlatformAdapterType(enum.Flag):
|
||||
AIOCQHTTP = enum.auto()
|
||||
QQOFFICIAL = enum.auto()
|
||||
VCHAT = enum.auto()
|
||||
GEWECHAT = enum.auto()
|
||||
TELEGRAM = enum.auto()
|
||||
WECOM = enum.auto()
|
||||
LARK = enum.auto()
|
||||
ALL = AIOCQHTTP | QQOFFICIAL | VCHAT | GEWECHAT | TELEGRAM | WECOM | LARK
|
||||
WECHATPADPRO = enum.auto()
|
||||
DINGTALK = enum.auto()
|
||||
DISCORD = enum.auto()
|
||||
SLACK = enum.auto()
|
||||
KOOK = enum.auto()
|
||||
VOCECHAT = enum.auto()
|
||||
WEIXIN_OFFICIAL_ACCOUNT = enum.auto()
|
||||
ALL = (
|
||||
AIOCQHTTP
|
||||
| QQOFFICIAL
|
||||
| GEWECHAT
|
||||
| TELEGRAM
|
||||
| WECOM
|
||||
| LARK
|
||||
| WECHATPADPRO
|
||||
| DINGTALK
|
||||
| DISCORD
|
||||
| SLACK
|
||||
| KOOK
|
||||
| VOCECHAT
|
||||
| WEIXIN_OFFICIAL_ACCOUNT
|
||||
)
|
||||
|
||||
|
||||
ADAPTER_NAME_2_TYPE = {
|
||||
"aiocqhttp": PlatformAdapterType.AIOCQHTTP,
|
||||
"qq_official": PlatformAdapterType.QQOFFICIAL,
|
||||
"vchat": PlatformAdapterType.VCHAT,
|
||||
"gewechat": PlatformAdapterType.GEWECHAT,
|
||||
"telegram": PlatformAdapterType.TELEGRAM,
|
||||
"wecom": PlatformAdapterType.WECOM,
|
||||
"lark": PlatformAdapterType.LARK,
|
||||
"dingtalk": PlatformAdapterType.DINGTALK,
|
||||
"discord": PlatformAdapterType.DISCORD,
|
||||
"slack": PlatformAdapterType.SLACK,
|
||||
"kook": PlatformAdapterType.KOOK,
|
||||
"wechatpadpro": PlatformAdapterType.WECHATPADPRO,
|
||||
"vocechat": PlatformAdapterType.VOCECHAT,
|
||||
"weixin_official_account": PlatformAdapterType.WEIXIN_OFFICIAL_ACCOUNT,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -117,7 +117,7 @@ async def audio_to_tencent_silk_base64(audio_path: str) -> tuple[str, float]:
|
||||
try:
|
||||
import pilk
|
||||
except ImportError as e:
|
||||
raise Exception("未安装 pysilk,请执行: pip install pysilk") from e
|
||||
raise Exception("未安装 pilk: pip install pilk") from e
|
||||
|
||||
temp_dir = os.path.join(get_astrbot_data_path(), "temp")
|
||||
os.makedirs(temp_dir, exist_ok=True)
|
||||
|
||||
@@ -166,13 +166,12 @@ class ChatRoute(Route):
|
||||
type = result.get("type")
|
||||
cid = result.get("cid")
|
||||
streaming = result.get("streaming", False)
|
||||
chain_type = result.get("chain_type")
|
||||
yield f"data: {json.dumps(result, ensure_ascii=False)}\n\n"
|
||||
await asyncio.sleep(0.05)
|
||||
|
||||
if streaming and type != "end":
|
||||
continue
|
||||
|
||||
if type == "update_title":
|
||||
# If the result is still streaming, we continue to wait for more data
|
||||
continue
|
||||
|
||||
if result_text:
|
||||
@@ -189,7 +188,11 @@ class ChatRoute(Route):
|
||||
self.db.update_conversation(
|
||||
username, cid, history=json.dumps(history)
|
||||
)
|
||||
break
|
||||
if chain_type not in ["tool_call", "tool_call_result"]:
|
||||
# If the result is not a tool call or tool call result,
|
||||
# we can break the loop and end the stream
|
||||
break
|
||||
|
||||
except BaseException as _:
|
||||
logger.debug(f"用户 {username} 断开聊天长连接。")
|
||||
return
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
# What's Changed
|
||||
|
||||
1. 修复: 通过 provider 指令设置提供商,重启后失效
|
||||
2. 新增: WebChat 支持直接选择提供商和模型
|
||||
3. 优化: WebUI 视觉效果、WebChat 视觉效果
|
||||
4. 优化: WebUI 测试提供商功能
|
||||
5. 优化: 修复潜在的 README XSS 注入问题
|
||||
6. 修复: WechatPadPro 授权码提取逻辑以适配上游新版本,并提高安全性
|
||||
7. 修复: Gemini 下,多轮工具调用时可能报错的问题
|
||||
8. 其他修复与优化
|
||||
@@ -26,6 +26,7 @@
|
||||
"js-md5": "^0.8.3",
|
||||
"lodash": "4.17.21",
|
||||
"marked": "^15.0.7",
|
||||
"markdown-it": "^14.1.0",
|
||||
"pinia": "2.1.6",
|
||||
"remixicon": "3.5.0",
|
||||
"vee-validate": "4.11.3",
|
||||
|
||||
@@ -49,6 +49,11 @@ const reloadExtension = () => {
|
||||
};
|
||||
|
||||
const $confirm = inject("$confirm");
|
||||
|
||||
const installExtension = async () => {
|
||||
emit('install', props.extension);
|
||||
};
|
||||
|
||||
const uninstallExtension = async () => {
|
||||
if (typeof $confirm !== "function") {
|
||||
console.error(tm("card.errors.confirmNotRegistered"));
|
||||
@@ -117,6 +122,10 @@ const viewReadme = () => {
|
||||
<v-icon icon="mdi-cogs" start></v-icon>
|
||||
{{ extension.handlers?.length }}{{ tm("card.status.handlersCount") }}
|
||||
</v-chip>
|
||||
<v-chip v-for="tag in extension.tags" :key="tag" :color="tag === 'danger' ? 'error' : 'primary'" label
|
||||
size="small" class="ml-2">
|
||||
{{ tag === 'danger' ? tm('tags.danger') : tag }}
|
||||
</v-chip>
|
||||
</div>
|
||||
|
||||
<div class="mt-2" :class="{ 'text-caption': $vuetify.display.xs }" style="max-height: 65px; overflow-y: auto;">
|
||||
@@ -139,7 +148,7 @@ const viewReadme = () => {
|
||||
<v-btn color="teal-accent-4" :text="tm('buttons.viewDocs')" variant="text" @click="viewReadme"></v-btn>
|
||||
<v-btn v-if="!marketMode" color="teal-accent-4" :text="tm('buttons.actions')" variant="text" @click="reveal = true"></v-btn>
|
||||
<v-btn v-if="marketMode && !extension?.installed" color="teal-accent-4" :text="tm('buttons.install')" variant="text"
|
||||
@click="emit('install', extension)"></v-btn>
|
||||
@click="installExtension"></v-btn>
|
||||
<v-btn v-if="marketMode && extension?.installed" color="teal-accent-4" :text="tm('status.installed')" variant="text" disabled></v-btn>
|
||||
</v-card-actions>
|
||||
|
||||
@@ -200,6 +209,7 @@ const viewReadme = () => {
|
||||
</v-card>
|
||||
</v-expand-transition>
|
||||
</v-card>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
<template>
|
||||
<v-card class="item-card hover-elevation" style="padding: 4px;" elevation="0">
|
||||
<v-card-title class="d-flex justify-space-between align-center pb-1 pt-3">
|
||||
<span class="text-h2 text-truncate" :title="getItemTitle()">{{ getItemTitle() }}</span>
|
||||
<v-tooltip location="top">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-switch
|
||||
color="primary"
|
||||
hide-details
|
||||
density="compact"
|
||||
:model-value="getItemEnabled()"
|
||||
v-bind="props"
|
||||
@update:model-value="toggleEnabled"
|
||||
></v-switch>
|
||||
</template>
|
||||
<span>{{ getItemEnabled() ? t('core.common.itemCard.enabled') : t('core.common.itemCard.disabled') }}</span>
|
||||
</v-tooltip>
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text>
|
||||
<slot name="item-details" :item="item"></slot>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions style="margin: 8px;">
|
||||
<v-btn
|
||||
variant="outlined"
|
||||
color="error"
|
||||
rounded="xl"
|
||||
@click="$emit('delete', item)"
|
||||
>
|
||||
{{ t('core.common.itemCard.delete') }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
variant="tonal"
|
||||
color="primary"
|
||||
rounded="xl"
|
||||
@click="$emit('edit', item)"
|
||||
>
|
||||
{{ t('core.common.itemCard.edit') }}
|
||||
</v-btn>
|
||||
<v-spacer></v-spacer>
|
||||
</v-card-actions>
|
||||
|
||||
<div class="d-flex justify-end align-center" style="position: absolute; bottom: 16px; right: 16px; opacity: 0.2;" v-if="bglogo">
|
||||
<v-img
|
||||
:src="bglogo"
|
||||
contain
|
||||
width="120"
|
||||
height="120"
|
||||
class="rounded-circle"
|
||||
></v-img>
|
||||
</div>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { useI18n } from '@/i18n/composables';
|
||||
|
||||
export default {
|
||||
name: 'ItemCard',
|
||||
setup() {
|
||||
const { t } = useI18n();
|
||||
return { t };
|
||||
},
|
||||
props: {
|
||||
item: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
titleField: {
|
||||
type: String,
|
||||
default: 'id'
|
||||
},
|
||||
enabledField: {
|
||||
type: String,
|
||||
default: 'enable'
|
||||
},
|
||||
bglogo: {
|
||||
type: String,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
emits: ['toggle-enabled', 'delete', 'edit'],
|
||||
methods: {
|
||||
getItemTitle() {
|
||||
return this.item[this.titleField];
|
||||
},
|
||||
getItemEnabled() {
|
||||
return this.item[this.enabledField];
|
||||
},
|
||||
toggleEnabled() {
|
||||
this.$emit('toggle-enabled', this.item);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.item-card {
|
||||
position: relative;
|
||||
border-radius: 18px;
|
||||
transition: all 0.3s ease;
|
||||
overflow: hidden;
|
||||
min-height: 220px;
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.hover-elevation:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.item-status-indicator {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
left: 8px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background-color: #ccc;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.item-status-indicator.active {
|
||||
background-color: #4caf50;
|
||||
}
|
||||
</style>
|
||||
@@ -9,10 +9,10 @@
|
||||
|
||||
<v-row v-else>
|
||||
<v-col v-for="(item, index) in items" :key="index" cols="12" md="6" lg="4" xl="3">
|
||||
<v-card class="item-card hover-elevation" :color="getItemEnabled(item) ? '' : 'grey-lighten-4'">
|
||||
<v-card class="item-card hover-elevation" style="padding: 4px;" elevation="0">
|
||||
<div class="item-status-indicator" :class="{'active': getItemEnabled(item)}"></div>
|
||||
<v-card-title class="d-flex justify-space-between align-center pb-1 pt-3">
|
||||
<span class="text-h4 text-truncate" :title="getItemTitle(item)">{{ getItemTitle(item) }}</span>
|
||||
<span class="text-h2 text-truncate" :title="getItemTitle(item)">{{ getItemTitle(item) }}</span>
|
||||
<v-tooltip location="top">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-switch
|
||||
@@ -32,29 +32,36 @@
|
||||
<slot name="item-details" :item="item"></slot>
|
||||
</v-card-text>
|
||||
|
||||
<v-divider></v-divider>
|
||||
|
||||
<v-card-actions class="pa-2">
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
variant="text"
|
||||
size="small"
|
||||
color="error"
|
||||
prepend-icon="mdi-delete"
|
||||
<v-card-actions style="margin: 8px;">
|
||||
<v-btn
|
||||
variant="outlined"
|
||||
color="error"
|
||||
rounded="xl"
|
||||
@click="$emit('delete', item)"
|
||||
>
|
||||
{{ t('core.common.itemCard.delete') }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
variant="text"
|
||||
size="small"
|
||||
color="primary"
|
||||
prepend-icon="mdi-pencil"
|
||||
<v-btn
|
||||
variant="tonal"
|
||||
color="primary"
|
||||
rounded="xl"
|
||||
@click="$emit('edit', item)"
|
||||
>
|
||||
{{ t('core.common.itemCard.edit') }}
|
||||
</v-btn>
|
||||
<v-spacer></v-spacer>
|
||||
</v-card-actions>
|
||||
|
||||
<div class="d-flex justify-end align-center" style="position: absolute; bottom: 16px; right: 16px; opacity: 0.2;" v-if="bglogo">
|
||||
<v-img
|
||||
:src="bglogo"
|
||||
contain
|
||||
width="120"
|
||||
height="120"
|
||||
class="rounded-circle"
|
||||
></v-img>
|
||||
</div>
|
||||
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
@@ -90,6 +97,10 @@ export default {
|
||||
emptyText: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
bglogo: {
|
||||
type: String,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
emits: ['toggle-enabled', 'delete', 'edit'],
|
||||
@@ -112,10 +123,11 @@ export default {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
<style>
|
||||
|
||||
.item-card {
|
||||
position: relative;
|
||||
border-radius: 8px;
|
||||
border-radius: 18px;
|
||||
transition: all 0.3s ease;
|
||||
overflow: hidden;
|
||||
min-height: 220px;
|
||||
@@ -126,7 +138,6 @@ export default {
|
||||
}
|
||||
|
||||
.hover-elevation:hover {
|
||||
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.1);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup>
|
||||
import { ref, watch, onMounted, computed } from 'vue';
|
||||
import axios from 'axios';
|
||||
import { marked } from 'marked';
|
||||
import MarkdownIt from 'markdown-it';
|
||||
import hljs from 'highlight.js';
|
||||
import 'highlight.js/styles/github.css';
|
||||
import { useI18n } from '@/i18n/composables';
|
||||
@@ -74,29 +74,28 @@ function openRepoInNewTab() {
|
||||
}
|
||||
}
|
||||
|
||||
// 配置markdown-it,启用代码高亮
|
||||
const md = new MarkdownIt({
|
||||
html: true, // 启用HTML标签
|
||||
breaks: true, // 换行转<br>
|
||||
linkify: true, // 自动转链接
|
||||
typographer: false, // 禁用智能引号
|
||||
highlight: function(code, lang) {
|
||||
if (lang && hljs.getLanguage(lang)) {
|
||||
try {
|
||||
return hljs.highlight(code, { language: lang }).value;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
return hljs.highlightAuto(code).value;
|
||||
}
|
||||
});
|
||||
|
||||
// 渲染Markdown内容
|
||||
function renderMarkdown(content) {
|
||||
if (!content) return '';
|
||||
|
||||
// 配置marked使用highlight.js进行语法高亮
|
||||
marked.setOptions({
|
||||
highlight: function(code, lang) {
|
||||
if (lang && hljs.getLanguage(lang)) {
|
||||
try {
|
||||
return hljs.highlight(code, { language: lang }).value;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
return hljs.highlightAuto(code).value;
|
||||
},
|
||||
gfm: true, // GitHub Flavored Markdown
|
||||
breaks: true, // Convert \n to <br>
|
||||
headerIds: true, // Add id attributes to headers
|
||||
mangle: false // Don't mangle email addresses
|
||||
});
|
||||
|
||||
return marked(content);
|
||||
return md.render(content);
|
||||
}
|
||||
|
||||
// 刷新README内容
|
||||
@@ -120,7 +119,7 @@ const _show = computed({
|
||||
<v-card>
|
||||
<v-card-title class="d-flex justify-space-between align-center">
|
||||
<span class="text-h5">{{ t('core.common.readme.title') }}</span>
|
||||
<v-btn icon @click="$emit('update:show', false)">
|
||||
<v-btn icon @click="$emit('update:show', false)" variant="text">
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"save": "Save",
|
||||
"cancel": "Cancel",
|
||||
"close": "Close",
|
||||
"copy": "Copy",
|
||||
"delete": "Delete",
|
||||
"edit": "Edit",
|
||||
"add": "Add",
|
||||
@@ -32,6 +33,7 @@
|
||||
"longPress": "Long press",
|
||||
"yes": "Yes",
|
||||
"no": "No",
|
||||
"imagePreview": "Image Preview",
|
||||
"dialog": {
|
||||
"confirmTitle": "Confirm Action",
|
||||
"confirmMessage": "Are you sure you want to perform this action?",
|
||||
|
||||
@@ -79,6 +79,9 @@
|
||||
"devDocs": "Extension Development Docs",
|
||||
"submitRepo": "Submit Extension Repository"
|
||||
},
|
||||
"tags": {
|
||||
"danger": "Danger"
|
||||
},
|
||||
"dialogs": {
|
||||
"error": {
|
||||
"title": "Error Information",
|
||||
@@ -112,6 +115,12 @@
|
||||
"title": "Install Extension",
|
||||
"fromFile": "Install from File",
|
||||
"fromUrl": "Install from URL"
|
||||
},
|
||||
"danger_warning": {
|
||||
"title": "Dangerous Plugin Warning",
|
||||
"message": "This plugin has been flagged as containing security risks, including unsafe code or functionalities that may cause system malfunctions or data loss. Do you wish to proceed with the installation?",
|
||||
"confirm": "Continue",
|
||||
"cancel": "Cancel"
|
||||
}
|
||||
},
|
||||
"messages": {
|
||||
@@ -164,4 +173,4 @@
|
||||
"confirmNotRegistered": "$confirm not properly registered"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"save": "保存",
|
||||
"cancel": "取消",
|
||||
"close": "关闭",
|
||||
"copy": "复制",
|
||||
"delete": "删除",
|
||||
"edit": "编辑",
|
||||
"add": "添加",
|
||||
@@ -32,6 +33,7 @@
|
||||
"longPress": "长按",
|
||||
"yes": "是",
|
||||
"no": "否",
|
||||
"imagePreview": "图片预览",
|
||||
"dialog": {
|
||||
"confirmTitle": "确认操作",
|
||||
"confirmMessage": "你确定要执行此操作吗?",
|
||||
|
||||
@@ -79,6 +79,9 @@
|
||||
"devDocs": "插件开发文档",
|
||||
"submitRepo": "提交插件仓库"
|
||||
},
|
||||
"tags": {
|
||||
"danger": "危险"
|
||||
},
|
||||
"dialogs": {
|
||||
"error": {
|
||||
"title": "错误信息",
|
||||
@@ -112,6 +115,12 @@
|
||||
"title": "安装插件",
|
||||
"fromFile": "从文件安装",
|
||||
"fromUrl": "从链接安装"
|
||||
},
|
||||
"danger_warning": {
|
||||
"title": "警告",
|
||||
"message": "该插件可能包含不安全的代码或功能,可能导致系统异常或数据损失等。请确认是否继续安装?",
|
||||
"confirm": "继续",
|
||||
"cancel": "取消"
|
||||
}
|
||||
},
|
||||
"messages": {
|
||||
@@ -164,4 +173,4 @@
|
||||
"confirmNotRegistered": "$confirm 未正确注册"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,9 +7,17 @@ import LanguageSwitcher from '@/components/shared/LanguageSwitcher.vue';
|
||||
import {md5} from 'js-md5';
|
||||
import {useAuthStore} from '@/stores/auth';
|
||||
import {useCommonStore} from '@/stores/common';
|
||||
import {marked} from 'marked';
|
||||
import MarkdownIt from 'markdown-it';
|
||||
import { useI18n } from '@/i18n/composables';
|
||||
|
||||
// 配置markdown-it,默认安全设置
|
||||
const md = new MarkdownIt({
|
||||
html: true, // 启用HTML标签
|
||||
breaks: true, // 换行转<br>
|
||||
linkify: true, // 自动转链接
|
||||
typographer: false // 禁用智能引号
|
||||
});
|
||||
|
||||
const customizer = useCustomizerStore();
|
||||
const { t } = useI18n();
|
||||
let dialog = ref(false);
|
||||
@@ -323,7 +331,7 @@ commonStore.getStartTime();
|
||||
|
||||
<div v-if="releaseMessage"
|
||||
style="background-color: #646cff24; padding: 16px; border-radius: 10px; font-size: 14px; max-height: 400px; overflow-y: auto;"
|
||||
v-html="marked(releaseMessage)" class="markdown-content">
|
||||
v-html="md.render(releaseMessage)" class="markdown-content">
|
||||
</div>
|
||||
|
||||
<div class="mb-4 mt-4">
|
||||
|
||||
@@ -19,7 +19,7 @@ export default createVuetify({
|
||||
defaults: {
|
||||
VBtn: {},
|
||||
VCard: {
|
||||
rounded: 'md'
|
||||
rounded: 'lg'
|
||||
},
|
||||
VTextField: {
|
||||
rounded: 'lg'
|
||||
|
||||
@@ -27,7 +27,7 @@ const PurpleTheme: ThemeTypes = {
|
||||
borderLight: '#d0d0d0',
|
||||
border: '#d0d0d0',
|
||||
inputBorder: '#787878',
|
||||
containerBg: '#eef2f6',
|
||||
containerBg: '#f7f1f6',
|
||||
surface: '#fff',
|
||||
'on-surface-variant': '#fff',
|
||||
facebook: '#4267b2',
|
||||
|
||||
@@ -80,7 +80,7 @@
|
||||
<div class="conversation-header-content" v-if="currCid && getCurrentConversation">
|
||||
<h2 class="conversation-header-title">{{ getCurrentConversation.title ||
|
||||
tm('conversation.newConversation')
|
||||
}}</h2>
|
||||
}}</h2>
|
||||
<div class="conversation-header-time">{{ formatDate(getCurrentConversation.updated_at) }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -149,6 +149,7 @@
|
||||
<!-- 用户消息 -->
|
||||
<div v-if="msg.type == 'user'" class="user-message">
|
||||
<div class="message-bubble user-bubble"
|
||||
:class="{ 'has-audio': msg.audio_url }"
|
||||
:style="{ backgroundColor: isDark ? '#2d2e30' : '#e7ebf4' }">
|
||||
<span>{{ msg.message }}</span>
|
||||
|
||||
@@ -156,7 +157,7 @@
|
||||
<div class="image-attachments" v-if="msg.image_url && msg.image_url.length > 0">
|
||||
<div v-for="(img, index) in msg.image_url" :key="index"
|
||||
class="image-attachment">
|
||||
<img :src="img" class="attached-image" />
|
||||
<img :src="img" class="attached-image" @click="openImagePreview(img)" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -175,8 +176,17 @@
|
||||
<v-avatar class="bot-avatar" size="36">
|
||||
<span class="text-h2">✨</span>
|
||||
</v-avatar>
|
||||
<div class="message-bubble bot-bubble">
|
||||
<div v-html="marked(msg.message)" class="markdown-content"></div>
|
||||
<div class="bot-message-content">
|
||||
<div class="message-bubble bot-bubble">
|
||||
<div v-html="md.render(msg.message)" class="markdown-content"></div>
|
||||
</div>
|
||||
<div class="message-actions">
|
||||
<v-btn :icon="getCopyIcon(index)" size="small" variant="text"
|
||||
class="copy-message-btn"
|
||||
:class="{ 'copy-success': isCopySuccess(index) }"
|
||||
@click="copyBotMessage(msg.message, index)"
|
||||
:title="t('core.common.copy')" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -247,12 +257,25 @@
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- 图片预览对话框 -->
|
||||
<v-dialog v-model="imagePreviewDialog" max-width="90vw" max-height="90vh">
|
||||
<v-card class="image-preview-card" elevation="8">
|
||||
<v-card-title class="d-flex justify-space-between align-center pa-4">
|
||||
<span>{{ t('core.common.imagePreview') }}</span>
|
||||
<v-btn icon="mdi-close" variant="text" @click="imagePreviewDialog = false" />
|
||||
</v-card-title>
|
||||
<v-card-text class="text-center pa-4">
|
||||
<img :src="previewImageUrl" class="preview-image-large" />
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { router } from '@/router';
|
||||
import axios from 'axios';
|
||||
import { marked } from 'marked';
|
||||
import MarkdownIt from 'markdown-it';
|
||||
import { ref } from 'vue';
|
||||
import { useCustomizerStore } from '@/stores/customizer';
|
||||
import { useI18n, useModuleI18n } from '@/i18n/composables';
|
||||
@@ -261,8 +284,11 @@ import ProviderModelSelector from '@/components/chat/ProviderModelSelector.vue';
|
||||
import hljs from 'highlight.js';
|
||||
import 'highlight.js/styles/github.css';
|
||||
|
||||
marked.setOptions({
|
||||
breaks: true,
|
||||
// 配置markdown-it,启用代码高亮
|
||||
const md = new MarkdownIt({
|
||||
html: false, // 禁用HTML标签,防XSS
|
||||
breaks: true, // 换行转<br>
|
||||
linkify: true, // 自动转链接
|
||||
highlight: function (code, lang) {
|
||||
if (lang && hljs.getLanguage(lang)) {
|
||||
try {
|
||||
@@ -294,7 +320,7 @@ export default {
|
||||
t,
|
||||
tm,
|
||||
router,
|
||||
marked,
|
||||
md,
|
||||
ref
|
||||
};
|
||||
},
|
||||
@@ -340,6 +366,15 @@ export default {
|
||||
sidebarHoverExpanded: false,
|
||||
sidebarHoverDelay: 100, // 悬停延迟,单位毫秒
|
||||
pendingCid: null, // Store pending conversation ID for route handling
|
||||
|
||||
// 复制成功提示
|
||||
copySuccessMessage: null,
|
||||
copySuccessTimeout: null,
|
||||
copiedMessages: new Set(), // 存储已复制的消息索引
|
||||
|
||||
// 图片预览相关变量
|
||||
imagePreviewDialog: false,
|
||||
previewImageUrl: ''
|
||||
}
|
||||
},
|
||||
|
||||
@@ -545,6 +580,25 @@ export default {
|
||||
this.stagedAudioUrl = null;
|
||||
},
|
||||
|
||||
openImagePreview(imageUrl) {
|
||||
this.previewImageUrl = imageUrl;
|
||||
this.imagePreviewDialog = true;
|
||||
},
|
||||
|
||||
initImageClickEvents() {
|
||||
this.$nextTick(() => {
|
||||
// 查找所有动态生成的图片(在markdown-content中)
|
||||
const images = document.querySelectorAll('.markdown-content img');
|
||||
images.forEach((img) => {
|
||||
if (!img.hasAttribute('data-click-enabled')) {
|
||||
img.style.cursor = 'pointer';
|
||||
img.setAttribute('data-click-enabled', 'true');
|
||||
img.onclick = () => this.openImagePreview(img.src);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
checkStatus() {
|
||||
axios.get('/api/chat/status').then(response => {
|
||||
console.log(response.data);
|
||||
@@ -691,6 +745,7 @@ export default {
|
||||
}
|
||||
this.messages = message;
|
||||
this.initCodeCopyButtons();
|
||||
this.initImageClickEvents();
|
||||
}).catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
@@ -909,8 +964,9 @@ export default {
|
||||
}
|
||||
} else if (chunk_json.type === 'end') {
|
||||
in_streaming = false;
|
||||
// 在消息流结束后初始化代码复制按钮
|
||||
// 在消息流结束后初始化代码复制按钮和图片点击事件
|
||||
this.initCodeCopyButtons();
|
||||
this.initImageClickEvents();
|
||||
continue;
|
||||
} else if (chunk_json.type === 'update_title') {
|
||||
// 更新对话标题
|
||||
@@ -950,8 +1006,9 @@ export default {
|
||||
this.$nextTick(() => {
|
||||
const container = this.$refs.messageContainer;
|
||||
container.scrollTop = container.scrollHeight;
|
||||
// 在滚动后初始化代码复制按钮
|
||||
// 在滚动后初始化代码复制按钮和图片点击事件
|
||||
this.initCodeCopyButtons();
|
||||
this.initImageClickEvents();
|
||||
});
|
||||
},
|
||||
handleInputKeyDown(e) {
|
||||
@@ -1000,7 +1057,6 @@ export default {
|
||||
// 复制代码到剪贴板
|
||||
copyCodeToClipboard(code) {
|
||||
navigator.clipboard.writeText(code).then(() => {
|
||||
// 可以添加一个简单的提示
|
||||
console.log('代码已复制到剪贴板');
|
||||
}).catch(err => {
|
||||
console.error('复制失败:', err);
|
||||
@@ -1019,6 +1075,54 @@ export default {
|
||||
});
|
||||
},
|
||||
|
||||
// 复制bot消息到剪贴板
|
||||
copyBotMessage(message, messageIndex) {
|
||||
// 移除HTML标签,获取纯文本
|
||||
const tempDiv = document.createElement('div');
|
||||
tempDiv.innerHTML = message;
|
||||
const plainText = tempDiv.textContent || tempDiv.innerText || message;
|
||||
|
||||
navigator.clipboard.writeText(plainText).then(() => {
|
||||
console.log('消息已复制到剪贴板');
|
||||
this.showCopySuccess(messageIndex);
|
||||
}).catch(err => {
|
||||
console.error('复制失败:', err);
|
||||
// 如果现代API失败,使用传统方法
|
||||
const textArea = document.createElement('textarea');
|
||||
textArea.value = plainText;
|
||||
document.body.appendChild(textArea);
|
||||
textArea.select();
|
||||
try {
|
||||
document.execCommand('copy');
|
||||
console.log('消息已复制到剪贴板 (fallback)');
|
||||
this.showCopySuccess(messageIndex);
|
||||
} catch (fallbackErr) {
|
||||
console.error('复制失败 (fallback):', fallbackErr);
|
||||
}
|
||||
document.body.removeChild(textArea);
|
||||
});
|
||||
},
|
||||
|
||||
// 显示复制成功提示
|
||||
showCopySuccess(messageIndex) {
|
||||
this.copiedMessages.add(messageIndex);
|
||||
|
||||
// 2秒后移除成功状态
|
||||
setTimeout(() => {
|
||||
this.copiedMessages.delete(messageIndex);
|
||||
}, 2000);
|
||||
},
|
||||
|
||||
// 获取复制按钮图标
|
||||
getCopyIcon(messageIndex) {
|
||||
return this.copiedMessages.has(messageIndex) ? 'mdi-check' : 'mdi-content-copy';
|
||||
},
|
||||
|
||||
// 检查是否为复制成功状态
|
||||
isCopySuccess(messageIndex) {
|
||||
return this.copiedMessages.has(messageIndex);
|
||||
},
|
||||
|
||||
// 获取复制图标SVG
|
||||
getCopyIconSvg() {
|
||||
return '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>';
|
||||
@@ -1368,16 +1472,58 @@ export default {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.bot-message-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
max-width: 80%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.message-actions {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.bot-message:hover .message-actions {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.copy-message-btn {
|
||||
opacity: 0.6;
|
||||
transition: all 0.2s ease;
|
||||
color: var(--v-theme-secondary);
|
||||
}
|
||||
|
||||
.copy-message-btn:hover {
|
||||
opacity: 1;
|
||||
background-color: rgba(103, 58, 183, 0.1);
|
||||
}
|
||||
|
||||
.copy-message-btn.copy-success {
|
||||
color: #4caf50;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.copy-message-btn.copy-success:hover {
|
||||
color: #4caf50;
|
||||
background-color: rgba(76, 175, 80, 0.1);
|
||||
}
|
||||
|
||||
.message-bubble {
|
||||
padding: 8px 16px;
|
||||
border-radius: 12px;
|
||||
max-width: 80%;
|
||||
}
|
||||
|
||||
.user-bubble {
|
||||
color: var(--v-theme-primaryText);
|
||||
padding: 12px 16px;
|
||||
padding: 18px 20px;
|
||||
font-size: 16px;
|
||||
max-width: 60%;
|
||||
border-radius: 1.5rem;
|
||||
}
|
||||
|
||||
.bot-bubble {
|
||||
@@ -1415,13 +1561,59 @@ export default {
|
||||
|
||||
.attached-image:hover {
|
||||
transform: scale(1.02);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* 图片预览对话框样式 */
|
||||
.image-preview-card {
|
||||
background-color: var(--v-theme-surface) !important;
|
||||
border: 1px solid var(--v-theme-border);
|
||||
}
|
||||
|
||||
/* 亮色主题下的图片预览对话框 */
|
||||
.v-theme--light .image-preview-card,
|
||||
.v-theme--PurpleTheme .image-preview-card {
|
||||
background-color: #ffffff !important;
|
||||
border-color: #e0e0e0 !important;
|
||||
}
|
||||
|
||||
/* 暗色主题下的图片预览对话框 */
|
||||
.v-theme--dark .image-preview-card,
|
||||
.v-theme--PurpleThemeDark .image-preview-card {
|
||||
background-color: #1e1e1e !important;
|
||||
border-color: #333333 !important;
|
||||
}
|
||||
|
||||
/* 确保对话框标题栏和内容区域的背景色 */
|
||||
.image-preview-card .v-card-title {
|
||||
background-color: inherit;
|
||||
}
|
||||
|
||||
.image-preview-card .v-card-text {
|
||||
background-color: inherit;
|
||||
}
|
||||
|
||||
.preview-image-large {
|
||||
max-width: 100%;
|
||||
max-height: 75vh;
|
||||
width: auto;
|
||||
height: auto;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.audio-attachment {
|
||||
margin-top: 8px;
|
||||
min-width: 250px;
|
||||
}
|
||||
|
||||
/* 包含音频的消息气泡最小宽度 */
|
||||
.message-bubble.has-audio {
|
||||
min-width: 280px;
|
||||
}
|
||||
|
||||
.audio-player {
|
||||
width: 100%;
|
||||
height: 36px;
|
||||
border-radius: 18px;
|
||||
}
|
||||
@@ -1727,6 +1919,5 @@ export default {
|
||||
width: 100%;
|
||||
padding-right: 32px;
|
||||
flex-shrink: 0;
|
||||
/* 防止header被压缩 */
|
||||
}
|
||||
</style>
|
||||
@@ -318,12 +318,16 @@
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
import { VueMonacoEditor } from '@guolao/vue-monaco-editor';
|
||||
import { marked } from 'marked';
|
||||
import MarkdownIt from 'markdown-it';
|
||||
import { useCommonStore } from '@/stores/common';
|
||||
import { useI18n, useModuleI18n } from '@/i18n/composables';
|
||||
|
||||
marked.setOptions({
|
||||
breaks: true
|
||||
// 配置markdown-it,默认安全设置
|
||||
const md = new MarkdownIt({
|
||||
html: false, // 禁用HTML标签(关键!)
|
||||
breaks: true, // 换行转<br>
|
||||
linkify: true, // 自动转链接
|
||||
typographer: false // 禁用智能引号(避免干扰)
|
||||
});
|
||||
|
||||
export default {
|
||||
@@ -879,8 +883,9 @@ export default {
|
||||
// 处理字符串内容
|
||||
final_content = content;
|
||||
} else if (!final_content) return this.tm('status.emptyContent');
|
||||
// 使用marked处理Markdown格式
|
||||
return marked(final_content);
|
||||
|
||||
// 使用markdown-it处理,默认安全(html: false会禁用HTML标签)
|
||||
return md.render(final_content);
|
||||
},
|
||||
|
||||
// 显示成功消息
|
||||
|
||||
@@ -58,6 +58,10 @@ const isListView = ref(false);
|
||||
const pluginSearch = ref("");
|
||||
const loading_ = ref(false);
|
||||
|
||||
// 危险插件确认对话框
|
||||
const dangerConfirmDialog = ref(false);
|
||||
const selectedDangerPlugin = ref(null);
|
||||
|
||||
// 插件市场相关
|
||||
const extension_url = ref("");
|
||||
const dialog = ref(false);
|
||||
@@ -421,6 +425,35 @@ const open = (link) => {
|
||||
}
|
||||
};
|
||||
|
||||
// 为表格视图创建一个处理安装插件的函数
|
||||
const handleInstallPlugin = async (plugin) => {
|
||||
if (plugin.tags && plugin.tags.includes('danger')) {
|
||||
selectedDangerPlugin.value = plugin;
|
||||
dangerConfirmDialog.value = true;
|
||||
} else {
|
||||
extension_url.value = plugin.repo;
|
||||
dialog.value = true;
|
||||
uploadTab.value = 'url';
|
||||
}
|
||||
};
|
||||
|
||||
// 确认安装危险插件
|
||||
const confirmDangerInstall = () => {
|
||||
if (selectedDangerPlugin.value) {
|
||||
extension_url.value = selectedDangerPlugin.value.repo;
|
||||
dialog.value = true;
|
||||
uploadTab.value = 'url';
|
||||
}
|
||||
dangerConfirmDialog.value = false;
|
||||
selectedDangerPlugin.value = null;
|
||||
};
|
||||
|
||||
// 取消安装危险插件
|
||||
const cancelDangerInstall = () => {
|
||||
dangerConfirmDialog.value = false;
|
||||
selectedDangerPlugin.value = null;
|
||||
};
|
||||
|
||||
// 插件市场显示完整插件名称
|
||||
const trimExtensionName = () => {
|
||||
pluginMarketData.value.forEach(plugin => {
|
||||
@@ -554,7 +587,7 @@ onMounted(async () => {
|
||||
<template>
|
||||
<v-row>
|
||||
<v-col cols="12" md="12">
|
||||
<v-card variant="flat" class="rounded-xl">
|
||||
<v-card variant="flat">
|
||||
<v-card-item>
|
||||
<template v-slot:prepend>
|
||||
<div class="plugin-page-icon d-flex justify-center align-center rounded-lg mr-4">
|
||||
@@ -823,7 +856,7 @@ onMounted(async () => {
|
||||
<v-row style="margin-top: 8px;">
|
||||
<v-col cols="12" md="6" lg="6" v-for="plugin in pinnedPlugins" :key="plugin.name">
|
||||
<ExtensionCard :extension="plugin" class="h-120 rounded-lg" market-mode="true" :highlight="true"
|
||||
@install="extension_url = plugin.repo; dialog = true; uploadTab = 'url'" @view-readme="open(plugin.repo)">
|
||||
@install="handleInstallPlugin(plugin)" @view-readme="open(plugin.repo)">
|
||||
</ExtensionCard>
|
||||
</v-col>
|
||||
</v-row>
|
||||
@@ -871,12 +904,12 @@ onMounted(async () => {
|
||||
</template>
|
||||
<template v-slot:item.tags="{ item }">
|
||||
<span v-if="item.tags.length === 0">-</span>
|
||||
<v-chip v-for="tag in item.tags" :key="tag" color="primary" size="x-small">
|
||||
<v-chip v-for="tag in item.tags" :key="tag" :color="tag === 'danger' ? 'error' : 'primary'" size="x-small" v-show="tag !== 'danger'">
|
||||
{{ tag }}</v-chip>
|
||||
</template>
|
||||
<template v-slot:item.actions="{ item }">
|
||||
<v-btn v-if="!item.installed" class="text-none mr-2" size="x-small" variant="flat"
|
||||
@click="extension_url = item.repo; dialog = true; uploadTab = 'url'">
|
||||
@click="handleInstallPlugin(item)">
|
||||
<v-icon>mdi-download</v-icon></v-btn>
|
||||
<v-btn v-else class="text-none mr-2" size="x-small" variant="flat" border
|
||||
disabled><v-icon>mdi-check</v-icon></v-btn>
|
||||
@@ -960,7 +993,7 @@ onMounted(async () => {
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</div>
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -1078,6 +1111,28 @@ onMounted(async () => {
|
||||
<ReadmeDialog v-model:show="readmeDialog.show" :plugin-name="readmeDialog.pluginName"
|
||||
:repo-url="readmeDialog.repoUrl" />
|
||||
|
||||
<!-- 危险插件确认对话框 -->
|
||||
<v-dialog v-model="dangerConfirmDialog" width="500" persistent>
|
||||
<v-card>
|
||||
<v-card-title class="text-h5 d-flex align-center">
|
||||
<v-icon color="warning" class="mr-2">mdi-alert-circle</v-icon>
|
||||
{{ tm('dialogs.danger_warning.title') }}
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<div>{{ tm('dialogs.danger_warning.message') }}</div>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="grey" @click="cancelDangerInstall">
|
||||
{{ tm('dialogs.danger_warning.cancel') }}
|
||||
</v-btn>
|
||||
<v-btn color="warning" @click="confirmDangerInstall">
|
||||
{{ tm('dialogs.danger_warning.confirm') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- 上传插件对话框 -->
|
||||
<v-dialog v-model="dialog" width="500">
|
||||
<v-card>
|
||||
|
||||
@@ -1,62 +1,48 @@
|
||||
<template>
|
||||
<div class="platform-page">
|
||||
<v-container fluid class="pa-0">
|
||||
<!-- 页面标题 -->
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<h1 class="text-h4 font-weight-bold mb-2">
|
||||
<v-icon size="x-large" color="primary" class="me-2">mdi-connection</v-icon>{{ tm('title') }}
|
||||
<v-row class="d-flex justify-space-between align-center px-4 py-3 pb-8">
|
||||
<div>
|
||||
<h1 class="text-h1 font-weight-bold mb-2">
|
||||
<v-icon color="black" class="me-2">mdi-connection</v-icon>{{ tm('title') }}
|
||||
</h1>
|
||||
<p class="text-subtitle-1 text-medium-emphasis mb-4">
|
||||
{{ tm('subtitle') }}
|
||||
</p>
|
||||
</v-col>
|
||||
</div>
|
||||
<v-btn color="primary" prepend-icon="mdi-plus" variant="tonal" @click="showAddPlatformDialog = true" rounded="xl" size="x-large">
|
||||
{{ tm('addAdapter') }}
|
||||
</v-btn>
|
||||
</v-row>
|
||||
|
||||
<!-- 平台适配器部分 -->
|
||||
<v-card class="mb-6" elevation="2">
|
||||
<v-card-title class="d-flex align-center py-3 px-4">
|
||||
<v-icon color="primary" class="me-2">mdi-apps</v-icon>
|
||||
<span class="text-h6">{{ tm('adapters') }}</span>
|
||||
<v-chip color="info" size="small" class="ml-2">{{ config_data.platform?.length || 0 }}</v-chip>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="primary" prepend-icon="mdi-plus" variant="tonal" @click="showAddPlatformDialog = true">
|
||||
{{ tm('addAdapter') }}
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
<div>
|
||||
<v-row v-if="(config_data.platform || []).length === 0">
|
||||
<v-col cols="12" class="text-center pa-8">
|
||||
<v-icon size="64" color="grey-lighten-1">mdi-connection</v-icon>
|
||||
<p class="text-grey mt-4">{{ tm('emptyText') }}</p>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-divider></v-divider>
|
||||
|
||||
<v-card-text class="px-4 py-3">
|
||||
<item-card-grid :items="config_data.platform || []" title-field="id" enabled-field="enable"
|
||||
empty-icon="mdi-connection" :empty-text="tm('emptyText')" @toggle-enabled="platformStatusChange"
|
||||
@delete="deletePlatform" @edit="editPlatform">
|
||||
<template v-slot:item-details="{ item }">
|
||||
<div class="d-flex align-center mb-2">
|
||||
<v-icon size="small" color="grey" class="me-2">mdi-tag</v-icon>
|
||||
<span class="text-caption text-medium-emphasis">
|
||||
{{ tm('details.adapterType') }}:
|
||||
<v-chip size="x-small" color="primary" class="ml-1">{{ item.type }}</v-chip>
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="item.token" class="d-flex align-center mb-2">
|
||||
<v-icon size="small" color="grey" class="me-2">mdi-key</v-icon>
|
||||
<span class="text-caption text-medium-emphasis">{{ tm('details.token') }}: ••••••••</span>
|
||||
</div>
|
||||
<div v-if="item.description" class="d-flex align-center">
|
||||
<v-icon size="small" color="grey" class="me-2">mdi-information-outline</v-icon>
|
||||
<span class="text-caption text-medium-emphasis text-truncate">{{ item.description }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</item-card-grid>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
<v-row v-else>
|
||||
<v-col v-for="(platform, index) in config_data.platform || []" :key="index" cols="12" md="6" lg="4" xl="3">
|
||||
<item-card
|
||||
:item="platform"
|
||||
title-field="id"
|
||||
enabled-field="enable"
|
||||
:bglogo="getPlatformIcon(platform.type || platform.id)"
|
||||
@toggle-enabled="platformStatusChange"
|
||||
@delete="deletePlatform"
|
||||
@edit="editPlatform">
|
||||
</item-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
|
||||
<!-- 日志部分 -->
|
||||
<v-card elevation="2">
|
||||
<v-card elevation="0" class="mt-4">
|
||||
<v-card-title class="d-flex align-center py-3 px-4">
|
||||
<v-icon color="primary" class="me-2">mdi-console-line</v-icon>
|
||||
<span class="text-h6">{{ tm('logs.title') }}</span>
|
||||
<v-icon class="me-2">mdi-console-line</v-icon>
|
||||
<span class="text-h4">{{ tm('logs.title') }}</span>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn variant="text" color="primary" @click="showConsole = !showConsole">
|
||||
{{ showConsole ? tm('logs.collapse') : tm('logs.expand') }}
|
||||
@@ -99,7 +85,7 @@
|
||||
</v-card-text>
|
||||
</div>
|
||||
<div class="platform-card-logo">
|
||||
<img :src="getPlatformIcon(name)" v-if="getPlatformIcon(name)" class="platform-logo-img">
|
||||
<img :src="getPlatformIcon(template.type)" v-if="getPlatformIcon(template.type)" class="platform-logo-img">
|
||||
<div v-else class="platform-logo-fallback">
|
||||
{{ name[0].toUpperCase() }}
|
||||
</div>
|
||||
@@ -179,7 +165,8 @@
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="grey" variant="text" @click="handleIdConflictConfirm(false)">{{ tm('dialog.idConflict.confirm') }}</v-btn>
|
||||
<v-btn color="grey" variant="text" @click="handleIdConflictConfirm(false)">{{ tm('dialog.idConflict.confirm')
|
||||
}}</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
@@ -191,7 +178,7 @@ import axios from 'axios';
|
||||
import AstrBotConfig from '@/components/shared/AstrBotConfig.vue';
|
||||
import WaitingForRestart from '@/components/shared/WaitingForRestart.vue';
|
||||
import ConsoleDisplayer from '@/components/shared/ConsoleDisplayer.vue';
|
||||
import ItemCardGrid from '@/components/shared/ItemCardGrid.vue';
|
||||
import ItemCard from '@/components/shared/ItemCard.vue';
|
||||
import { useCommonStore } from '@/stores/common';
|
||||
import { useI18n, useModuleI18n } from '@/i18n/composables';
|
||||
|
||||
@@ -201,7 +188,7 @@ export default {
|
||||
AstrBotConfig,
|
||||
WaitingForRestart,
|
||||
ConsoleDisplayer,
|
||||
ItemCardGrid
|
||||
ItemCard
|
||||
},
|
||||
setup() {
|
||||
const { t } = useI18n();
|
||||
@@ -274,25 +261,25 @@ export default {
|
||||
},
|
||||
|
||||
getPlatformIcon(name) {
|
||||
if (name.includes('QQ')) {
|
||||
if (name === 'aiocqhttp' || name === 'qq_official' || name === 'qq_official_webhook') {
|
||||
return new URL('@/assets/images/platform_logos/qq.png', import.meta.url).href
|
||||
} else if (name.includes('企业微信')) {
|
||||
} else if (name === 'wecom') {
|
||||
return new URL('@/assets/images/platform_logos/wecom.png', import.meta.url).href
|
||||
} else if (name.includes('微信')) {
|
||||
} else if (name === 'gewechat' || name === 'wechatpadpro' || name === 'weixin_official_account' || name === 'wechat') {
|
||||
return new URL('@/assets/images/platform_logos/wechat.png', import.meta.url).href
|
||||
} else if (name.includes('Lark')) {
|
||||
} else if (name === 'lark') {
|
||||
return new URL('@/assets/images/platform_logos/lark.png', import.meta.url).href
|
||||
} else if (name.includes('DingTalk')) {
|
||||
} else if (name === 'dingtalk') {
|
||||
return new URL('@/assets/images/platform_logos/dingtalk.svg', import.meta.url).href
|
||||
} else if (name.includes('Telegram')) {
|
||||
} else if (name === 'telegram') {
|
||||
return new URL('@/assets/images/platform_logos/telegram.svg', import.meta.url).href
|
||||
} else if (name.includes('Discord')) {
|
||||
} else if (name === 'discord') {
|
||||
return new URL('@/assets/images/platform_logos/discord.svg', import.meta.url).href
|
||||
} else if (name.includes('Slack')) {
|
||||
} else if (name === 'slack') {
|
||||
return new URL('@/assets/images/platform_logos/slack.svg', import.meta.url).href
|
||||
} else if (name.includes('kook')) {
|
||||
} else if (name === 'kook') {
|
||||
return new URL('@/assets/images/platform_logos/kook.png', import.meta.url).href
|
||||
} else if (name.includes('vocechat')) {
|
||||
} else if (name === 'vocechat') {
|
||||
return new URL('@/assets/images/platform_logos/vocechat.png', import.meta.url).href
|
||||
}
|
||||
},
|
||||
|
||||
@@ -2,153 +2,137 @@
|
||||
<div class="provider-page">
|
||||
<v-container fluid class="pa-0">
|
||||
<!-- 页面标题 -->
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<h1 class="text-h4 font-weight-bold mb-2">
|
||||
<v-icon size="x-large" color="primary" class="me-2">mdi-creation</v-icon>{{ tm('title') }}
|
||||
<v-row class="d-flex justify-space-between align-center px-4 py-3 pb-8">
|
||||
<div>
|
||||
<h1 class="text-h1 font-weight-bold mb-2">
|
||||
<v-icon color="black" class="me-2">mdi-creation</v-icon>{{ tm('title') }}
|
||||
</h1>
|
||||
<p class="text-subtitle-1 text-medium-emphasis mb-4">
|
||||
{{ tm('subtitle') }}
|
||||
</p>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- 服务提供商部分 -->
|
||||
<v-card class="mb-6" elevation="2">
|
||||
<v-card-title class="d-flex align-center py-3 px-4">
|
||||
<v-icon color="primary" class="me-2">mdi-api</v-icon>
|
||||
<span class="text-h6">{{ tm('providers.title') }}</span>
|
||||
<v-chip color="info" size="small" class="ml-2">{{ config_data.provider?.length || 0 }}</v-chip>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="success" prepend-icon="mdi-cog" variant="tonal" class="me-2" @click="showSettingsDialog = true">
|
||||
</div>
|
||||
<div>
|
||||
<v-btn color="success" prepend-icon="mdi-cog" variant="tonal" class="me-2" @click="showSettingsDialog = true" rounded="xl" size="x-large">
|
||||
{{ tm('providers.settings') }}
|
||||
</v-btn>
|
||||
<v-btn color="primary" prepend-icon="mdi-plus" variant="tonal" @click="showAddProviderDialog = true">
|
||||
<v-btn color="primary" prepend-icon="mdi-plus" variant="tonal" @click="showAddProviderDialog = true" rounded="xl" size="x-large">
|
||||
{{ tm('providers.addProvider') }}
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
|
||||
<v-divider></v-divider>
|
||||
</div>
|
||||
</v-row>
|
||||
|
||||
<div>
|
||||
<!-- 添加分类标签页 -->
|
||||
<v-card-text class="px-4 pt-3 pb-0">
|
||||
<v-tabs v-model="activeProviderTypeTab" bg-color="transparent">
|
||||
<v-tab value="all" class="font-weight-medium px-3">
|
||||
<v-icon start>mdi-filter-variant</v-icon>
|
||||
{{ tm('providers.tabs.all') }}
|
||||
</v-tab>
|
||||
<v-tab value="chat_completion" class="font-weight-medium px-3">
|
||||
<v-icon start>mdi-message-text</v-icon>
|
||||
{{ tm('providers.tabs.chatCompletion') }}
|
||||
</v-tab>
|
||||
<v-tab value="speech_to_text" class="font-weight-medium px-3">
|
||||
<v-icon start>mdi-microphone-message</v-icon>
|
||||
{{ tm('providers.tabs.speechToText') }}
|
||||
</v-tab>
|
||||
<v-tab value="text_to_speech" class="font-weight-medium px-3">
|
||||
<v-icon start>mdi-volume-high</v-icon>
|
||||
{{ tm('providers.tabs.textToSpeech') }}
|
||||
</v-tab>
|
||||
<v-tab value="embedding" class="font-weight-medium px-3">
|
||||
<v-icon start>mdi-code-json</v-icon>
|
||||
{{ tm('providers.tabs.embedding') }}
|
||||
</v-tab>
|
||||
</v-tabs>
|
||||
</v-card-text>
|
||||
<v-tabs v-model="activeProviderTypeTab" bg-color="transparent" class="mb-4">
|
||||
<v-tab value="all" class="font-weight-medium px-3">
|
||||
<v-icon start>mdi-filter-variant</v-icon>
|
||||
{{ tm('providers.tabs.all') }}
|
||||
</v-tab>
|
||||
<v-tab value="chat_completion" class="font-weight-medium px-3">
|
||||
<v-icon start>mdi-message-text</v-icon>
|
||||
{{ tm('providers.tabs.chatCompletion') }}
|
||||
</v-tab>
|
||||
<v-tab value="speech_to_text" class="font-weight-medium px-3">
|
||||
<v-icon start>mdi-microphone-message</v-icon>
|
||||
{{ tm('providers.tabs.speechToText') }}
|
||||
</v-tab>
|
||||
<v-tab value="text_to_speech" class="font-weight-medium px-3">
|
||||
<v-icon start>mdi-volume-high</v-icon>
|
||||
{{ tm('providers.tabs.textToSpeech') }}
|
||||
</v-tab>
|
||||
<v-tab value="embedding" class="font-weight-medium px-3">
|
||||
<v-icon start>mdi-code-json</v-icon>
|
||||
{{ tm('providers.tabs.embedding') }}
|
||||
</v-tab>
|
||||
</v-tabs>
|
||||
|
||||
<v-card-text class="px-4 py-3">
|
||||
<item-card-grid
|
||||
:items="filteredProviders"
|
||||
title-field="id"
|
||||
enabled-field="enable"
|
||||
empty-icon="mdi-api-off"
|
||||
:empty-text="getEmptyText()"
|
||||
@toggle-enabled="providerStatusChange"
|
||||
@delete="deleteProvider"
|
||||
@edit="configExistingProvider"
|
||||
>
|
||||
<template v-slot:item-details="{ item }">
|
||||
<div class="d-flex align-center mb-2">
|
||||
<v-icon size="small" color="grey" class="me-2">mdi-tag</v-icon>
|
||||
<span class="text-caption text-medium-emphasis">
|
||||
{{ tm('providers.providerType') }}:
|
||||
<v-chip size="x-small" color="primary" class="ml-1">{{ item.type }}</v-chip>
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="item.api_base" class="d-flex align-center mb-2">
|
||||
<v-icon size="small" color="grey" class="me-2">mdi-web</v-icon>
|
||||
<span class="text-caption text-medium-emphasis text-truncate" :title="item.api_base">
|
||||
API Base: {{ item.api_base }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="item.api_key" class="d-flex align-center">
|
||||
<v-icon size="small" color="grey" class="me-2">mdi-key</v-icon>
|
||||
<span class="text-caption text-medium-emphasis">API Key: ••••••••</span>
|
||||
</div>
|
||||
</template>
|
||||
</item-card-grid>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
<v-row v-if="filteredProviders.length === 0">
|
||||
<v-col cols="12" class="text-center pa-8">
|
||||
<v-icon size="64" color="grey-lighten-1">mdi-api-off</v-icon>
|
||||
<p class="text-grey mt-4">{{ getEmptyText() }}</p>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row v-else>
|
||||
<v-col v-for="(provider, index) in filteredProviders" :key="index" cols="12" md="6" lg="4" xl="3">
|
||||
<item-card
|
||||
:item="provider"
|
||||
title-field="id"
|
||||
enabled-field="enable"
|
||||
@toggle-enabled="providerStatusChange"
|
||||
@delete="deleteProvider"
|
||||
@edit="configExistingProvider">
|
||||
<template v-slot:details="{ item }">
|
||||
</template>
|
||||
</item-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
|
||||
<!-- 供应商状态部分 -->
|
||||
<v-card class="mb-6" elevation="2">
|
||||
<v-card elevation="0" class="mt-4">
|
||||
<v-card-title class="d-flex align-center py-3 px-4">
|
||||
<v-icon color="primary" class="me-2">mdi-heart-pulse</v-icon>
|
||||
<span class="text-h6">{{ tm('availability.title') }}</span>
|
||||
<v-icon class="me-2">mdi-heart-pulse</v-icon>
|
||||
<span class="text-h4">{{ tm('availability.title') }}</span>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="primary" variant="tonal" :loading="loadingStatus" @click="fetchProviderStatus">
|
||||
<v-icon left>mdi-refresh</v-icon>
|
||||
{{ tm('availability.refresh') }}
|
||||
</v-btn>
|
||||
<v-btn variant="text" color="primary" @click="showStatus = !showStatus" style="margin-left: 8px;">
|
||||
{{ showStatus ? tm('logs.collapse') : tm('logs.expand') }}
|
||||
<v-icon>{{ showStatus ? 'mdi-chevron-up' : 'mdi-chevron-down' }}</v-icon>
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
<v-card-subtitle class="px-4 py-1 text-caption text-medium-emphasis">
|
||||
{{ tm('availability.subtitle') }}
|
||||
</v-card-subtitle>
|
||||
|
||||
<v-divider></v-divider>
|
||||
|
||||
<v-card-text class="px-4 py-3">
|
||||
<v-alert v-if="providerStatuses.length === 0" type="info" variant="tonal">
|
||||
{{ tm('availability.noData') }}
|
||||
</v-alert>
|
||||
|
||||
<v-container v-else class="pa-0">
|
||||
<v-row>
|
||||
<v-col v-for="status in providerStatuses" :key="status.id" cols="12" sm="6" md="4">
|
||||
<v-card variant="outlined" class="status-card" :class="`status-${status.status}`">
|
||||
<v-card-item>
|
||||
<v-icon v-if="status.status === 'available'" color="success" class="me-2">mdi-check-circle</v-icon>
|
||||
<v-icon v-else-if="status.status === 'unavailable'" color="error" class="me-2">mdi-alert-circle</v-icon>
|
||||
<v-progress-circular
|
||||
v-else-if="status.status === 'pending'"
|
||||
indeterminate
|
||||
color="primary"
|
||||
size="20"
|
||||
width="2"
|
||||
class="me-2"
|
||||
></v-progress-circular>
|
||||
<v-expand-transition>
|
||||
<v-card-text class="pa-0" v-if="showStatus">
|
||||
<v-card-text class="px-4 py-3">
|
||||
<v-alert v-if="providerStatuses.length === 0" type="info" variant="tonal">
|
||||
{{ tm('availability.noData') }}
|
||||
</v-alert>
|
||||
|
||||
<v-container v-else class="pa-0">
|
||||
<v-row>
|
||||
<v-col v-for="status in providerStatuses" :key="status.id" cols="12" sm="6" md="4">
|
||||
<v-card variant="outlined" class="status-card" :class="`status-${status.status}`">
|
||||
<v-card-item>
|
||||
<v-icon v-if="status.status === 'available'" color="success" class="me-2">mdi-check-circle</v-icon>
|
||||
<v-icon v-else-if="status.status === 'unavailable'" color="error" class="me-2">mdi-alert-circle</v-icon>
|
||||
<v-progress-circular
|
||||
v-else-if="status.status === 'pending'"
|
||||
indeterminate
|
||||
color="primary"
|
||||
size="20"
|
||||
width="2"
|
||||
class="me-2"
|
||||
></v-progress-circular>
|
||||
|
||||
<span class="font-weight-bold">{{ status.id }}</span>
|
||||
|
||||
<v-chip :color="getStatusColor(status.status)" size="small" class="ml-2">
|
||||
{{ getStatusText(status.status) }}
|
||||
</v-chip>
|
||||
</v-card-item>
|
||||
<v-card-text v-if="status.status === 'unavailable'" class="text-caption text-medium-emphasis">
|
||||
<span class="font-weight-bold">{{ tm('availability.errorMessage') }}:</span> {{ status.error }}
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</v-card-text>
|
||||
<span class="font-weight-bold">{{ status.id }}</span>
|
||||
|
||||
<v-chip :color="getStatusColor(status.status)" size="small" class="ml-2">
|
||||
{{ getStatusText(status.status) }}
|
||||
</v-chip>
|
||||
</v-card-item>
|
||||
<v-card-text v-if="status.status === 'unavailable'" class="text-caption text-medium-emphasis">
|
||||
<span class="font-weight-bold">{{ tm('availability.errorMessage') }}:</span> {{ status.error }}
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</v-card-text>
|
||||
</v-card-text>
|
||||
</v-expand-transition>
|
||||
</v-card>
|
||||
|
||||
<!-- 日志部分 -->
|
||||
<v-card elevation="2">
|
||||
<v-card elevation="0" class="mt-4">
|
||||
<v-card-title class="d-flex align-center py-3 px-4">
|
||||
<v-icon color="primary" class="me-2">mdi-console-line</v-icon>
|
||||
<span class="text-h6">{{ tm('logs.title') }}</span>
|
||||
<v-icon class="me-2">mdi-console-line</v-icon>
|
||||
<span class="text-h4">{{ tm('logs.title') }}</span>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn variant="text" color="primary" @click="showConsole = !showConsole">
|
||||
{{ showConsole ? tm('logs.collapse') : tm('logs.expand') }}
|
||||
@@ -358,7 +342,7 @@ import axios from 'axios';
|
||||
import AstrBotConfig from '@/components/shared/AstrBotConfig.vue';
|
||||
import WaitingForRestart from '@/components/shared/WaitingForRestart.vue';
|
||||
import ConsoleDisplayer from '@/components/shared/ConsoleDisplayer.vue';
|
||||
import ItemCardGrid from '@/components/shared/ItemCardGrid.vue';
|
||||
import ItemCard from '@/components/shared/ItemCard.vue';
|
||||
import { useModuleI18n } from '@/i18n/composables';
|
||||
|
||||
export default {
|
||||
@@ -367,7 +351,7 @@ export default {
|
||||
AstrBotConfig,
|
||||
WaitingForRestart,
|
||||
ConsoleDisplayer,
|
||||
ItemCardGrid
|
||||
ItemCard
|
||||
},
|
||||
setup() {
|
||||
const { tm } = useModuleI18n('features/provider');
|
||||
@@ -406,6 +390,9 @@ export default {
|
||||
|
||||
showConsole: false,
|
||||
|
||||
// 显示状态部分
|
||||
showStatus: false,
|
||||
|
||||
// 供应商状态相关
|
||||
providerStatuses: [],
|
||||
loadingStatus: false,
|
||||
@@ -559,7 +546,7 @@ export default {
|
||||
'Whisper': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/openai.svg',
|
||||
'xAI': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/xai.svg',
|
||||
'Anthropic': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/anthropic.svg',
|
||||
'Ollama': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/anthropic.svg',
|
||||
'Ollama': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/ollama.svg',
|
||||
'Gemini': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/gemini-color.svg',
|
||||
'Gemini(OpenAI兼容)': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/gemini-color.svg',
|
||||
'DeepSeek': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/deepseek.svg',
|
||||
@@ -574,6 +561,7 @@ export default {
|
||||
'FishAudio': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/fishaudio.svg',
|
||||
'Azure': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/azure.svg',
|
||||
'MiniMax': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/minimax.svg',
|
||||
'302.AI': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/1.53.0/files/icons/ai302.svg',
|
||||
};
|
||||
for (const key in icons) {
|
||||
if (type.startsWith(key)) {
|
||||
@@ -782,6 +770,7 @@ export default {
|
||||
if (this.loadingStatus) return;
|
||||
|
||||
this.loadingStatus = true;
|
||||
this.showStatus = true; // 自动展开状态部分
|
||||
|
||||
// 1. 立即初始化UI为pending状态
|
||||
this.providerStatuses = this.config_data.provider.map(p => ({
|
||||
@@ -886,11 +875,6 @@ export default {
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.provider-selection-dialog .v-card-title {
|
||||
border-top-left-radius: 4px;
|
||||
border-top-right-radius: 4px;
|
||||
}
|
||||
|
||||
.provider-card {
|
||||
transition: all 0.3s ease;
|
||||
height: 100%;
|
||||
|
||||
+130
-114
@@ -1,11 +1,11 @@
|
||||
<template>
|
||||
<div class="tools-page">
|
||||
<v-container fluid class="pa-0">
|
||||
<v-container fluid class="pa-0" elevation="0">
|
||||
<!-- 页面标题 -->
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<h1 class="text-h4 font-weight-bold mb-2">
|
||||
<v-icon size="x-large" color="primary" class="me-2">mdi-function-variant</v-icon>{{ tm('title') }}
|
||||
<v-row class="d-flex justify-space-between align-center px-4 py-3 pb-8">
|
||||
<div>
|
||||
<h1 class="text-h1 font-weight-bold mb-2">
|
||||
<v-icon color="black" class="me-2">mdi-function-variant</v-icon>{{ tm('title') }}
|
||||
</h1>
|
||||
<p class="text-subtitle-1 text-medium-emphasis mb-4 d-flex align-center">
|
||||
{{ tm('subtitle') }}
|
||||
@@ -19,11 +19,14 @@
|
||||
<span>{{ tm('tooltip.info') }}</span>
|
||||
</v-tooltip>
|
||||
</p>
|
||||
</v-col>
|
||||
</div>
|
||||
<v-btn color="primary" prepend-icon="mdi-plus" variant="tonal" @click="showMcpServerDialog = true" rounded="xl" size="x-large">
|
||||
{{ tm('mcpServers.buttons.add') }}
|
||||
</v-btn>
|
||||
</v-row>
|
||||
|
||||
<!-- 标签页切换 -->
|
||||
<v-tabs v-model="activeTab" color="primary" class="mb-4" show-arrows>
|
||||
<v-tabs v-model="activeTab" color="primary" class="mb-6" show-arrows>
|
||||
<v-tab value="local" class="font-weight-medium">
|
||||
<v-icon start>mdi-server</v-icon>
|
||||
{{ tm('tabs.local') }}
|
||||
@@ -58,47 +61,57 @@
|
||||
<v-divider></v-divider>
|
||||
|
||||
<v-card-text class="px-4 py-3">
|
||||
<div v-if="mcpServers.length === 0" class="text-center pa-8">
|
||||
<v-icon size="64" color="grey-lighten-1">mdi-server-off</v-icon>
|
||||
<p class="text-grey mt-4">{{ tm('mcpServers.empty') }}</p>
|
||||
</div>
|
||||
|
||||
<item-card-grid :items="mcpServers || []" title-field="name" enabled-field="active"
|
||||
empty-icon="mdi-server-off" :empty-text="tm('mcpServers.empty')" @toggle-enabled="updateServerStatus"
|
||||
@delete="deleteServer" @edit="editServer">
|
||||
<template v-slot:item-details="{ item }">
|
||||
|
||||
<div class="d-flex align-center mb-2">
|
||||
<v-icon size="small" color="grey" class="me-2">mdi-file-code</v-icon>
|
||||
<span class="text-caption text-medium-emphasis text-truncate" :title="getServerConfigSummary(item)">
|
||||
{{ getServerConfigSummary(item) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div v-if="item.tools && item.tools.length > 0">
|
||||
<div class="d-flex align-center mb-1">
|
||||
<v-icon size="small" color="grey" class="me-2">mdi-tools</v-icon>
|
||||
<span class="text-caption text-medium-emphasis">{{ tm('mcpServers.status.availableTools') }} ({{ item.tools.length }})</span>
|
||||
</div>
|
||||
<v-chip-group class="tool-chips">
|
||||
<v-chip v-for="(tool, idx) in item.tools" :key="idx" size="x-small" density="compact" color="info"
|
||||
class="text-caption">
|
||||
{{ tool }}
|
||||
</v-chip>
|
||||
</v-chip-group>
|
||||
</div>
|
||||
<div v-else class="text-caption text-medium-emphasis mt-2">
|
||||
<v-icon size="small" color="warning" class="me-1">mdi-alert-circle</v-icon>
|
||||
{{ tm('mcpServers.status.noTools') }}
|
||||
</div>
|
||||
|
||||
</template>
|
||||
</item-card-grid>
|
||||
<v-row v-else>
|
||||
<v-col v-for="(server, index) in mcpServers || []" :key="index" cols="12" md="6" lg="4" xl="3">
|
||||
<item-card
|
||||
style="background-color: #f7f2f9;"
|
||||
:item="server"
|
||||
title-field="name"
|
||||
enabled-field="active"
|
||||
@toggle-enabled="updateServerStatus"
|
||||
@delete="deleteServer"
|
||||
@edit="editServer">
|
||||
<template v-slot:item-details="{ item }">
|
||||
<div class="d-flex align-center mb-2">
|
||||
<v-icon size="small" color="grey" class="me-2">mdi-file-code</v-icon>
|
||||
<span class="text-caption text-medium-emphasis text-truncate" :title="getServerConfigSummary(item)">
|
||||
{{ getServerConfigSummary(item) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div v-if="item.tools && item.tools.length > 0">
|
||||
<div class="d-flex align-center mb-1">
|
||||
<v-icon size="small" color="grey" class="me-2">mdi-tools</v-icon>
|
||||
<span class="text-caption text-medium-emphasis">{{ tm('mcpServers.status.availableTools') }} ({{ item.tools.length }})</span>
|
||||
</div>
|
||||
<v-chip-group class="tool-chips">
|
||||
<v-chip v-for="(tool, idx) in item.tools" :key="idx" size="x-small" density="compact" color="info"
|
||||
class="text-caption">
|
||||
{{ tool }}
|
||||
</v-chip>
|
||||
</v-chip-group>
|
||||
</div>
|
||||
<div v-else class="text-caption text-medium-emphasis mt-2">
|
||||
<v-icon size="small" color="warning" class="me-1">mdi-alert-circle</v-icon>
|
||||
{{ tm('mcpServers.status.noTools') }}
|
||||
</div>
|
||||
</template>
|
||||
</item-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<!-- 函数工具部分 -->
|
||||
<v-card elevation="2">
|
||||
<v-card elevation="0" class="mt-4">
|
||||
<v-card-title class="d-flex align-center py-3 px-4">
|
||||
<v-icon color="primary" class="me-2">mdi-function</v-icon>
|
||||
<span class="text-h6">{{ tm('functionTools.title') }}</span>
|
||||
<span class="text-h4">{{ tm('functionTools.title') }}</span>
|
||||
<v-chip color="info" size="small" class="ml-2">{{ tools.length }}</v-chip>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn variant="text" color="primary" @click="showTools = !showTools">
|
||||
@@ -110,84 +123,86 @@
|
||||
<v-divider></v-divider>
|
||||
|
||||
<v-expand-transition>
|
||||
<v-card-text class="pa-3" v-if="showTools">
|
||||
<div v-if="tools.length === 0" class="text-center pa-8">
|
||||
<v-icon size="64" color="grey-lighten-1">mdi-api-off</v-icon>
|
||||
<p class="text-grey mt-4">{{ tm('functionTools.empty') }}</p>
|
||||
</div>
|
||||
<v-card-text class="pa-0" v-if="showTools">
|
||||
<div class="pa-4">
|
||||
<div v-if="tools.length === 0" class="text-center pa-8">
|
||||
<v-icon size="64" color="grey-lighten-1">mdi-api-off</v-icon>
|
||||
<p class="text-grey mt-4">{{ tm('functionTools.empty') }}</p>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<v-text-field v-model="toolSearch" prepend-inner-icon="mdi-magnify" :label="tm('functionTools.search')" variant="outlined"
|
||||
density="compact" class="mb-4" hide-details clearable></v-text-field>
|
||||
<div v-else>
|
||||
<v-text-field v-model="toolSearch" prepend-inner-icon="mdi-magnify" :label="tm('functionTools.search')" variant="outlined"
|
||||
density="compact" class="mb-4" hide-details clearable></v-text-field>
|
||||
|
||||
<v-expansion-panels v-model="openedPanel" multiple>
|
||||
<v-expansion-panel v-for="(tool, index) in filteredTools" :key="index" :value="index"
|
||||
class="mb-2 tool-panel" rounded="lg">
|
||||
<v-expansion-panel-title>
|
||||
<v-row no-gutters align="center">
|
||||
<v-col cols="3">
|
||||
<div class="d-flex align-center">
|
||||
<v-icon color="primary" class="me-2" size="small">
|
||||
{{ tool.function.name.includes(':') ? 'mdi-server-network' : 'mdi-function-variant' }}
|
||||
</v-icon>
|
||||
<span class="text-body-1 text-high-emphasis font-weight-medium text-truncate"
|
||||
:title="tool.function.name">
|
||||
{{ formatToolName(tool.function.name) }}
|
||||
</span>
|
||||
</div>
|
||||
</v-col>
|
||||
<v-col cols="9" class="text-grey">
|
||||
{{ tool.function.description }}
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-expansion-panel-title>
|
||||
<v-expansion-panels v-model="openedPanel" multiple>
|
||||
<v-expansion-panel v-for="(tool, index) in filteredTools" :key="index" :value="index"
|
||||
class="mb-2 tool-panel" rounded="lg">
|
||||
<v-expansion-panel-title>
|
||||
<v-row no-gutters align="center">
|
||||
<v-col cols="3">
|
||||
<div class="d-flex align-center">
|
||||
<v-icon color="primary" class="me-2" size="small">
|
||||
{{ tool.function.name.includes(':') ? 'mdi-server-network' : 'mdi-function-variant' }}
|
||||
</v-icon>
|
||||
<span class="text-body-1 text-high-emphasis font-weight-medium text-truncate"
|
||||
:title="tool.function.name">
|
||||
{{ formatToolName(tool.function.name) }}
|
||||
</span>
|
||||
</div>
|
||||
</v-col>
|
||||
<v-col cols="9" class="text-grey">
|
||||
{{ tool.function.description }}
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-expansion-panel-title>
|
||||
|
||||
<v-expansion-panel-text>
|
||||
<v-card flat>
|
||||
<v-card-text>
|
||||
<p class="text-body-1 font-weight-medium mb-3">
|
||||
<v-icon color="primary" size="small" class="me-1">mdi-information</v-icon>
|
||||
{{ tm('functionTools.description') }}
|
||||
</p>
|
||||
<p class="text-body-2 ml-6 mb-4">{{ tool.function.description }}</p>
|
||||
|
||||
<template v-if="tool.function.parameters && tool.function.parameters.properties">
|
||||
<v-expansion-panel-text>
|
||||
<v-card flat>
|
||||
<v-card-text>
|
||||
<p class="text-body-1 font-weight-medium mb-3">
|
||||
<v-icon color="primary" size="small" class="me-1">mdi-code-json</v-icon>
|
||||
{{ tm('functionTools.parameters') }}
|
||||
<v-icon color="primary" size="small" class="me-1">mdi-information</v-icon>
|
||||
{{ tm('functionTools.description') }}
|
||||
</p>
|
||||
<p class="text-body-2 ml-6 mb-4">{{ tool.function.description }}</p>
|
||||
|
||||
<v-table density="compact" class="params-table mt-1">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ tm('functionTools.table.paramName') }}</th>
|
||||
<th>{{ tm('functionTools.table.type') }}</th>
|
||||
<th>{{ tm('functionTools.table.description') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(param, paramName) in tool.function.parameters.properties"
|
||||
:key="paramName">
|
||||
<td class="font-weight-medium">{{ paramName }}</td>
|
||||
<td>
|
||||
<v-chip size="x-small" color="primary" text class="text-caption">
|
||||
{{ param.type }}
|
||||
</v-chip>
|
||||
</td>
|
||||
<td>{{ param.description }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</v-table>
|
||||
</template>
|
||||
<div v-else class="text-center pa-4 text-medium-emphasis">
|
||||
<v-icon size="large" color="grey-lighten-1">mdi-code-brackets</v-icon>
|
||||
<p>{{ tm('functionTools.noParameters') }}</p>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-expansion-panel-text>
|
||||
</v-expansion-panel>
|
||||
</v-expansion-panels>
|
||||
<template v-if="tool.function.parameters && tool.function.parameters.properties">
|
||||
<p class="text-body-1 font-weight-medium mb-3">
|
||||
<v-icon color="primary" size="small" class="me-1">mdi-code-json</v-icon>
|
||||
{{ tm('functionTools.parameters') }}
|
||||
</p>
|
||||
|
||||
<v-table density="compact" class="params-table mt-1">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ tm('functionTools.table.paramName') }}</th>
|
||||
<th>{{ tm('functionTools.table.type') }}</th>
|
||||
<th>{{ tm('functionTools.table.description') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(param, paramName) in tool.function.parameters.properties"
|
||||
:key="paramName">
|
||||
<td class="font-weight-medium">{{ paramName }}</td>
|
||||
<td>
|
||||
<v-chip size="x-small" color="primary" text class="text-caption">
|
||||
{{ param.type }}
|
||||
</v-chip>
|
||||
</td>
|
||||
<td>{{ param.description }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</v-table>
|
||||
</template>
|
||||
<div v-else class="text-center pa-4 text-medium-emphasis">
|
||||
<v-icon size="large" color="grey-lighten-1">mdi-code-brackets</v-icon>
|
||||
<p>{{ tm('functionTools.noParameters') }}</p>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-expansion-panel-text>
|
||||
</v-expansion-panel>
|
||||
</v-expansion-panels>
|
||||
</div>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-expand-transition>
|
||||
@@ -466,7 +481,7 @@
|
||||
import axios from 'axios';
|
||||
import AstrBotConfig from '@/components/shared/AstrBotConfig.vue';
|
||||
import { VueMonacoEditor } from '@guolao/vue-monaco-editor';
|
||||
import ItemCardGrid from '@/components/shared/ItemCardGrid.vue';
|
||||
import ItemCard from '@/components/shared/ItemCard.vue';
|
||||
import { useI18n, useModuleI18n } from '@/i18n/composables';
|
||||
|
||||
export default {
|
||||
@@ -474,7 +489,7 @@ export default {
|
||||
components: {
|
||||
AstrBotConfig,
|
||||
VueMonacoEditor,
|
||||
ItemCardGrid
|
||||
ItemCard
|
||||
},
|
||||
setup() {
|
||||
const { t } = useI18n();
|
||||
@@ -939,6 +954,7 @@ export default {
|
||||
.text-truncate-2 {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
@@ -10,6 +10,7 @@ import astrbot.api.event.filter as filter
|
||||
from astrbot.api.event import AstrMessageEvent, MessageEventResult
|
||||
from astrbot.api import sp
|
||||
from astrbot.api.provider import ProviderRequest
|
||||
from astrbot.core import DEMO_MODE
|
||||
from astrbot.core.platform.astr_message_event import MessageSesion
|
||||
from astrbot.core.platform.message_type import MessageType
|
||||
from astrbot.core.provider.entities import ProviderType
|
||||
@@ -227,6 +228,11 @@ class Main(star.Star):
|
||||
@plugin.command("off")
|
||||
async def plugin_off(self, event: AstrMessageEvent, plugin_name: str = None):
|
||||
"""禁用插件"""
|
||||
if DEMO_MODE:
|
||||
event.set_result(
|
||||
MessageEventResult().message("演示模式下无法禁用插件。")
|
||||
)
|
||||
return
|
||||
if not plugin_name:
|
||||
event.set_result(
|
||||
MessageEventResult().message("/plugin off <插件名> 禁用插件。")
|
||||
@@ -239,6 +245,11 @@ class Main(star.Star):
|
||||
@plugin.command("on")
|
||||
async def plugin_on(self, event: AstrMessageEvent, plugin_name: str = None):
|
||||
"""启用插件"""
|
||||
if DEMO_MODE:
|
||||
event.set_result(
|
||||
MessageEventResult().message("演示模式下无法启用插件。")
|
||||
)
|
||||
return
|
||||
if not plugin_name:
|
||||
event.set_result(
|
||||
MessageEventResult().message("/plugin on <插件名> 启用插件。")
|
||||
@@ -251,6 +262,11 @@ class Main(star.Star):
|
||||
@plugin.command("get")
|
||||
async def plugin_get(self, event: AstrMessageEvent, plugin_repo: str = None):
|
||||
"""安装插件"""
|
||||
if DEMO_MODE:
|
||||
event.set_result(
|
||||
MessageEventResult().message("演示模式下无法安装插件。")
|
||||
)
|
||||
return
|
||||
if not plugin_repo:
|
||||
event.set_result(
|
||||
MessageEventResult().message("/plugin get <插件仓库地址> 安装插件")
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "AstrBot"
|
||||
version = "3.5.18"
|
||||
version = "3.5.19"
|
||||
description = "易上手的多平台 LLM 聊天机器人及开发框架"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
Reference in New Issue
Block a user