|
| 1 | +<script setup lang="ts"> |
| 2 | +import { ref } from 'vue' |
| 3 | +import { useI18n } from 'vue-i18n' |
| 4 | +import { SInput, STextarea, SSelect, SFormItem, SMultiSelect } from 'sbot-ui' |
| 5 | +import { ApprovalTimeoutValue } from 'sbot.commons' |
| 6 | +
|
| 7 | +/** |
| 8 | + * profile 上的可覆盖配置字段。null = 跟随 ChannelConfig 默认值。 |
| 9 | + * memories/wikis 使用数组形式(提交时父组件负责 JSON.stringify)。 |
| 10 | + */ |
| 11 | +export interface SessionOverrides { |
| 12 | + agentId: string | null |
| 13 | + saver: string | null |
| 14 | + memories: string[] | null |
| 15 | + wikis: string[] | null |
| 16 | + useChannelMemories: boolean | null |
| 17 | + useChannelWikis: boolean | null |
| 18 | + workPath: string | null |
| 19 | + streamVerbose: boolean | null |
| 20 | + autoApproveAllTools: boolean | null |
| 21 | + approvalTimeout: number | null |
| 22 | + approvalTimeoutValue: ApprovalTimeoutValue | null |
| 23 | + askTimeout: number | null |
| 24 | + askTimeoutMessage: string | null |
| 25 | + intentModel: string | null |
| 26 | + intentPrompt: string | null |
| 27 | + intentThreshold: number | null |
| 28 | +} |
| 29 | +
|
| 30 | +export type ConfigSource = 'session' | 'profile' | 'channel' | 'none' |
| 31 | +
|
| 32 | +interface Option { id: string; label: string; type?: string } |
| 33 | +
|
| 34 | +const props = defineProps<{ |
| 35 | + modelValue: SessionOverrides |
| 36 | + /** 来自后端 effective-config 的字段来源 */ |
| 37 | + sources?: Partial<Record<keyof SessionOverrides, ConfigSource>> |
| 38 | + resolved?: Partial<Record<keyof SessionOverrides, any>> |
| 39 | + agentOptions: Option[] |
| 40 | + saverOptions: Option[] |
| 41 | + memoryOptions: Option[] |
| 42 | + wikiOptions: Option[] |
| 43 | + modelOptions: Option[] |
| 44 | +}>() |
| 45 | +
|
| 46 | +const emit = defineEmits<{ |
| 47 | + (e: 'update:modelValue', v: SessionOverrides): void |
| 48 | +}>() |
| 49 | +
|
| 50 | +const { t } = useI18n() |
| 51 | +const showAdvanced = ref(false) |
| 52 | +
|
| 53 | +function update<K extends keyof SessionOverrides>(key: K, val: SessionOverrides[K]) { |
| 54 | + emit('update:modelValue', { ...props.modelValue, [key]: val }) |
| 55 | +} |
| 56 | +
|
| 57 | +type Formatter = (v: any) => string |
| 58 | +function inheritLabel<K extends keyof SessionOverrides>(field: K, formatter?: Formatter): string { |
| 59 | + const src = props.sources?.[field] |
| 60 | + if (!src || src === 'none' || src === 'session') return '' |
| 61 | + const v = props.resolved?.[field] |
| 62 | + if (v == null || v === '') return t('channels.inherit_label_unset', { source: sourceText(src) }) |
| 63 | + const display = formatter ? formatter(v) : String(v) |
| 64 | + return t('channels.inherit_label', { source: sourceText(src), value: display }) |
| 65 | +} |
| 66 | +
|
| 67 | +function sourceText(src: ConfigSource): string { |
| 68 | + switch (src) { |
| 69 | + case 'profile': return t('channels.source_profile') |
| 70 | + case 'channel': return t('channels.source_channel') |
| 71 | + default: return '' |
| 72 | + } |
| 73 | +} |
| 74 | +
|
| 75 | +const fmtAgent = (v: any) => props.agentOptions.find(a => a.id === v)?.label || String(v ?? '') |
| 76 | +const fmtSaver = (v: any) => props.saverOptions.find(s => s.id === v)?.label || String(v ?? '') |
| 77 | +const fmtModel = (v: any) => props.modelOptions.find(m => m.id === v)?.label || String(v ?? '') |
| 78 | +const fmtBool = (v: any) => v ? t('common.enabled') : t('common.disabled') |
| 79 | +const fmtList = (v: any) => Array.isArray(v) ? `${v.length} ${t('channels.items_count_suffix')}` : String(v ?? '') |
| 80 | +const fmtApprovalValue = (v: any) => v === ApprovalTimeoutValue.Allow ? t('channels.approval_timeout_value_allow') : v === ApprovalTimeoutValue.Deny ? t('channels.approval_timeout_value_deny') : String(v ?? '') |
| 81 | +</script> |
| 82 | + |
| 83 | +<template> |
| 84 | + <div class="overrides-editor"> |
| 85 | + <h4 class="form-section-title">{{ t('channels.section_common') }}</h4> |
| 86 | + |
| 87 | + <SFormItem :label="t('common.agent')" :hint="inheritLabel('agentId', fmtAgent)"> |
| 88 | + <SSelect :model-value="modelValue.agentId ?? ''" @update:model-value="v => update('agentId', v === '' ? null : String(v))"> |
| 89 | + <option value="">{{ t('channels.use_channel_default') }}</option> |
| 90 | + <option v-for="a in agentOptions" :key="a.id" :value="a.id">{{ a.label }}{{ a.type ? ` (${a.type})` : '' }}</option> |
| 91 | + </SSelect> |
| 92 | + </SFormItem> |
| 93 | + |
| 94 | + <SFormItem :label="t('common.storage')" :hint="inheritLabel('saver', fmtSaver)"> |
| 95 | + <SSelect :model-value="modelValue.saver ?? ''" @update:model-value="v => update('saver', v === '' ? null : String(v))"> |
| 96 | + <option value="">{{ t('channels.use_channel_default') }}</option> |
| 97 | + <option v-for="s in saverOptions" :key="s.id" :value="s.id">{{ s.label }}</option> |
| 98 | + </SSelect> |
| 99 | + </SFormItem> |
| 100 | + |
| 101 | + <SFormItem :label="t('common.memory')" :hint="inheritLabel('memories', fmtList)"> |
| 102 | + <SMultiSelect :model-value="modelValue.memories ?? []" :options="memoryOptions" @update:model-value="v => update('memories', v as string[])" /> |
| 103 | + <SFormItem :label="t('channels.use_channel_memories')" :hint="t('channels.use_channel_memories_hint')" class="nested-form-item"> |
| 104 | + <SSelect :model-value="modelValue.useChannelMemories === null ? '' : String(modelValue.useChannelMemories)" @update:model-value="v => update('useChannelMemories', v === '' ? null : v === 'true')"> |
| 105 | + <option value="">{{ t('channels.use_channel_default') }}</option> |
| 106 | + <option value="true">{{ t('common.enabled') }}</option> |
| 107 | + <option value="false">{{ t('common.disabled') }}</option> |
| 108 | + </SSelect> |
| 109 | + </SFormItem> |
| 110 | + </SFormItem> |
| 111 | + |
| 112 | + <SFormItem :label="t('common.wiki')" :hint="inheritLabel('wikis', fmtList)"> |
| 113 | + <SMultiSelect :model-value="modelValue.wikis ?? []" :options="wikiOptions" @update:model-value="v => update('wikis', v as string[])" /> |
| 114 | + <SFormItem :label="t('channels.use_channel_wikis')" :hint="t('channels.use_channel_wikis_hint')" class="nested-form-item"> |
| 115 | + <SSelect :model-value="modelValue.useChannelWikis === null ? '' : String(modelValue.useChannelWikis)" @update:model-value="v => update('useChannelWikis', v === '' ? null : v === 'true')"> |
| 116 | + <option value="">{{ t('channels.use_channel_default') }}</option> |
| 117 | + <option value="true">{{ t('common.enabled') }}</option> |
| 118 | + <option value="false">{{ t('common.disabled') }}</option> |
| 119 | + </SSelect> |
| 120 | + </SFormItem> |
| 121 | + </SFormItem> |
| 122 | + |
| 123 | + <SFormItem :label="t('directory.path_label')" :hint="inheritLabel('workPath')"> |
| 124 | + <SInput :model-value="modelValue.workPath ?? ''" type="text" @update:model-value="v => update('workPath', String(v).trim() ? String(v) : null)" /> |
| 125 | + </SFormItem> |
| 126 | + |
| 127 | + <button class="advanced-toggle" type="button" @click="showAdvanced = !showAdvanced"> |
| 128 | + {{ showAdvanced ? t('channels.section_advanced_hide') : t('channels.section_advanced_show', { n: 7 }) }} |
| 129 | + </button> |
| 130 | + |
| 131 | + <template v-if="showAdvanced"> |
| 132 | + <SFormItem :label="t('channels.stream_verbose')" :hint="inheritLabel('streamVerbose', fmtBool) || t('channels.stream_verbose_hint')"> |
| 133 | + <SSelect :model-value="modelValue.streamVerbose === null ? '' : String(modelValue.streamVerbose)" @update:model-value="v => update('streamVerbose', v === '' ? null : v === 'true')"> |
| 134 | + <option value="">{{ t('channels.use_channel_default') }}</option> |
| 135 | + <option value="true">{{ t('common.enabled') }}</option> |
| 136 | + <option value="false">{{ t('common.disabled') }}</option> |
| 137 | + </SSelect> |
| 138 | + </SFormItem> |
| 139 | + <SFormItem :label="t('settings.auto_approve_all')" :hint="inheritLabel('autoApproveAllTools', fmtBool) || t('settings.auto_approve_all_hint')"> |
| 140 | + <SSelect :model-value="modelValue.autoApproveAllTools === null ? '' : String(modelValue.autoApproveAllTools)" @update:model-value="v => update('autoApproveAllTools', v === '' ? null : v === 'true')"> |
| 141 | + <option value="">{{ t('channels.use_channel_default') }}</option> |
| 142 | + <option value="true">{{ t('common.enabled') }}</option> |
| 143 | + <option value="false">{{ t('common.disabled') }}</option> |
| 144 | + </SSelect> |
| 145 | + </SFormItem> |
| 146 | + <SFormItem :label="t('channels.approval_timeout')" :hint="inheritLabel('approvalTimeout') || t('channels.approval_timeout_hint')"> |
| 147 | + <SInput :model-value="modelValue.approvalTimeout ?? ''" type="number" placeholder="0" @update:model-value="v => update('approvalTimeout', (v === '' || v === null || Number(v) <= 0) ? null : Number(v))" /> |
| 148 | + </SFormItem> |
| 149 | + <SFormItem v-if="modelValue.approvalTimeout != null && modelValue.approvalTimeout > 0" :label="t('channels.approval_timeout_value')" :hint="inheritLabel('approvalTimeoutValue', fmtApprovalValue)"> |
| 150 | + <SSelect :model-value="modelValue.approvalTimeoutValue ?? ''" @update:model-value="v => update('approvalTimeoutValue', v === '' ? null : v as ApprovalTimeoutValue)"> |
| 151 | + <option value="">{{ t('channels.use_channel_default') }}</option> |
| 152 | + <option :value="ApprovalTimeoutValue.Deny">{{ t('channels.approval_timeout_value_deny') }}</option> |
| 153 | + <option :value="ApprovalTimeoutValue.Allow">{{ t('channels.approval_timeout_value_allow') }}</option> |
| 154 | + </SSelect> |
| 155 | + </SFormItem> |
| 156 | + <SFormItem :label="t('channels.ask_timeout')" :hint="inheritLabel('askTimeout') || t('channels.ask_timeout_hint')"> |
| 157 | + <SInput :model-value="modelValue.askTimeout ?? ''" type="number" placeholder="0" @update:model-value="v => update('askTimeout', (v === '' || v === null || Number(v) <= 0) ? null : Number(v))" /> |
| 158 | + </SFormItem> |
| 159 | + <SFormItem v-if="modelValue.askTimeout != null && modelValue.askTimeout > 0" :label="t('channels.ask_timeout_message')" :hint="inheritLabel('askTimeoutMessage') || t('channels.ask_timeout_message_hint')"> |
| 160 | + <SInput :model-value="modelValue.askTimeoutMessage ?? ''" type="text" @update:model-value="v => update('askTimeoutMessage', String(v).trim() ? String(v) : null)" /> |
| 161 | + </SFormItem> |
| 162 | + <SFormItem :label="t('channels.intent_model')" :hint="inheritLabel('intentModel', fmtModel) || t('channels.intent_model_hint')"> |
| 163 | + <SSelect :model-value="modelValue.intentModel ?? '__default__'" @update:model-value="v => update('intentModel', v === '__default__' ? null : String(v))"> |
| 164 | + <option value="__default__">{{ t('channels.use_channel_default') }}</option> |
| 165 | + <option value="">{{ t('common.not_use') }}</option> |
| 166 | + <option v-for="m in modelOptions" :key="m.id" :value="m.id">{{ m.label }}</option> |
| 167 | + </SSelect> |
| 168 | + </SFormItem> |
| 169 | + <template v-if="modelValue.intentModel"> |
| 170 | + <SFormItem :label="t('channels.intent_threshold')" :hint="inheritLabel('intentThreshold') || t('channels.intent_threshold_hint')"> |
| 171 | + <SInput :model-value="modelValue.intentThreshold ?? ''" type="number" placeholder="0.7" @update:model-value="v => update('intentThreshold', v === '' || v === null ? null : Number(v))" /> |
| 172 | + </SFormItem> |
| 173 | + <SFormItem :label="t('channels.intent_prompt')" :hint="inheritLabel('intentPrompt')"> |
| 174 | + <STextarea :model-value="modelValue.intentPrompt ?? ''" :rows="4" :placeholder="t('channels.intent_prompt_placeholder')" @update:model-value="v => update('intentPrompt', String(v).trim() ? String(v) : null)" /> |
| 175 | + </SFormItem> |
| 176 | + </template> |
| 177 | + </template> |
| 178 | + </div> |
| 179 | +</template> |
| 180 | + |
| 181 | +<style scoped> |
| 182 | +.form-section-title { |
| 183 | + font-size: var(--sui-fs-sm); |
| 184 | + font-weight: 600; |
| 185 | + color: var(--sui-fg-secondary); |
| 186 | + margin: var(--sui-sp-2) 0 var(--sui-sp-3); |
| 187 | + padding-bottom: var(--sui-sp-2); |
| 188 | + border-bottom: 1px solid var(--sui-border); |
| 189 | + text-transform: uppercase; |
| 190 | + letter-spacing: 0.04em; |
| 191 | +} |
| 192 | +.advanced-toggle { |
| 193 | + display: block; |
| 194 | + width: 100%; |
| 195 | + margin: var(--sui-sp-5) 0 var(--sui-sp-3); |
| 196 | + padding: var(--sui-sp-3) var(--sui-sp-4); |
| 197 | + background: var(--sui-bg-subtle); |
| 198 | + border: 1px dashed var(--sui-border); |
| 199 | + border-radius: var(--sui-radius-md); |
| 200 | + color: var(--sui-fg-secondary); |
| 201 | + font-size: var(--sui-fs-sm); |
| 202 | + cursor: pointer; |
| 203 | + text-align: center; |
| 204 | + transition: background var(--sui-transition-fast); |
| 205 | +} |
| 206 | +.advanced-toggle:hover { background: var(--sui-bg-hover); color: var(--sui-fg); } |
| 207 | +.nested-form-item { margin-top: var(--sui-sp-3); } |
| 208 | +</style> |
0 commit comments