- 全局样式类(df-sec-h/df-toast族)替换散装实现,主题token去硬编码兜底 - 拆分子组件:ai-chat/queue-panel(队列时钟与编辑随迁)、 conversation-sidebar/ConversationList、settings/provider-panel - 父组件死副本清理(队列编辑/时钟/键位表,TS6133清零)并补 fill-draft父侧转发(重构半成品断链修复) - 验收:vue-tsc EXIT 0
162 lines
6.3 KiB
Vue
162 lines
6.3 KiB
Vue
<template>
|
|
<!-- ═══ 飞书集成(桌面端内置飞书渠道:WS 直连飞书开放平台,凭证在此配置) ═══ -->
|
|
<section class="panel">
|
|
<div class="panel-header">
|
|
<h2>{{ $t('settings.panelFeishu') }}</h2>
|
|
</div>
|
|
<div class="feishu-settings">
|
|
<SettingRow :label="$t('settings.labelFeishuEnabled')" :desc="$t('settings.descFeishuEnabled')">
|
|
<label class="toggle">
|
|
<input type="checkbox" v-model="form.enabled" />
|
|
<span class="toggle-slider"></span>
|
|
</label>
|
|
</SettingRow>
|
|
|
|
<SettingRow :label="$t('settings.labelFeishuAppId')" :desc="$t('settings.descFeishuAppId')">
|
|
<input
|
|
v-model="form.app_id"
|
|
type="text"
|
|
class="setting-input feishu-input"
|
|
:placeholder="$t('settings.phFeishuAppId')"
|
|
autocomplete="off"
|
|
/>
|
|
</SettingRow>
|
|
|
|
<SettingRow :label="$t('settings.labelFeishuAppSecret')" :desc="$t('settings.descFeishuAppSecret')">
|
|
<input
|
|
v-model="form.app_secret"
|
|
:type="secretVisible ? 'text' : 'password'"
|
|
class="setting-input feishu-input"
|
|
autocomplete="new-password"
|
|
/>
|
|
<!-- 掩码显隐复用 Provider 密钥的「显示/隐藏」文案,语义一致不另造键 -->
|
|
<button class="btn btn-ghost btn-sm" type="button" @click="secretVisible = !secretVisible">
|
|
{{ secretVisible ? $t('settings.apiKeyHide') : $t('settings.apiKeyShow') }}
|
|
</button>
|
|
</SettingRow>
|
|
|
|
<SettingRow :label="$t('settings.labelFeishuWhitelist')" :desc="$t('settings.descFeishuWhitelist')">
|
|
<textarea
|
|
v-model="form.whitelist"
|
|
class="setting-input feishu-whitelist"
|
|
:placeholder="$t('settings.phFeishuWhitelist')"
|
|
></textarea>
|
|
</SettingRow>
|
|
|
|
<!-- 白名单配置引导(2026-10-08):用户不知 open_id 从哪来,给出自助获取路径 -->
|
|
<p class="feishu-hint">{{ $t('settings.hintFeishuWhitelist') }}</p>
|
|
|
|
<div class="actions">
|
|
<button class="btn btn-primary btn-sm" :disabled="saving" @click="save">
|
|
{{ $t('settings.feishuSave') }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { reactive, ref, onMounted } from 'vue'
|
|
import { useI18n } from 'vue-i18n'
|
|
import { useAppSettingsStore } from '@/stores/appSettings'
|
|
import SettingRow from './SettingRow.vue'
|
|
|
|
// ============================================================
|
|
// 飞书集成配置 — 桌面端内置飞书渠道(WS 直连飞书开放平台)的凭证表单
|
|
// ------------------------------------------------------------
|
|
// 持久化走 appSettings KV('feishu-config',store 层把对象 JSON.stringify 成
|
|
// JSON 字符串落 app_settings 表,与后端 'df-knowledge-config' 同一存法)。
|
|
// 四字段作为一份整体「保存」提交而非逐字段即时落库:WS 连接逻辑只认完整凭证,
|
|
// 半份配置(有 id 无 secret)落库会被连接方读到,故 enabled=true 时 id/secret
|
|
// 必填的校验拦在写库之前,校验不过不写。
|
|
// ============================================================
|
|
|
|
/** app_settings KV 键(与后端渠道侧约定一致,勿单方改动) */
|
|
const FEISHU_CONFIG_KEY = 'feishu-config'
|
|
|
|
/** 飞书凭证形态(与后端字段契约对齐,snake_case 直存不转驼峰) */
|
|
interface FeishuConfig {
|
|
enabled: boolean
|
|
app_id: string
|
|
app_secret: string
|
|
/** 逗号分隔 open_id,空 = 放行所有(表单态;落库时拆为数组) */
|
|
whitelist: string
|
|
}
|
|
|
|
// 接共享 toast 通道(Settings.vue showToast),与 AllowedDirsPanel 同范式
|
|
const emit = defineEmits<{ (e: 'toast', msg: string, type: 'error' | 'warning' | 'info' | 'success'): void }>()
|
|
|
|
const { t } = useI18n()
|
|
const appSettings = useAppSettingsStore()
|
|
|
|
const form = reactive<FeishuConfig>({ enabled: false, app_id: '', app_secret: '', whitelist: '' })
|
|
const secretVisible = ref(false)
|
|
const saving = ref(false)
|
|
|
|
onMounted(() => {
|
|
// KV 值由 store 启动 loadAll 统一 JSON.parse 进缓存,此处同步读;
|
|
// 逐字段回退默认,防历史脏数据缺字段时 v-model 绑到 undefined
|
|
const saved = appSettings.get<Record<string, unknown>>(FEISHU_CONFIG_KEY, {})
|
|
form.enabled = saved.enabled === true
|
|
form.app_id = typeof saved.app_id === 'string' ? saved.app_id : ''
|
|
form.app_secret = typeof saved.app_secret === 'string' ? saved.app_secret : ''
|
|
// 白名单落库形态为数组(后端契约 Vec<String>);兼容历史字符串脏数据,
|
|
// 统一转逗号分隔字符串回填表单
|
|
form.whitelist = Array.isArray(saved.whitelist)
|
|
? (saved.whitelist as unknown[]).filter((x): x is string => typeof x === 'string').join(',')
|
|
: typeof saved.whitelist === 'string'
|
|
? saved.whitelist
|
|
: ''
|
|
})
|
|
|
|
/** 保存:enabled=true 时 id/secret 必填,不过校验不落库;整体替换写单一 KV 键 */
|
|
async function save() {
|
|
if (form.enabled && (!form.app_id.trim() || !form.app_secret.trim())) {
|
|
emit('toast', t('settings.feishuValidateIncomplete'), 'warning')
|
|
return
|
|
}
|
|
// 白名单落库为数组(后端 FeishuConfig.whitelist: Vec<String> 契约):
|
|
// 表单逗号分隔字符串拆分为去空数组,空串 → 空数组(=放行所有)
|
|
const whitelist = form.whitelist
|
|
.split(',')
|
|
.map((s) => s.trim())
|
|
.filter((s) => s.length > 0)
|
|
saving.value = true
|
|
try {
|
|
await appSettings.set(FEISHU_CONFIG_KEY, {
|
|
enabled: form.enabled,
|
|
app_id: form.app_id.trim(),
|
|
app_secret: form.app_secret.trim(),
|
|
whitelist,
|
|
})
|
|
emit('toast', t('settings.feishuSaveOk'), 'success')
|
|
} catch (e) {
|
|
console.error('[FEISHU-CONFIG] 保存失败:', e)
|
|
emit('toast', t('settings.toastSaveFail', { msg: String(e) }), 'error')
|
|
} finally {
|
|
saving.value = false
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
/* panel/panel-header/btn 系列/setting-input 均在 settings.css 全局,此处仅本面板特有样式 */
|
|
.feishu-settings { display: flex; flex-direction: column; gap: 4px; }
|
|
/* 定宽输入补 max-width,窄窗收缩防横向溢出 */
|
|
.feishu-input { width: 280px; max-width: 100%; }
|
|
.feishu-whitelist {
|
|
width: 320px;
|
|
max-width: 100%;
|
|
min-height: 64px;
|
|
resize: vertical;
|
|
font-family: var(--df-font-mono, monospace);
|
|
}
|
|
.feishu-hint {
|
|
margin: 0 0 4px;
|
|
font-size: 12px;
|
|
line-height: 1.5;
|
|
color: var(--df-text-dim);
|
|
}
|
|
.actions { display: flex; gap: 8px; margin-top: 8px; }
|
|
</style>
|