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/main.tsx` mounts the digital employee page directly at `#/digital`.
|
||||||
- `src/lib/basePath.ts` fixes static assets under `/sgrobot-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/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.
|
- `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.
|
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';
|
} from '../types/api';
|
||||||
import { clearToken, getToken, setToken } from './auth';
|
import { clearToken, getToken, setToken } from './auth';
|
||||||
import { apiOrigin, apiBase } from './basePath';
|
import { apiOrigin, apiBase } from './basePath';
|
||||||
|
import { handleQimingclawDigitalApi } from './qimingclawAdapter';
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Base fetch wrapper
|
// Base fetch wrapper
|
||||||
@@ -55,6 +56,11 @@ export async function apiFetch<T = unknown>(
|
|||||||
path: string,
|
path: string,
|
||||||
options: ApiFetchOptions = {},
|
options: ApiFetchOptions = {},
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
|
const adapterData = await handleQimingclawDigitalApi<T>(path, options);
|
||||||
|
if (adapterData !== undefined) {
|
||||||
|
return adapterData;
|
||||||
|
}
|
||||||
|
|
||||||
const { timeoutMs = API_FETCH_TIMEOUT_MS, ...fetchOptions } = options;
|
const { timeoutMs = API_FETCH_TIMEOUT_MS, ...fetchOptions } = options;
|
||||||
const token = getToken();
|
const token = getToken();
|
||||||
const headers = new Headers(fetchOptions.headers);
|
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(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -13,7 +13,7 @@
|
|||||||
console.warn("[qimingclaw] digital employee auth bootstrap skipped", error);
|
console.warn("[qimingclaw] digital employee auth bootstrap skipped", error);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<script type="module" crossorigin src="./assets/index-DX0h8EMp.js"></script>
|
<script type="module" crossorigin src="./assets/index-Brx6-oKs.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="./assets/index-DqV9Vw1H.css">
|
<link rel="stylesheet" crossorigin href="./assets/index-DqV9Vw1H.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -47,6 +47,24 @@ function stripTauriWindowImport() {
|
|||||||
fs.writeFileSync(shellPath, source);
|
fs.writeFileSync(shellPath, source);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function injectQimingclawApiAdapter() {
|
||||||
|
const apiPath = path.join(targetRoot, 'src/lib/api.ts');
|
||||||
|
let source = fs.readFileSync(apiPath, 'utf8');
|
||||||
|
if (!source.includes("import { handleQimingclawDigitalApi } from './qimingclawAdapter';")) {
|
||||||
|
source = source.replace(
|
||||||
|
"import { apiOrigin, apiBase } from './basePath';\n",
|
||||||
|
"import { apiOrigin, apiBase } from './basePath';\nimport { handleQimingclawDigitalApi } from './qimingclawAdapter';\n",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!source.includes('const adapterData = await handleQimingclawDigitalApi<T>(path, options);')) {
|
||||||
|
source = source.replace(
|
||||||
|
"): Promise<T> {\n const { timeoutMs = API_FETCH_TIMEOUT_MS, ...fetchOptions } = options;",
|
||||||
|
"): Promise<T> {\n const adapterData = await handleQimingclawDigitalApi<T>(path, options);\n if (adapterData !== undefined) {\n return adapterData;\n }\n\n const { timeoutMs = API_FETCH_TIMEOUT_MS, ...fetchOptions } = options;",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
fs.writeFileSync(apiPath, source);
|
||||||
|
}
|
||||||
|
|
||||||
function main() {
|
function main() {
|
||||||
assertExists(sourceRoot);
|
assertExists(sourceRoot);
|
||||||
assertExists(targetRoot);
|
assertExists(targetRoot);
|
||||||
@@ -56,6 +74,7 @@ function main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
stripTauriWindowImport();
|
stripTauriWindowImport();
|
||||||
|
injectQimingclawApiAdapter();
|
||||||
|
|
||||||
console.log(`Synced sgRobot digital UI from ${sourceRoot}`);
|
console.log(`Synced sgRobot digital UI from ${sourceRoot}`);
|
||||||
console.log(`Target: ${targetRoot}`);
|
console.log(`Target: ${targetRoot}`);
|
||||||
|
|||||||
@@ -224,6 +224,49 @@ qimingclaw 专用 patch:
|
|||||||
- qimingclaw 还没有正式落地 Plan / Task / Run / Event / Artifact / Approval 数据模型。
|
- qimingclaw 还没有正式落地 Plan / Task / Run / Event / Artifact / Approval 数据模型。
|
||||||
- 下一阶段应开始建设 qimingclaw 本地数字员工 API 适配层。
|
- 下一阶段应开始建设 qimingclaw 本地数字员工 API 适配层。
|
||||||
|
|
||||||
|
## Phase 2:qimingclaw 数字员工 API Adapter(已开始)
|
||||||
|
|
||||||
|
目标:让数字员工页面开始消费 qimingclaw 语义的数据,而不是完全依赖 sgRobot demo fallback。
|
||||||
|
|
||||||
|
当前 qimingclaw 已新增:
|
||||||
|
|
||||||
|
```text
|
||||||
|
crates/agent-electron-client/embedded/sgrobot-digital/src/lib/qimingclawAdapter.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
当前覆盖的页面接口:
|
||||||
|
|
||||||
|
```text
|
||||||
|
GET /api/digital-employee/workday
|
||||||
|
POST /api/digital-employee/workday/actions
|
||||||
|
GET /api/scheduler/jobs
|
||||||
|
GET /api/debug/plans
|
||||||
|
GET /api/debug/tasks
|
||||||
|
GET /api/debug/plan/:planId/flow
|
||||||
|
POST /api/debug/plan/:planId/dispatch
|
||||||
|
GET /api/plan/:planId/messages
|
||||||
|
GET /api/skill
|
||||||
|
POST /api/skill/:skillId/enabled
|
||||||
|
```
|
||||||
|
|
||||||
|
当前数据来源:
|
||||||
|
|
||||||
|
- qimingclaw adapter 内置的客户端任务职责。
|
||||||
|
- `localStorage` 保存的今日任务选择、特性设置、日报生成状态。
|
||||||
|
- qimingclaw 语义的 ACP / MCP / computer server / artifact-report 技能定义。
|
||||||
|
|
||||||
|
当前页面表现:
|
||||||
|
|
||||||
|
- `source` 返回 `live`,数字员工首页会使用真实 adapter 数据通道。
|
||||||
|
- 今日任务从“白领一天 demo”切换为 qimingclaw 客户端职责。
|
||||||
|
- 任务中心、技能库、运行明细、日报预览开始使用 qimingclaw adapter 数据。
|
||||||
|
|
||||||
|
下一步:
|
||||||
|
|
||||||
|
- 将 adapter 的静态职责替换为 qimingclaw 主进程 IPC 数据源。
|
||||||
|
- 新增本地数字员工状态服务,持久化 Plan / Task / Run / Event / Artifact / Approval。
|
||||||
|
- 把 `/computer/chat`、ACP session、MCP tool status 写入 Run/Event。
|
||||||
|
|
||||||
### 产出
|
### 产出
|
||||||
|
|
||||||
- 数字员工页面可以从源码重新构建。
|
- 数字员工页面可以从源码重新构建。
|
||||||
|
|||||||
Reference in New Issue
Block a user