Add presence probe configuration to Assistant model and API. Introduce new fields for enabling presence probes, idle and cooldown durations, maximum prompts, context inclusion, and custom questions. Update schemas, routers, and frontend components to support these features, along with corresponding tests to ensure functionality.
This commit is contained in:
@@ -185,6 +185,12 @@ export const AssistantsPage: React.FC = () => {
|
||||
tools: [],
|
||||
botCannotBeInterrupted: false,
|
||||
interruptionSensitivity: 180,
|
||||
presenceProbeEnabled: false,
|
||||
presenceProbeIdleSeconds: 20,
|
||||
presenceProbeCooldownSeconds: 45,
|
||||
presenceProbeMaxPrompts: 2,
|
||||
presenceProbeIncludeContext: true,
|
||||
presenceProbeQuestion: '',
|
||||
configMode: 'platform',
|
||||
};
|
||||
try {
|
||||
@@ -271,6 +277,18 @@ export const AssistantsPage: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const coerceBoundedNumber = (raw: string, fallback: number, min: number, max: number) => {
|
||||
const parsed = Number(raw);
|
||||
if (!Number.isFinite(parsed)) return fallback;
|
||||
return Math.max(min, Math.min(max, parsed));
|
||||
};
|
||||
|
||||
const coerceBoundedInt = (raw: string, fallback: number, min: number, max: number) => {
|
||||
const parsed = Number.parseInt(raw, 10);
|
||||
if (!Number.isFinite(parsed)) return fallback;
|
||||
return Math.max(min, Math.min(max, parsed));
|
||||
};
|
||||
|
||||
const updateTemplateSuggestionState = (
|
||||
field: 'prompt' | 'opener',
|
||||
value: string,
|
||||
@@ -858,6 +876,128 @@ export const AssistantsPage: React.FC = () => {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<label className="text-sm font-medium text-white flex items-center">
|
||||
<Timer className="w-4 h-4 mr-2 text-primary" /> Presence Probe(在线探询)
|
||||
</label>
|
||||
<div className="inline-flex rounded-lg border border-white/10 bg-white/5 p-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => updateAssistant('presenceProbeEnabled', false)}
|
||||
className={`px-3 py-1 text-xs rounded-md transition-colors ${
|
||||
selectedAssistant.presenceProbeEnabled === true
|
||||
? 'text-muted-foreground hover:text-foreground'
|
||||
: 'bg-primary text-primary-foreground shadow-sm'
|
||||
}`}
|
||||
>
|
||||
关闭
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => updateAssistant('presenceProbeEnabled', true)}
|
||||
className={`px-3 py-1 text-xs rounded-md transition-colors ${
|
||||
selectedAssistant.presenceProbeEnabled === true
|
||||
? 'bg-primary text-primary-foreground shadow-sm'
|
||||
: 'text-muted-foreground hover:text-foreground'
|
||||
}`}
|
||||
>
|
||||
开启
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
会话空闲时,assistant 会试探性询问用户是否还在线;TTS 开启时语音+文字,关闭时仅文字。
|
||||
</p>
|
||||
{selectedAssistant.presenceProbeEnabled === true && (
|
||||
<div className="space-y-3 rounded-lg border border-white/10 bg-white/[0.03] p-3">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
|
||||
<label className="space-y-1">
|
||||
<span className="text-[10px] text-muted-foreground uppercase tracking-wider">Idle(s)</span>
|
||||
<Input
|
||||
type="number"
|
||||
min={5}
|
||||
max={3600}
|
||||
step={1}
|
||||
value={selectedAssistant.presenceProbeIdleSeconds ?? 20}
|
||||
onChange={(e) =>
|
||||
updateAssistant(
|
||||
'presenceProbeIdleSeconds',
|
||||
coerceBoundedNumber(
|
||||
e.target.value,
|
||||
Number(selectedAssistant.presenceProbeIdleSeconds ?? 20),
|
||||
5,
|
||||
3600
|
||||
)
|
||||
)
|
||||
}
|
||||
className="h-9 bg-white/5 border-white/10"
|
||||
/>
|
||||
</label>
|
||||
<label className="space-y-1">
|
||||
<span className="text-[10px] text-muted-foreground uppercase tracking-wider">Cooldown(s)</span>
|
||||
<Input
|
||||
type="number"
|
||||
min={5}
|
||||
max={7200}
|
||||
step={1}
|
||||
value={selectedAssistant.presenceProbeCooldownSeconds ?? 45}
|
||||
onChange={(e) =>
|
||||
updateAssistant(
|
||||
'presenceProbeCooldownSeconds',
|
||||
coerceBoundedNumber(
|
||||
e.target.value,
|
||||
Number(selectedAssistant.presenceProbeCooldownSeconds ?? 45),
|
||||
5,
|
||||
7200
|
||||
)
|
||||
)
|
||||
}
|
||||
className="h-9 bg-white/5 border-white/10"
|
||||
/>
|
||||
</label>
|
||||
<label className="space-y-1">
|
||||
<span className="text-[10px] text-muted-foreground uppercase tracking-wider">Max Prompts</span>
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
max={10}
|
||||
step={1}
|
||||
value={selectedAssistant.presenceProbeMaxPrompts ?? 2}
|
||||
onChange={(e) =>
|
||||
updateAssistant(
|
||||
'presenceProbeMaxPrompts',
|
||||
coerceBoundedInt(
|
||||
e.target.value,
|
||||
Number(selectedAssistant.presenceProbeMaxPrompts ?? 2),
|
||||
1,
|
||||
10
|
||||
)
|
||||
)
|
||||
}
|
||||
className="h-9 bg-white/5 border-white/10"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<label className="inline-flex items-center gap-2 text-xs text-muted-foreground">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedAssistant.presenceProbeIncludeContext !== false}
|
||||
onChange={(e) => updateAssistant('presenceProbeIncludeContext', e.target.checked)}
|
||||
className="accent-primary"
|
||||
/>
|
||||
结合最近上下文生成问句
|
||||
</label>
|
||||
<Input
|
||||
value={selectedAssistant.presenceProbeQuestion || ''}
|
||||
onChange={(e) => updateAssistant('presenceProbeQuestion', e.target.value.slice(0, 160))}
|
||||
placeholder="可选:自定义问句(留空则自动生成)"
|
||||
className="h-9 bg-white/5 border-white/10"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isBotFirstTurn && (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
@@ -2199,6 +2339,38 @@ export const DebugDrawer: React.FC<{
|
||||
};
|
||||
}).filter(Boolean) as Array<Record<string, any>>;
|
||||
}, [assistant.tools, tools, clientToolEnabledMap]);
|
||||
const presenceProbeConfig = useMemo(() => {
|
||||
if (assistant.presenceProbeEnabled !== true) return null;
|
||||
const toBoundedNumber = (raw: unknown, fallback: number, min: number, max: number) => {
|
||||
const parsed = Number(raw);
|
||||
if (!Number.isFinite(parsed)) return fallback;
|
||||
return Math.max(min, Math.min(max, parsed));
|
||||
};
|
||||
const toBoundedInt = (raw: unknown, fallback: number, min: number, max: number) => {
|
||||
const parsed = Number.parseInt(String(raw), 10);
|
||||
if (!Number.isFinite(parsed)) return fallback;
|
||||
return Math.max(min, Math.min(max, parsed));
|
||||
};
|
||||
const idleSeconds = toBoundedNumber(assistant.presenceProbeIdleSeconds, 20, 5, 3600);
|
||||
const cooldownSeconds = toBoundedNumber(assistant.presenceProbeCooldownSeconds, 45, 5, 7200);
|
||||
const maxPrompts = toBoundedInt(assistant.presenceProbeMaxPrompts, 2, 1, 10);
|
||||
const question = String(assistant.presenceProbeQuestion || '').trim();
|
||||
return {
|
||||
enabled: true,
|
||||
idleSeconds,
|
||||
cooldownSeconds,
|
||||
maxPrompts,
|
||||
includeContext: assistant.presenceProbeIncludeContext !== false,
|
||||
...(question ? { question } : {}),
|
||||
};
|
||||
}, [
|
||||
assistant.presenceProbeEnabled,
|
||||
assistant.presenceProbeIdleSeconds,
|
||||
assistant.presenceProbeCooldownSeconds,
|
||||
assistant.presenceProbeMaxPrompts,
|
||||
assistant.presenceProbeIncludeContext,
|
||||
assistant.presenceProbeQuestion,
|
||||
]);
|
||||
|
||||
const clearResponseTracking = () => {
|
||||
assistantDraftIndexRef.current = null;
|
||||
@@ -3027,6 +3199,9 @@ export const DebugDrawer: React.FC<{
|
||||
if (Object.keys(dynamicVariablesResult.variables).length > 0) {
|
||||
mergedMetadata.dynamicVariables = dynamicVariablesResult.variables;
|
||||
}
|
||||
if (presenceProbeConfig) {
|
||||
mergedMetadata.presenceProbe = presenceProbeConfig;
|
||||
}
|
||||
// Engine resolves trusted runtime config by top-level assistant/app ID.
|
||||
// Keep these IDs at metadata root so backend /assistants/{id}/config is reachable.
|
||||
if (!mergedMetadata.assistantId && assistant.id) {
|
||||
@@ -3805,6 +3980,47 @@ export const DebugDrawer: React.FC<{
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-md border border-white/10 bg-black/20 p-2 space-y-2">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<p className="text-[10px] uppercase tracking-widest text-muted-foreground">Presence Probe</p>
|
||||
<Badge variant={presenceProbeConfig ? 'outline' : 'secondary'} className="text-xs">
|
||||
{presenceProbeConfig ? 'ON' : 'OFF'}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-[11px] text-muted-foreground">
|
||||
在助手 Global 设置中配置;Debug Drawer 只用于体验当前配置。
|
||||
</p>
|
||||
{presenceProbeConfig ? (
|
||||
<div className="space-y-2">
|
||||
<div className="grid grid-cols-3 gap-2 text-[11px]">
|
||||
<div className="rounded border border-white/10 bg-black/20 px-2 py-1.5">
|
||||
<div className="text-[10px] text-muted-foreground uppercase tracking-wider">Idle</div>
|
||||
<div className="font-mono text-foreground">{Number(presenceProbeConfig.idleSeconds)}s</div>
|
||||
</div>
|
||||
<div className="rounded border border-white/10 bg-black/20 px-2 py-1.5">
|
||||
<div className="text-[10px] text-muted-foreground uppercase tracking-wider">Cooldown</div>
|
||||
<div className="font-mono text-foreground">{Number(presenceProbeConfig.cooldownSeconds)}s</div>
|
||||
</div>
|
||||
<div className="rounded border border-white/10 bg-black/20 px-2 py-1.5">
|
||||
<div className="text-[10px] text-muted-foreground uppercase tracking-wider">Max</div>
|
||||
<div className="font-mono text-foreground">{Number(presenceProbeConfig.maxPrompts)}</div>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-[11px] text-muted-foreground">
|
||||
问句生成:{presenceProbeConfig.includeContext ? '结合上下文' : '固定模板'}
|
||||
</p>
|
||||
{presenceProbeConfig.question && (
|
||||
<p className="text-[11px] text-muted-foreground break-words">
|
||||
自定义问句:{presenceProbeConfig.question}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-[11px] text-muted-foreground">
|
||||
当前助手未开启 Presence Probe,可在「全局设置」中打开。
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="rounded-md border border-white/10 bg-black/20 p-2 space-y-2">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<p className="text-[10px] uppercase tracking-widest text-muted-foreground">Dynamic Variables</p>
|
||||
|
||||
Reference in New Issue
Block a user