feat(client): bridge digital employee status
This commit is contained in:
@@ -8,5 +8,6 @@ This directory records qimingclaw-only adaptations around the sgRobot digital em
|
|||||||
- `src/lib/qimingclawAdapter.ts` provides the first qimingclaw-native digital employee API projection.
|
- `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.
|
- `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.
|
||||||
|
- `window.QimingClawBridge.digital.getSnapshot()` is read when the page runs inside qimingclaw's Electron webview.
|
||||||
|
|
||||||
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.
|
||||||
|
|||||||
@@ -17,6 +17,11 @@ import type {
|
|||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
__QIMINGCLAW_EMBEDDED_DIGITAL__?: boolean;
|
__QIMINGCLAW_EMBEDDED_DIGITAL__?: boolean;
|
||||||
|
QimingClawBridge?: {
|
||||||
|
digital?: {
|
||||||
|
getSnapshot?: () => Promise<QimingclawSnapshot>;
|
||||||
|
};
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,6 +41,25 @@ interface AdapterState {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface QimingclawServiceStatus {
|
||||||
|
running?: boolean;
|
||||||
|
error?: string;
|
||||||
|
engineType?: string;
|
||||||
|
port?: number;
|
||||||
|
serverCount?: number;
|
||||||
|
serverNames?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
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[] = [
|
const SKILLS: SkillSpec[] = [
|
||||||
{
|
{
|
||||||
skill_id: 'qimingclaw-acp-session',
|
skill_id: 'qimingclaw-acp-session',
|
||||||
@@ -180,23 +204,23 @@ export async function handleQimingclawDigitalApi<T>(
|
|||||||
const pathname = url.pathname;
|
const pathname = url.pathname;
|
||||||
|
|
||||||
if (method === 'GET' && pathname === '/api/digital-employee/workday') {
|
if (method === 'GET' && pathname === '/api/digital-employee/workday') {
|
||||||
return buildWorkday(url.searchParams.get('date') || todayInputDate()) as T;
|
return buildWorkday(url.searchParams.get('date') || todayInputDate(), await readQimingclawSnapshot()) as T;
|
||||||
}
|
}
|
||||||
if (method === 'POST' && pathname === '/api/digital-employee/workday/actions') {
|
if (method === 'POST' && pathname === '/api/digital-employee/workday/actions') {
|
||||||
return handleWorkdayAction(parseJsonBody<DigitalEmployeeWorkdayActionRequest>(options.body)) as T;
|
return handleWorkdayAction(parseJsonBody<DigitalEmployeeWorkdayActionRequest>(options.body), await readQimingclawSnapshot()) as T;
|
||||||
}
|
}
|
||||||
if (method === 'GET' && pathname === '/api/scheduler/jobs') {
|
if (method === 'GET' && pathname === '/api/scheduler/jobs') {
|
||||||
return { jobs: buildSchedulerJobs() } as T;
|
return { jobs: buildSchedulerJobs() } as T;
|
||||||
}
|
}
|
||||||
if (method === 'GET' && pathname === '/api/debug/plans') {
|
if (method === 'GET' && pathname === '/api/debug/plans') {
|
||||||
return { plans: filterPlans(url.searchParams.get('q')) } as T;
|
return { plans: filterPlans(url.searchParams.get('q'), await readQimingclawSnapshot()) } as T;
|
||||||
}
|
}
|
||||||
if (method === 'GET' && pathname === '/api/debug/tasks') {
|
if (method === 'GET' && pathname === '/api/debug/tasks') {
|
||||||
const planId = url.searchParams.get('plan_id');
|
const planId = url.searchParams.get('plan_id');
|
||||||
return buildTasksResponse(planId) as T;
|
return buildTasksResponse(planId, await readQimingclawSnapshot()) as T;
|
||||||
}
|
}
|
||||||
if (method === 'GET' && pathname.startsWith('/api/debug/plan/') && pathname.endsWith('/flow')) {
|
if (method === 'GET' && pathname.startsWith('/api/debug/plan/') && pathname.endsWith('/flow')) {
|
||||||
return buildPlanFlow(decodeURIComponent(pathname.split('/')[4] || '')) as T;
|
return buildPlanFlow(decodeURIComponent(pathname.split('/')[4] || ''), await readQimingclawSnapshot()) as T;
|
||||||
}
|
}
|
||||||
if (method === 'POST' && pathname.startsWith('/api/debug/plan/') && pathname.endsWith('/dispatch')) {
|
if (method === 'POST' && pathname.startsWith('/api/debug/plan/') && pathname.endsWith('/dispatch')) {
|
||||||
return dispatchPlan(decodeURIComponent(pathname.split('/')[4] || '')) as T;
|
return dispatchPlan(decodeURIComponent(pathname.split('/')[4] || '')) as T;
|
||||||
@@ -244,6 +268,14 @@ function writeState(state: AdapterState): void {
|
|||||||
window.localStorage.setItem(STATE_KEY, JSON.stringify(state));
|
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 {
|
function parseJsonBody<T>(body: BodyInit | null | undefined): T {
|
||||||
if (typeof body !== 'string' || !body.trim()) return {} as T;
|
if (typeof body !== 'string' || !body.trim()) return {} as T;
|
||||||
return JSON.parse(body) as T;
|
return JSON.parse(body) as T;
|
||||||
@@ -279,6 +311,43 @@ function planStatusTone(status: string): string {
|
|||||||
return 'waiting';
|
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 {
|
function planStatusLabel(status: string): string {
|
||||||
if (status === 'running') return '执行中';
|
if (status === 'running') return '执行中';
|
||||||
if (status === 'completed' || status === 'succeeded') return '已完成';
|
if (status === 'completed' || status === 'succeeded') return '已完成';
|
||||||
@@ -313,9 +382,10 @@ function buildSchedulerJobs(): CronJob[] {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildDuties(state: AdapterState, date: string): DigitalEmployeeDuty[] {
|
function buildDuties(state: AdapterState, date: string, snapshot: QimingclawSnapshot | null): DigitalEmployeeDuty[] {
|
||||||
const selected = selectedSetForDate(state, date);
|
const selected = selectedSetForDate(state, date);
|
||||||
return PLANS.map((plan) => {
|
return PLANS.map((rawPlan) => {
|
||||||
|
const plan = withRuntimePlan(rawPlan, snapshot);
|
||||||
const schedule = PLAN_SCHEDULES[plan.plan_id] ?? PLAN_SCHEDULES['qimingclaw-client-readiness']!;
|
const schedule = PLAN_SCHEDULES[plan.plan_id] ?? PLAN_SCHEDULES['qimingclaw-client-readiness']!;
|
||||||
return {
|
return {
|
||||||
id: plan.plan_id,
|
id: plan.plan_id,
|
||||||
@@ -328,19 +398,38 @@ function buildDuties(state: AdapterState, date: string): DigitalEmployeeDuty[] {
|
|||||||
status_label: planStatusLabel(plan.status),
|
status_label: planStatusLabel(plan.status),
|
||||||
status_tone: planStatusTone(plan.status),
|
status_tone: planStatusTone(plan.status),
|
||||||
schedule_text: schedule.scheduleText,
|
schedule_text: schedule.scheduleText,
|
||||||
result_text: plan.status === 'running' ? '正在同步客户端运行状态' : '等待任务执行',
|
result_text: planResultText(plan, snapshot),
|
||||||
conversation_id: plan.plan_id,
|
conversation_id: plan.plan_id,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildTasks(planId?: string | null): DebugTask[] {
|
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
|
return PLANS
|
||||||
.filter((plan) => !planId || plan.plan_id === planId)
|
.filter((plan) => !planId || plan.plan_id === planId)
|
||||||
|
.map((plan) => withRuntimePlan(plan, snapshot))
|
||||||
.flatMap((plan) => (plan.steps ?? []).flatMap((step) => (step.tasks ?? []).map((task) => ({
|
.flatMap((plan) => (plan.steps ?? []).flatMap((step) => (step.tasks ?? []).map((task) => ({
|
||||||
task_id: task.task_id,
|
task_id: task.task_id,
|
||||||
title: task.title,
|
title: task.title,
|
||||||
status: task.status,
|
status: plan.status === 'running' && task.status === 'pending' ? 'running' : task.status,
|
||||||
plan_id: plan.plan_id,
|
plan_id: plan.plan_id,
|
||||||
step_id: step.step_id,
|
step_id: step.step_id,
|
||||||
assigned_skill_id: step.preferred_skills?.[0] ?? '',
|
assigned_skill_id: step.preferred_skills?.[0] ?? '',
|
||||||
@@ -375,22 +464,23 @@ function buildRuns(tasks: DebugTask[]): DebugRun[] {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildTasksResponse(planId?: string | null): { tasks: DebugTask[]; runs: DebugRun[]; total: number; truncated: boolean } {
|
function buildTasksResponse(planId?: string | null, snapshot: QimingclawSnapshot | null = null): { tasks: DebugTask[]; runs: DebugRun[]; total: number; truncated: boolean } {
|
||||||
const tasks = buildTasks(planId);
|
const tasks = buildTasks(planId, snapshot);
|
||||||
return { tasks, runs: buildRuns(tasks), total: tasks.length, truncated: false };
|
return { tasks, runs: buildRuns(tasks), total: tasks.length, truncated: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
function filterPlans(query?: string | null): DebugPlan[] {
|
function filterPlans(query?: string | null, snapshot: QimingclawSnapshot | null = null): DebugPlan[] {
|
||||||
const normalized = (query ?? '').trim().toLowerCase();
|
const normalized = (query ?? '').trim().toLowerCase();
|
||||||
if (!normalized) return PLANS;
|
const plans = PLANS.map((plan) => withRuntimePlan(plan, snapshot));
|
||||||
return PLANS.filter((plan) => (
|
if (!normalized) return plans;
|
||||||
|
return plans.filter((plan) => (
|
||||||
plan.title.toLowerCase().includes(normalized)
|
plan.title.toLowerCase().includes(normalized)
|
||||||
|| plan.plan_id.toLowerCase().includes(normalized)
|
|| plan.plan_id.toLowerCase().includes(normalized)
|
||||||
|| (plan.objective ?? '').toLowerCase().includes(normalized)
|
|| (plan.objective ?? '').toLowerCase().includes(normalized)
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildEvents(duties: DigitalEmployeeDuty[], confirmed: boolean): DigitalEmployeeWorkEvent[] {
|
function buildEvents(duties: DigitalEmployeeDuty[], confirmed: boolean, snapshot: QimingclawSnapshot | null): DigitalEmployeeWorkEvent[] {
|
||||||
if (!confirmed) {
|
if (!confirmed) {
|
||||||
return [{
|
return [{
|
||||||
id: 'event-await-confirmation',
|
id: 'event-await-confirmation',
|
||||||
@@ -408,18 +498,18 @@ function buildEvents(duties: DigitalEmployeeDuty[], confirmed: boolean): Digital
|
|||||||
}
|
}
|
||||||
|
|
||||||
const selectedDuties = duties.filter((duty) => duty.selected);
|
const selectedDuties = duties.filter((duty) => duty.selected);
|
||||||
return selectedDuties.map((duty, index) => ({
|
return selectedDuties.map((duty) => ({
|
||||||
id: `event-${duty.id}`,
|
id: `event-${duty.id}`,
|
||||||
time: index === 0 ? '09:05' : '待执行',
|
time: duty.status === 'running' ? '实时' : '待执行',
|
||||||
plan_title: duty.title,
|
plan_title: duty.title,
|
||||||
task_title: duty.title,
|
task_title: duty.title,
|
||||||
step_title: index === 0 ? '接入 qimingclaw 执行链路' : '等待调度',
|
step_title: duty.status === 'running' ? '同步 qimingclaw 服务状态' : '等待调度',
|
||||||
detail: index === 0
|
detail: duty.status === 'running'
|
||||||
? '已接入 qimingclaw 客户端服务状态与执行入口,等待后续绑定真实 Run 事件。'
|
? `${duty.result_text}。快照时间:${snapshot?.generatedAt ?? '未连接 bridge'}`
|
||||||
: '任务已确认,将在后续阶段接入 ACP/MCP 真实执行事件。',
|
: '任务已确认,将在后续阶段接入 ACP/MCP 真实执行事件。',
|
||||||
status: index === 0 ? 'running' : 'pending',
|
status: duty.status === 'running' ? 'running' : 'pending',
|
||||||
status_label: index === 0 ? '执行中' : '待执行',
|
status_label: duty.status === 'running' ? '执行中' : '待执行',
|
||||||
status_tone: index === 0 ? 'running' : 'waiting',
|
status_tone: duty.status === 'running' ? 'running' : 'waiting',
|
||||||
updated_at: new Date().toISOString(),
|
updated_at: new Date().toISOString(),
|
||||||
conversation_id: duty.conversation_id,
|
conversation_id: duty.conversation_id,
|
||||||
}));
|
}));
|
||||||
@@ -465,12 +555,12 @@ function buildRunDetails(events: DigitalEmployeeWorkEvent[]): DigitalEmployeeRun
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildWorkday(date: string): DigitalEmployeeWorkdayProjection {
|
function buildWorkday(date: string, snapshot: QimingclawSnapshot | null): DigitalEmployeeWorkdayProjection {
|
||||||
const state = readState();
|
const state = readState();
|
||||||
const confirmedAt = state.confirmedDates[date] ?? null;
|
const confirmedAt = state.confirmedDates[date] ?? null;
|
||||||
const confirmed = Boolean(confirmedAt);
|
const confirmed = Boolean(confirmedAt);
|
||||||
const duties = buildDuties(state, date);
|
const duties = buildDuties(state, date, snapshot);
|
||||||
const events = buildEvents(duties, confirmed);
|
const events = buildEvents(duties, confirmed, snapshot);
|
||||||
const executionSummaries = buildExecutionSummaries(duties, events);
|
const executionSummaries = buildExecutionSummaries(duties, events);
|
||||||
const generatedAt = state.reportGeneratedAtByDate[date] ?? null;
|
const generatedAt = state.reportGeneratedAtByDate[date] ?? null;
|
||||||
const selectedCount = duties.filter((duty) => duty.selected).length;
|
const selectedCount = duties.filter((duty) => duty.selected).length;
|
||||||
@@ -521,11 +611,14 @@ function buildWorkday(date: string): DigitalEmployeeWorkdayProjection {
|
|||||||
decisions: [],
|
decisions: [],
|
||||||
delivery: {
|
delivery: {
|
||||||
headline: confirmed ? 'qimingclaw 执行链路已接入' : '等待确认任务设置',
|
headline: confirmed ? 'qimingclaw 执行链路已接入' : '等待确认任务设置',
|
||||||
summary: '当前阶段已将数字员工页面切换到 qimingclaw adapter,下一阶段接入真实 ACP/MCP Run 事件。',
|
summary: '当前阶段已将数字员工页面切换到 qimingclaw adapter,并开始读取客户端服务状态。',
|
||||||
lines: [
|
lines: [
|
||||||
'已吸收 sgRobot 数字员工 UI 和核心展示模型。',
|
'已吸收 sgRobot 数字员工 UI 和核心展示模型。',
|
||||||
'已建立 qimingclaw workday/plans/tasks/skills API adapter。',
|
'已建立 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: [
|
artifacts: [
|
||||||
{ name: '数字员工运行摘要', status: generatedAt ? 'ready' : 'pending' },
|
{ name: '数字员工运行摘要', status: generatedAt ? 'ready' : 'pending' },
|
||||||
@@ -541,7 +634,7 @@ function buildWorkday(date: string): DigitalEmployeeWorkdayProjection {
|
|||||||
model: 'qimingclaw-adapter',
|
model: 'qimingclaw-adapter',
|
||||||
source: 'qimingclaw',
|
source: 'qimingclaw',
|
||||||
summary: generatedAt
|
summary: generatedAt
|
||||||
? '今日已完成数字员工页面到 qimingclaw adapter 的接入,可继续接入真实执行事件。'
|
? '今日已完成数字员工页面到 qimingclaw adapter 的接入,并读取了 qimingclaw 服务状态。'
|
||||||
: '日报将在生成后汇总任务选择、运行明细和后续接入建议。',
|
: '日报将在生成后汇总任务选择、运行明细和后续接入建议。',
|
||||||
totals: {
|
totals: {
|
||||||
task_count: selectedCount,
|
task_count: selectedCount,
|
||||||
@@ -558,7 +651,7 @@ function buildWorkday(date: string): DigitalEmployeeWorkdayProjection {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleWorkdayAction(body: DigitalEmployeeWorkdayActionRequest): DigitalEmployeeWorkdayProjection {
|
function handleWorkdayAction(body: DigitalEmployeeWorkdayActionRequest, snapshot: QimingclawSnapshot | null): DigitalEmployeeWorkdayProjection {
|
||||||
const state = readState();
|
const state = readState();
|
||||||
const date = body.date || todayInputDate();
|
const date = body.date || todayInputDate();
|
||||||
|
|
||||||
@@ -584,11 +677,11 @@ function handleWorkdayAction(body: DigitalEmployeeWorkdayActionRequest): Digital
|
|||||||
}
|
}
|
||||||
|
|
||||||
writeState(state);
|
writeState(state);
|
||||||
return buildWorkday(date);
|
return buildWorkday(date, snapshot);
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildPlanFlow(planId: string): FlowResponse {
|
function buildPlanFlow(planId: string, snapshot: QimingclawSnapshot | null = null): FlowResponse {
|
||||||
const plan = PLANS.find((candidate) => candidate.plan_id === planId) ?? PLANS[0]!;
|
const plan = withRuntimePlan(PLANS.find((candidate) => candidate.plan_id === planId) ?? PLANS[0]!, snapshot);
|
||||||
const timestamp = new Date().toISOString();
|
const timestamp = new Date().toISOString();
|
||||||
return {
|
return {
|
||||||
plan_id: plan.plan_id,
|
plan_id: plan.plan_id,
|
||||||
@@ -622,8 +715,8 @@ function buildPlanFlow(planId: string): FlowResponse {
|
|||||||
{
|
{
|
||||||
seq: 3,
|
seq: 3,
|
||||||
lane: 'execution',
|
lane: 'execution',
|
||||||
label: plan.status === 'running' ? '等待真实 Run 事件' : '等待调度',
|
label: plan.status === 'running' ? '已读取 qimingclaw 服务状态' : '等待调度',
|
||||||
sub_label: '后续阶段接入 qimingclaw 本地状态模型',
|
sub_label: planResultText(plan, snapshot),
|
||||||
timestamp,
|
timestamp,
|
||||||
color: '#f59e0b',
|
color: '#f59e0b',
|
||||||
source_type: 'run',
|
source_type: 'run',
|
||||||
|
|||||||
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-Brx6-oKs.js"></script>
|
<script type="module" crossorigin src="./assets/index-DcEd9LJg.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="./assets/index-DqV9Vw1H.css">
|
<link rel="stylesheet" crossorigin href="./assets/index-DqV9Vw1H.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -166,7 +166,8 @@ const WEBVIEW_PERF_BRIDGE_PRELOAD = path.join(
|
|||||||
function shouldInjectWebviewPerfBridge(url: string): boolean {
|
function shouldInjectWebviewPerfBridge(url: string): boolean {
|
||||||
try {
|
try {
|
||||||
const parsed = new URL(url);
|
const parsed = new URL(url);
|
||||||
return /^https?:$/.test(parsed.protocol);
|
if (/^https?:$/.test(parsed.protocol)) return true;
|
||||||
|
return parsed.protocol === "file:" && parsed.pathname.includes("/sgrobot-digital/");
|
||||||
} catch {
|
} catch {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { contextBridge, ipcRenderer } from "electron";
|
import { contextBridge, ipcRenderer } from "electron";
|
||||||
|
|
||||||
type PerfPayload = Record<string, unknown>;
|
type PerfPayload = Record<string, unknown>;
|
||||||
|
type ServiceStatus = Record<string, unknown> | null;
|
||||||
|
|
||||||
const CHAT_ROUTE_RE = /^\/home\/chat\/\d+\/\d+$/;
|
const CHAT_ROUTE_RE = /^\/home\/chat\/\d+\/\d+$/;
|
||||||
const CHAT_ROOT_SELECTOR = '[data-qimingclaw-perf-scope="chat-root"]';
|
const CHAT_ROOT_SELECTOR = '[data-qimingclaw-perf-scope="chat-root"]';
|
||||||
@@ -91,4 +92,70 @@ const perf = {
|
|||||||
|
|
||||||
contextBridge.exposeInMainWorld("QimingClawBridge", {
|
contextBridge.exposeInMainWorld("QimingClawBridge", {
|
||||||
perf,
|
perf,
|
||||||
|
digital: {
|
||||||
|
async getSnapshot() {
|
||||||
|
const [
|
||||||
|
agent,
|
||||||
|
mcp,
|
||||||
|
computerServer,
|
||||||
|
lanproxy,
|
||||||
|
fileServer,
|
||||||
|
guiServer,
|
||||||
|
sessionsResult,
|
||||||
|
] = await Promise.all([
|
||||||
|
ipcRenderer.invoke("agent:serviceStatus").catch((error) => ({
|
||||||
|
running: false,
|
||||||
|
error: String(error),
|
||||||
|
})),
|
||||||
|
ipcRenderer.invoke("mcp:status").catch((error) => ({
|
||||||
|
running: false,
|
||||||
|
error: String(error),
|
||||||
|
})),
|
||||||
|
ipcRenderer.invoke("computerServer:status").catch((error) => ({
|
||||||
|
running: false,
|
||||||
|
error: String(error),
|
||||||
|
})),
|
||||||
|
ipcRenderer.invoke("lanproxy:status").catch((error) => ({
|
||||||
|
running: false,
|
||||||
|
error: String(error),
|
||||||
|
})),
|
||||||
|
ipcRenderer.invoke("fileServer:status").catch((error) => ({
|
||||||
|
running: false,
|
||||||
|
error: String(error),
|
||||||
|
})),
|
||||||
|
ipcRenderer.invoke("guiServer:status").catch((error) => ({
|
||||||
|
running: false,
|
||||||
|
error: String(error),
|
||||||
|
})),
|
||||||
|
ipcRenderer.invoke("agent:listSessionsDetailed").catch((error) => ({
|
||||||
|
success: false,
|
||||||
|
data: [],
|
||||||
|
error: String(error),
|
||||||
|
})),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const sessions = Array.isArray(sessionsResult?.data)
|
||||||
|
? sessionsResult.data
|
||||||
|
: [];
|
||||||
|
|
||||||
|
return {
|
||||||
|
generatedAt: new Date().toISOString(),
|
||||||
|
services: {
|
||||||
|
agent: agent as ServiceStatus,
|
||||||
|
mcp: mcp as ServiceStatus,
|
||||||
|
computerServer: computerServer as ServiceStatus,
|
||||||
|
lanproxy: lanproxy as ServiceStatus,
|
||||||
|
fileServer: fileServer as ServiceStatus,
|
||||||
|
guiServer: guiServer as ServiceStatus,
|
||||||
|
},
|
||||||
|
sessions: {
|
||||||
|
total: sessions.length,
|
||||||
|
active: sessions.filter((session: { status?: string }) =>
|
||||||
|
session.status === "active" || session.status === "pending",
|
||||||
|
).length,
|
||||||
|
items: sessions.slice(0, 10),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -267,6 +267,52 @@ POST /api/skill/:skillId/enabled
|
|||||||
- 新增本地数字员工状态服务,持久化 Plan / Task / Run / Event / Artifact / Approval。
|
- 新增本地数字员工状态服务,持久化 Plan / Task / Run / Event / Artifact / Approval。
|
||||||
- 把 `/computer/chat`、ACP session、MCP tool status 写入 Run/Event。
|
- 把 `/computer/chat`、ACP session、MCP tool status 写入 Run/Event。
|
||||||
|
|
||||||
|
## Phase 3:webview 只读状态桥(已开始)
|
||||||
|
|
||||||
|
目标:让 embedded 数字员工页能读取 qimingclaw 客户端真实状态,同时保持 webview 权限收敛。
|
||||||
|
|
||||||
|
当前 qimingclaw 已扩展:
|
||||||
|
|
||||||
|
```text
|
||||||
|
crates/agent-electron-client/src/preload/webviewPerfBridge.ts
|
||||||
|
crates/agent-electron-client/src/main/main.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
桥接对象:
|
||||||
|
|
||||||
|
```text
|
||||||
|
window.QimingClawBridge.digital.getSnapshot()
|
||||||
|
```
|
||||||
|
|
||||||
|
当前 snapshot 内容:
|
||||||
|
|
||||||
|
- Agent service status
|
||||||
|
- MCP proxy status
|
||||||
|
- Computer Server status
|
||||||
|
- Lanproxy status
|
||||||
|
- File Server status
|
||||||
|
- GUI Server status
|
||||||
|
- ACP detailed sessions summary
|
||||||
|
|
||||||
|
安全边界:
|
||||||
|
|
||||||
|
- 只读。
|
||||||
|
- 不暴露 start / stop / restart。
|
||||||
|
- 不暴露 prompt / shell / computer chat。
|
||||||
|
- webview 页面只能拿到状态快照,再由 adapter 映射到数字员工页面。
|
||||||
|
|
||||||
|
当前页面表现:
|
||||||
|
|
||||||
|
- “客户端运行状态巡检”会显示 Agent、MCP、活跃会话数。
|
||||||
|
- “远程任务接入准备”会显示 Computer Server、Lanproxy、File Server 状态。
|
||||||
|
- 在非 Electron webview 环境下,adapter 自动降级为“未连接 bridge”的 fallback 文案。
|
||||||
|
|
||||||
|
下一步:
|
||||||
|
|
||||||
|
- 新增 qimingclaw digital employee state service。
|
||||||
|
- 将 bridge snapshot 写入 Run/Event,而不是只作为页面即时 projection。
|
||||||
|
- 让 `/computer/chat` 请求生成真实 Task / Run / Event。
|
||||||
|
|
||||||
### 产出
|
### 产出
|
||||||
|
|
||||||
- 数字员工页面可以从源码重新构建。
|
- 数字员工页面可以从源码重新构建。
|
||||||
|
|||||||
Reference in New Issue
Block a user