Merge branch 'master' into deprecate/register_star-decorator

This commit is contained in:
Soulter
2025-07-08 11:52:33 +08:00
committed by GitHub
37 changed files with 2380 additions and 1857 deletions
+1
View File
@@ -17,6 +17,7 @@ assignees: ''
{
"name": "插件名",
"desc": "插件介绍",
"author": "作者名",
"repo": "插件仓库链接",
"tags": [],
"social_link": ""
+13
View File
@@ -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
+1 -1
View File
@@ -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'
+11 -11
View File
@@ -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 }}
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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'
+24 -12
View File
@@ -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:
+24 -19
View File
@@ -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,
}
+1 -1
View File
@@ -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)
+7 -4
View File
@@ -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
+10
View File
@@ -0,0 +1,10 @@
# What's Changed
1. 修复: 通过 provider 指令设置提供商,重启后失效
2. 新增: WebChat 支持直接选择提供商和模型
3. 优化: WebUI 视觉效果、WebChat 视觉效果
4. 优化: WebUI 测试提供商功能
5. 优化: 修复潜在的 README XSS 注入问题
6. 修复: WechatPadPro 授权码提取逻辑以适配上游新版本,并提高安全性
7. 修复: Gemini 下,多轮工具调用时可能报错的问题
8. 其他修复与优化
+1
View File
@@ -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">
+1 -1
View File
@@ -19,7 +19,7 @@ export default createVuetify({
defaults: {
VBtn: {},
VCard: {
rounded: 'md'
rounded: 'lg'
},
VTextField: {
rounded: 'lg'
+1 -1
View File
@@ -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',
+205 -14
View File
@@ -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, // HTMLXSS
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>
+10 -5
View File
@@ -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');
// 使markedMarkdown
return marked(final_content);
// 使markdown-ithtml: falseHTML
return md.render(final_content);
},
//
+60 -5
View File
@@ -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>
+47 -60
View File
@@ -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
}
},
+111 -127
View File
@@ -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. UIpending
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
View File
@@ -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;
+16
View File
@@ -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
View File
@@ -1,6 +1,6 @@
[project]
name = "AstrBot"
version = "3.5.18"
version = "3.5.19"
description = "易上手的多平台 LLM 聊天机器人及开发框架"
readme = "README.md"
requires-python = ">=3.10"
Generated
+1403 -1403
View File
File diff suppressed because it is too large Load Diff