mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-16 17:47:06 +08:00
fix(maturin-hook): handle broken dashboard dist symlink gracefully
The dev branch has astrbot/dashboard/dist as a symlink to ../../dashboard/dist, which is valid in the dev workspace but becomes a broken symlink when cloned to /opt/astrbot for installation. Fix the maturin build hook to: - Remove broken symlinks before creating placeholder directories - Handle symlink vs directory removal in copy_dashboard_dist() - Always generate placeholder when dashboard build is skipped or fails
This commit is contained in:
@@ -238,6 +238,9 @@ class QQOfficialPlatformAdapter(Platform):
|
||||
)
|
||||
|
||||
elif session.message_type == MessageType.FRIEND_MESSAGE:
|
||||
# 参考 https://bot.q.qq.com/wiki/develop/pythonsdk/api/message/post_message.html
|
||||
# msg_id 缺失时认为是主动推送,而似乎至少在私聊上主动推送是没有被限制的,这里直接移除 msg_id 可以避免越权或 msg_id 不可用的bug
|
||||
payload.pop("msg_id", None)
|
||||
payload["msg_seq"] = random.randint(1, 10000)
|
||||
if image_base64:
|
||||
media = await QQOfficialMessageEvent.upload_group_and_c2c_image(
|
||||
@@ -268,9 +271,6 @@ class QQOfficialPlatformAdapter(Platform):
|
||||
if media:
|
||||
payload["media"] = media
|
||||
payload["msg_type"] = 7
|
||||
# QQ API rejects msg_id for media (video/file) messages sent
|
||||
# via the proactive tool-call path; remove it to avoid 越权 error.
|
||||
payload.pop("msg_id", None)
|
||||
if file_source:
|
||||
media = await QQOfficialMessageEvent.upload_group_and_c2c_media(
|
||||
send_helper,
|
||||
@@ -282,7 +282,6 @@ class QQOfficialPlatformAdapter(Platform):
|
||||
if media:
|
||||
payload["media"] = media
|
||||
payload["msg_type"] = 7
|
||||
payload.pop("msg_id", None)
|
||||
|
||||
ret = await QQOfficialMessageEvent.post_c2c_message(
|
||||
send_helper,
|
||||
|
||||
@@ -895,7 +895,13 @@ class WeixinOCAdapter(Platform):
|
||||
await asyncio.sleep(self.qr_poll_interval)
|
||||
continue
|
||||
|
||||
await self._poll_inbound_updates()
|
||||
try:
|
||||
await self._poll_inbound_updates()
|
||||
except asyncio.TimeoutError:
|
||||
logger.debug(
|
||||
"weixin_oc(%s): inbound long-poll timeout",
|
||||
self.meta().id,
|
||||
)
|
||||
except asyncio.CancelledError:
|
||||
raise
|
||||
except Exception as e:
|
||||
|
||||
@@ -24,9 +24,15 @@ class OpenAIEmbeddingProvider(EmbeddingProvider):
|
||||
if proxy:
|
||||
logger.info(f"[OpenAI Embedding] {provider_id} Using proxy: {proxy}")
|
||||
http_client = httpx.AsyncClient(proxy=proxy)
|
||||
api_base = provider_config.get(
|
||||
"embedding_api_base", "https://api.openai.com/v1"
|
||||
).strip()
|
||||
api_base = (
|
||||
provider_config.get("embedding_api_base", "https://api.openai.com/v1")
|
||||
.strip()
|
||||
.rstrip("/")
|
||||
.rstrip("/embeddings")
|
||||
)
|
||||
if api_base and not api_base.endswith("/v1") and not api_base.endswith("/v4"):
|
||||
# /v4 see #5699
|
||||
api_base = api_base + "/v1"
|
||||
logger.info(f"[OpenAI Embedding] {provider_id} Using API Base: {api_base}")
|
||||
self.client = AsyncOpenAI(
|
||||
api_key=provider_config.get("embedding_api_key"),
|
||||
|
||||
@@ -335,11 +335,15 @@ class ProviderOpenAIOfficial(Provider):
|
||||
choice = chunk.choices[0]
|
||||
delta = choice.delta
|
||||
|
||||
# siliconflow workaround
|
||||
if dtcs := delta.tool_calls:
|
||||
for tc in dtcs:
|
||||
for idx, tc in enumerate(dtcs):
|
||||
# siliconflow workaround
|
||||
if tc.function and tc.function.arguments:
|
||||
tc.type = "function"
|
||||
# Fix for #6661: Add missing 'index' field to tool_call deltas
|
||||
# Gemini and some OpenAI-compatible proxies omit this field
|
||||
if not hasattr(tc, "index") or tc.index is None:
|
||||
tc.index = idx
|
||||
try:
|
||||
state.handle_chunk(chunk)
|
||||
except Exception as e:
|
||||
|
||||
@@ -51,19 +51,19 @@ class SessionManagementRoute(Route):
|
||||
async def _get_umo_rules(
|
||||
self, page: int = 1, page_size: int = 10, search: str = ""
|
||||
) -> tuple[dict, int]:
|
||||
"""获取所有带有自定义规则的 umo 及其规则内容(支持分页和搜索)。
|
||||
"""获取所有带有自定义规则的 umo 及其规则内容(支持分页和搜索)。
|
||||
|
||||
如果某个 umo 在 preference 中有以下字段,则表示有自定义规则:
|
||||
如果某个 umo 在 preference 中有以下字段,则表示有自定义规则:
|
||||
|
||||
1. session_service_config (包含了 是否启用这个umo, 这个umo是否启用 llm, 这个umo是否启用tts, umo自定义名称。)
|
||||
1. session_service_config (包含了 是否启用这个umo, 这个umo是否启用 llm, 这个umo是否启用tts, umo自定义名称。)
|
||||
2. session_plugin_config (包含了 这个 umo 的 plugin set)
|
||||
3. provider_perf_{ProviderType.value} (包含了这个 umo 所选择使用的 provider 信息)
|
||||
4. kb_config (包含了这个 umo 的知识库相关配置)
|
||||
|
||||
Args:
|
||||
page: 页码,从 1 开始
|
||||
page: 页码,从 1 开始
|
||||
page_size: 每页数量
|
||||
search: 搜索关键词,匹配 umo 或 custom_name
|
||||
search: 搜索关键词,匹配 umo 或 custom_name
|
||||
|
||||
Returns:
|
||||
tuple[dict, int]: (umo_rules, total) - 分页后的 umo 规则和总数
|
||||
@@ -118,14 +118,14 @@ class SessionManagementRoute(Route):
|
||||
return paginated_rules, total
|
||||
|
||||
async def list_session_rule(self):
|
||||
"""获取所有自定义的规则(支持分页和搜索)
|
||||
"""获取所有自定义的规则(支持分页和搜索)
|
||||
|
||||
返回已配置规则的 umo 列表及其规则内容,以及可用的 personas 和 providers
|
||||
返回已配置规则的 umo 列表及其规则内容,以及可用的 personas 和 providers
|
||||
|
||||
Query 参数:
|
||||
page: 页码,默认为 1
|
||||
page_size: 每页数量,默认为 10
|
||||
search: 搜索关键词,匹配 umo 或 custom_name
|
||||
page: 页码,默认为 1
|
||||
page_size: 每页数量,默认为 10
|
||||
search: 搜索关键词,匹配 umo 或 custom_name
|
||||
"""
|
||||
try:
|
||||
# 获取分页和搜索参数
|
||||
@@ -196,7 +196,7 @@ class SessionManagementRoute(Route):
|
||||
for p in provider_manager.tts_provider_insts
|
||||
]
|
||||
|
||||
# 获取可用的插件列表(排除 reserved 的系统插件)
|
||||
# 获取可用的插件列表(排除 reserved 的系统插件)
|
||||
plugin_manager = self.core_lifecycle.plugin_manager
|
||||
available_plugins = [
|
||||
{
|
||||
@@ -255,7 +255,7 @@ class SessionManagementRoute(Route):
|
||||
{
|
||||
"umo": "平台:消息类型:会话ID",
|
||||
"rule_key": "session_service_config" | "session_plugin_config" | "kb_config" | "provider_perf_xxx",
|
||||
"rule_value": {...} // 规则值,具体结构根据 rule_key 不同而不同
|
||||
"rule_value": {...} // 规则值,具体结构根据 rule_key 不同而不同
|
||||
}
|
||||
"""
|
||||
try:
|
||||
@@ -294,7 +294,7 @@ class SessionManagementRoute(Route):
|
||||
请求体:
|
||||
{
|
||||
"umo": "平台:消息类型:会话ID",
|
||||
"rule_key": "session_service_config" | "session_plugin_config" | ... (可选,不传则删除所有规则)
|
||||
"rule_key": "session_service_config" | "session_plugin_config" | ... (可选,不传则删除所有规则)
|
||||
}
|
||||
"""
|
||||
try:
|
||||
@@ -328,37 +328,87 @@ class SessionManagementRoute(Route):
|
||||
|
||||
请求体:
|
||||
{
|
||||
"umos": ["平台:消息类型:会话ID", ...] // umo 列表
|
||||
"umos": ["平台:消息类型:会话ID", ...], // 可选
|
||||
"scope": "all" | "group" | "private" | "custom_group", // 可选,批量范围
|
||||
"group_id": "分组ID", // 当 scope 为 custom_group 时必填
|
||||
"rule_key": "session_service_config" | ... (可选,不传则删除所有规则)
|
||||
}
|
||||
"""
|
||||
|
||||
try:
|
||||
data = await request.get_json()
|
||||
umos = data.get("umos", [])
|
||||
scope = data.get("scope", "")
|
||||
group_id = data.get("group_id", "")
|
||||
rule_key = data.get("rule_key")
|
||||
|
||||
# 如果指定了 scope,获取符合条件的所有 umo
|
||||
if scope and not umos:
|
||||
# 如果是自定义分组
|
||||
if scope == "custom_group":
|
||||
if not group_id:
|
||||
return Response().error("请指定分组 ID").__dict__
|
||||
groups = self._get_groups()
|
||||
if group_id not in groups:
|
||||
return Response().error(f"分组 '{group_id}' 不存在").__dict__
|
||||
umos = groups[group_id].get("umos", [])
|
||||
else:
|
||||
async with self.db_helper.get_db() as session:
|
||||
session: AsyncSession
|
||||
result = await session.execute(
|
||||
select(ConversationV2.user_id).distinct()
|
||||
)
|
||||
all_umos = [row[0] for row in result.fetchall()]
|
||||
|
||||
if scope == "group":
|
||||
umos = [
|
||||
u
|
||||
for u in all_umos
|
||||
if ":group:" in u.lower() or ":groupmessage:" in u.lower()
|
||||
]
|
||||
elif scope == "private":
|
||||
umos = [
|
||||
u
|
||||
for u in all_umos
|
||||
if ":private:" in u.lower() or ":friend" in u.lower()
|
||||
]
|
||||
elif scope == "all":
|
||||
umos = all_umos
|
||||
|
||||
if not umos:
|
||||
return Response().error("缺少必要参数: umos").__dict__
|
||||
return Response().error("缺少必要参数: umos 或有效的 scope").__dict__
|
||||
|
||||
if not isinstance(umos, list):
|
||||
return Response().error("参数 umos 必须是数组").__dict__
|
||||
|
||||
if rule_key and rule_key not in AVAILABLE_SESSION_RULE_KEYS:
|
||||
return Response().error(f"不支持的规则键: {rule_key}").__dict__
|
||||
|
||||
# 批量删除
|
||||
deleted_count = 0
|
||||
success_count = 0
|
||||
failed_umos = []
|
||||
for umo in umos:
|
||||
try:
|
||||
await sp.clear_async("umo", umo)
|
||||
deleted_count += 1
|
||||
if rule_key:
|
||||
await sp.session_remove(umo, rule_key)
|
||||
else:
|
||||
await sp.clear_async("umo", umo)
|
||||
success_count += 1
|
||||
except Exception as e:
|
||||
logger.error(f"删除 umo {umo} 的规则失败: {e!s}")
|
||||
failed_umos.append(umo)
|
||||
|
||||
message = f"已删除 {success_count} 条规则"
|
||||
if rule_key:
|
||||
message = f"已删除 {success_count} 条 {rule_key} 规则"
|
||||
|
||||
if failed_umos:
|
||||
return (
|
||||
Response()
|
||||
.ok(
|
||||
{
|
||||
"message": f"已删除 {deleted_count} 条规则,{len(failed_umos)} 条删除失败",
|
||||
"deleted_count": deleted_count,
|
||||
"message": f"{message},{len(failed_umos)} 条删除失败",
|
||||
"success_count": success_count,
|
||||
"failed_umos": failed_umos,
|
||||
}
|
||||
)
|
||||
@@ -369,8 +419,8 @@ class SessionManagementRoute(Route):
|
||||
Response()
|
||||
.ok(
|
||||
{
|
||||
"message": f"已删除 {deleted_count} 条规则",
|
||||
"deleted_count": deleted_count,
|
||||
"message": message,
|
||||
"success_count": success_count,
|
||||
}
|
||||
)
|
||||
.__dict__
|
||||
@@ -380,9 +430,9 @@ class SessionManagementRoute(Route):
|
||||
return Response().error(f"批量删除会话规则失败: {e!s}").__dict__
|
||||
|
||||
async def list_umos(self):
|
||||
"""列出所有有对话记录的 umo,从 Conversations 表中找
|
||||
"""列出所有有对话记录的 umo,从 Conversations 表中找
|
||||
|
||||
仅返回 umo 字符串列表,用于用户在创建规则时选择 umo
|
||||
仅返回 umo 字符串列表,用于用户在创建规则时选择 umo
|
||||
"""
|
||||
try:
|
||||
# 从 Conversation 表获取所有 distinct user_id (即 umo)
|
||||
@@ -401,11 +451,11 @@ class SessionManagementRoute(Route):
|
||||
return Response().error(f"获取 UMO 列表失败: {e!s}").__dict__
|
||||
|
||||
async def list_all_umos_with_status(self):
|
||||
"""获取所有有对话记录的 UMO 及其服务状态(支持分页、搜索、筛选)
|
||||
"""获取所有有对话记录的 UMO 及其服务状态(支持分页、搜索、筛选)
|
||||
|
||||
Query 参数:
|
||||
page: 页码,默认为 1
|
||||
page_size: 每页数量,默认为 20
|
||||
page: 页码,默认为 1
|
||||
page_size: 每页数量,默认为 20
|
||||
search: 搜索关键词
|
||||
message_type: 筛选消息类型 (group/private/all)
|
||||
platform: 筛选平台
|
||||
@@ -560,10 +610,10 @@ class SessionManagementRoute(Route):
|
||||
|
||||
请求体:
|
||||
{
|
||||
"umos": ["平台:消息类型:会话ID", ...], // 可选,如果不传则根据 scope 筛选
|
||||
"scope": "all" | "group" | "private" | "custom_group", // 可选,批量范围
|
||||
"umos": ["平台:消息类型:会话ID", ...], // 可选,如果不传则根据 scope 筛选
|
||||
"scope": "all" | "group" | "private" | "custom_group", // 可选,批量范围
|
||||
"group_id": "分组ID", // 当 scope 为 custom_group 时必填
|
||||
"llm_enabled": true/false/null, // 可选,null表示不修改
|
||||
"llm_enabled": true/false/null, // 可选,null表示不修改
|
||||
"tts_enabled": true/false/null, // 可选
|
||||
"session_enabled": true/false/null // 可选
|
||||
}
|
||||
@@ -581,7 +631,7 @@ class SessionManagementRoute(Route):
|
||||
if llm_enabled is None and tts_enabled is None and session_enabled is None:
|
||||
return Response().error("至少需要指定一个要修改的状态").__dict__
|
||||
|
||||
# 如果指定了 scope,获取符合条件的所有 umo
|
||||
# 如果指定了 scope,获取符合条件的所有 umo
|
||||
if scope and not umos:
|
||||
# 如果是自定义分组
|
||||
if scope == "custom_group":
|
||||
@@ -713,7 +763,7 @@ class SessionManagementRoute(Route):
|
||||
|
||||
provider_type_enum = provider_type_map[provider_type]
|
||||
|
||||
# 如果指定了 scope,获取符合条件的所有 umo
|
||||
# 如果指定了 scope,获取符合条件的所有 umo
|
||||
group_id = data.get("group_id", "")
|
||||
if scope and not umos:
|
||||
# 如果是自定义分组
|
||||
@@ -797,7 +847,7 @@ class SessionManagementRoute(Route):
|
||||
"""获取所有分组列表"""
|
||||
try:
|
||||
groups = self._get_groups()
|
||||
# 转换为列表格式,方便前端使用
|
||||
# 转换为列表格式,方便前端使用
|
||||
groups_list = []
|
||||
for group_id, group_data in groups.items():
|
||||
groups_list.append(
|
||||
@@ -857,7 +907,7 @@ class SessionManagementRoute(Route):
|
||||
return Response().error(f"创建分组失败: {e!s}").__dict__
|
||||
|
||||
async def update_group(self):
|
||||
"""更新分组(改名、增删成员)"""
|
||||
"""更新分组(改名、增删成员)"""
|
||||
try:
|
||||
data = await request.json
|
||||
group_id = data.get("id")
|
||||
|
||||
@@ -39,6 +39,16 @@ const emit = defineEmits([
|
||||
const handlePinnedImgError = (e) => {
|
||||
e.target.src = defaultPluginIcon;
|
||||
};
|
||||
|
||||
const authorDisplay = computed(() => {
|
||||
const p = props.plugin || {};
|
||||
if (typeof p.author === 'string' && p.author.trim()) return p.author;
|
||||
if (Array.isArray(p.authors) && p.authors.length) return p.authors.join(', ');
|
||||
if (typeof p.author_name === 'string' && p.author_name.trim()) return p.author_name;
|
||||
if (typeof p.owner === 'string' && p.owner.trim()) return p.owner;
|
||||
if (p.author && typeof p.author === 'object' && p.author.name) return p.author.name;
|
||||
return '';
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -65,119 +75,71 @@ const handlePinnedImgError = (e) => {
|
||||
:src="(typeof plugin.logo === 'string' && plugin.logo.trim()) ? plugin.logo : defaultPluginIcon"
|
||||
:alt="plugin.name"
|
||||
@error="handlePinnedImgError"
|
||||
>
|
||||
/>
|
||||
</v-avatar>
|
||||
</template>
|
||||
|
||||
<v-card>
|
||||
<v-card-text
|
||||
class="d-flex"
|
||||
style="gap:8px; padding:12px;"
|
||||
>
|
||||
<v-tooltip
|
||||
location="top"
|
||||
:text="tm('buttons.viewDocs')"
|
||||
>
|
||||
<v-card-title class="d-flex" style="gap:8px; padding:12px; align-items:center;">
|
||||
<div style="display:flex; align-items:center; gap:8px; min-width:0;">
|
||||
<v-avatar size="40" class="pinned-avatar" style="width:40px; height:40px;">
|
||||
<img
|
||||
:src="(typeof plugin.logo === 'string' && plugin.logo.trim()) ? plugin.logo : defaultPluginIcon"
|
||||
:alt="plugin.name"
|
||||
@error="handlePinnedImgError"
|
||||
/>
|
||||
</v-avatar>
|
||||
<div style="min-width:0; overflow:hidden;">
|
||||
<div style="font-weight:600; font-size:0.95rem; white-space:nowrap; text-overflow:ellipsis; overflow:hidden;">{{ plugin.display_name || plugin.name }}</div>
|
||||
<div style="font-size:0.8rem; color:var(--v-theme-on-surface); opacity:0.8; white-space:nowrap; text-overflow:ellipsis; overflow:hidden;">{{ authorDisplay || (plugin.author || '') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-card-title>
|
||||
<v-divider></v-divider>
|
||||
<v-card-text class="d-flex" style="gap:8px; padding:12px;">
|
||||
<v-tooltip location="top" :text="tm('buttons.viewDocs')">
|
||||
<template #activator="{ props: a }">
|
||||
<v-btn
|
||||
v-bind="a"
|
||||
icon
|
||||
size="small"
|
||||
variant="tonal"
|
||||
color="info"
|
||||
@click.stop="$emit('view-readme', plugin)"
|
||||
>
|
||||
<v-btn v-bind="a" icon size="small" variant="tonal" color="info" @click.stop="$emit('view-readme', plugin)">
|
||||
<v-icon>mdi-book-open-page-variant</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
|
||||
<v-tooltip
|
||||
location="top"
|
||||
:text="tm('card.actions.pluginConfig')"
|
||||
>
|
||||
<v-tooltip location="top" :text="tm('card.actions.pluginConfig')">
|
||||
<template #activator="{ props: a }">
|
||||
<v-btn
|
||||
v-bind="a"
|
||||
icon
|
||||
size="small"
|
||||
variant="tonal"
|
||||
color="primary"
|
||||
@click.stop="$emit('open-config', plugin.name)"
|
||||
>
|
||||
<v-btn v-bind="a" icon size="small" variant="tonal" color="primary" @click.stop="$emit('open-config', plugin.name)">
|
||||
<v-icon>mdi-cog</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
|
||||
<v-tooltip
|
||||
location="top"
|
||||
:text="tm('card.actions.reloadPlugin')"
|
||||
>
|
||||
<v-tooltip location="top" :text="tm('card.actions.reloadPlugin')">
|
||||
<template #activator="{ props: a }">
|
||||
<v-btn
|
||||
v-bind="a"
|
||||
icon
|
||||
size="small"
|
||||
variant="tonal"
|
||||
color="primary"
|
||||
@click.stop="$emit('reload', plugin.name)"
|
||||
>
|
||||
<v-btn v-bind="a" icon size="small" variant="tonal" color="primary" @click.stop="$emit('reload', plugin.name)">
|
||||
<v-icon>mdi-refresh</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
|
||||
<v-tooltip
|
||||
location="top"
|
||||
:text="tm('buttons.update')"
|
||||
>
|
||||
<v-tooltip location="top" :text="tm('buttons.update')">
|
||||
<template #activator="{ props: a }">
|
||||
<v-btn
|
||||
v-bind="a"
|
||||
icon
|
||||
size="small"
|
||||
variant="tonal"
|
||||
color="warning"
|
||||
@click.stop="$emit('update', plugin.name)"
|
||||
>
|
||||
<v-btn v-bind="a" icon size="small" variant="tonal" color="warning" @click.stop="$emit('update', plugin.name)">
|
||||
<v-icon>mdi-update</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
|
||||
<v-tooltip
|
||||
location="top"
|
||||
:text="tm('buttons.viewInfo')"
|
||||
>
|
||||
<v-tooltip location="top" :text="tm('buttons.viewInfo')">
|
||||
<template #activator="{ props: a }">
|
||||
<v-btn
|
||||
v-bind="a"
|
||||
icon
|
||||
size="small"
|
||||
variant="tonal"
|
||||
color="secondary"
|
||||
@click.stop="$emit('show-info', plugin)"
|
||||
>
|
||||
<v-btn v-bind="a" icon size="small" variant="tonal" color="secondary" @click.stop="$emit('show-info', plugin)">
|
||||
<v-icon>mdi-information</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
|
||||
<v-tooltip
|
||||
location="top"
|
||||
:text="tm('buttons.uninstall')"
|
||||
>
|
||||
<v-tooltip location="top" :text="tm('buttons.uninstall')">
|
||||
<template #activator="{ props: a }">
|
||||
<v-btn
|
||||
v-if="!plugin.reserved"
|
||||
v-bind="a"
|
||||
icon
|
||||
size="small"
|
||||
variant="tonal"
|
||||
color="error"
|
||||
@click.stop="$emit('uninstall', plugin.name)"
|
||||
>
|
||||
<v-btn v-bind="a" icon size="small" variant="tonal" color="error" @click.stop="$emit('uninstall', plugin.name)" v-if="!plugin.reserved">
|
||||
<v-icon>mdi-delete</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
@@ -191,13 +153,11 @@ const handlePinnedImgError = (e) => {
|
||||
size="small"
|
||||
class="pinned-pin-btn"
|
||||
:color="isPinned ? 'primary' : 'secondary'"
|
||||
@click.stop="$emit('toggle-pin', plugin)"
|
||||
:title="isPinned ? tm('buttons.unpin') : tm('buttons.pin')"
|
||||
style="position:absolute; top:6px; right:6px; min-width:22px; width:22px; height:22px;"
|
||||
@click.stop="$emit('toggle-pin', plugin)"
|
||||
>
|
||||
<v-icon size="14">
|
||||
{{ isPinned ? 'mdi-pin' : 'mdi-pin-outline' }}
|
||||
</v-icon>
|
||||
<v-icon size="14">{{ isPinned ? 'mdi-pin' : 'mdi-pin-outline' }}</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -68,6 +68,17 @@ const astrbotVersionRequirement = computed(() => {
|
||||
: "";
|
||||
});
|
||||
|
||||
// 作者显示(兼容多种字段名)
|
||||
const authorDisplay = computed(() => {
|
||||
const ext = props.extension || {};
|
||||
if (typeof ext.author === 'string' && ext.author.trim()) return ext.author;
|
||||
if (Array.isArray(ext.authors) && ext.authors.length) return ext.authors.join(', ');
|
||||
if (typeof ext.author_name === 'string' && ext.author_name.trim()) return ext.author_name;
|
||||
if (typeof ext.owner === 'string' && ext.owner.trim()) return ext.owner;
|
||||
if (ext.author && typeof ext.author === 'object' && ext.author.name) return ext.author.name;
|
||||
return '';
|
||||
});
|
||||
|
||||
const logoLoadFailed = ref(false);
|
||||
|
||||
const logoSrc = computed(() => {
|
||||
@@ -377,6 +388,10 @@ const viewChangelog = () => {
|
||||
{{ tag === "danger" ? tm("tags.danger") : tag }}
|
||||
</v-chip>
|
||||
<PluginPlatformChip :platforms="supportPlatforms" />
|
||||
<v-chip v-if="authorDisplay" color="info" label size="small">
|
||||
<v-icon icon="mdi-account" start></v-icon>
|
||||
{{ authorDisplay }}
|
||||
</v-chip>
|
||||
<v-chip
|
||||
v-if="astrbotVersionRequirement"
|
||||
color="secondary"
|
||||
|
||||
@@ -1249,7 +1249,7 @@
|
||||
"description": "API Base URL"
|
||||
},
|
||||
"openai_embedding": {
|
||||
"hint": "OpenAI Embedding automatically appends /v1 at request time."
|
||||
"hint": "If testing fails, try adding /v1 at the end to be compatible with some OpenAI API versions."
|
||||
},
|
||||
"gemini_embedding": {
|
||||
"hint": "Gemini Embedding does not require manually adding /v1beta."
|
||||
|
||||
@@ -1246,7 +1246,7 @@
|
||||
"description": "Адрес прокси-сервера"
|
||||
},
|
||||
"openai_embedding": {
|
||||
"hint": "OpenAI Embedding автоматически добавляет /v1 при запросе."
|
||||
"hint": "Если тест не проходит, попробуйте добавить /v1 в конец embedding_api_base для совместимости с некоторыми версиями OpenAI API."
|
||||
},
|
||||
"gemini_embedding": {
|
||||
"hint": "Gemini Embedding не требует ручного добавления /v1beta."
|
||||
|
||||
@@ -1251,7 +1251,7 @@
|
||||
"description": "API Base URL"
|
||||
},
|
||||
"openai_embedding": {
|
||||
"hint": "OpenAI Embedding 会在请求时自动补上 /v1。"
|
||||
"hint": "如果测试不通过,可以尝试添加 /v1 在末尾以兼容部分 OpenAI API 版本。"
|
||||
},
|
||||
"gemini_embedding": {
|
||||
"hint": "Gemini Embedding 无需手动添加 /v1beta。"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -86,4 +86,4 @@ AstrBot 支持接入优云智算提供的模型 API。
|
||||
|
||||
## 更多功能
|
||||
|
||||
更多功能情参考 [AstrBot 官方文档](https://docs.astrbot.app)。
|
||||
更多功能请参考 [AstrBot 官方文档](https://docs.astrbot.app)。
|
||||
@@ -88,10 +88,12 @@
|
||||
|
||||
再点击上面的`保存`按钮。
|
||||
|
||||
接下来,点击权限管理,点击开通权限,输入 `im:message:send,im:message,im:message:send_as_bot`。添加筛选到的权限。
|
||||
接下来,点击权限管理,点击开通权限,输入 `im:message,im:message:send_as_bot`。添加筛选到的权限。
|
||||
|
||||
再次输入 `im:resource:upload,im:resource` 开通上传图片相关的权限。
|
||||
|
||||
如果需要在群聊里使用,请额外开通 `im:message.group_at_msg:readonly` 和 `im:message.group_msg` 权限。
|
||||
|
||||
如果需要使用流式输出,请额外开通 `创建与更新卡片(cardkit:card:write)` 权限。
|
||||
|
||||
最终开通的权限如下图:
|
||||
@@ -118,4 +120,4 @@
|
||||
|
||||
在群内发送一个 `/help` 指令,机器人将做出响应。
|
||||
|
||||

|
||||

|
||||
|
||||
@@ -77,10 +77,15 @@ def copy_dashboard_dist():
|
||||
|
||||
if not dist_src.exists():
|
||||
print("[maturin-hook] No dashboard/dist found after build")
|
||||
# Remove broken symlink if present so placeholder can be created
|
||||
if dist_target.is_symlink() and not dist_target.exists():
|
||||
dist_target.unlink()
|
||||
return False
|
||||
|
||||
# Remove existing target
|
||||
if dist_target.exists():
|
||||
# Remove existing target (symlink or directory)
|
||||
if dist_target.is_symlink():
|
||||
dist_target.unlink()
|
||||
elif dist_target.exists():
|
||||
shutil.rmtree(dist_target)
|
||||
|
||||
# Copy dist
|
||||
@@ -93,6 +98,11 @@ def generate_placeholder():
|
||||
"""Generate placeholder if no dashboard."""
|
||||
root = get_root()
|
||||
dist_target = get_dashboard_target()
|
||||
|
||||
# Remove broken symlink if present so we can create a real directory
|
||||
if dist_target.is_symlink() and not dist_target.exists():
|
||||
dist_target.unlink()
|
||||
|
||||
dist_target.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
placeholder = dist_target / ".placeholder"
|
||||
@@ -149,6 +159,8 @@ def pre_build_hook(ctx):
|
||||
print("[maturin-hook] Building dashboard...")
|
||||
if build_dashboard():
|
||||
copy_dashboard_dist()
|
||||
else:
|
||||
generate_placeholder()
|
||||
else:
|
||||
print("[maturin-hook] Skipping dashboard build (ASTRBOT_BUILD_DASHBOARD != 1)")
|
||||
generate_placeholder()
|
||||
|
||||
Reference in New Issue
Block a user