Files
qiming/qimingclaw/crates/agent-electron-client/embedded/sgrobot-digital/src/lib/consoleDataAdapter.ts
2026-06-05 15:02:42 +08:00

489 lines
18 KiB
TypeScript

// consoleDataAdapter.ts — API 类型 → 业务 ViewModel 映射
// API 类型参考: src/types/api.ts (DebugStoreResponse, DebugPlan, SkillSpec, etc.)
// ── View Models ──
export interface DashboardOverview {
generatedAt: string;
isPartial: boolean;
sources: DataSourceStatus[];
taskTotals: { total: number; queued: number; running: number; succeeded: number; failed: number };
approvals: { total: number; pending: number };
health: { activeTasks: number; failedTasks: number; pendingConfirmations: number };
}
export interface Mission {
id: string;
title: string;
objective: string;
status: string;
createdAt: string | null;
updatedAt: string | null;
steps: MissionStep[];
sourceIds: { planId?: string; taskIds: string[] };
}
export interface MissionStep {
id: string;
title: string;
description?: string;
status: string;
preferredSkills: string[];
tasks: Array<{
id: string;
title: string;
description?: string;
status: string;
assignedSkillId: string | null;
startedAt: string | null;
finishedAt: string | null;
}>;
}
export interface ApprovalItem {
id: string;
missionId: string | null;
taskId: string | null;
status: string;
title: string;
createdAt: string | null;
reversible: boolean;
}
export interface JournalEntry {
id: string;
kind: string;
source: string;
message: string;
occurredAt: string | null;
planId: string | null;
taskId: string | null;
sourceLink?: string;
}
export interface SkillSummary {
id: string;
name: string;
version: string | null;
enabled: boolean;
description: string;
category: string;
capabilityLevel: 'core' | 'standard' | 'specialized' | 'experimental';
source: 'skill-api';
}
export interface DataSourceStatus {
endpoint: string;
ok: boolean;
loadedAt?: string;
errorMessage?: string;
}
// ── 内部 helper ──
type Obj = Record<string, unknown>;
function arr(v: unknown): Obj[] {
return Array.isArray(v) ? (v as Obj[]) : [];
}
function str(v: unknown, fallback = ''): string {
if (typeof v === 'string') return v;
if (typeof v === 'number') return String(v);
return fallback;
}
function hasChinese(v: string): boolean {
return /[\u4e00-\u9fff]/.test(v);
}
const STATUS_ZH: Record<string, string> = {
queued: '排队中',
ready: '就绪',
idle: '待执行',
pending: '待处理',
pending_authorization: '待授权',
waiting_input: '待输入',
draft: '草稿',
reviewing: '审核中',
approved: '已批准',
preparing: '准备中',
running: '执行中',
active: '运行中',
blocked: '已阻塞',
succeeded: '已完成',
success: '成功',
finished: '已完成',
complete: '已完成',
completed: '已完成',
failed: '失败',
error: '错误',
cancelled: '已取消',
canceled: '已取消',
aborted: '已终止',
rejected: '已拒绝',
deferred: '已暂缓',
dispatched: '已下发',
scheduled: '已设置定时',
enabled: '已启用',
disabled: '已停用',
paused: '已暂停',
unknown: '未知',
};
const EVENT_KIND_ZH: Record<string, string> = {
plan: '计划',
task: '任务',
task_running: '处理中',
task_done: '任务完成',
run: '执行开始',
run_done: '执行完成',
skill_tool: '技能调用',
artifact: '产物',
task_summary: '结果',
entered_session: '会话',
start: '目标',
RunStarted: '开始调用',
RequestDispatched: '请求已发出',
ResponseReceived: '收到返回',
RunSkipped: '本次跳过',
RunCompleted: '执行完成',
waiting_for_authorization: '等待授权',
pending_authorization: '等待授权',
authorization_approved: '授权通过',
authorization_rejected: '授权拒绝',
authorization_deferred: '稍后处理',
failed: '执行失败',
completed: '执行完成',
};
export function displayStatus(status: unknown, fallback = '未知'): string {
const raw = str(status).trim();
if (!raw) return fallback;
const mapped = STATUS_ZH[raw] ?? STATUS_ZH[raw.toLowerCase()];
if (mapped) return mapped;
return hasChinese(raw) ? raw : fallback;
}
export function displayEventKind(kind: unknown): string {
const raw = str(kind).trim();
if (!raw) return '事件';
return EVENT_KIND_ZH[raw] ?? EVENT_KIND_ZH[raw.toLowerCase()] ?? (hasChinese(raw) ? raw : '事件');
}
export function displayBackendMessage(message: unknown, fallback = '暂无事件内容'): string {
const raw = str(message).trim();
if (!raw) return fallback;
if (hasChinese(raw)) return raw;
const status = displayStatus(raw, '');
if (status) return status;
const attempt = raw.match(/^(Succeeded|Failed|Running|Completed)\s*\(attempt\s*#(\d+)\)$/i);
if (attempt) {
return `${displayStatus(attempt[1])}(第 ${attempt[2]} 次)`;
}
if (/request failed/i.test(raw)) return '请求失败';
if (/backend unavailable/i.test(raw)) return '后端服务不可达';
if (/not found/i.test(raw)) return '未找到对应资源';
return fallback;
}
export function displayBusinessText(value: unknown, fallback = '未命名内容'): string {
const raw = str(value).trim();
if (!raw) return fallback;
if (/unnamed/i.test(raw)) return fallback;
if (hasChinese(raw)) return raw;
const status = displayStatus(raw, '');
if (status) return status;
const skillName = knownSkillName(raw);
if (skillName) return skillName;
return raw;
}
function isTerminalDoneStatus(status: string): boolean {
return ['succeeded', 'success', 'finished', 'complete', 'completed'].includes(status.toLowerCase());
}
function isTerminalFailedStatus(status: string): boolean {
return ['failed', 'error', 'cancelled', 'canceled', 'aborted', 'rejected', 'deadletter'].includes(status.toLowerCase());
}
function deriveStepStatus(rawStatus: unknown, tasks: MissionStep['tasks']): string {
const status = str(rawStatus).trim().toLowerCase();
if (tasks.length === 0) return status || 'pending';
if (tasks.some((task) => isTerminalFailedStatus(task.status))) return 'failed';
if (tasks.some((task) => ['running', 'active', 'leased'].includes(task.status.toLowerCase()))) return 'running';
if (tasks.some((task) => ['queued', 'ready', 'pending'].includes(task.status.toLowerCase()))) return 'queued';
if (tasks.every((task) => isTerminalDoneStatus(task.status))) return 'succeeded';
return status || 'pending';
}
function deriveMissionStatus(rawStatus: unknown, steps: MissionStep[]): string {
const status = str(rawStatus, 'unknown').trim().toLowerCase();
if (isTerminalDoneStatus(status) || isTerminalFailedStatus(status)) return status;
if (steps.some((step) => isTerminalFailedStatus(step.status))) return 'failed';
if (steps.some((step) => ['running', 'active', 'queued', 'leased'].includes(step.status.toLowerCase()))) return 'running';
if (steps.length > 0 && steps.every((step) => isTerminalDoneStatus(step.status))) return 'succeeded';
return status || 'unknown';
}
// ── 适配函数 ──
/**
* @param storeData — getDebugStore() 返回值 (DebugStoreResponse)
* @param skillsData — getSkills() 返回值 (SkillSpec[])
*
* DebugStoreResponse 形状: { tables: { tasks, runs, approvals, ... } }
* SkillSpec 形状: { skill_id, name, version, enabled, description }
* DebugPlan 形状: { plan_id, title, objective, status, created_at, steps }
*/
export function adaptDashboardOverview(
storeData: Obj | null
): DashboardOverview {
const now = new Date().toISOString();
const tables = (storeData?.tables ?? {}) as Obj;
const tasks = arr(tables.tasks);
const approvals = arr(tables.approvals);
const taskTotals = {
total: tasks.length,
queued: tasks.filter(t => t.status === 'queued').length,
running: tasks.filter(t => t.status === 'running').length,
succeeded: tasks.filter(t => t.status === 'succeeded').length,
failed: tasks.filter(t => t.status === 'failed').length,
};
return {
generatedAt: now,
isPartial: !storeData,
sources: [
{ endpoint: '/api/debug/store', ok: storeData !== null, loadedAt: now },
],
taskTotals,
approvals: {
total: approvals.length,
pending: approvals.filter(a => a.status === 'pending').length,
},
health: {
activeTasks: taskTotals.running,
failedTasks: taskTotals.failed,
pendingConfirmations: approvals.filter(a => a.status === 'pending').length,
},
};
}
export function adaptMissions(plansData: Obj[] | null): Mission[] {
if (!plansData) return [];
return plansData.map((p: Obj) => {
const planId = str(p.plan_id);
const stepsRaw = arr(p.steps);
const steps: MissionStep[] = stepsRaw.map((s: Obj) => {
const taskList = arr(s.tasks);
const tasks = taskList.map((t: Obj) => ({
id: str(t.task_id),
title: displayBusinessText(t.title, '子任务'),
description: str(t.description),
status: str(t.status, 'pending'),
assignedSkillId: t.assigned_skill_id ? str(t.assigned_skill_id) : null,
startedAt: t.started_at ? str(t.started_at) : null,
finishedAt: t.finished_at ? str(t.finished_at) : null,
}));
return {
id: str(s.step_id),
title: displayBusinessText(s.title, '任务步骤'),
description: str(s.description),
status: deriveStepStatus(s.status, tasks),
preferredSkills: Array.isArray(s.preferred_skills) ? (s.preferred_skills as string[]) : [],
tasks,
};
});
const taskIds: string[] = [];
for (const s of steps) {
for (const t of s.tasks) {
taskIds.push(t.id);
}
}
return {
id: planId,
title: displayBusinessText(p.title, '未命名任务'),
objective: displayBusinessText(p.objective || p.description, '暂无任务说明'),
status: deriveMissionStatus(p.status, steps),
createdAt: p.created_at ? str(p.created_at) : null,
updatedAt: p.updated_at ? str(p.updated_at) : null,
steps,
sourceIds: { planId: planId || undefined, taskIds },
};
});
}
const SKILL_ZH: Record<string, string> = {
'decision-analyzer': '决策分析',
'git-log-collector': 'Git 日志采集',
'number-generator': '数据生成',
'office-export-xlsx': 'Excel 导出',
'task-tracker': '任务跟踪',
'weekly-report-writer': '周报撰写',
'zhihu-hotlist-detail': '知乎热榜明细',
'zhihu-hotlist': '知乎热榜',
'tq-lineloss-report': '台区线损报表',
'archive-workorder-grid-push-monitor': '归档工单推送监控',
'available-balance-below-zero-monitor': '可用余额监控',
'command-center-fee-control-monitor': '费用管控监控',
'sgcc-todo-crawler': 'SGCC 待办爬取',
'appointment-workorder-monitor': '约时工单监控',
'fault-location-monitor': '故障定位监控',
'fudian-failure-monitor': '复电失败监控',
'important-service-expiry-monitor': '服务到期预警',
'by-95598-customer-satisfaction-report': '95598 满意率日报',
'by-daily-report-statistics': '日报统计',
'by-fire-fault-monthly-report': '火警故障月报',
'by-risk-control-next-day-plan-report': '风控次日计划',
'by-risk-control-week-plan-report': '风控周计划',
'by-supervision-workorder-detail-report': '督办工单明细',
'age-file-encryption': '文件加解密',
'anonymous-file-upload': '匿名文件上传',
'browser-automation-agent': '浏览器自动化',
'bulk-github-star': '批量仓库标星',
'changelog-generator': '更新日志生成',
'code-reviewer': '代码审查',
'commit-automation': '自动提交',
'crypto-trading-signal': '交易信号分析',
'csv-to-json': 'CSV 转 JSON',
'data-analysis': '数据分析',
'deep-research': '深度调研',
'dependency-updater': '依赖更新',
'deploy-automation': '自动部署',
'dns-lookup': 'DNS 查询',
'document-generator': '文档生成',
'email-automation': '邮件自动化',
'file-converter': '文件格式转换',
'git-history-analyzer': 'Git 历史分析',
'github-issue-manager': 'Issue 管理',
'image-optimizer': '图片优化',
'json-schema-validator': 'JSON Schema 校验',
'log-analyzer': '日志分析',
'markdown-to-pdf': 'Markdown 转 PDF',
'meeting-summarizer': '会议纪要',
'news-aggregator': '新闻聚合',
'pdf-extractor': 'PDF 提取',
'performance-profiler': '性能分析',
'pr-review-bot': 'PR 审查',
'release-notes-generator': '发布说明生成',
'screenshot-automation': '截图自动化',
'search-engine-scraper': '搜索引擎抓取',
'sentiment-analysis': '情感分析',
'shell-command-runner': '命令执行',
'slack-notification': 'Slack 通知',
'spreadsheet-generator': '电子表格生成',
'text-summarizer': '文本摘要',
'torrent-search': '资源搜索',
'translate-text': '文本翻译',
'web-scraper': '网页抓取',
'website-monitor': '网站监控',
'whitepaper-analyzer': '白皮书分析',
'xml-to-json': 'XML 转 JSON',
};
function knownSkillName(skillId: string): string | null {
if (SKILL_ZH[skillId]) return SKILL_ZH[skillId];
for (const [key, val] of Object.entries(SKILL_ZH)) {
if (skillId.includes(key) || key.includes(skillId)) return val;
}
return null;
}
function zhName(skillId: string, fallback: string): string {
const known = knownSkillName(skillId);
if (known) return known;
return hasChinese(fallback) ? fallback : '自动化技能';
}
const SKILL_CATEGORY: Record<string, string> = {
'decision-analyzer': '数字化', 'git-log-collector': '数字化', 'number-generator': '数字化',
'office-export-xlsx': '办公', 'task-tracker': '办公', 'weekly-report-writer': '报表',
'zhihu-hotlist-detail': '数字化', 'zhihu-hotlist': '数字化', 'tq-lineloss-report': '报表',
'archive-workorder-grid-push-monitor': '监控巡检', 'available-balance-below-zero-monitor': '监控巡检',
'command-center-fee-control-monitor': '监控巡检', 'sgcc-todo-crawler': '监控巡检',
'appointment-workorder-monitor': '监控巡检', 'fault-location-monitor': '监控巡检',
'fudian-failure-monitor': '监控巡检', 'important-service-expiry-monitor': '监控巡检',
'by-95598-customer-satisfaction-report': '报表', 'by-daily-report-statistics': '报表',
'by-fire-fault-monthly-report': '报表', 'by-risk-control-next-day-plan-report': '报表',
'by-risk-control-week-plan-report': '报表', 'by-supervision-workorder-detail-report': '报表',
'age-file-encryption': '数字化', 'anonymous-file-upload': '通信', 'browser-automation-agent': '数字化',
'bulk-github-star': '发展', 'changelog-generator': '发展', 'chat-logger': '通信',
'check-crypto-address-balance': '财务', 'city-distance': '发展', 'city-tourism-website-builder': '营销',
'crawl-websites-at-scale': '数字化', 'csv-data-summarizer': '财务', 'd3js-data-visualization': '数字化',
'database-query-and-export': '数字化', 'file-tracker': '综合', 'free-geocoding-and-maps': '发展',
'free-translation-api': '综合', 'free-weather-data': '综合', 'generate-asset-price-chart': '财务',
'generate-qr-code-natively': '数字化', 'get-crypto-price': '财务', 'git-worktree-cli': '发展',
'github-stats': '发展', 'html-to-markdown': '数字化', 'image-compressor': '综合',
'json-to-csv': '数字化', 'llm-council': '数字化', 'markdown-table-generator': '综合',
'multi-format-converter': '综合', 'npm-package-info': '发展', 'pdf-generator': '综合',
'random-number-generator': '综合', 'readability-score': '综合', 'rss-feed-reader': '通信',
'sql-formatter': '数字化', 'text-diff': '综合', 'torrent-search': '通信',
'unit-converter': '综合', 'url-shortener': '通信', 'youtube-transcript': '通信', 'zip-extractor': '综合',
};
function categoryFor(skillId: string): string {
if (SKILL_CATEGORY[skillId]) return SKILL_CATEGORY[skillId];
return '综合';
}
function zhDescription(skillId: string, rawDescription: unknown, name: string): string {
const description = str(rawDescription).trim();
if (description && hasChinese(description)) return description;
if (skillId.includes('monitor')) return `${name}能力,用于按排班自动巡检并提示异常。`;
if (skillId.includes('report')) return `${name}能力,用于自动生成和整理业务报表。`;
return `${name}能力,可由数字员工按需执行或定时处理。`;
}
export function adaptSkillSummaries(skillsData: Obj[] | null): SkillSummary[] {
if (!skillsData) return [];
return skillsData.map((s: Obj) => {
const sid = str(s.skill_id, str(s.id));
return {
id: sid,
name: zhName(sid, str(s.name, sid)),
version: s.version ? str(s.version) : null,
enabled: Boolean(s.enabled),
description: zhDescription(sid, s.description, zhName(sid, str(s.name, sid))),
category: categoryFor(sid),
capabilityLevel: 'standard' as const,
source: 'skill-api' as const,
};
});
}
export function adaptJournalEntries(storeData: Obj | null): JournalEntry[] {
if (!storeData) return [];
const tables = (storeData.tables ?? {}) as Obj;
const events = arr(tables.canonical_events);
return events.map((e: Obj) => ({
id: str(e.event_id, str(e.id)),
kind: str(e.event_type ?? e.kind, 'unknown'),
source: 'debug-store',
message: str(e.message, ''),
occurredAt: e.occurred_at ? str(e.occurred_at) : e.timestamp ? str(e.timestamp) : null,
planId: e.plan_id ? str(e.plan_id) : null,
taskId: e.task_id ? str(e.task_id) : null,
}));
}
export function adaptApprovals(storeData: Obj | null): ApprovalItem[] {
if (!storeData) return [];
const tables = (storeData.tables ?? {}) as Obj;
const approvals = arr(tables.approvals);
return approvals.map((a: Obj) => ({
id: str(a.approval_id, str(a.id)),
missionId: a.plan_id ? str(a.plan_id) : null,
taskId: a.task_id ? str(a.task_id) : null,
status: str(a.status, 'pending'),
title: displayBusinessText(a.title, '待处理授权'),
createdAt: a.created_at ? str(a.created_at) : null,
reversible: Boolean(a.reversible),
}));
}