feat(client): add qimingclaw digital adapter
This commit is contained in:
@@ -5,6 +5,8 @@ This directory records qimingclaw-only adaptations around the sgRobot digital em
|
||||
- `src/main.tsx` mounts the digital employee page directly at `#/digital`.
|
||||
- `src/lib/basePath.ts` fixes static assets under `/sgrobot-digital`.
|
||||
- `src/lib/tauri.ts` disables sgRobot/Tauri gateway detection inside the Electron webview.
|
||||
- `src/lib/qimingclawAdapter.ts` provides the first qimingclaw-native digital employee API projection.
|
||||
- `src/lib/api.ts` calls the qimingclaw adapter before falling back to normal HTTP fetch.
|
||||
- `index.html` injects a local token so copied sgRobot pages do not show the pairing screen.
|
||||
|
||||
Run `npm run sync:sgrobot-digital` from `crates/agent-electron-client` after changing the upstream sgRobot digital UI.
|
||||
|
||||
@@ -32,6 +32,7 @@ import type {
|
||||
} from '../types/api';
|
||||
import { clearToken, getToken, setToken } from './auth';
|
||||
import { apiOrigin, apiBase } from './basePath';
|
||||
import { handleQimingclawDigitalApi } from './qimingclawAdapter';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Base fetch wrapper
|
||||
@@ -55,6 +56,11 @@ export async function apiFetch<T = unknown>(
|
||||
path: string,
|
||||
options: ApiFetchOptions = {},
|
||||
): Promise<T> {
|
||||
const adapterData = await handleQimingclawDigitalApi<T>(path, options);
|
||||
if (adapterData !== undefined) {
|
||||
return adapterData;
|
||||
}
|
||||
|
||||
const { timeoutMs = API_FETCH_TIMEOUT_MS, ...fetchOptions } = options;
|
||||
const token = getToken();
|
||||
const headers = new Headers(fetchOptions.headers);
|
||||
|
||||
@@ -0,0 +1,678 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
};
|
||||
}
|
||||
|
||||
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()) as T;
|
||||
}
|
||||
if (method === 'POST' && pathname === '/api/digital-employee/workday/actions') {
|
||||
return handleWorkdayAction(parseJsonBody<DigitalEmployeeWorkdayActionRequest>(options.body)) 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')) } as T;
|
||||
}
|
||||
if (method === 'GET' && pathname === '/api/debug/tasks') {
|
||||
const planId = url.searchParams.get('plan_id');
|
||||
return buildTasksResponse(planId) as T;
|
||||
}
|
||||
if (method === 'GET' && pathname.startsWith('/api/debug/plan/') && pathname.endsWith('/flow')) {
|
||||
return buildPlanFlow(decodeURIComponent(pathname.split('/')[4] || '')) 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));
|
||||
}
|
||||
|
||||
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 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): DigitalEmployeeDuty[] {
|
||||
const selected = selectedSetForDate(state, date);
|
||||
return PLANS.map((plan) => {
|
||||
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: plan.status === 'running' ? '正在同步客户端运行状态' : '等待任务执行',
|
||||
conversation_id: plan.plan_id,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function buildTasks(planId?: string | null): DebugTask[] {
|
||||
return PLANS
|
||||
.filter((plan) => !planId || plan.plan_id === planId)
|
||||
.flatMap((plan) => (plan.steps ?? []).flatMap((step) => (step.tasks ?? []).map((task) => ({
|
||||
task_id: task.task_id,
|
||||
title: task.title,
|
||||
status: 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): { tasks: DebugTask[]; runs: DebugRun[]; total: number; truncated: boolean } {
|
||||
const tasks = buildTasks(planId);
|
||||
return { tasks, runs: buildRuns(tasks), total: tasks.length, truncated: false };
|
||||
}
|
||||
|
||||
function filterPlans(query?: string | null): DebugPlan[] {
|
||||
const normalized = (query ?? '').trim().toLowerCase();
|
||||
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): 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, index) => ({
|
||||
id: `event-${duty.id}`,
|
||||
time: index === 0 ? '09:05' : '待执行',
|
||||
plan_title: duty.title,
|
||||
task_title: duty.title,
|
||||
step_title: index === 0 ? '接入 qimingclaw 执行链路' : '等待调度',
|
||||
detail: index === 0
|
||||
? '已接入 qimingclaw 客户端服务状态与执行入口,等待后续绑定真实 Run 事件。'
|
||||
: '任务已确认,将在后续阶段接入 ACP/MCP 真实执行事件。',
|
||||
status: index === 0 ? 'running' : 'pending',
|
||||
status_label: index === 0 ? '执行中' : '待执行',
|
||||
status_tone: index === 0 ? '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): DigitalEmployeeWorkdayProjection {
|
||||
const state = readState();
|
||||
const confirmedAt = state.confirmedDates[date] ?? null;
|
||||
const confirmed = Boolean(confirmedAt);
|
||||
const duties = buildDuties(state, date);
|
||||
const events = buildEvents(duties, confirmed);
|
||||
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,下一阶段接入真实 ACP/MCP Run 事件。',
|
||||
lines: [
|
||||
'已吸收 sgRobot 数字员工 UI 和核心展示模型。',
|
||||
'已建立 qimingclaw workday/plans/tasks/skills API adapter。',
|
||||
'后续将把客户端服务状态、会话和执行事件写入真实状态模型。',
|
||||
],
|
||||
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 的接入,可继续接入真实执行事件。'
|
||||
: '日报将在生成后汇总任务选择、运行明细和后续接入建议。',
|
||||
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): 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);
|
||||
}
|
||||
|
||||
function buildPlanFlow(planId: string): FlowResponse {
|
||||
const plan = PLANS.find((candidate) => candidate.plan_id === planId) ?? PLANS[0]!;
|
||||
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' ? '等待真实 Run 事件' : '等待调度',
|
||||
sub_label: '后续阶段接入 qimingclaw 本地状态模型',
|
||||
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(),
|
||||
},
|
||||
];
|
||||
}
|
||||
Reference in New Issue
Block a user