[codex] add QQBot and DingTalk channel settings (#787)
* add qqbot and dingtalk channel settings * remove history session context menu
This commit is contained in:
@@ -59,6 +59,7 @@ export interface AppConfig {
|
||||
wecom?: Record<string, any>
|
||||
feishu?: Record<string, any>
|
||||
dingtalk?: Record<string, any>
|
||||
qqbot?: Record<string, any>
|
||||
platforms?: Record<string, any>
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
@@ -52,6 +52,10 @@ function getCreds(key: string) {
|
||||
return (settingsStore.platforms[key] || {}) as Record<string, any>
|
||||
}
|
||||
|
||||
function boolValue(value: unknown) {
|
||||
return value === true || value === 'true'
|
||||
}
|
||||
|
||||
// Weixin QR code login state
|
||||
const wxQrUrl = ref('')
|
||||
const wxQrId = ref('')
|
||||
@@ -152,6 +156,18 @@ const platforms = [
|
||||
exclusive: true,
|
||||
icon: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M6.59 3.41a2.25 2.25 0 0 1 3.182 0L13.5 7.14l-3.182 3.182L6.59 7.59a2.25 2.25 0 0 1 0-3.182zm5.303 5.303L15.075 5.53a2.25 2.25 0 0 1 3.182 3.182L15.075 11.894 11.893 8.713zM3.41 6.59a2.25 2.25 0 0 1 3.182 0l3.182 3.182-3.182 3.182a2.25 2.25 0 0 1-3.182-3.182L3.41 6.59zm5.303 5.303L11.894 15.075a2.25 2.25 0 0 1-3.182 3.182L5.53 15.075 8.713 11.893zm5.303-5.303L17.478 9.778a2.25 2.25 0 0 1-3.182 3.182L10.53 10.075l3.182-3.182 0 .023z"/></svg>',
|
||||
},
|
||||
{
|
||||
key: 'dingtalk',
|
||||
name: 'DingTalk',
|
||||
exclusive: true,
|
||||
icon: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M19.76 7.05c-.23-.52-.7-.9-1.26-1.02L5.35 3.2c-.77-.16-1.51.38-1.58 1.16-.22 2.55.17 5.4 1.13 7.66.97 2.29 2.52 4.11 4.45 4.82l-1.28 3.03c-.17.4.24.79.63.59l9.47-4.83c.34-.17.55-.52.55-.9v-3.12c.73-.4 1.22-1.17 1.22-2.06 0-.87-.08-1.73-.18-2.5zm-3.66 5.95-5.19 2.65.76-1.8c.12-.29-.03-.62-.33-.72-2.1-.73-3.56-3.54-3.95-6.73l9.27 2c.04.38.07.76.07 1.15 0 .45-.36.81-.81.81h-2.79c-.35 0-.63.28-.63.63s.28.63.63.63h2.97V13z"/></svg>',
|
||||
},
|
||||
{
|
||||
key: 'qqbot',
|
||||
name: 'QQBot',
|
||||
exclusive: true,
|
||||
icon: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C7.58 2 4 5.27 4 9.31c0 2.3 1.15 4.34 2.95 5.68-.13.58-.48 1.62-1.26 2.53-.24.28-.05.72.32.73 1.72.05 3.02-.68 3.69-1.15.72.16 1.49.25 2.3.25 4.42 0 8-3.27 8-7.31S16.42 2 12 2zm-3.2 7.63c-.63 0-1.14-.55-1.14-1.23s.51-1.23 1.14-1.23 1.14.55 1.14 1.23-.51 1.23-1.14 1.23zm6.4 0c-.63 0-1.14-.55-1.14-1.23s.51-1.23 1.14-1.23 1.14.55 1.14 1.23-.51 1.23-1.14 1.23zM5.5 20.5a.5.5 0 0 1 .5-.5h12a.5.5 0 0 1 0 1H6a.5.5 0 0 1-.5-.5z"/></svg>',
|
||||
},
|
||||
{
|
||||
key: 'weixin',
|
||||
name: 'Weixin',
|
||||
@@ -302,6 +318,12 @@ const platforms = [
|
||||
<SettingRow :label="t('platform.clientSecret')" :hint="t('platform.clientSecretHint')">
|
||||
<NInput :default-value="getCreds('dingtalk').extra?.client_secret || ''" :loading="isSaving('dingtalk', 'client_secret')" clearable size="small" class="input-lg" placeholder="Client Secret" @change="v => saveCredentials('dingtalk', 'client_secret', { extra: { ...getCreds('dingtalk').extra, client_secret: v } })" />
|
||||
</SettingRow>
|
||||
<SettingRow :label="t('platform.allowAllUsers')" :hint="t('platform.allowAllUsersHint')">
|
||||
<NSwitch :value="boolValue(getCreds('dingtalk').allow_all_users)" :loading="isSaving('dingtalk', 'allow_all_users')" @update:value="v => saveCredentials('dingtalk', 'allow_all_users', { allow_all_users: v })" />
|
||||
</SettingRow>
|
||||
<SettingRow :label="t('platform.allowedUsers')" :hint="t('platform.allowedUsersHint')">
|
||||
<NInput :default-value="getCreds('dingtalk').allowed_users || ''" :loading="isSaving('dingtalk', 'allowed_users')" clearable size="small" class="input-lg" placeholder="user_id1,user_id2" @change="v => saveCredentials('dingtalk', 'allowed_users', { allowed_users: v })" />
|
||||
</SettingRow>
|
||||
<SettingRow :label="t('platform.requireMention')" :hint="t('platform.requireMentionGroup')">
|
||||
<NSwitch :value="settingsStore.dingtalk.require_mention" :loading="isSaving('dingtalk', 'require_mention')" @update:value="v => saveChannel('dingtalk', 'require_mention', { require_mention: v })" />
|
||||
</SettingRow>
|
||||
@@ -310,6 +332,25 @@ const platforms = [
|
||||
</SettingRow>
|
||||
</template>
|
||||
|
||||
<!-- QQBot -->
|
||||
<template v-if="p.key === 'qqbot'">
|
||||
<SettingRow :label="t('platform.qqAppId')" :hint="t('platform.qqAppIdHint')">
|
||||
<NInput :default-value="getCreds('qqbot').extra?.app_id || ''" :loading="isSaving('qqbot', 'app_id')" clearable size="small" class="input-lg" placeholder="App ID" @change="v => saveCredentials('qqbot', 'app_id', { extra: { ...getCreds('qqbot').extra, app_id: v } })" />
|
||||
</SettingRow>
|
||||
<SettingRow :label="t('platform.qqAppSecret')" :hint="t('platform.qqAppSecretHint')">
|
||||
<NInput :default-value="getCreds('qqbot').extra?.client_secret || ''" :loading="isSaving('qqbot', 'client_secret')" clearable size="small" class="input-lg" placeholder="App Secret" @change="v => saveCredentials('qqbot', 'client_secret', { extra: { ...getCreds('qqbot').extra, client_secret: v } })" />
|
||||
</SettingRow>
|
||||
<SettingRow :label="t('platform.allowedUsers')" :hint="t('platform.allowedUsersHint')">
|
||||
<NInput :default-value="getCreds('qqbot').allowed_users || ''" :loading="isSaving('qqbot', 'allowed_users')" clearable size="small" class="input-lg" placeholder="openid1,openid2" @change="v => saveCredentials('qqbot', 'allowed_users', { allowed_users: v })" />
|
||||
</SettingRow>
|
||||
<SettingRow :label="t('platform.allowAllUsers')" :hint="t('platform.allowAllUsersHint')">
|
||||
<NSwitch :value="boolValue(getCreds('qqbot').allow_all_users)" :loading="isSaving('qqbot', 'allow_all_users')" @update:value="v => saveCredentials('qqbot', 'allow_all_users', { allow_all_users: v })" />
|
||||
</SettingRow>
|
||||
<SettingRow :label="t('platform.qqMarkdown')" :hint="t('platform.qqMarkdownHint')">
|
||||
<NSwitch :value="settingsStore.qqbot.extra?.markdown_support ?? true" :loading="isSaving('qqbot', 'markdown_support')" @update:value="v => saveChannel('qqbot', 'markdown_support', { extra: { ...settingsStore.qqbot.extra, markdown_support: v } })" />
|
||||
</SettingRow>
|
||||
</template>
|
||||
|
||||
<!-- Weixin -->
|
||||
<template v-if="p.key === 'weixin'">
|
||||
<div class="weixin-qr-section">
|
||||
|
||||
@@ -920,6 +920,10 @@ export default {
|
||||
clientIdHint: 'DingTalk Client ID',
|
||||
clientSecret: 'Client Secret',
|
||||
clientSecretHint: 'DingTalk Client Secret',
|
||||
allowedUsers: 'Allowed Users',
|
||||
allowedUsersHint: 'Whitelist user IDs or OpenIDs (comma-separated)',
|
||||
allowAllUsers: 'Allow All Users',
|
||||
allowAllUsersHint: 'Allow messages from any user; keep off to use the allowlist',
|
||||
botId: 'Bot ID',
|
||||
botIdHint: 'WeCom Bot ID',
|
||||
wecomSecretHint: 'WeCom Bot Secret',
|
||||
|
||||
@@ -909,6 +909,10 @@ export default {
|
||||
clientIdHint: '釘釘 Client ID',
|
||||
clientSecret: 'Client Secret',
|
||||
clientSecretHint: '釘釘 Client Secret',
|
||||
allowedUsers: '允許使用者',
|
||||
allowedUsersHint: '使用者 ID 或 OpenID 白名單,多個請用英文逗號分隔',
|
||||
allowAllUsers: '允許所有使用者',
|
||||
allowAllUsersHint: '允許任意使用者發起訊息;關閉後使用白名單',
|
||||
botId: 'Bot ID',
|
||||
botIdHint: '企業微信 Bot ID',
|
||||
wecomSecretHint: '企業微信 Bot Secret',
|
||||
|
||||
@@ -912,6 +912,10 @@ export default {
|
||||
clientIdHint: '钉钉 Client ID',
|
||||
clientSecret: 'Client Secret',
|
||||
clientSecretHint: '钉钉 Client Secret',
|
||||
allowedUsers: '允许用户',
|
||||
allowedUsersHint: '用户 ID 或 OpenID 白名单,多个用英文逗号分隔',
|
||||
allowAllUsers: '允许所有用户',
|
||||
allowAllUsersHint: '允许任意用户发起消息;关闭后使用白名单',
|
||||
botId: 'Bot ID',
|
||||
botIdHint: '企业微信 Bot ID',
|
||||
wecomSecretHint: '企业微信 Bot Secret',
|
||||
|
||||
@@ -21,6 +21,7 @@ export const useSettingsStore = defineStore('settings', () => {
|
||||
const wecom = ref<Record<string, any>>({})
|
||||
const feishu = ref<Record<string, any>>({})
|
||||
const dingtalk = ref<Record<string, any>>({})
|
||||
const qqbot = ref<Record<string, any>>({})
|
||||
const weixin = ref<Record<string, any>>({})
|
||||
const platforms = ref<Record<string, any>>({})
|
||||
|
||||
@@ -42,6 +43,7 @@ export const useSettingsStore = defineStore('settings', () => {
|
||||
wecom.value = data.wecom || {}
|
||||
feishu.value = data.feishu || {}
|
||||
dingtalk.value = data.dingtalk || {}
|
||||
qqbot.value = data.qqbot || {}
|
||||
weixin.value = data.weixin || {}
|
||||
platforms.value = data.platforms || {}
|
||||
} catch (err) {
|
||||
@@ -67,6 +69,7 @@ export const useSettingsStore = defineStore('settings', () => {
|
||||
case 'wecom': wecom.value = { ...wecom.value, ...values }; break
|
||||
case 'feishu': feishu.value = { ...feishu.value, ...values }; break
|
||||
case 'dingtalk': dingtalk.value = { ...dingtalk.value, ...values }; break
|
||||
case 'qqbot': qqbot.value = { ...qqbot.value, ...values }; break
|
||||
case 'weixin': weixin.value = { ...weixin.value, ...values }; break
|
||||
case 'platforms': {
|
||||
for (const [key, val] of Object.entries(values)) {
|
||||
@@ -99,6 +102,7 @@ export const useSettingsStore = defineStore('settings', () => {
|
||||
case 'wechat': case 'wecom': wecom.value = { ...wecom.value, ...values }; break
|
||||
case 'feishu': feishu.value = { ...feishu.value, ...values }; break
|
||||
case 'dingtalk': dingtalk.value = { ...dingtalk.value, ...values }; break
|
||||
case 'qqbot': qqbot.value = { ...qqbot.value, ...values }; break
|
||||
case 'weixin': weixin.value = { ...weixin.value, ...values }; break
|
||||
case 'platforms': {
|
||||
// Deep-merge each platform's credentials
|
||||
@@ -119,7 +123,7 @@ export const useSettingsStore = defineStore('settings', () => {
|
||||
return {
|
||||
loading, saving,
|
||||
display, agent, memory, sessionReset, privacy, approvals,
|
||||
telegram, discord, slack, whatsapp, matrix, wecom, feishu, dingtalk, weixin, platforms,
|
||||
telegram, discord, slack, whatsapp, matrix, wecom, feishu, dingtalk, qqbot, weixin, platforms,
|
||||
fetchSettings, saveSection, updateLocal,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { useChatStore, type Session } from '@/stores/hermes/chat'
|
||||
import { useAppStore } from '@/stores/hermes/app'
|
||||
import { useProfilesStore } from '@/stores/hermes/profiles'
|
||||
import { useSessionBrowserPrefsStore } from '@/stores/hermes/session-browser-prefs'
|
||||
import { NButton, NDropdown, NInput, NModal, NTooltip, useMessage } from 'naive-ui'
|
||||
import { NButton, NTooltip, useMessage } from 'naive-ui'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { getSourceLabel } from '@/shared/session-display'
|
||||
import { copyToClipboard } from '@/utils/clipboard'
|
||||
import FolderPicker from '@/components/hermes/chat/FolderPicker.vue'
|
||||
import HistoryMessageList from '@/components/hermes/chat/HistoryMessageList.vue'
|
||||
import SessionListItem from '@/components/hermes/chat/SessionListItem.vue'
|
||||
import { renameSession, setSessionWorkspace, fetchHermesSessions, fetchHermesSession, exportSession, type SessionSummary } from '@/api/hermes/sessions'
|
||||
import { fetchHermesSessions, fetchHermesSession, type SessionSummary } from '@/api/hermes/sessions'
|
||||
|
||||
const chatStore = useChatStore()
|
||||
const appStore = useAppStore()
|
||||
@@ -125,10 +124,6 @@ onUnmounted(() => {
|
||||
mobileQuery?.removeEventListener('change', handleMobileChange)
|
||||
})
|
||||
|
||||
const showRenameModal = ref(false)
|
||||
const renameValue = ref('')
|
||||
const renameSessionId = ref<string | null>(null)
|
||||
const renameInputRef = ref<InstanceType<typeof NInput> | null>(null)
|
||||
const collapsedGroups = ref<Set<string>>(new Set(JSON.parse(localStorage.getItem('hermes_collapsed_groups') || '[]')))
|
||||
|
||||
// Convert SessionSummary to Session format
|
||||
@@ -272,129 +267,6 @@ async function copySessionId(id?: string) {
|
||||
}
|
||||
}
|
||||
|
||||
const contextSessionId = ref<string | null>(null)
|
||||
const contextSessionPinned = computed(() =>
|
||||
contextSessionId.value ? sessionBrowserPrefsStore.isPinned(contextSessionId.value) : false,
|
||||
)
|
||||
|
||||
const contextMenuOptions = computed(() => [
|
||||
{ label: t(contextSessionPinned.value ? 'chat.unpin' : 'chat.pin'), key: 'pin' },
|
||||
{ label: t('chat.rename'), key: 'rename' },
|
||||
{ label: t('chat.setWorkspace'), key: 'workspace' },
|
||||
{
|
||||
label: t('chat.export'),
|
||||
key: 'export',
|
||||
children: [
|
||||
{
|
||||
label: t('chat.exportFull'),
|
||||
key: 'export-full',
|
||||
children: [
|
||||
{ label: 'JSON', key: 'export-full-json' },
|
||||
{ label: 'TXT', key: 'export-full-txt' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: t('chat.exportCompressed'),
|
||||
key: 'export-compressed',
|
||||
children: [
|
||||
{ label: 'JSON', key: 'export-compressed-json' },
|
||||
{ label: 'TXT', key: 'export-compressed-txt' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{ label: t('chat.copySessionId'), key: 'copy-id' },
|
||||
])
|
||||
|
||||
function handleContextMenu(e: MouseEvent, sessionId: string) {
|
||||
e.preventDefault()
|
||||
contextSessionId.value = sessionId
|
||||
showContextMenu.value = true
|
||||
contextMenuX.value = e.clientX
|
||||
contextMenuY.value = e.clientY
|
||||
}
|
||||
|
||||
const showContextMenu = ref(false)
|
||||
const contextMenuX = ref(0)
|
||||
const contextMenuY = ref(0)
|
||||
|
||||
function parseExportKey(key: string): { mode: 'full' | 'compressed'; ext: 'json' | 'txt' } | null {
|
||||
if (key === 'export-full-json') return { mode: 'full', ext: 'json' }
|
||||
if (key === 'export-full-txt') return { mode: 'full', ext: 'txt' }
|
||||
if (key === 'export-compressed-json') return { mode: 'compressed', ext: 'json' }
|
||||
if (key === 'export-compressed-txt') return { mode: 'compressed', ext: 'txt' }
|
||||
return null
|
||||
}
|
||||
|
||||
async function handleContextMenuSelect(key: string) {
|
||||
showContextMenu.value = false
|
||||
if (!contextSessionId.value) return
|
||||
if (key === 'pin') {
|
||||
sessionBrowserPrefsStore.togglePinned(contextSessionId.value)
|
||||
return
|
||||
}
|
||||
if (key === 'copy-id') {
|
||||
copySessionId(contextSessionId.value)
|
||||
} else if (parseExportKey(key)) {
|
||||
const { mode, ext } = parseExportKey(key)!
|
||||
const loadingMsg = mode === 'compressed' ? message.loading(t('chat.exportCompressing'), { duration: 0 }) : null
|
||||
try {
|
||||
await exportSession(contextSessionId.value, mode, ext)
|
||||
loadingMsg?.destroy()
|
||||
message.success(t('chat.exportSuccess'))
|
||||
} catch {
|
||||
loadingMsg?.destroy()
|
||||
message.error(t('chat.exportFailed'))
|
||||
}
|
||||
} else if (key === 'workspace') {
|
||||
const session = historySessions.value.find(s => s.id === contextSessionId.value)
|
||||
workspaceSessionId.value = contextSessionId.value
|
||||
workspaceValue.value = session?.workspace || ''
|
||||
showWorkspaceModal.value = true
|
||||
} else if (key === 'rename') {
|
||||
const session = historySessions.value.find(s => s.id === contextSessionId.value)
|
||||
renameSessionId.value = contextSessionId.value
|
||||
renameValue.value = session?.title || ''
|
||||
showRenameModal.value = true
|
||||
nextTick(() => {
|
||||
renameInputRef.value?.focus()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function handleClickOutside() {
|
||||
showContextMenu.value = false
|
||||
}
|
||||
|
||||
async function handleRenameConfirm() {
|
||||
if (!renameSessionId.value || !renameValue.value.trim()) return
|
||||
const ok = await renameSession(renameSessionId.value, renameValue.value.trim())
|
||||
if (ok) {
|
||||
// Reload Hermes sessions to get updated title
|
||||
await loadHermesSessions()
|
||||
message.success(t('chat.renamed'))
|
||||
} else {
|
||||
message.error(t('chat.renameFailed'))
|
||||
}
|
||||
showRenameModal.value = false
|
||||
}
|
||||
|
||||
const showWorkspaceModal = ref(false)
|
||||
const workspaceValue = ref('')
|
||||
const workspaceSessionId = ref<string | null>(null)
|
||||
|
||||
async function handleWorkspaceConfirm() {
|
||||
if (!workspaceSessionId.value) return
|
||||
const ok = await setSessionWorkspace(workspaceSessionId.value, workspaceValue.value || null)
|
||||
if (ok) {
|
||||
// Reload Hermes sessions to get updated workspace
|
||||
await loadHermesSessions()
|
||||
message.success(t('chat.workspaceSet'))
|
||||
} else {
|
||||
message.error(t('chat.workspaceSetFailed'))
|
||||
}
|
||||
showWorkspaceModal.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -430,7 +302,6 @@ async function handleWorkspaceConfirm() {
|
||||
:can-delete="false"
|
||||
:streaming="false"
|
||||
@select="handleSessionClick(s.id)"
|
||||
@contextmenu="handleContextMenu($event, s.id)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -450,52 +321,12 @@ async function handleWorkspaceConfirm() {
|
||||
:can-delete="false"
|
||||
:streaming="false"
|
||||
@select="handleSessionClick(s.id)"
|
||||
@contextmenu="handleContextMenu($event, s.id)"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<NDropdown
|
||||
placement="bottom-start"
|
||||
trigger="manual"
|
||||
:x="contextMenuX"
|
||||
:y="contextMenuY"
|
||||
:options="contextMenuOptions"
|
||||
:show="showContextMenu"
|
||||
@select="handleContextMenuSelect"
|
||||
@clickoutside="handleClickOutside"
|
||||
/>
|
||||
|
||||
<NModal
|
||||
v-model:show="showRenameModal"
|
||||
preset="dialog"
|
||||
:title="t('chat.renameSession')"
|
||||
:positive-text="t('common.ok')"
|
||||
:negative-text="t('common.cancel')"
|
||||
@positive-click="handleRenameConfirm"
|
||||
>
|
||||
<NInput
|
||||
ref="renameInputRef"
|
||||
v-model:value="renameValue"
|
||||
:placeholder="t('chat.enterNewTitle')"
|
||||
@keydown.enter="handleRenameConfirm"
|
||||
/>
|
||||
</NModal>
|
||||
|
||||
<NModal
|
||||
v-model:show="showWorkspaceModal"
|
||||
preset="dialog"
|
||||
:title="t('chat.setWorkspaceTitle')"
|
||||
:positive-text="t('common.ok')"
|
||||
:negative-text="t('common.cancel')"
|
||||
style="width: 520px"
|
||||
@positive-click="handleWorkspaceConfirm"
|
||||
>
|
||||
<FolderPicker v-model="workspaceValue" />
|
||||
</NModal>
|
||||
|
||||
<div class="chat-main">
|
||||
<header class="chat-header">
|
||||
<div class="header-left">
|
||||
|
||||
@@ -7,7 +7,7 @@ import { safeFileStore } from '../../services/safe-file-store'
|
||||
|
||||
const PLATFORM_SECTIONS = new Set([
|
||||
'telegram', 'discord', 'slack', 'whatsapp', 'matrix',
|
||||
'weixin', 'wecom', 'feishu', 'dingtalk',
|
||||
'weixin', 'wecom', 'feishu', 'dingtalk', 'qqbot',
|
||||
'approvals',
|
||||
])
|
||||
|
||||
@@ -25,6 +25,12 @@ const envPlatformMap: Record<string, [string, string]> = {
|
||||
DINGTALK_CLIENT_ID: ['dingtalk', 'extra.client_id'],
|
||||
DINGTALK_CLIENT_SECRET: ['dingtalk', 'extra.client_secret'],
|
||||
DINGTALK_APP_KEY: ['dingtalk', 'extra.app_key'],
|
||||
DINGTALK_ALLOWED_USERS: ['dingtalk', 'allowed_users'],
|
||||
DINGTALK_ALLOW_ALL_USERS: ['dingtalk', 'allow_all_users'],
|
||||
QQ_APP_ID: ['qqbot', 'extra.app_id'],
|
||||
QQ_CLIENT_SECRET: ['qqbot', 'extra.client_secret'],
|
||||
QQ_ALLOWED_USERS: ['qqbot', 'allowed_users'],
|
||||
QQ_ALLOW_ALL_USERS: ['qqbot', 'allow_all_users'],
|
||||
WECOM_BOT_ID: ['wecom', 'extra.bot_id'],
|
||||
WECOM_SECRET: ['wecom', 'extra.secret'],
|
||||
WEIXIN_TOKEN: ['weixin', 'token'],
|
||||
@@ -82,7 +88,7 @@ async function readEnvPlatforms(): Promise<Record<string, any>> {
|
||||
if (val === undefined || val === '') continue
|
||||
if (!platforms[platform]) platforms[platform] = {}
|
||||
let finalVal: any = val
|
||||
if (cfgPath === 'enabled') finalVal = val === 'true'
|
||||
if (cfgPath === 'enabled' || cfgPath === 'allow_all_users') finalVal = val === 'true'
|
||||
setNested(platforms[platform], cfgPath, finalVal)
|
||||
}
|
||||
return platforms
|
||||
@@ -100,7 +106,7 @@ export async function getConfig(ctx: any) {
|
||||
if (Object.keys(envPlatforms).length > 0) {
|
||||
const existing = config.platforms || {}
|
||||
for (const [platform, vals] of Object.entries(envPlatforms)) {
|
||||
existing[platform] = { ...(existing[platform] || {}), ...(vals as Record<string, any>) }
|
||||
existing[platform] = deepMerge(existing[platform] || {}, vals as Record<string, any>)
|
||||
}
|
||||
config.platforms = existing
|
||||
}
|
||||
|
||||
@@ -105,4 +105,40 @@ describe('config controller locked file updates', () => {
|
||||
expect(config.platforms.weixin.extra.base_url).toBe('https://old.example')
|
||||
expect(config.model.default).toBe('glm-5.1')
|
||||
})
|
||||
|
||||
it('writes QQBot credentials to env and overlays them into platform config reads', async () => {
|
||||
await writeFile(join(hermesHome, 'config.yaml'), [
|
||||
'platforms:',
|
||||
' qqbot:',
|
||||
' extra:',
|
||||
' markdown_support: true',
|
||||
'',
|
||||
].join('\n'), 'utf-8')
|
||||
await writeFile(join(hermesHome, '.env'), 'OPENROUTER_API_KEY=keep\n', 'utf-8')
|
||||
const { updateCredentials, getConfig } = await loadController()
|
||||
|
||||
await updateCredentials(makeCtx({
|
||||
platform: 'qqbot',
|
||||
values: {
|
||||
extra: { app_id: 'qq-app', client_secret: 'qq-secret' },
|
||||
allowed_users: 'user-1,user-2',
|
||||
allow_all_users: false,
|
||||
},
|
||||
}))
|
||||
|
||||
const env = await readFile(join(hermesHome, '.env'), 'utf-8')
|
||||
expect(env).toContain('OPENROUTER_API_KEY=keep')
|
||||
expect(env).toContain('QQ_APP_ID=qq-app')
|
||||
expect(env).toContain('QQ_CLIENT_SECRET=qq-secret')
|
||||
expect(env).toContain('QQ_ALLOWED_USERS=user-1,user-2')
|
||||
expect(env).toContain('QQ_ALLOW_ALL_USERS=false')
|
||||
|
||||
const ctx = makeCtx({})
|
||||
await getConfig(ctx)
|
||||
expect(ctx.body.platforms.qqbot.extra.app_id).toBe('qq-app')
|
||||
expect(ctx.body.platforms.qqbot.extra.client_secret).toBe('qq-secret')
|
||||
expect(ctx.body.platforms.qqbot.extra.markdown_support).toBe(true)
|
||||
expect(ctx.body.platforms.qqbot.allowed_users).toBe('user-1,user-2')
|
||||
expect(ctx.body.platforms.qqbot.allow_all_users).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user