Skip to content

Commit b70dcd2

Browse files
author
linyuan.yang
committed
心跳配置
1 parent dc68835 commit b70dcd2

15 files changed

Lines changed: 591 additions & 79 deletions

File tree

packages/admin/src/i18n/en.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,10 @@ export default {
169169
intent_prompt_placeholder: 'Leave empty to use the built-in default prompt',
170170
merge_window: 'Message Merge Window',
171171
merge_window_hint: 'Consecutive messages in the same session within this time will be merged before processing (ms). 0 or empty to disable',
172+
tools: 'Tool Whitelist',
173+
tools_hint: 'Select tools allowed for daily conversations. Leave empty to allow all',
174+
heartbeat_tools: 'Heartbeat Tool Whitelist',
175+
heartbeat_tools_hint: 'Select tools available during heartbeat. Leave empty to allow all',
172176
stream_verbose: 'Intermediate messages & streaming',
173177
stream_verbose_hint: 'When enabled, output intermediate messages and streaming instead of only the final reply',
174178
use_channel_default: 'Use channel default',
@@ -424,6 +428,10 @@ export default {
424428
cat_wiki: 'Wiki',
425429
cat_skills: 'Skills',
426430
cat_tools: 'Tools',
431+
cat_heartbeat: 'Heartbeat',
432+
create_file: 'New File',
433+
confirm_delete: 'Delete "{name}"?',
434+
user_only: 'User Created',
427435
},
428436
scheduler: {
429437
title: 'Scheduler',
@@ -667,8 +675,6 @@ export default {
667675
promptFile_hint: 'Relative to ~/.sbot/prompts/',
668676
target: 'Execution Target',
669677
target_hint: 'The heartbeat agent runs in this session context',
670-
notifyTargets: 'Notify Targets',
671-
notifyTargets_hint: 'Substantive responses are forwarded to these sessions',
672678
enabled: 'Enabled',
673679
activeHours: 'Active Hours',
674680
activeHours_hint: 'Leave empty to run anytime',
@@ -684,6 +690,9 @@ export default {
684690
confirm_delete: 'Delete heartbeat "{name}"?',
685691
trigger: 'Trigger',
686692
triggered: 'Triggered',
693+
create_prompt: 'New Heartbeat Prompt',
694+
prompt_filename: 'Filename',
695+
prompt_content: 'Content',
687696
},
688697
processes: {
689698
title: 'Process Manager',

packages/admin/src/i18n/zh.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,10 @@ export default {
169169
intent_prompt_placeholder: '留空使用内置默认提示词',
170170
merge_window: '消息合并窗口',
171171
merge_window_hint: '同一会话在此时间内的连续消息会合并后再处理(毫秒),0 或留空表示不合并',
172+
tools: '工具白名单',
173+
tools_hint: '选择日常对话可用的工具,不选则全部可用',
174+
heartbeat_tools: '心跳工具白名单',
175+
heartbeat_tools_hint: '选择心跳可用的工具,不选则全部可用',
172176
stream_verbose: '中间消息和流式输出',
173177
stream_verbose_hint: '启用后输出中间过程消息和流式输出,而非仅发送最终回复',
174178
use_channel_default: '使用渠道默认值',
@@ -424,6 +428,10 @@ export default {
424428
cat_wiki: '知识库',
425429
cat_skills: '技能',
426430
cat_tools: '工具',
431+
cat_heartbeat: '心跳',
432+
create_file: '新建文件',
433+
confirm_delete: '确定删除 "{name}" ?',
434+
user_only: '用户自建',
427435
},
428436
scheduler: {
429437
title: '计时器管理',
@@ -667,8 +675,6 @@ export default {
667675
promptFile_hint: '相对于 ~/.sbot/prompts/ 的路径',
668676
target: '执行目标',
669677
target_hint: '心跳 Agent 将在此 Session 的上下文中运行',
670-
notifyTargets: '通知目标',
671-
notifyTargets_hint: '有意义的响应会额外转发到这些 Session',
672678
enabled: '启用',
673679
activeHours: '活跃时段',
674680
activeHours_hint: '留空表示全天触发',
@@ -684,6 +690,9 @@ export default {
684690
confirm_delete: '确定删除心跳 "{name}" ?',
685691
trigger: '手动触发',
686692
triggered: '已触发',
693+
create_prompt: '新建心跳 Prompt',
694+
prompt_filename: '文件名',
695+
prompt_content: '内容',
687696
},
688697
processes: {
689698
title: '进程管理',

packages/admin/src/views/ChannelsView.vue

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ interface PluginInfo {
1919
label: string
2020
builtin: boolean
2121
configSchema: Record<string, { label: string; type: string; required?: boolean; description?: string; default?: string | boolean | number; options?: Array<{ label: string; value: string }> }>
22+
tools?: { name: string; label: string }[]
2223
}
2324
2425
interface ChannelSessionRow {
@@ -76,6 +77,11 @@ const currentSchema = computed(() => {
7677
return p?.configSchema ?? {}
7778
})
7879
80+
const currentToolOptions = computed(() => {
81+
const p = plugins.value.find(p => p.type === form.value.type)
82+
return (p?.tools ?? []).map(t => ({ id: t.name, label: t.label }))
83+
})
84+
7985
const channels = computed(() => store.settings.channels || {})
8086
const agentOptions = computed(() => Object.entries(store.settings.agents || {}).map(([id, a]) => ({ id, label: (a as any).name || id, type: (a as any).type || '' })))
8187
const saverOptions = computed(() => Object.entries(store.settings.savers || {}).map(([id, s]) => ({ id, label: (s as any).name || id })))
@@ -166,6 +172,9 @@ const form = ref<ChannelConfig>({
166172
intentModel: '', intentPrompt: '', intentThreshold: 0.7,
167173
mergeWindow: 0,
168174
})
175+
const formTools = ref<string[]>([])
176+
const formHeartbeatTools = ref<string[]>([])
177+
169178
170179
async function loadChannelData(id: string) {
171180
channelLoading.value[id] = true
@@ -304,6 +313,8 @@ function openAdd() {
304313
clearActionState()
305314
const defaultType = plugins.value.find(p => !p.builtin)?.type || ''
306315
form.value = { name: '', type: defaultType, config: {}, agent: '', saver: '', memories: [], wikis: [], workPath: '', streamVerbose: false, autoApproveAllTools: false, intentModel: '', intentPrompt: '', intentThreshold: 0.7, mergeWindow: 0 }
316+
formTools.value = []
317+
formHeartbeatTools.value = []
307318
showModal.value = true
308319
}
309320
@@ -312,6 +323,8 @@ function openEdit(id: string) {
312323
editingId.value = id
313324
clearActionState()
314325
form.value = { name: c.name, type: c.type, config: { ...c.config }, agent: c.agent, saver: c.saver, memories: c.memories || [], wikis: (c as any).wikis || [], workPath: c.workPath || '', streamVerbose: !!c.streamVerbose, autoApproveAllTools: !!c.autoApproveAllTools, intentModel: c.intentModel || '', intentPrompt: c.intentPrompt || '', intentThreshold: c.intentThreshold ?? 0.7, mergeWindow: c.mergeWindow || 0 }
326+
formTools.value = [...(c.tools ?? [])]
327+
formHeartbeatTools.value = [...(c.heartbeatTools ?? [])]
315328
showModal.value = true
316329
}
317330
@@ -347,6 +360,8 @@ async function save() {
347360
intentPrompt: form.value.intentPrompt?.trim() || undefined,
348361
intentThreshold: form.value.intentModel ? form.value.intentThreshold : undefined,
349362
mergeWindow: form.value.mergeWindow || undefined,
363+
tools: formTools.value.length ? formTools.value : undefined,
364+
heartbeatTools: formHeartbeatTools.value.length ? formHeartbeatTools.value : undefined,
350365
}
351366
352367
if (editingId.value) {
@@ -435,6 +450,8 @@ async function refresh() {
435450
<span class="session-meta-chip" :class="c.streamVerbose ? 'green' : 'muted'">{{ t('channels.stream_verbose') }}: {{ c.streamVerbose ? t('common.enabled') : t('common.disabled') }}</span>
436451
<span class="session-meta-chip" :class="c.autoApproveAllTools ? 'orange' : 'muted'">{{ t('settings.auto_approve_all') }}: {{ c.autoApproveAllTools ? t('common.enabled') : t('common.disabled') }}</span>
437452
<span class="session-meta-chip" :class="c.intentModel ? '' : 'muted'">{{ t('channels.intent_model') }}: {{ c.intentModel ? (modelOptions.find(m => m.id === c.intentModel)?.label || c.intentModel) : t('common.not_configured') }}</span>
453+
<span v-if="c.tools?.length" class="session-meta-chip">{{ t('channels.tools') }}: {{ c.tools.map(n => plugins.find(p => p.type === c.type)?.tools?.find(t => t.name === n)?.label || n).join(', ') }}</span>
454+
<span v-if="c.heartbeatTools?.length" class="session-meta-chip">{{ t('channels.heartbeat_tools') }}: {{ c.heartbeatTools.map(n => plugins.find(p => p.type === c.type)?.tools?.find(t => t.name === n)?.label || n).join(', ') }}</span>
438455
</div>
439456
<!-- Expanded content -->
440457
<div v-if="expandedChannels[id as string]" class="channel-card-detail">
@@ -716,6 +733,18 @@ async function refresh() {
716733
<input v-model.number="form.mergeWindow" type="number" min="0" step="100" placeholder="0" />
717734
<span style="font-size:11px;color:#888">{{ t('channels.merge_window_hint') }}</span>
718735
</div>
736+
<template v-if="currentToolOptions.length > 0">
737+
<div class="form-group">
738+
<label>{{ t('channels.tools') }}</label>
739+
<MultiSelect v-model="formTools" :options="currentToolOptions" />
740+
<span style="font-size:11px;color:#888">{{ t('channels.tools_hint') }}</span>
741+
</div>
742+
<div class="form-group">
743+
<label>{{ t('channels.heartbeat_tools') }}</label>
744+
<MultiSelect v-model="formHeartbeatTools" :options="currentToolOptions" />
745+
<span style="font-size:11px;color:#888">{{ t('channels.heartbeat_tools_hint') }}</span>
746+
</div>
747+
</template>
719748
</div>
720749
<div class="drawer-footer">
721750
<button class="btn-outline" @click="showModal = false">{{ t('common.cancel') }}</button>

packages/admin/src/views/HeartbeatView.vue

Lines changed: 70 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ const form = ref({
3636
expr: '30m',
3737
promptFile: 'heartbeat/default.md',
3838
target: null as number | null,
39-
notifyTargets: [] as number[],
4039
enabled: true,
4140
activeHoursEnabled: false,
4241
activeHoursStart: 9,
@@ -74,7 +73,6 @@ function openAdd() {
7473
expr: '30m',
7574
promptFile: 'heartbeat/default.md',
7675
target: null,
77-
notifyTargets: [],
7876
enabled: true,
7977
activeHoursEnabled: false,
8078
activeHoursStart: 9,
@@ -93,7 +91,6 @@ function openEdit(id: string) {
9391
expr: hb.expr || '30m',
9492
promptFile: hb.promptFile || 'heartbeat/default.md',
9593
target: hb.target ?? null,
96-
notifyTargets: hb.notifyTargets ?? [],
9794
enabled: hb.enabled !== false,
9895
activeHoursEnabled: !!hb.activeHours,
9996
activeHoursStart: hb.activeHours?.start ?? 9,
@@ -113,9 +110,6 @@ function buildBody() {
113110
enabled: form.value.enabled,
114111
pruneIdle: form.value.pruneIdle,
115112
}
116-
if (form.value.notifyTargets.length > 0) {
117-
body.notifyTargets = form.value.notifyTargets
118-
}
119113
if (form.value.activeHoursEnabled) {
120114
body.activeHours = {
121115
start: form.value.activeHoursStart,
@@ -180,6 +174,40 @@ async function loadSessions() {
180174
} catch {}
181175
}
182176
177+
const heartbeatPrompts = ref<{ path: string; isUserOnly?: boolean }[]>([])
178+
179+
async function loadHeartbeatPrompts() {
180+
try {
181+
const res = await apiFetch('/api/prompts/files?prefix=heartbeat')
182+
heartbeatPrompts.value = res.data || []
183+
} catch {}
184+
}
185+
186+
const showCreatePrompt = ref(false)
187+
const newPromptName = ref('')
188+
const newPromptContent = ref('')
189+
190+
function openCreatePrompt() {
191+
newPromptName.value = ''
192+
newPromptContent.value = ''
193+
showCreatePrompt.value = true
194+
}
195+
196+
async function createPrompt() {
197+
const name = newPromptName.value.trim()
198+
if (!name) { show(t('common.name_required'), 'error'); return }
199+
const filePath = `heartbeat/${name}${name.endsWith('.md') ? '' : '.md'}`
200+
try {
201+
await apiFetch('/api/prompts/content', 'PUT', { path: filePath, content: newPromptContent.value })
202+
show(t('common.created'))
203+
showCreatePrompt.value = false
204+
await loadHeartbeatPrompts()
205+
form.value.promptFile = filePath
206+
} catch (e: any) {
207+
show(e.message, 'error')
208+
}
209+
}
210+
183211
async function refresh() {
184212
try {
185213
const res = await apiFetch('/api/settings')
@@ -191,7 +219,7 @@ async function refresh() {
191219
}
192220
193221
onMounted(async () => {
194-
await Promise.all([loadStatus(), loadSessions()])
222+
await Promise.all([loadStatus(), loadSessions(), loadHeartbeatPrompts()])
195223
})
196224
</script>
197225

@@ -298,7 +326,14 @@ onMounted(async () => {
298326
</div>
299327
<div class="form-group">
300328
<label>{{ t('heartbeats.promptFile') }}</label>
301-
<input v-model="form.promptFile" placeholder="heartbeat/default.md" />
329+
<div style="display:flex;gap:6px;align-items:center">
330+
<select v-model="form.promptFile" style="flex:1">
331+
<option v-for="p in heartbeatPrompts" :key="p.path" :value="p.path">
332+
{{ p.path }}
333+
</option>
334+
</select>
335+
<button type="button" class="btn-outline btn-sm" @click="openCreatePrompt" title="+">+</button>
336+
</div>
302337
<div class="hint">{{ t('heartbeats.promptFile_hint') }}</div>
303338
</div>
304339
<div class="form-group">
@@ -311,15 +346,6 @@ onMounted(async () => {
311346
</select>
312347
<div class="hint">{{ t('heartbeats.target_hint') }}</div>
313348
</div>
314-
<div class="form-group">
315-
<label>{{ t('heartbeats.notifyTargets') }}</label>
316-
<select v-model="form.notifyTargets" multiple style="min-height:60px">
317-
<option v-for="s in channelSessions" :key="s.id" :value="s.id">
318-
{{ s.sessionName || `${s.channelId} / ${s.sessionId}` }} (#{{ s.id }})
319-
</option>
320-
</select>
321-
<div class="hint">{{ t('heartbeats.notifyTargets_hint') }}</div>
322-
</div>
323349
<div class="form-group">
324350
<label class="checkbox-label">
325351
<input type="checkbox" v-model="form.enabled" />
@@ -363,5 +389,32 @@ onMounted(async () => {
363389
</div>
364390
</div>
365391
</div>
392+
393+
<!-- Quick create prompt sub-dialog -->
394+
<div v-if="showCreatePrompt" class="modal-overlay" style="z-index:1001" @click.self="showCreatePrompt = false">
395+
<div class="modal-box" style="width:440px">
396+
<div class="modal-header">
397+
<h3>{{ t('heartbeats.create_prompt') }}</h3>
398+
<button class="modal-close" @click="showCreatePrompt = false">&times;</button>
399+
</div>
400+
<div class="modal-body">
401+
<div class="form-group">
402+
<label>{{ t('heartbeats.prompt_filename') }}</label>
403+
<div style="display:flex;align-items:center;gap:4px">
404+
<span style="color:#9b9b9b;font-size:13px;flex-shrink:0">heartbeat/</span>
405+
<input v-model="newPromptName" placeholder="my-prompt.md" style="flex:1" @keyup.enter="createPrompt" />
406+
</div>
407+
</div>
408+
<div class="form-group">
409+
<label>{{ t('heartbeats.prompt_content') }}</label>
410+
<textarea v-model="newPromptContent" rows="8" style="font-family:'Consolas','Monaco',monospace;font-size:13px" />
411+
</div>
412+
</div>
413+
<div class="modal-footer">
414+
<button class="btn-outline" @click="showCreatePrompt = false">{{ t('common.cancel') }}</button>
415+
<button class="btn-primary" @click="createPrompt">{{ t('common.create') }}</button>
416+
</div>
417+
</div>
418+
</div>
366419
</div>
367420
</template>

0 commit comments

Comments
 (0)