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:
Xin Wang
2026-02-28 15:47:53 +08:00
parent 0821d73e7c
commit 8f1317860f
11 changed files with 1006 additions and 3 deletions

View File

@@ -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>

View File

@@ -50,6 +50,12 @@ const mapAssistant = (raw: AnyRecord): Assistant => ({
tools: readField(raw, ['tools'], []),
botCannotBeInterrupted: Boolean(readField(raw, ['botCannotBeInterrupted', 'bot_cannot_be_interrupted'], false)),
interruptionSensitivity: Number(readField(raw, ['interruptionSensitivity', 'interruption_sensitivity'], 500)),
presenceProbeEnabled: Boolean(readField(raw, ['presenceProbeEnabled', 'presence_probe_enabled'], false)),
presenceProbeIdleSeconds: Number(readField(raw, ['presenceProbeIdleSeconds', 'presence_probe_idle_seconds'], 20)),
presenceProbeCooldownSeconds: Number(readField(raw, ['presenceProbeCooldownSeconds', 'presence_probe_cooldown_seconds'], 45)),
presenceProbeMaxPrompts: Number(readField(raw, ['presenceProbeMaxPrompts', 'presence_probe_max_prompts'], 2)),
presenceProbeIncludeContext: Boolean(readField(raw, ['presenceProbeIncludeContext', 'presence_probe_include_context'], true)),
presenceProbeQuestion: readField(raw, ['presenceProbeQuestion', 'presence_probe_question'], ''),
configMode: readField(raw, ['configMode', 'config_mode'], 'platform') as 'platform' | 'dify' | 'fastgpt' | 'none',
apiUrl: readField(raw, ['apiUrl', 'api_url'], ''),
apiKey: readField(raw, ['apiKey', 'api_key'], ''),
@@ -246,6 +252,12 @@ export const createAssistant = async (data: Partial<Assistant>): Promise<Assista
tools: data.tools || [],
botCannotBeInterrupted: data.botCannotBeInterrupted ?? false,
interruptionSensitivity: data.interruptionSensitivity ?? 500,
presenceProbeEnabled: data.presenceProbeEnabled ?? false,
presenceProbeIdleSeconds: data.presenceProbeIdleSeconds ?? 20,
presenceProbeCooldownSeconds: data.presenceProbeCooldownSeconds ?? 45,
presenceProbeMaxPrompts: data.presenceProbeMaxPrompts ?? 2,
presenceProbeIncludeContext: data.presenceProbeIncludeContext ?? true,
presenceProbeQuestion: data.presenceProbeQuestion ?? '',
configMode: data.configMode || 'platform',
apiUrl: data.apiUrl || '',
apiKey: data.apiKey || '',
@@ -275,6 +287,12 @@ export const updateAssistant = async (id: string, data: Partial<Assistant>): Pro
tools: data.tools,
botCannotBeInterrupted: data.botCannotBeInterrupted,
interruptionSensitivity: data.interruptionSensitivity,
presenceProbeEnabled: data.presenceProbeEnabled,
presenceProbeIdleSeconds: data.presenceProbeIdleSeconds,
presenceProbeCooldownSeconds: data.presenceProbeCooldownSeconds,
presenceProbeMaxPrompts: data.presenceProbeMaxPrompts,
presenceProbeIncludeContext: data.presenceProbeIncludeContext,
presenceProbeQuestion: data.presenceProbeQuestion,
configMode: data.configMode,
apiUrl: data.apiUrl,
apiKey: data.apiKey,

View File

@@ -20,6 +20,12 @@ export interface Assistant {
tools?: string[]; // IDs of enabled tools
botCannotBeInterrupted?: boolean;
interruptionSensitivity?: number; // In ms
presenceProbeEnabled?: boolean;
presenceProbeIdleSeconds?: number;
presenceProbeCooldownSeconds?: number;
presenceProbeMaxPrompts?: number;
presenceProbeIncludeContext?: boolean;
presenceProbeQuestion?: string;
configMode?: 'platform' | 'dify' | 'fastgpt' | 'none';
apiUrl?: string;
apiKey?: string;