feat(client): add qimingclaw digital adapter

This commit is contained in:
baiyanyun
2026-06-05 15:45:55 +08:00
parent 2eb9a99da3
commit 31b4b5a281
8 changed files with 865 additions and 117 deletions

View File

@@ -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.

View File

@@ -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);

View File

@@ -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

View File

@@ -13,7 +13,7 @@
console.warn("[qimingclaw] digital employee auth bootstrap skipped", error);
}
</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">
</head>
<body>

View File

@@ -47,6 +47,24 @@ function stripTauriWindowImport() {
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() {
assertExists(sourceRoot);
assertExists(targetRoot);
@@ -56,6 +74,7 @@ function main() {
}
stripTauriWindowImport();
injectQimingclawApiAdapter();
console.log(`Synced sgRobot digital UI from ${sourceRoot}`);
console.log(`Target: ${targetRoot}`);

View File

@@ -224,6 +224,49 @@ qimingclaw 专用 patch
- qimingclaw 还没有正式落地 Plan / Task / Run / Event / Artifact / Approval 数据模型。
- 下一阶段应开始建设 qimingclaw 本地数字员工 API 适配层。
## Phase 2qimingclaw 数字员工 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。
### 产出
- 数字员工页面可以从源码重新构建。