Skip to content

Commit 372180e

Browse files
author
linyuan.yang
committed
创建 sessIon profile
1 parent f904260 commit 372180e

17 files changed

Lines changed: 1377 additions & 327 deletions

File tree

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
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>

packages/admin/src/i18n/en.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ export default {
33
chat: 'Chat',
44

55
channels: 'Channels',
6+
session_profiles: 'Session Profiles',
67
group_basics: 'Basics',
78
settings: 'Settings',
89
group_models: 'Models',
@@ -177,6 +178,36 @@ export default {
177178
section_plugin: 'Plugin Config',
178179
section_resources: 'Resources',
179180
section_advanced: 'Advanced',
181+
section_common: 'Common',
182+
section_advanced_show: 'Show advanced ({n})',
183+
section_advanced_hide: 'Hide advanced',
184+
inherit_label: 'inherits from {source}: {value}',
185+
inherit_label_unset: 'inherits from {source} (unset)',
186+
source_profile: 'Profile',
187+
source_channel: 'channel',
188+
items_count_suffix: 'item(s)',
189+
profile: 'Session Profile',
190+
profile_none: 'Default (independent)',
191+
profile_hint: 'Editing fields below affects all sessions sharing this profile',
192+
profile_clone: 'Create shared Profile',
193+
profile_clone_done: 'Profile created and switched',
194+
profile_detach: 'Switch to independent',
195+
profile_detach_done: 'Switched to independent',
196+
profile_shared_with: 'This profile is also used by {n} other session(s) — edits affect them too',
197+
profile_shared_warn: 'This profile is used by {n} session(s). Editing affects all of them. Save?',
198+
},
199+
session_profiles: {
200+
title: 'Session Profiles',
201+
add: '+ New Profile',
202+
empty: 'No shared profiles yet',
203+
name: 'Name',
204+
confirm_delete: 'Delete profile "{name}"?',
205+
delete_in_use: 'Profile is still used by {n} session(s)',
206+
edit_title: 'Edit Profile — {name}',
207+
add_title: 'New Profile',
208+
name_placeholder: 'a recognizable name',
209+
used_by_n: 'used by {n} session(s)',
210+
used_by_none: 'not in use',
180211
},
181212
settings: {
182213
http_port: 'HTTP Port',

packages/admin/src/i18n/zh.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ export default {
33
chat: '聊天',
44

55
channels: '频道管理',
6+
session_profiles: '会话 Profile',
67
group_basics: '基础',
78
settings: '基本设置',
89
group_models: '模型',
@@ -177,6 +178,36 @@ export default {
177178
section_plugin: '插件配置',
178179
section_resources: '资源配置',
179180
section_advanced: '高级设置',
181+
section_common: '常用',
182+
section_advanced_show: '展开高级({n})',
183+
section_advanced_hide: '收起高级',
184+
inherit_label: '继承自 {source}: {value}',
185+
inherit_label_unset: '继承自 {source}(未设置)',
186+
source_profile: 'Profile',
187+
source_channel: '频道',
188+
items_count_suffix: '项',
189+
profile: '会话 Profile',
190+
profile_none: '默认(独立配置)',
191+
profile_hint: '选中共享 Profile 时编辑下方字段会影响所有共享方',
192+
profile_clone: '创建为共享 Profile',
193+
profile_clone_done: '已创建共享 Profile 并切换',
194+
profile_detach: '切回独立',
195+
profile_detach_done: '已切回独立',
196+
profile_shared_with: '此 Profile 还被另外 {n} 个会话使用,编辑会同步影响它们',
197+
profile_shared_warn: '此 Profile 被 {n} 个会话使用,编辑会同步影响它们,确定保存?',
198+
},
199+
session_profiles: {
200+
title: '会话 Profile',
201+
add: '+ 新建 Profile',
202+
empty: '暂无共享 Profile',
203+
name: '名称',
204+
confirm_delete: '确定要删除 Profile "{name}" 吗?',
205+
delete_in_use: '此 Profile 仍被 {n} 个会话使用',
206+
edit_title: '编辑 Profile — {name}',
207+
add_title: '新建 Profile',
208+
name_placeholder: '便于识别的名字',
209+
used_by_n: '{n} 个会话使用',
210+
used_by_none: '暂未被使用',
180211
},
181212
settings: {
182213
http_port: 'HTTP 端口',

packages/admin/src/layouts/Layout.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ const menuGroups = computed(() => [
6464
items: [
6565
{ label: t('nav.chat'), key: '/chat' },
6666
{ label: t('nav.channels'), key: '/channels' },
67+
{ label: t('nav.session_profiles'), key: '/session-profiles' },
6768
],
6869
},
6970
{

packages/admin/src/router/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const routes = [
55
// 聊天
66
{ path: '/chat', component: () => import('@/views/chat/ChatView.vue') },
77
{ path: '/channels', component: () => import('@/views/chat/ChannelsView.vue') },
8+
{ path: '/session-profiles', component: () => import('@/views/chat/SessionProfilesView.vue') },
89
// 基础
910
{ path: '/settings', component: () => import('@/views/SettingsView.vue') },
1011
// 模型

0 commit comments

Comments
 (0)