diff --git a/dashboard/src/assets/images/platform_logos/dingtalk.svg b/dashboard/src/assets/images/platform_logos/dingtalk.svg new file mode 100644 index 00000000..323aaf14 --- /dev/null +++ b/dashboard/src/assets/images/platform_logos/dingtalk.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/dashboard/src/assets/images/platform_logos/discord.svg b/dashboard/src/assets/images/platform_logos/discord.svg new file mode 100644 index 00000000..fb73caa5 --- /dev/null +++ b/dashboard/src/assets/images/platform_logos/discord.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/dashboard/src/assets/images/platform_logos/kook.png b/dashboard/src/assets/images/platform_logos/kook.png new file mode 100644 index 00000000..c99421c5 Binary files /dev/null and b/dashboard/src/assets/images/platform_logos/kook.png differ diff --git a/dashboard/src/assets/images/platform_logos/lark.png b/dashboard/src/assets/images/platform_logos/lark.png new file mode 100644 index 00000000..d1372f13 Binary files /dev/null and b/dashboard/src/assets/images/platform_logos/lark.png differ diff --git a/dashboard/src/assets/images/platform_logos/qq.png b/dashboard/src/assets/images/platform_logos/qq.png new file mode 100644 index 00000000..51227994 Binary files /dev/null and b/dashboard/src/assets/images/platform_logos/qq.png differ diff --git a/dashboard/src/assets/images/platform_logos/slack.svg b/dashboard/src/assets/images/platform_logos/slack.svg new file mode 100644 index 00000000..69a4eb6a --- /dev/null +++ b/dashboard/src/assets/images/platform_logos/slack.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/dashboard/src/assets/images/platform_logos/telegram.svg b/dashboard/src/assets/images/platform_logos/telegram.svg new file mode 100644 index 00000000..8e4e9849 --- /dev/null +++ b/dashboard/src/assets/images/platform_logos/telegram.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/dashboard/src/assets/images/platform_logos/vocechat.png b/dashboard/src/assets/images/platform_logos/vocechat.png new file mode 100644 index 00000000..97ceb1d0 Binary files /dev/null and b/dashboard/src/assets/images/platform_logos/vocechat.png differ diff --git a/dashboard/src/assets/images/platform_logos/wechat.png b/dashboard/src/assets/images/platform_logos/wechat.png new file mode 100644 index 00000000..89f8aed2 Binary files /dev/null and b/dashboard/src/assets/images/platform_logos/wechat.png differ diff --git a/dashboard/src/assets/images/platform_logos/wecom.png b/dashboard/src/assets/images/platform_logos/wecom.png new file mode 100644 index 00000000..95bab3e2 Binary files /dev/null and b/dashboard/src/assets/images/platform_logos/wecom.png differ diff --git a/dashboard/src/i18n/locales/en-US/features/platform.json b/dashboard/src/i18n/locales/en-US/features/platform.json index 20844c7b..976aa364 100644 --- a/dashboard/src/i18n/locales/en-US/features/platform.json +++ b/dashboard/src/i18n/locales/en-US/features/platform.json @@ -20,7 +20,8 @@ "adapter": "Platform Adapter", "refresh": "Refresh", "cancel": "Cancel", - "save": "Save" + "save": "Save", + "addPlatform": "Add Platform Adapter" }, "messages": { "updateSuccess": "Update successful!", diff --git a/dashboard/src/i18n/locales/en-US/features/provider.json b/dashboard/src/i18n/locales/en-US/features/provider.json index cf0fa584..c64f23d1 100644 --- a/dashboard/src/i18n/locales/en-US/features/provider.json +++ b/dashboard/src/i18n/locales/en-US/features/provider.json @@ -18,8 +18,8 @@ "typed": "No {type} type service providers available, click Add Provider to add one" }, "description": { - "openai": "{type} service provider. Also supports all OpenAI API compatible model providers.", - "default": "{type} service provider" + "openai": "Supports all OpenAI API compatible providers.", + "default": "" } }, "availability": { diff --git a/dashboard/src/i18n/locales/zh-CN/features/platform.json b/dashboard/src/i18n/locales/zh-CN/features/platform.json index 0bcdf282..4e624c8f 100644 --- a/dashboard/src/i18n/locales/zh-CN/features/platform.json +++ b/dashboard/src/i18n/locales/zh-CN/features/platform.json @@ -20,7 +20,8 @@ "adapter": "平台适配器", "refresh": "刷新", "cancel": "取消", - "save": "保存" + "save": "保存", + "addPlatform": "添加平台适配器" }, "messages": { "updateSuccess": "更新成功!", diff --git a/dashboard/src/i18n/locales/zh-CN/features/provider.json b/dashboard/src/i18n/locales/zh-CN/features/provider.json index d056f880..e9bb63c6 100644 --- a/dashboard/src/i18n/locales/zh-CN/features/provider.json +++ b/dashboard/src/i18n/locales/zh-CN/features/provider.json @@ -18,8 +18,8 @@ "typed": "暂无{type}类型的服务提供商,点击 新增服务提供商 添加" }, "description": { - "openai": "{type} 服务提供商。同时也支持所有兼容 OpenAI API 的模型提供商。", - "default": "{type} 服务提供商" + "openai": "也支持所有兼容 OpenAI API 的模型提供商。", + "default": "" } }, "availability": { diff --git a/dashboard/src/stores/common.js b/dashboard/src/stores/common.js index 8983eef4..8fc48976 100644 --- a/dashboard/src/stores/common.js +++ b/dashboard/src/stores/common.js @@ -12,19 +12,6 @@ export const useCommonStore = defineStore({ log_cache_max_len: 1000, startTime: -1, - tutorial_map: { - "qq_official_webhook": "https://astrbot.app/deploy/platform/qqofficial/webhook.html", - "qq_official": "https://astrbot.app/deploy/platform/qqofficial/websockets.html", - "aiocqhttp": "https://astrbot.app/deploy/platform/aiocqhttp/napcat.html", - "wecom": "https://astrbot.app/deploy/platform/wecom.html", - "gewechat": "https://astrbot.app/deploy/platform/wechat/gewechat.html", - "lark": "https://astrbot.app/deploy/platform/lark.html", - "telegram": "https://astrbot.app/deploy/platform/telegram.html", - "dingtalk": "https://astrbot.app/deploy/platform/dingtalk.html", - "wechatpadpro": "https://astrbot.app/deploy/platform/wechat/wechatpadpro.html", - "weixin_official_account": "https://astrbot.app/deploy/platform/weixin-official-account.html", - }, - pluginMarketData: [], }), actions: { @@ -124,9 +111,6 @@ export const useCommonStore = defineStore({ this.startTime = res.data.data.start_time }) }, - getTutorialLink(platform) { - return this.tutorial_map[platform] - }, async getPluginCollections(force = false) { // 获取插件市场数据 if (!force && this.pluginMarketData.length > 0) { diff --git a/dashboard/src/views/PlatformPage.vue b/dashboard/src/views/PlatformPage.vue index 7afb1105..76f275ff 100644 --- a/dashboard/src/views/PlatformPage.vue +++ b/dashboard/src/views/PlatformPage.vue @@ -20,43 +20,22 @@ {{ tm('adapters') }} {{ config_data.platform?.length || 0 }} - - - - - {{ index }} - - - + + {{ tm('addAdapter') }} + - + @@ -167,7 +206,7 @@ export default { setup() { const { t } = useI18n(); const { tm } = useModuleI18n('features/platform'); - + return { t, tm @@ -191,6 +230,7 @@ export default { fetched: false, metadata: {}, showPlatformCfg: false, + showAddPlatformDialog: false, newSelectedPlatformName: '', newSelectedPlatformConfig: {}, @@ -203,23 +243,89 @@ export default { save_message_success: "success", showConsole: false, - iframeLoading: true, + + // ID冲突确认对话框 + showIdConflictDialog: false, + conflictId: '', + idConflictResolve: null, store: useCommonStore() } }, + watch: { + showIdConflictDialog(newValue) { + // 当对话框关闭时,如果 Promise 还在等待,则拒绝它以防止内存泄漏 + if (!newValue && this.idConflictResolve) { + this.idConflictResolve(false); + this.idConflictResolve = null; + } + } + }, + mounted() { this.getConfig(); }, methods: { - refreshIframe() { - this.iframeLoading = true; - const iframe = document.querySelector('iframe'); - iframe.src = iframe.src + '?t=' + new Date().getTime(); + openTutorial() { + const tutorialUrl = this.getTutorialLink(this.newSelectedPlatformConfig.type); + window.open(tutorialUrl, '_blank'); }, - + + getPlatformIcon(name) { + if (name.includes('QQ')) { + return '/src/assets/images/platform_logos/qq.png' + } else if (name.includes('企业微信')) { + return '/src/assets/images/platform_logos/wecom.png' + } else if (name.includes('微信')) { + return '/src/assets/images/platform_logos/wechat.png'; + } else if (name.includes('Lark')) { + return '/src/assets/images/platform_logos/lark.png'; + } else if (name.includes('DingTalk')) { + return '/src/assets/images/platform_logos/dingtalk.svg'; + } else if (name.includes('Telegram')) { + return '/src/assets/images/platform_logos/telegram.svg'; + } else if (name.includes('Discord')) { + return '/src/assets/images/platform_logos/discord.svg'; + } else if (name.includes('Slack')) { + return '/src/assets/images/platform_logos/slack.svg'; + } else if (name.includes('kook')) { + return '/src/assets/images/platform_logos/kook.png'; + } else if (name.includes('vocechat')) { + return '/src/assets/images/platform_logos/vocechat.png'; + } + }, + + getTutorialLink(platform_type) { + let tutorial_map = { + "qq_official_webhook": "https://astrbot.app/deploy/platform/qqofficial/webhook.html", + "qq_official": "https://astrbot.app/deploy/platform/qqofficial/websockets.html", + "aiocqhttp": "https://astrbot.app/deploy/platform/aiocqhttp/napcat.html", + "wecom": "https://astrbot.app/deploy/platform/wecom.html", + "gewechat": "https://astrbot.app/deploy/platform/wechat/gewechat.html", + "lark": "https://astrbot.app/deploy/platform/lark.html", + "telegram": "https://astrbot.app/deploy/platform/telegram.html", + "dingtalk": "https://astrbot.app/deploy/platform/dingtalk.html", + "wechatpadpro": "https://astrbot.app/deploy/platform/wechat/wechatpadpro.html", + "weixin_official_account": "https://astrbot.app/deploy/platform/weixin-official-account.html", + "discord": "https://astrbot.app/deploy/platform/discord.html", + "slack": "https://astrbot.app/deploy/platform/slack.html", + "kook": "https://astrbot.app/deploy/platform/kook.html", + "vocechat": "https://astrbot.app/deploy/platform/vocechat.html", + } + return tutorial_map[platform_type] || "https://docs.astrbot.app"; + }, + + getPlatformDescription(template, name) { + // special judge for community platforms + if (name.includes('vocechat')) { + return "由 @HikariFroya 提供。"; + } else if (name.includes('kook')) { + return "由 @wuyan1003 提供。" + } + }, + getConfig() { axios.get('/api/config/get').then((res) => { this.config_data = res.data.data.config; @@ -230,6 +336,17 @@ export default { }); }, + // 添加一个新方法来选择平台模板 + selectPlatformTemplate(name) { + this.newSelectedPlatformName = name; + this.showPlatformCfg = true; + this.updatingMode = false; + this.newSelectedPlatformConfig = JSON.parse(JSON.stringify( + this.metadata['platform_group']?.metadata?.platform?.config_template[name] || {} + )); + this.showAddPlatformDialog = false; + }, + addFromDefaultConfigTmpl(index) { this.newSelectedPlatformName = index[0]; this.showPlatformCfg = true; @@ -264,18 +381,48 @@ export default { }); this.updatingMode = false; } else { - axios.post('/api/config/platform/new', this.newSelectedPlatformConfig).then((res) => { - this.loading = false; - this.showPlatformCfg = false; - this.getConfig(); - this.showSuccess(res.data.message || this.messages.addSuccess); - }).catch((err) => { - this.loading = false; - this.showError(err.response?.data?.message || err.message); - }); + this.savePlatform(); } }, + async savePlatform() { + // 检查 ID 是否已存在 + const existingPlatform = this.config_data.platform?.find(p => p.id === this.newSelectedPlatformConfig.id); + if (existingPlatform) { + const confirmed = await this.confirmIdConflict(this.newSelectedPlatformConfig.id); + if (!confirmed) { + this.loading = false; + return; // 如果用户取消,则中止保存 + } + } + + try { + const res = await axios.post('/api/config/platform/new', this.newSelectedPlatformConfig); + this.loading = false; + this.showPlatformCfg = false; + this.getConfig(); + this.showSuccess(res.data.message || this.messages.addSuccess); + } catch (err) { + this.loading = false; + this.showError(err.response?.data?.message || err.message); + } + }, + + confirmIdConflict(id) { + this.conflictId = id; + this.showIdConflictDialog = true; + return new Promise((resolve) => { + this.idConflictResolve = resolve; + }); + }, + + handleIdConflictConfirm(confirmed) { + if (this.idConflictResolve) { + this.idConflictResolve(confirmed); + } + this.showIdConflictDialog = false; + }, + deletePlatform(platform) { if (confirm(`${this.messages.deleteConfirm} ${platform.id}?`)) { axios.post('/api/config/platform/delete', { id: platform.id }).then((res) => { @@ -290,7 +437,7 @@ export default { platformStatusChange(platform) { platform.enable = !platform.enable; // 切换状态 - + axios.post('/api/config/platform/update', { id: platform.id, config: platform @@ -303,13 +450,13 @@ export default { this.showError(err.response?.data?.message || err.message); }); }, - + showSuccess(message) { this.save_message = message; this.save_message_success = "success"; this.save_message_snack = true; }, - + showError(message) { this.save_message = message; this.save_message_success = "error"; @@ -324,4 +471,84 @@ export default { padding: 20px; padding-top: 8px; } + +.platform-selection-dialog .v-card-title { + border-top-left-radius: 4px; + border-top-right-radius: 4px; +} + +.platform-card { + transition: all 0.3s ease; + height: 100%; + cursor: pointer; + overflow: hidden; + position: relative; +} + +.platform-card:hover { + transform: translateY(-4px); + box-shadow: 0 4px 25px 0 rgba(0, 0, 0, 0.05); + border-color: var(--v-primary-base); +} + +.platform-card-content { + display: flex; + align-items: center; + height: 100px; + padding: 16px; + position: relative; + z-index: 2; +} + +.platform-card-text { + flex: 1; + display: flex; + flex-direction: column; + justify-content: center; +} + +.platform-card-title { + font-size: 15px; + font-weight: 600; + margin-bottom: 4px; + padding: 0; +} + +.platform-card-description { + padding: 0; + margin: 0; +} + +.platform-card-logo { + position: absolute; + right: 0; + top: 0; + bottom: 0; + width: 80px; + display: flex; + align-items: center; + justify-content: center; + z-index: 1; +} + +.platform-logo-img { + max-width: 60px; + max-height: 60px; + opacity: 0.6; + object-fit: contain; +} + +.platform-logo-fallback { + width: 50px; + height: 50px; + border-radius: 50%; + background-color: var(--v-primary-base); + color: white; + display: flex; + align-items: center; + justify-content: center; + font-size: 24px; + font-weight: bold; + opacity: 0.3; +} \ No newline at end of file diff --git a/dashboard/src/views/ProviderPage.vue b/dashboard/src/views/ProviderPage.vue index eecf1663..220393de 100644 --- a/dashboard/src/views/ProviderPage.vue +++ b/dashboard/src/views/ProviderPage.vue @@ -198,20 +198,20 @@ :key="name" cols="12" sm="6" md="4"> - - - {{ name }} - - - {{ getProviderDescription(template, name) }} - +
+
+ 接入 {{ name }} + + {{ getProviderDescription(template, name) }} + +
+ +
@@ -307,6 +307,23 @@ + + + + + mdi-alert-circle-outline + ID 冲突警告 + + + 检测到 ID "{{ conflictId }}" 重复。请使用一个新的 ID。 + + + + 好的 + + + + @@ -359,6 +376,11 @@ export default { sessionSeparationEnabled: false, sessionSettingLoading: false, + // ID冲突确认对话框 + showIdConflictDialog: false, + conflictId: '', + idConflictResolve: null, + // Key确认对话框 showKeyConfirm: false, keyConfirmResolve: null, @@ -411,6 +433,13 @@ export default { }, watch: { + showIdConflictDialog(newValue) { + // 当对话框关闭时,如果 Promise 还在等待,则拒绝它以防止内存泄漏 + if (!newValue && this.idConflictResolve) { + this.idConflictResolve(false); + this.idConflictResolve = null; + } + }, showKeyConfirm(newValue) { // 当对话框关闭时,如果 Promise 还在等待,则拒绝它以防止内存泄漏 if (!newValue && this.keyConfirmResolve) { @@ -645,6 +674,16 @@ export default { }); this.showSuccess(res.data.message || "更新成功!"); } else { + // 检查 ID 是否已存在 + const existingProvider = this.config_data.provider?.find(p => p.id === this.newSelectedProviderConfig.id); + if (existingProvider) { + const confirmed = await this.confirmIdConflict(this.newSelectedProviderConfig.id); + if (!confirmed) { + this.loading = false; + return; // 如果用户取消,则中止保存 + } + } + const res = await axios.post('/api/config/provider/new', this.newSelectedProviderConfig); this.showSuccess(res.data.message || "添加成功!"); } @@ -753,6 +792,21 @@ export default { } this.showKeyConfirm = false; }, + + confirmIdConflict(id) { + this.conflictId = id; + this.showIdConflictDialog = true; + return new Promise((resolve) => { + this.idConflictResolve = resolve; + }); + }, + + handleIdConflictConfirm(confirmed) { + if (this.idConflictResolve) { + this.idConflictResolve(confirmed); + } + this.showIdConflictDialog = false; + }, } } @@ -772,6 +826,8 @@ export default { transition: all 0.3s ease; height: 100%; cursor: pointer; + overflow: hidden; + position: relative; } .provider-card:hover { @@ -780,6 +836,67 @@ export default { border-color: var(--v-primary-base); } +.provider-card-content { + display: flex; + align-items: center; + height: 100px; + padding: 16px; + position: relative; + z-index: 2; +} + +.provider-card-text { + flex: 1; + display: flex; + flex-direction: column; + justify-content: center; +} + +.provider-card-title { + font-size: 15px; + font-weight: 600; + margin-bottom: 4px; + padding: 0; +} + +.provider-card-description { + padding: 0; + margin: 0; +} + +.provider-card-logo { + position: absolute; + right: 0; + top: 0; + bottom: 0; + width: 80px; + display: flex; + align-items: center; + justify-content: center; + z-index: 1; +} + +.provider-logo-img { + width: 60px; + height: 60px; + opacity: 0.6; + object-fit: contain; +} + +.provider-logo-fallback { + width: 50px; + height: 50px; + border-radius: 50%; + background-color: var(--v-primary-base); + color: white; + display: flex; + align-items: center; + justify-content: center; + font-size: 24px; + font-weight: bold; + opacity: 0.3; +} + .v-tabs { border-radius: 8px; }