feat: refine business chat workflow

This commit is contained in:
inman
2026-05-13 23:52:11 +08:00
parent 043d0f0bfe
commit 6b503dcbe9
30 changed files with 4609 additions and 126 deletions

View File

@@ -0,0 +1,243 @@
type JsonRecord = Record<string, unknown>;
export type CronDesktopReminderTone = 'success' | 'warning' | 'danger';
export interface CronDesktopReminderEvent {
jobId: string;
agentId: string;
sessionKey: string;
runKey: string;
phase: string;
tone: CronDesktopReminderTone;
summary?: string;
error?: string;
}
export interface CronDesktopReminderJob {
id: string;
name?: string;
state?: {
lastStatus?: string;
lastError?: string;
};
}
export interface CronDesktopReminderDisplay {
title: string;
body: string;
tone: CronDesktopReminderTone;
route: string;
}
export interface CronDesktopReminderHandlerDeps {
resolveJob?: (jobId: string) => Promise<CronDesktopReminderJob | undefined>;
notify: (display: CronDesktopReminderDisplay, event: CronDesktopReminderEvent) => void;
now?: () => number;
dedupeMs?: number;
}
interface ParsedCronSessionKey {
agentId: string;
jobId: string;
runSessionId?: string;
fixedSessionKey: string;
}
function asRecord(value: unknown): JsonRecord | undefined {
return value && typeof value === 'object' ? value as JsonRecord : undefined;
}
function readString(record: JsonRecord | undefined, key: string): string | undefined {
const value = record?.[key];
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
}
function parseCronSessionKey(value: unknown): ParsedCronSessionKey | null {
if (typeof value !== 'string' || !value.startsWith('agent:')) return null;
const parts = value.split(':');
if (parts.length < 4 || parts[2] !== 'cron') return null;
const agentId = parts[1] || 'main';
const jobId = parts[3];
if (!jobId) return null;
const runSessionId = parts.length >= 6 && parts[4] === 'run' && parts[5] ? parts[5] : undefined;
return {
agentId,
jobId,
...(runSessionId ? { runSessionId } : {}),
fixedSessionKey: `agent:${agentId}:cron:${jobId}`,
};
}
function getNestedRecord(record: JsonRecord | undefined, key: string): JsonRecord | undefined {
return asRecord(record?.[key]);
}
function isTerminalCronPhase(phase: string | undefined): boolean {
if (!phase) return false;
return ['completed', 'done', 'finished', 'failed', 'error'].includes(phase.toLowerCase());
}
function normalizeStatus(value: string | undefined): CronDesktopReminderTone | undefined {
const normalized = value?.trim().toLowerCase();
if (!normalized) return undefined;
if (['error', 'failed', 'failure', 'cancelled', 'timeout'].includes(normalized)) return 'danger';
if (['warning', 'warn', 'reconciled', 'partial'].includes(normalized)) return 'warning';
if (['ok', 'success', 'succeeded', 'completed', 'done', 'finished'].includes(normalized)) return 'success';
return undefined;
}
function firstText(values: Array<unknown>): string | undefined {
for (const value of values) {
const text = extractText(value);
if (text) return text;
}
return undefined;
}
function extractText(value: unknown): string | undefined {
if (typeof value === 'string') return value.trim() || undefined;
if (Array.isArray(value)) {
const text = value.map(extractText).filter(Boolean).join('\n').trim();
return text || undefined;
}
const record = asRecord(value);
if (!record) return undefined;
return firstText([
record.content,
record.text,
record.message,
record.summary,
record.error,
]);
}
function inferTone(phase: string | undefined, params: JsonRecord | undefined, data: JsonRecord | undefined): CronDesktopReminderTone {
const state = getNestedRecord(params, 'state') ?? getNestedRecord(data, 'state');
const tone = normalizeStatus(readString(data, 'status'))
?? normalizeStatus(readString(params, 'status'))
?? normalizeStatus(readString(data, 'finalStatus'))
?? normalizeStatus(readString(params, 'finalStatus'))
?? normalizeStatus(readString(state, 'status'))
?? normalizeStatus(readString(state, 'lastStatus'))
?? normalizeStatus(phase);
if (tone) return tone;
if (readString(data, 'error') || readString(params, 'error') || readString(state, 'lastError')) return 'danger';
return 'success';
}
function truncateText(value: string | undefined, maxLength: number): string {
const clean = (value || '').replace(/\s+/g, ' ').trim();
if (!clean) return '';
if (clean.length <= maxLength) return clean;
return `${clean.slice(0, Math.max(0, maxLength - 1)).trimEnd()}`;
}
export function buildCronDesktopReminderEvent(notification: unknown): CronDesktopReminderEvent | null {
const root = asRecord(notification);
if (readString(root, 'method') !== 'agent') return null;
const params = getNestedRecord(root, 'params');
const data = getNestedRecord(params, 'data');
const phase = (readString(data, 'phase') ?? readString(params, 'phase'))?.toLowerCase();
if (!isTerminalCronPhase(phase)) return null;
const parsedSession = parseCronSessionKey(readString(params, 'sessionKey') ?? readString(data, 'sessionKey'));
if (!parsedSession) return null;
const state = getNestedRecord(params, 'state') ?? getNestedRecord(data, 'state');
const error = firstText([
readString(data, 'error'),
readString(params, 'error'),
readString(state, 'lastError'),
]);
const summary = firstText([
readString(data, 'resolvedSummary'),
readString(params, 'resolvedSummary'),
readString(data, 'summary'),
readString(params, 'summary'),
data?.message,
params?.message,
]);
const runId = readString(params, 'runId')
?? readString(data, 'runId')
?? readString(params, 'sessionId')
?? readString(data, 'sessionId')
?? parsedSession.runSessionId
?? parsedSession.fixedSessionKey;
return {
jobId: parsedSession.jobId,
agentId: parsedSession.agentId,
sessionKey: parsedSession.fixedSessionKey,
runKey: `${parsedSession.jobId}:${runId}`,
phase: phase || 'completed',
tone: inferTone(phase, params, data),
...(summary ? { summary: truncateText(summary, 180) } : {}),
...(error ? { error: truncateText(error, 180) } : {}),
};
}
export function buildCronDesktopReminderDisplay(
event: CronDesktopReminderEvent,
job?: CronDesktopReminderJob,
): CronDesktopReminderDisplay {
const jobName = job?.name?.trim() || event.jobId;
const jobTone = normalizeStatus(job?.state?.lastStatus);
const tone = jobTone === 'danger' ? 'danger' : event.tone;
const error = event.error || job?.state?.lastError;
const detail = tone === 'danger' ? error || event.summary : event.summary || error;
if (tone === 'danger') {
return {
tone,
title: `任务需要处理:${jobName}`,
body: truncateText(detail || '任务运行失败,点击查看执行记录。', 220),
route: `/tasks?task=${encodeURIComponent(event.jobId)}`,
};
}
if (tone === 'warning') {
return {
tone,
title: `任务需复核:${jobName}`,
body: truncateText(detail || '任务已完成,但存在需要复核的提示。', 220),
route: `/tasks?task=${encodeURIComponent(event.jobId)}`,
};
}
return {
tone,
title: `任务已完成:${jobName}`,
body: truncateText(detail || '结果已生成,点击查看任务会话。', 220),
route: `/tasks?task=${encodeURIComponent(event.jobId)}`,
};
}
export function createCronDesktopReminderHandler(deps: CronDesktopReminderHandlerDeps) {
const notifiedRuns = new Map<string, number>();
const dedupeMs = deps.dedupeMs ?? 10 * 60 * 1000;
const now = deps.now ?? Date.now;
return async (notification: unknown): Promise<CronDesktopReminderDisplay | null> => {
const event = buildCronDesktopReminderEvent(notification);
if (!event) return null;
const currentTime = now();
for (const [key, timestamp] of notifiedRuns) {
if (currentTime - timestamp > dedupeMs) {
notifiedRuns.delete(key);
}
}
const notifiedAt = notifiedRuns.get(event.runKey);
if (notifiedAt != null && currentTime - notifiedAt <= dedupeMs) {
return null;
}
notifiedRuns.set(event.runKey, currentTime);
const job = await deps.resolveJob?.(event.jobId).catch(() => undefined);
const display = buildCronDesktopReminderDisplay(event, job);
deps.notify(display, event);
return display;
};
}