785 lines
28 KiB
TypeScript
785 lines
28 KiB
TypeScript
import type {
|
||
CronJob,
|
||
DebugPlan,
|
||
DebugRun,
|
||
DebugTask,
|
||
DigitalEmployeeDuty,
|
||
DigitalEmployeeRunDetail,
|
||
DigitalEmployeeTaskExecutionSummary,
|
||
DigitalEmployeeWorkEvent,
|
||
DigitalEmployeeWorkdayActionRequest,
|
||
DigitalEmployeeWorkdayProjection,
|
||
FlowResponse,
|
||
PlanDispatchResponse,
|
||
SkillSpec,
|
||
} from '@/types/api';
|
||
|
||
declare global {
|
||
interface Window {
|
||
__QIMINGCLAW_EMBEDDED_DIGITAL__?: boolean;
|
||
QimingClawBridge?: {
|
||
digital?: {
|
||
getSnapshot?: () => Promise<QimingclawSnapshot>;
|
||
getSyncStatus?: () => Promise<QimingclawSyncStatus>;
|
||
flushSync?: () => Promise<QimingclawSyncStatus>;
|
||
};
|
||
};
|
||
}
|
||
}
|
||
|
||
const STATE_KEY = 'qimingclaw:digital-employee-adapter-state';
|
||
const DEFAULT_REPORT_TIME = '18:00';
|
||
|
||
interface AdapterState {
|
||
selectedDutyIdsByDate: Record<string, string[]>;
|
||
confirmedDates: Record<string, string>;
|
||
reportScheduleTime: string;
|
||
reportGeneratedAtByDate: Record<string, string>;
|
||
profile: {
|
||
name: string;
|
||
company: string;
|
||
position: string;
|
||
currentStatus: string;
|
||
};
|
||
}
|
||
|
||
interface QimingclawServiceStatus {
|
||
running?: boolean;
|
||
error?: string;
|
||
engineType?: string;
|
||
port?: number;
|
||
serverCount?: number;
|
||
serverNames?: string[];
|
||
}
|
||
|
||
interface QimingclawSyncStatus {
|
||
running: boolean;
|
||
syncing: boolean;
|
||
enabled: boolean;
|
||
endpoint: string | null;
|
||
pending: number;
|
||
failed: number;
|
||
lastSyncAt: string | null;
|
||
lastSyncError: string | null;
|
||
}
|
||
|
||
interface QimingclawSnapshot {
|
||
generatedAt: string;
|
||
services: Record<string, QimingclawServiceStatus | null>;
|
||
sessions: {
|
||
total: number;
|
||
active: number;
|
||
items: Array<{ id: string; title?: string; status?: string; engineType?: string; projectId?: string }>;
|
||
};
|
||
}
|
||
|
||
const SKILLS: SkillSpec[] = [
|
||
{
|
||
skill_id: 'qimingclaw-acp-session',
|
||
name: 'ACP 会话执行',
|
||
version: '1.0.0',
|
||
enabled: true,
|
||
description: '通过 qimingclaw 的 ACP 引擎创建会话、执行任务并跟踪结果。',
|
||
category: '执行',
|
||
capability_level: 'core',
|
||
},
|
||
{
|
||
skill_id: 'qimingclaw-mcp-tools',
|
||
name: 'MCP 工具编排',
|
||
version: '1.0.0',
|
||
enabled: true,
|
||
description: '读取已配置 MCP 服务与工具,作为数字员工的工具能力池。',
|
||
category: '工具',
|
||
capability_level: 'core',
|
||
},
|
||
{
|
||
skill_id: 'qimingclaw-computer-control',
|
||
name: '客户端电脑控制',
|
||
version: '1.0.0',
|
||
enabled: true,
|
||
description: '承接 /computer/chat 执行入口,面向远程任务和本地自动化操作。',
|
||
category: '桌面',
|
||
capability_level: 'specialized',
|
||
},
|
||
{
|
||
skill_id: 'qimingclaw-artifact-report',
|
||
name: '产物与日报整理',
|
||
version: '1.0.0',
|
||
enabled: true,
|
||
description: '把执行记录、关键产物和异常汇总成数字员工日报。',
|
||
category: '汇报',
|
||
capability_level: 'standard',
|
||
},
|
||
];
|
||
|
||
const PLANS: DebugPlan[] = [
|
||
{
|
||
plan_id: 'qimingclaw-client-readiness',
|
||
title: '客户端运行状态巡检',
|
||
objective: '确认 qimingclaw 客户端、Agent、MCP 和远程接入能力是否处于可执行状态。',
|
||
status: 'running',
|
||
created_at: todayAt(9, 0),
|
||
steps: [
|
||
{
|
||
step_id: 'agent-status',
|
||
title: '检查 Agent 引擎状态',
|
||
depends_on: [],
|
||
preferred_skills: ['qimingclaw-acp-session'],
|
||
tasks: [{ task_id: 'task-agent-status', title: '读取 Agent 服务状态', status: 'running' }],
|
||
},
|
||
{
|
||
step_id: 'mcp-status',
|
||
title: '检查 MCP 工具能力',
|
||
depends_on: ['agent-status'],
|
||
preferred_skills: ['qimingclaw-mcp-tools'],
|
||
tasks: [{ task_id: 'task-mcp-status', title: '同步 MCP 工具列表', status: 'pending' }],
|
||
},
|
||
],
|
||
},
|
||
{
|
||
plan_id: 'qimingclaw-remote-task-ingress',
|
||
title: '远程任务接入准备',
|
||
objective: '检查 lanproxy、computer server 和文件服务,为后端下发数字员工任务做准备。',
|
||
status: 'pending',
|
||
created_at: todayAt(9, 30),
|
||
steps: [
|
||
{
|
||
step_id: 'computer-server',
|
||
title: '确认 /computer/chat 执行入口',
|
||
depends_on: [],
|
||
preferred_skills: ['qimingclaw-computer-control'],
|
||
tasks: [{ task_id: 'task-computer-server', title: '检查电脑控制服务', status: 'pending' }],
|
||
},
|
||
{
|
||
step_id: 'file-artifact',
|
||
title: '准备产物回传目录',
|
||
depends_on: ['computer-server'],
|
||
preferred_skills: ['qimingclaw-artifact-report'],
|
||
tasks: [{ task_id: 'task-file-artifact', title: '整理执行产物通道', status: 'pending' }],
|
||
},
|
||
],
|
||
},
|
||
{
|
||
plan_id: 'qimingclaw-daily-report',
|
||
title: '数字员工日报生成',
|
||
objective: '汇总今日执行记录、服务状态和待介入事项,生成面向业务的日报。',
|
||
status: 'pending',
|
||
created_at: todayAt(17, 30),
|
||
steps: [
|
||
{
|
||
step_id: 'collect-events',
|
||
title: '汇总运行事件',
|
||
depends_on: [],
|
||
preferred_skills: ['qimingclaw-artifact-report'],
|
||
tasks: [{ task_id: 'task-collect-events', title: '整理今日运行明细', status: 'pending' }],
|
||
},
|
||
{
|
||
step_id: 'write-report',
|
||
title: '生成日报摘要',
|
||
depends_on: ['collect-events'],
|
||
preferred_skills: ['qimingclaw-artifact-report'],
|
||
tasks: [{ task_id: 'task-write-report', title: '生成数字员工日报', status: 'pending' }],
|
||
},
|
||
],
|
||
},
|
||
];
|
||
|
||
const PLAN_SCHEDULES: Record<string, { expression: string; nextRun: string; scheduleText: string }> = {
|
||
'qimingclaw-client-readiness': {
|
||
expression: '0 9 * * *',
|
||
nextRun: todayAt(9, 0),
|
||
scheduleText: '已定时 09:00 执行',
|
||
},
|
||
'qimingclaw-remote-task-ingress': {
|
||
expression: '30 9 * * *',
|
||
nextRun: todayAt(9, 30),
|
||
scheduleText: '已定时 09:30 执行',
|
||
},
|
||
'qimingclaw-daily-report': {
|
||
expression: '0 18 * * *',
|
||
nextRun: todayAt(18, 0),
|
||
scheduleText: '已定时 18:00 执行',
|
||
},
|
||
};
|
||
|
||
export function isQimingclawDigitalApiEnabled(): boolean {
|
||
return window.__QIMINGCLAW_EMBEDDED_DIGITAL__ === true;
|
||
}
|
||
|
||
export async function handleQimingclawDigitalApi<T>(
|
||
path: string,
|
||
options: RequestInit = {},
|
||
): Promise<T | undefined> {
|
||
if (!isQimingclawDigitalApiEnabled()) return undefined;
|
||
|
||
const url = new URL(path, window.location.origin);
|
||
const method = (options.method || 'GET').toUpperCase();
|
||
const pathname = url.pathname;
|
||
|
||
if (method === 'GET' && pathname === '/api/digital-employee/workday') {
|
||
return buildWorkday(url.searchParams.get('date') || todayInputDate(), await readQimingclawSnapshot()) as T;
|
||
}
|
||
if (method === 'POST' && pathname === '/api/digital-employee/workday/actions') {
|
||
return handleWorkdayAction(parseJsonBody<DigitalEmployeeWorkdayActionRequest>(options.body), await readQimingclawSnapshot()) as T;
|
||
}
|
||
if (method === 'GET' && pathname === '/api/scheduler/jobs') {
|
||
return { jobs: buildSchedulerJobs() } as T;
|
||
}
|
||
if (method === 'GET' && pathname === '/api/debug/plans') {
|
||
return { plans: filterPlans(url.searchParams.get('q'), await readQimingclawSnapshot()) } as T;
|
||
}
|
||
if (method === 'GET' && pathname === '/api/debug/tasks') {
|
||
const planId = url.searchParams.get('plan_id');
|
||
return buildTasksResponse(planId, await readQimingclawSnapshot()) as T;
|
||
}
|
||
if (method === 'GET' && pathname.startsWith('/api/debug/plan/') && pathname.endsWith('/flow')) {
|
||
return buildPlanFlow(decodeURIComponent(pathname.split('/')[4] || ''), await readQimingclawSnapshot()) as T;
|
||
}
|
||
if (method === 'POST' && pathname.startsWith('/api/debug/plan/') && pathname.endsWith('/dispatch')) {
|
||
return dispatchPlan(decodeURIComponent(pathname.split('/')[4] || '')) as T;
|
||
}
|
||
if (method === 'GET' && pathname.startsWith('/api/plan/') && pathname.endsWith('/messages')) {
|
||
return { messages: buildPlanMessages(decodeURIComponent(pathname.split('/')[3] || '')) } as T;
|
||
}
|
||
if (method === 'GET' && pathname === '/api/skill') {
|
||
return { skills: SKILLS } as T;
|
||
}
|
||
if (method === 'POST' && pathname.startsWith('/api/skill/') && pathname.endsWith('/enabled')) {
|
||
const skillId = decodeURIComponent(pathname.split('/')[3] || '');
|
||
const body = parseJsonBody<{ enabled?: boolean }>(options.body);
|
||
return { ok: true, skill_id: skillId, enabled: body.enabled ?? true } as T;
|
||
}
|
||
|
||
return undefined;
|
||
}
|
||
|
||
function defaultState(): AdapterState {
|
||
return {
|
||
selectedDutyIdsByDate: {},
|
||
confirmedDates: {},
|
||
reportScheduleTime: DEFAULT_REPORT_TIME,
|
||
reportGeneratedAtByDate: {},
|
||
profile: {
|
||
name: '飞天数字员工',
|
||
company: 'qimingclaw 客户端',
|
||
position: '客户端任务执行员',
|
||
currentStatus: 'working',
|
||
},
|
||
};
|
||
}
|
||
|
||
function readState(): AdapterState {
|
||
try {
|
||
const raw = window.localStorage.getItem(STATE_KEY);
|
||
return raw ? { ...defaultState(), ...JSON.parse(raw) as Partial<AdapterState> } : defaultState();
|
||
} catch {
|
||
return defaultState();
|
||
}
|
||
}
|
||
|
||
function writeState(state: AdapterState): void {
|
||
window.localStorage.setItem(STATE_KEY, JSON.stringify(state));
|
||
}
|
||
|
||
async function readQimingclawSnapshot(): Promise<QimingclawSnapshot | null> {
|
||
try {
|
||
return await window.QimingClawBridge?.digital?.getSnapshot?.() ?? null;
|
||
} catch {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
function parseJsonBody<T>(body: BodyInit | null | undefined): T {
|
||
if (typeof body !== 'string' || !body.trim()) return {} as T;
|
||
return JSON.parse(body) as T;
|
||
}
|
||
|
||
function todayInputDate(): string {
|
||
const date = new Date();
|
||
return [
|
||
date.getFullYear(),
|
||
String(date.getMonth() + 1).padStart(2, '0'),
|
||
String(date.getDate()).padStart(2, '0'),
|
||
].join('-');
|
||
}
|
||
|
||
function todayAt(hour: number, minute: number): string {
|
||
const date = new Date();
|
||
date.setHours(hour, minute, 0, 0);
|
||
return date.toISOString();
|
||
}
|
||
|
||
function displayDate(date: string): string {
|
||
return date.split('-').join('/');
|
||
}
|
||
|
||
function selectedSetForDate(state: AdapterState, date: string): Set<string> {
|
||
return new Set(state.selectedDutyIdsByDate[date] ?? []);
|
||
}
|
||
|
||
function planStatusTone(status: string): string {
|
||
if (['completed', 'succeeded', 'success'].includes(status)) return 'success';
|
||
if (['running', 'active'].includes(status)) return 'running';
|
||
if (['failed', 'error'].includes(status)) return 'danger';
|
||
return 'waiting';
|
||
}
|
||
|
||
function isRunning(snapshot: QimingclawSnapshot | null, key: string): boolean {
|
||
return snapshot?.services[key]?.running === true;
|
||
}
|
||
|
||
function serviceError(snapshot: QimingclawSnapshot | null, key: string): string | undefined {
|
||
return snapshot?.services[key]?.error;
|
||
}
|
||
|
||
function runtimeStatusForPlan(plan: DebugPlan, snapshot: QimingclawSnapshot | null): string {
|
||
if (!snapshot) return plan.status;
|
||
if (plan.plan_id === 'qimingclaw-client-readiness') {
|
||
if (isRunning(snapshot, 'agent') && isRunning(snapshot, 'mcp')) return 'running';
|
||
if (serviceError(snapshot, 'agent') || serviceError(snapshot, 'mcp')) return 'failed';
|
||
return 'pending';
|
||
}
|
||
if (plan.plan_id === 'qimingclaw-remote-task-ingress') {
|
||
if (isRunning(snapshot, 'computerServer') || isRunning(snapshot, 'lanproxy') || isRunning(snapshot, 'fileServer')) return 'running';
|
||
if (serviceError(snapshot, 'computerServer') || serviceError(snapshot, 'lanproxy') || serviceError(snapshot, 'fileServer')) return 'failed';
|
||
return 'pending';
|
||
}
|
||
return plan.status;
|
||
}
|
||
|
||
function serviceLine(snapshot: QimingclawSnapshot | null, key: string, label: string): string {
|
||
const service = snapshot?.services[key];
|
||
if (!service) return `${label}:未连接`;
|
||
if (service.running) {
|
||
const suffix = service.port ? `(端口 ${service.port})` : '';
|
||
return `${label}:运行中${suffix}`;
|
||
}
|
||
return `${label}:未运行${service.error ? `,${service.error}` : ''}`;
|
||
}
|
||
|
||
function withRuntimePlan(plan: DebugPlan, snapshot: QimingclawSnapshot | null): DebugPlan {
|
||
return { ...plan, status: runtimeStatusForPlan(plan, snapshot) };
|
||
}
|
||
|
||
function planStatusLabel(status: string): string {
|
||
if (status === 'running') return '执行中';
|
||
if (status === 'completed' || status === 'succeeded') return '已完成';
|
||
if (status === 'failed') return '异常';
|
||
return '待执行';
|
||
}
|
||
|
||
function buildSchedulerJobs(): CronJob[] {
|
||
return PLANS.map((plan) => {
|
||
const schedule = PLAN_SCHEDULES[plan.plan_id] ?? PLAN_SCHEDULES['qimingclaw-client-readiness']!;
|
||
return {
|
||
id: `schedule-${plan.plan_id}`,
|
||
name: plan.title,
|
||
expression: schedule.expression,
|
||
command: `digital-employee:${plan.plan_id}`,
|
||
prompt: plan.objective ?? null,
|
||
job_type: 'plan_template',
|
||
schedule: { expression: schedule.expression, template_id: plan.plan_id },
|
||
enabled: true,
|
||
delivery: { mode: 'plan_template', template_id: plan.plan_id },
|
||
template_id: plan.plan_id,
|
||
trigger_kind: 'scheduled',
|
||
step_count: plan.steps?.length ?? 0,
|
||
skill_ids: [...new Set((plan.steps ?? []).flatMap((step) => step.preferred_skills ?? []))],
|
||
delete_after_run: false,
|
||
created_at: plan.created_at,
|
||
next_run: schedule.nextRun,
|
||
last_run: plan.status === 'running' ? todayAt(9, 5) : null,
|
||
last_status: plan.status === 'running' ? 'running' : null,
|
||
last_output: null,
|
||
};
|
||
});
|
||
}
|
||
|
||
function buildDuties(state: AdapterState, date: string, snapshot: QimingclawSnapshot | null): DigitalEmployeeDuty[] {
|
||
const selected = selectedSetForDate(state, date);
|
||
return PLANS.map((rawPlan) => {
|
||
const plan = withRuntimePlan(rawPlan, snapshot);
|
||
const schedule = PLAN_SCHEDULES[plan.plan_id] ?? PLAN_SCHEDULES['qimingclaw-client-readiness']!;
|
||
return {
|
||
id: plan.plan_id,
|
||
title: plan.title,
|
||
summary: plan.objective ?? '等待 qimingclaw 执行链路补充任务说明。',
|
||
implemented: true,
|
||
selectable: true,
|
||
selected: selected.has(plan.plan_id),
|
||
status: plan.status,
|
||
status_label: planStatusLabel(plan.status),
|
||
status_tone: planStatusTone(plan.status),
|
||
schedule_text: schedule.scheduleText,
|
||
result_text: planResultText(plan, snapshot),
|
||
conversation_id: plan.plan_id,
|
||
};
|
||
});
|
||
}
|
||
|
||
function planResultText(plan: DebugPlan, snapshot: QimingclawSnapshot | null): string {
|
||
if (plan.plan_id === 'qimingclaw-client-readiness') {
|
||
return [
|
||
serviceLine(snapshot, 'agent', 'Agent'),
|
||
serviceLine(snapshot, 'mcp', 'MCP'),
|
||
`活跃会话:${snapshot?.sessions.active ?? 0}`,
|
||
].join(';');
|
||
}
|
||
if (plan.plan_id === 'qimingclaw-remote-task-ingress') {
|
||
return [
|
||
serviceLine(snapshot, 'computerServer', 'Computer Server'),
|
||
serviceLine(snapshot, 'lanproxy', 'Lanproxy'),
|
||
serviceLine(snapshot, 'fileServer', 'File Server'),
|
||
].join(';');
|
||
}
|
||
return plan.status === 'running' ? '正在同步客户端运行状态' : '等待任务执行';
|
||
}
|
||
|
||
function buildTasks(planId?: string | null, snapshot: QimingclawSnapshot | null = null): DebugTask[] {
|
||
return PLANS
|
||
.filter((plan) => !planId || plan.plan_id === planId)
|
||
.map((plan) => withRuntimePlan(plan, snapshot))
|
||
.flatMap((plan) => (plan.steps ?? []).flatMap((step) => (step.tasks ?? []).map((task) => ({
|
||
task_id: task.task_id,
|
||
title: task.title,
|
||
status: plan.status === 'running' && task.status === 'pending' ? 'running' : task.status,
|
||
plan_id: plan.plan_id,
|
||
step_id: step.step_id,
|
||
assigned_skill_id: step.preferred_skills?.[0] ?? '',
|
||
description: `${plan.title} · ${step.title}`,
|
||
created_at: plan.created_at,
|
||
updated_at: plan.status === 'running' ? new Date().toISOString() : plan.created_at,
|
||
started_at: task.status === 'running' ? todayAt(9, 5) : null,
|
||
finished_at: task.status === 'completed' ? todayAt(9, 10) : null,
|
||
}))));
|
||
}
|
||
|
||
function buildRuns(tasks: DebugTask[]): DebugRun[] {
|
||
return tasks
|
||
.filter((task) => task.status === 'running' || task.status === 'completed')
|
||
.map((task, index) => ({
|
||
run_id: `qimingclaw-run-${index + 1}`,
|
||
task_id: task.task_id,
|
||
plan_id: task.plan_id,
|
||
skill_id: task.assigned_skill_id,
|
||
attempt_no: 1,
|
||
lifecycle: task.status === 'running' ? 'Running' : 'Succeeded',
|
||
created_at: task.started_at ?? task.created_at,
|
||
completed_at: task.finished_at,
|
||
events: [
|
||
{
|
||
event_id: `${task.task_id}-event-start`,
|
||
kind: 'RunStarted',
|
||
occurred_at: task.started_at ?? task.created_at,
|
||
message: `${task.title} 已进入 qimingclaw 执行链路`,
|
||
},
|
||
],
|
||
}));
|
||
}
|
||
|
||
function buildTasksResponse(planId?: string | null, snapshot: QimingclawSnapshot | null = null): { tasks: DebugTask[]; runs: DebugRun[]; total: number; truncated: boolean } {
|
||
const tasks = buildTasks(planId, snapshot);
|
||
return { tasks, runs: buildRuns(tasks), total: tasks.length, truncated: false };
|
||
}
|
||
|
||
function filterPlans(query?: string | null, snapshot: QimingclawSnapshot | null = null): DebugPlan[] {
|
||
const normalized = (query ?? '').trim().toLowerCase();
|
||
const plans = PLANS.map((plan) => withRuntimePlan(plan, snapshot));
|
||
if (!normalized) return plans;
|
||
return plans.filter((plan) => (
|
||
plan.title.toLowerCase().includes(normalized)
|
||
|| plan.plan_id.toLowerCase().includes(normalized)
|
||
|| (plan.objective ?? '').toLowerCase().includes(normalized)
|
||
));
|
||
}
|
||
|
||
function buildEvents(duties: DigitalEmployeeDuty[], confirmed: boolean, snapshot: QimingclawSnapshot | null): DigitalEmployeeWorkEvent[] {
|
||
if (!confirmed) {
|
||
return [{
|
||
id: 'event-await-confirmation',
|
||
time: '待确认',
|
||
plan_title: '任务设置',
|
||
task_title: '等待确认今日任务',
|
||
step_title: '选择职责',
|
||
detail: '请先确认今天要执行的任务,确认后数字员工会开始执行。',
|
||
status: 'pending',
|
||
status_label: '待确认',
|
||
status_tone: 'waiting',
|
||
updated_at: new Date().toISOString(),
|
||
conversation_id: null,
|
||
}];
|
||
}
|
||
|
||
const selectedDuties = duties.filter((duty) => duty.selected);
|
||
return selectedDuties.map((duty) => ({
|
||
id: `event-${duty.id}`,
|
||
time: duty.status === 'running' ? '实时' : '待执行',
|
||
plan_title: duty.title,
|
||
task_title: duty.title,
|
||
step_title: duty.status === 'running' ? '同步 qimingclaw 服务状态' : '等待调度',
|
||
detail: duty.status === 'running'
|
||
? `${duty.result_text}。快照时间:${snapshot?.generatedAt ?? '未连接 bridge'}`
|
||
: '任务已确认,将在后续阶段接入 ACP/MCP 真实执行事件。',
|
||
status: duty.status === 'running' ? 'running' : 'pending',
|
||
status_label: duty.status === 'running' ? '执行中' : '待执行',
|
||
status_tone: duty.status === 'running' ? 'running' : 'waiting',
|
||
updated_at: new Date().toISOString(),
|
||
conversation_id: duty.conversation_id,
|
||
}));
|
||
}
|
||
|
||
function buildExecutionSummaries(duties: DigitalEmployeeDuty[], events: DigitalEmployeeWorkEvent[]): DigitalEmployeeTaskExecutionSummary[] {
|
||
return duties.filter((duty) => duty.selected).map((duty) => {
|
||
const related = events.filter((event) => event.conversation_id === duty.conversation_id);
|
||
const running = related.filter((event) => event.status === 'running').length;
|
||
const success = related.filter((event) => event.status === 'completed').length;
|
||
return {
|
||
id: duty.id,
|
||
plan_id: duty.id,
|
||
title: duty.title,
|
||
description: duty.summary,
|
||
selected: duty.selected,
|
||
total_count: Math.max(related.length, 1),
|
||
success_count: success,
|
||
failure_count: 0,
|
||
running_count: running,
|
||
waiting_count: running > 0 ? 0 : 1,
|
||
latest_status_label: running > 0 ? '执行中' : '待执行',
|
||
latest_status_tone: running > 0 ? 'running' : 'waiting',
|
||
business_summary: duty.result_text,
|
||
ai_summary: '当前由 qimingclaw adapter 生成,后续接入真实执行日志。',
|
||
};
|
||
});
|
||
}
|
||
|
||
function buildRunDetails(events: DigitalEmployeeWorkEvent[]): DigitalEmployeeRunDetail[] {
|
||
return events.map((event) => ({
|
||
id: `run-detail-${event.id}`,
|
||
time: event.time,
|
||
task_name: event.task_title,
|
||
step_name: event.step_title,
|
||
operation_name: event.plan_title,
|
||
status_label: event.status_label,
|
||
status_tone: event.status_tone,
|
||
business_detail: event.detail,
|
||
raw_detail: event.detail,
|
||
updated_at: event.updated_at,
|
||
conversation_id: event.conversation_id,
|
||
}));
|
||
}
|
||
|
||
function buildWorkday(date: string, snapshot: QimingclawSnapshot | null): DigitalEmployeeWorkdayProjection {
|
||
const state = readState();
|
||
const confirmedAt = state.confirmedDates[date] ?? null;
|
||
const confirmed = Boolean(confirmedAt);
|
||
const duties = buildDuties(state, date, snapshot);
|
||
const events = buildEvents(duties, confirmed, snapshot);
|
||
const executionSummaries = buildExecutionSummaries(duties, events);
|
||
const generatedAt = state.reportGeneratedAtByDate[date] ?? null;
|
||
const selectedCount = duties.filter((duty) => duty.selected).length;
|
||
const runningCount = events.filter((event) => event.status === 'running').length;
|
||
|
||
return {
|
||
source: 'live',
|
||
date,
|
||
employee: {
|
||
name: state.profile.name,
|
||
title: '数字员工',
|
||
major: 'qimingclaw 客户端任务执行',
|
||
company: state.profile.company,
|
||
position: state.profile.position,
|
||
current_status: state.profile.currentStatus,
|
||
current_status_label: state.profile.currentStatus === 'resting' ? '休息中' : '工作中',
|
||
motto: '承接客户端任务,调度 ACP 与 MCP 能力完成执行。',
|
||
},
|
||
workday: {
|
||
confirmed,
|
||
confirmed_at: confirmedAt,
|
||
current_status: state.profile.currentStatus,
|
||
current_status_label: state.profile.currentStatus === 'resting' ? '休息中' : '工作中',
|
||
status_label: confirmed ? '运行中' : '待确认',
|
||
status_tone: confirmed ? 'running' : 'waiting',
|
||
summary: confirmed
|
||
? `当前日期 ${displayDate(date)} 已确认 ${selectedCount} 项任务。`
|
||
: `当前日期 ${displayDate(date)} 待确认今日任务。`,
|
||
bubble_text: confirmed
|
||
? '我已接入 qimingclaw 客户端执行链路,正在等待真实 Run/Event 数据接入。'
|
||
: '请先确认今天要执行的任务,我会把它们映射到 qimingclaw 执行链路。',
|
||
},
|
||
overview: {
|
||
title: '今日执行',
|
||
status_label: confirmed ? '真实数据' : '待确认',
|
||
status_tone: confirmed ? 'success' : 'waiting',
|
||
metrics: [
|
||
{ label: '已选任务', value: String(selectedCount) },
|
||
{ label: '运行记录', value: String(events.length) },
|
||
{ label: '执行中', value: String(runningCount) },
|
||
{ label: '异常', value: '0' },
|
||
],
|
||
},
|
||
duties,
|
||
events,
|
||
execution_summaries: executionSummaries,
|
||
run_details: buildRunDetails(events),
|
||
decisions: [],
|
||
delivery: {
|
||
headline: confirmed ? 'qimingclaw 执行链路已接入' : '等待确认任务设置',
|
||
summary: '当前阶段已将数字员工页面切换到 qimingclaw adapter,并开始读取客户端服务状态。',
|
||
lines: [
|
||
'已吸收 sgRobot 数字员工 UI 和核心展示模型。',
|
||
'已建立 qimingclaw workday/plans/tasks/skills API adapter。',
|
||
serviceLine(snapshot, 'agent', 'Agent'),
|
||
serviceLine(snapshot, 'mcp', 'MCP'),
|
||
serviceLine(snapshot, 'computerServer', 'Computer Server'),
|
||
`活跃会话:${snapshot?.sessions.active ?? 0} / ${snapshot?.sessions.total ?? 0}`,
|
||
],
|
||
artifacts: [
|
||
{ name: '数字员工运行摘要', status: generatedAt ? 'ready' : 'pending' },
|
||
],
|
||
},
|
||
daily_report: {
|
||
date,
|
||
title: `${displayDate(date)} 飞天数字员工日报`,
|
||
report_schedule_time: state.reportScheduleTime,
|
||
status: generatedAt ? 'ready' : 'not_generated',
|
||
generated_at: generatedAt,
|
||
generated_by: generatedAt ? 'ai' : 'fallback',
|
||
model: 'qimingclaw-adapter',
|
||
source: 'qimingclaw',
|
||
summary: generatedAt
|
||
? '今日已完成数字员工页面到 qimingclaw adapter 的接入,并读取了 qimingclaw 服务状态。'
|
||
: '日报将在生成后汇总任务选择、运行明细和后续接入建议。',
|
||
totals: {
|
||
task_count: selectedCount,
|
||
execution_count: events.length,
|
||
success_count: 0,
|
||
failure_count: 0,
|
||
running_count: runningCount,
|
||
},
|
||
task_sections: executionSummaries,
|
||
detail_lines: events.map((event) => `${event.time} ${event.task_title}:${event.detail}`),
|
||
pdf_available: false,
|
||
pdf_url: null,
|
||
},
|
||
};
|
||
}
|
||
|
||
function handleWorkdayAction(body: DigitalEmployeeWorkdayActionRequest, snapshot: QimingclawSnapshot | null): DigitalEmployeeWorkdayProjection {
|
||
const state = readState();
|
||
const date = body.date || todayInputDate();
|
||
|
||
if (body.action === 'confirm_workday') {
|
||
state.selectedDutyIdsByDate[date] = body.duty_ids ?? [];
|
||
state.confirmedDates[date] = new Date().toISOString();
|
||
}
|
||
if (body.action === 'update_profile') {
|
||
state.profile = {
|
||
name: body.name || state.profile.name,
|
||
company: body.company || state.profile.company,
|
||
position: body.position || state.profile.position,
|
||
currentStatus: body.current_status || state.profile.currentStatus,
|
||
};
|
||
}
|
||
if (body.action === 'update_report_schedule') {
|
||
state.reportScheduleTime = body.report_schedule_time || state.reportScheduleTime;
|
||
}
|
||
if (body.action === 'generate_daily_report') {
|
||
if (body.duty_ids) state.selectedDutyIdsByDate[date] = body.duty_ids;
|
||
if (body.report_schedule_time) state.reportScheduleTime = body.report_schedule_time;
|
||
state.reportGeneratedAtByDate[date] = new Date().toISOString();
|
||
}
|
||
|
||
writeState(state);
|
||
return buildWorkday(date, snapshot);
|
||
}
|
||
|
||
function buildPlanFlow(planId: string, snapshot: QimingclawSnapshot | null = null): FlowResponse {
|
||
const plan = withRuntimePlan(PLANS.find((candidate) => candidate.plan_id === planId) ?? PLANS[0]!, snapshot);
|
||
const timestamp = new Date().toISOString();
|
||
return {
|
||
plan_id: plan.plan_id,
|
||
plan_title: plan.title,
|
||
plan_status: plan.status,
|
||
plan_objective: plan.objective,
|
||
lanes: [
|
||
{ key: 'intent', label: '目标', color: '#38bdf8' },
|
||
{ key: 'qimingclaw', label: 'qimingclaw', color: '#22c55e' },
|
||
{ key: 'execution', label: '执行', color: '#f59e0b' },
|
||
],
|
||
steps: [
|
||
{
|
||
seq: 1,
|
||
lane: 'intent',
|
||
label: plan.title,
|
||
sub_label: plan.objective,
|
||
timestamp: plan.created_at,
|
||
color: '#38bdf8',
|
||
source_type: 'plan',
|
||
},
|
||
{
|
||
seq: 2,
|
||
lane: 'qimingclaw',
|
||
label: '接入客户端能力',
|
||
sub_label: 'ACP / MCP / computer server adapter',
|
||
timestamp,
|
||
color: '#22c55e',
|
||
source_type: 'adapter',
|
||
},
|
||
{
|
||
seq: 3,
|
||
lane: 'execution',
|
||
label: plan.status === 'running' ? '已读取 qimingclaw 服务状态' : '等待调度',
|
||
sub_label: planResultText(plan, snapshot),
|
||
timestamp,
|
||
color: '#f59e0b',
|
||
source_type: 'run',
|
||
},
|
||
],
|
||
events: [
|
||
{
|
||
event_id: `${plan.plan_id}-adapter-event`,
|
||
kind: 'qimingclaw_adapter',
|
||
occurred_at: timestamp,
|
||
message: '数字员工页面已通过 qimingclaw adapter 获取数据。',
|
||
},
|
||
],
|
||
};
|
||
}
|
||
|
||
function dispatchPlan(planId: string): PlanDispatchResponse {
|
||
const state = readState();
|
||
const date = todayInputDate();
|
||
const selected = new Set(state.selectedDutyIdsByDate[date] ?? []);
|
||
selected.add(planId);
|
||
state.selectedDutyIdsByDate[date] = [...selected];
|
||
state.confirmedDates[date] = state.confirmedDates[date] ?? new Date().toISOString();
|
||
writeState(state);
|
||
|
||
return {
|
||
ok: true,
|
||
plan_id: planId,
|
||
source_plan_id: planId,
|
||
dispatched_tasks: buildTasks(planId).map((task) => ({
|
||
task_id: task.task_id,
|
||
title: task.title,
|
||
status: task.status === 'pending' ? 'running' : task.status,
|
||
})),
|
||
};
|
||
}
|
||
|
||
function buildPlanMessages(planId: string): Array<{ role: string; content: string; created_at?: string | null }> {
|
||
const plan = PLANS.find((candidate) => candidate.plan_id === planId) ?? PLANS[0]!;
|
||
return [
|
||
{
|
||
role: 'user',
|
||
content: `执行数字员工任务:${plan.title}`,
|
||
created_at: plan.created_at,
|
||
},
|
||
{
|
||
role: 'assistant',
|
||
content: `已接入 qimingclaw adapter。目标:${plan.objective ?? plan.title}`,
|
||
created_at: new Date().toISOString(),
|
||
},
|
||
];
|
||
}
|