feat(client): vendor digital employee source

This commit is contained in:
baiyanyun
2026-06-05 15:02:42 +08:00
parent c6fdff20e8
commit 2eb9a99da3
66 changed files with 15160 additions and 2646 deletions

View File

@@ -0,0 +1,3 @@
node_modules/
dist/
*.tsbuildinfo

View File

@@ -0,0 +1,21 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>数字员工</title>
<script>
window.__QIMINGCLAW_EMBEDDED_DIGITAL__ = true;
try {
window.localStorage.setItem("zeroclaw_token", "qimingclaw_embedded_digital_employee");
window.sessionStorage.setItem("sgrobot_logged_in", "1");
} catch (error) {
console.warn("[qimingclaw] digital employee auth bootstrap skipped", error);
}
</script>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,26 @@
{
"name": "@qiming-ai/sgrobot-digital-embedded",
"private": true,
"version": "0.1.0",
"type": "module",
"scripts": {
"build": "tsc -b && vite build",
"dev": "vite"
},
"dependencies": {
"lucide-react": "^0.468.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-router-dom": "^7.1.1"
},
"devDependencies": {
"@tailwindcss/vite": "^4.3.0",
"@types/node": "^25.3.0",
"@types/react": "^19.0.7",
"@types/react-dom": "^19.0.3",
"@vitejs/plugin-react": "^4.3.4",
"tailwindcss": "^4.0.0",
"typescript": "~5.7.2",
"vite": "^6.0.7"
}
}

View File

@@ -0,0 +1,10 @@
# qimingclaw embedded patches
This directory records qimingclaw-only adaptations around the sgRobot digital employee UI.
- `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.
- `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

@@ -0,0 +1,7 @@
# Digital Employee Static Assets
This directory is owned by the React digital employee frontend.
- `avatars/` keeps the original digital employee character images used by the home page `Avatar3D` stage.
- Do not place the legacy Vue standalone application bundle here.
- If an avatar file is renamed, update `src/pages/digital/CommandDeck.tsx` in the same change.

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 483 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 507 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 552 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

View File

@@ -0,0 +1,29 @@
import Gem from './Gem';
import Stage from './Stage';
interface Avatar3DProps {
avatarSrc: string;
altText?: string;
statusLabel?: string;
}
export default function Avatar3D({ avatarSrc, altText = '数字员工', statusLabel }: Avatar3DProps) {
return (
<div className="de-avatar-container" aria-hidden="true">
<div className="de-avatar-glass-space">
<div className="de-avatar-roof">
<div className="de-avatar-roof-face" />
<div className="de-avatar-roof-glow" />
</div>
<div className="de-avatar-glass-left" />
<div className="de-avatar-glass-right" />
<Gem className="de-gem-1" />
<Gem className="de-gem-2" />
<Gem className="de-gem-3" />
</div>
{statusLabel && <div className="de-avatar-state-label">{statusLabel}</div>}
<img src={avatarSrc} alt={altText} className="de-avatar-img" />
<Stage />
</div>
);
}

View File

@@ -0,0 +1,15 @@
import type { ReactNode } from 'react';
interface CardTitleBarProps {
title: string;
children?: ReactNode;
}
export default function CardTitleBar({ title, children }: CardTitleBarProps) {
return (
<div className="de-card-title-bar">
<span className="de-card-title-bar-text">{title}</span>
{children}
</div>
);
}

View File

@@ -0,0 +1,620 @@
import { useEffect, useMemo, useState } from 'react';
import {
buildDemoFlowEvents,
buildDemoPlanMessages,
buildDemoRuns,
buildDemoTasks,
getDemoMissions,
} from '@/pages/digital/demoScenario';
import {
displayBackendMessage,
displayBusinessText,
displayEventKind,
displayStatus,
type Mission,
} from '@/lib/consoleDataAdapter';
import { getDebugPlanFlow, getDebugTasksForPlan, getPlanMessages, type PlanMessage } from '@/lib/api';
import type { DebugRun, DebugTask, FlowResponse } from '@/types/api';
import StatusBadge from './StatusBadge';
interface FlowDetailPanelProps {
mission: Mission;
onClose: () => void;
}
function stepVariant(status: string): 'info' | 'success' | 'warning' | 'danger' {
switch (status) {
case 'succeeded': case 'completed': case 'Succeeded': return 'success';
case 'failed': return 'danger';
case 'Failed': return 'danger';
case 'running': case 'active': case 'Running': return 'info';
default: return 'warning';
}
}
function fmtTime(ts?: string | null): string {
if (!ts) return '未记录';
return new Date(ts).toLocaleString('zh-CN', { hour12: false });
}
function missionPeriodLabel(mission: Mission): string {
const text = `${mission.id} ${mission.title} ${mission.objective}`.toLowerCase();
if (text.includes('month') || text.includes('monthly') || text.includes('月')) return '月度任务';
if (text.includes('week') || text.includes('weekly') || text.includes('周')) return '周任务';
if (text.includes('临时') || text.includes('adhoc') || text.includes('hotlist') || text.includes('monitor')) return '临时任务';
return '日常任务';
}
function isDone(status: string): boolean {
return ['succeeded', 'success', 'finished', 'complete', 'completed'].includes(status.toLowerCase());
}
function isFailed(status: string): boolean {
return ['failed', 'error', 'cancelled', 'canceled', 'aborted', 'rejected', 'deadletter'].includes(status.toLowerCase());
}
function progressText(mission: Mission): string {
const total = mission.steps.length;
if (total === 0) return displayStatus(mission.status);
const done = mission.steps.filter((step) => isDone(step.status)).length;
const failed = mission.steps.filter((step) => isFailed(step.status)).length;
const running = mission.steps.filter((step) => ['running', 'active', 'queued', 'leased'].includes(step.status.toLowerCase())).length;
const parts = [`已完成 ${done}/${total}`];
if (failed > 0) parts.push(`失败 ${failed}`);
if (running > 0 && failed === 0) parts.push(`处理中 ${running}`);
return parts.join('');
}
function businessTimelineMessage(message: string): string {
return message
.replace(/^启动计划:/, '接收委托:')
.replace(/^准备步骤:/, '准备工作:')
.replace(/^创建任务:/, '安排工作:')
.replace(/^任务失败:/, '工作失败:')
.replace(/^开始处理:/, '开始处理:')
.replace(/^完成任务:/, '完成工作:')
.replace(/^调用技能:/, '数字员工处理:')
.replace(/^执行开始:/, '开始执行:')
.replace(/^执行完成:/, '执行完成:')
.replace(/^执行失败:/, '执行失败:')
.replace(/,将使用[^,。]+/, '')
.replace(/,分配给[^,。]+/, '');
}
function isFailureMessage(message: string): boolean {
return /失败|failed|error|timeout|unavailable/i.test(message);
}
function businessKindLabel(kind: string, message = ''): string {
if (isFailureMessage(message)) return '失败';
if (kind === 'artifact') return '产物';
if (kind === 'task_summary') return '结果';
if (kind === 'task_running' || kind === 'run') return '处理中';
if (kind === 'task_done' || kind === 'run_done') return '完成';
return '过程';
}
function messageRoleLabel(role: string): string {
if (role === 'user') return '用户';
if (role === 'assistant') return '数字员工';
if (role === 'system') return '系统';
return '消息';
}
type DetailEvent = {
event_id: string;
kind: string;
occurred_at: string | null;
message: string;
};
function skillLabel(skillId: string | null | undefined): string {
return displayBusinessText(skillId || '', skillId || '自动化技能');
}
function toolBusinessMessage(toolName: string, success: string, elapsedMs: string): string {
const result = success === 'true' ? '成功' : '失败';
if (toolName.includes('zhihu-hotlist') && toolName.includes('extract_hotlist')) {
return `调用技能:知乎热榜采集,结果:${result},耗时 ${elapsedMs}ms`;
}
if (toolName.includes('office-export-xlsx') && toolName.includes('export_xlsx')) {
return `调用技能Excel 导出,结果:${result},耗时 ${elapsedMs}ms`;
}
return `调用技能:${toolName},结果:${result},耗时 ${elapsedMs}ms`;
}
function valueText(value: unknown): string {
return typeof value === 'string' ? value : '';
}
function eventTime(event: DetailEvent): string {
return event.occurred_at || '';
}
function earliestTime(values: Array<string | null | undefined>): string | null {
return values
.filter((value): value is string => Boolean(value))
.sort((a, b) => a.localeCompare(b))[0] || null;
}
function latestByResultSubject(events: DetailEvent[]): DetailEvent[] {
const bySubject = new Map<string, DetailEvent>();
for (const event of events) {
const subject = event.message.split(',结果:')[0]?.split(',状态:')[0] || event.message;
const current = bySubject.get(subject);
if (!current || eventTime(event).localeCompare(eventTime(current)) > 0) {
bySubject.set(subject, event);
}
}
return Array.from(bySubject.values());
}
function objectValue(value: unknown): Record<string, unknown> | null {
return value && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : null;
}
function normalizeOutputPath(value: string): string {
return value.replace(/\\\\/g, '\\');
}
function buildOperationalTimeline(
mission: Mission,
tasks: DebugTask[],
runs: DebugRun[],
messages: PlanMessage[],
flowEvents?: FlowResponse['events'],
): DetailEvent[] {
const events: DetailEvent[] = [];
const add = (kind: string, occurredAt: string | null | undefined, message: string, id: string) => {
if (!message.trim()) return;
events.push({ event_id: id, kind, occurred_at: occurredAt ?? null, message });
};
const addOutputFacts = (rawOutput: string, occurredAt: string | null | undefined, idPrefix: string, sourceName: string) => {
let parsedOutputPath = false;
try {
const parsed = objectValue(JSON.parse(rawOutput));
if (parsed) {
const label = typeof parsed.label === 'string' ? parsed.label.trim() : '';
const value = parsed.value;
if (label && value !== undefined && value !== null) {
add('task_summary', occurredAt, `${label}${String(value)}`, `metric-${idPrefix}-${sourceName}`);
}
const outputPath = typeof parsed.output_path === 'string' ? parsed.output_path.trim() : '';
if (outputPath) {
parsedOutputPath = true;
add('artifact', occurredAt, `产物生成:${normalizeOutputPath(outputPath)}`, `artifact-${idPrefix}-${sourceName}`);
}
}
} catch {
// Some execution messages are human-readable text. Regex extraction below handles those.
}
const rowCountMatch = rawOutput.match(/"row_count"\s*:\s*(\d+)/);
if (rowCountMatch?.[1]) {
add('task_summary', occurredAt, `数据处理完成:共 ${rowCountMatch[1]} 条记录`, `rows-${idPrefix}-${sourceName}`);
}
const pathMatch = rawOutput.match(/"output_path"\s*:\s*"([^"]+\.(?:xlsx|xls|csv|json|html|pdf|png|jpg|jpeg|txt))"/i);
const outputPath = pathMatch?.[1];
if (outputPath && !parsedOutputPath) {
add('artifact', occurredAt, `产物生成:${normalizeOutputPath(outputPath)}`, `artifact-regex-${idPrefix}-${sourceName}`);
}
};
add('plan', mission.createdAt || mission.updatedAt, `启动计划:${mission.title},目标:${mission.objective}`, `plan-${mission.id}`);
if (tasks.length === 0) {
for (const step of mission.steps) {
const skills = step.preferredSkills.map(skillLabel).join('、') || '自动化技能';
add('task', mission.updatedAt || mission.createdAt, `准备步骤:${step.title},将使用 ${skills}`, `step-${step.id}`);
}
}
for (const task of tasks) {
const skill = skillLabel(task.assigned_skill_id);
const taskRuns = runs.filter((run) => run.task_id === task.task_id);
const firstRunAt = earliestTime(taskRuns.map((run) => run.created_at));
const taskCreatedAt = earliestTime([task.created_at, task.started_at, firstRunAt]);
add('task', taskCreatedAt, `创建任务:${task.title || '子任务'},分配给 ${skill}`, `task-create-${task.task_id}`);
if (task.started_at) add('task_running', task.started_at, `开始处理:${task.title || '子任务'}`, `task-start-${task.task_id}`);
if (task.finished_at) {
const taskDoneVerb = isFailed(task.status) ? '任务失败' : '完成任务';
const errorDetail = isFailed(task.status) && task.error_message ? `,原因:${task.error_message}` : '';
add('task_done', task.finished_at, `${taskDoneVerb}${task.title || '子任务'},状态:${displayStatus(task.status)}${errorDetail}`, `task-finish-${task.task_id}`);
}
}
for (const run of runs) {
const task = tasks.find((item) => item.task_id === run.task_id);
const taskTitle = task?.title || '任务执行';
const runSkill = skillLabel(run.skill_id || task?.assigned_skill_id);
const hasSessionToolForTask = messages.some((message) => (message.content || '').includes(`[Task ${run.task_id}] tool_call`));
if (!hasSessionToolForTask) {
add('skill_tool', run.created_at, `调用技能:${runSkill},处理:${taskTitle}`, `run-skill-${run.run_id}`);
}
add('run', run.created_at, `执行开始:${taskTitle},第 ${run.attempt_no || 1}`, `run-start-${run.run_id}`);
if (run.completed_at) {
const runDoneVerb = isFailed(run.lifecycle) ? '执行失败' : '执行完成';
add('run_done', run.completed_at, `${runDoneVerb}${taskTitle},结果:${displayStatus(run.lifecycle)}`, `run-done-${run.run_id}`);
}
for (const [index, runEvent] of (run.events || []).entries()) {
addOutputFacts(
valueText(runEvent.message),
runEvent.occurred_at || run.completed_at || run.created_at,
`run-${run.run_id}-${runEvent.event_id || index}`,
run.skill_id || task?.assigned_skill_id || 'run',
);
}
for (const [index, artifact] of (run.artifacts || []).entries()) {
const uri = valueText(artifact.uri);
const name = valueText(artifact.name) || uri;
if (uri || name) {
add('artifact', run.completed_at || run.created_at, `产物生成:${name}${uri && uri !== name ? `${uri}` : ''}`, `run-artifact-${run.run_id}-${index}`);
}
}
}
for (const message of messages) {
const content = message.content || '';
const toolMatch = content.match(/\[Task ([^\]]+)\] tool_call name=([^\s]+) success=(true|false) elapsed_ms=(\d+) args=(.*?) output=(.*)$/s);
if (toolMatch) {
const taskId = toolMatch[1] || '';
const toolName = toolMatch[2] || '';
const success = toolMatch[3] || 'false';
const elapsedMs = toolMatch[4] || '0';
const output = toolMatch[6] || '';
add('skill_tool', message.created_at, toolBusinessMessage(toolName, success, elapsedMs), `tool-${taskId}-${message.created_at}-${toolName}`);
addOutputFacts(output, message.created_at, `message-${message.created_at}`, toolName);
continue;
}
const hotlistCount = content.match(/共采集到\s*\**\s*(\d+)\s*条/);
if (hotlistCount?.[1]) {
add('task_summary', message.created_at, `采集完成:获得 ${hotlistCount[1]} 条知乎热榜数据`, `summary-hotlist-${message.created_at}`);
continue;
}
if (content.includes('[Task ') && content.includes('Excel')) {
add('task_summary', message.created_at, '导出完成Excel 文件已生成', `summary-excel-${message.created_at}`);
}
}
if (events.length <= 1) {
for (const event of flowEvents || []) {
add(event.kind, event.occurred_at, displayBackendMessage(event.message, event.message), event.event_id);
}
}
return events
.sort((a, b) => (a.occurred_at || '').localeCompare(b.occurred_at || ''))
.slice(-40);
}
export default function FlowDetailPanel({ mission, onClose }: FlowDetailPanelProps) {
const [tasks, setTasks] = useState<DebugTask[]>([]);
const [runs, setRuns] = useState<DebugRun[]>([]);
const [flow, setFlow] = useState<FlowResponse | null>(null);
const [loading, setLoading] = useState(true);
const [sessionMessages, setSessionMessages] = useState<PlanMessage[]>([]);
const [sessionOpen, setSessionOpen] = useState(false);
const [advancedOpen, setAdvancedOpen] = useState(false);
const [sessionLoading, setSessionLoading] = useState(false);
const [sessionError, setSessionError] = useState<string | null>(null);
const planId = mission.sourceIds.planId || mission.id;
useEffect(() => {
let cancelled = false;
const refresh = () => {
setLoading(true);
Promise.allSettled([
getDebugTasksForPlan(planId),
getDebugPlanFlow(planId),
getPlanMessages(planId),
]).then(([tasksResult, flowResult, messagesResult]) => {
if (cancelled) return;
const demoMission = getDemoMissions().find((item) => item.id === planId);
const fallbackTasks = demoMission ? buildDemoTasks([demoMission]) : [];
const fallbackRuns = fallbackTasks.length > 0 ? buildDemoRuns(fallbackTasks) : [];
if (tasksResult.status === 'fulfilled') {
setTasks(tasksResult.value.tasks && tasksResult.value.tasks.length > 0 ? tasksResult.value.tasks : fallbackTasks);
setRuns(tasksResult.value.runs && tasksResult.value.runs.length > 0 ? tasksResult.value.runs : fallbackRuns);
} else {
setTasks(fallbackTasks);
setRuns(fallbackRuns);
}
if (flowResult.status === 'fulfilled' && flowResult.value?.events && flowResult.value.events.length > 0) {
setFlow(flowResult.value);
} else {
setFlow(demoMission ? { events: buildDemoFlowEvents(demoMission) } as FlowResponse : null);
}
if (messagesResult.status === 'fulfilled' && messagesResult.value.messages.length > 0) {
setSessionMessages(messagesResult.value.messages);
} else if (demoMission) {
setSessionMessages(buildDemoPlanMessages(demoMission));
} else {
setSessionMessages([]);
}
}).finally(() => {
if (!cancelled) setLoading(false);
});
};
refresh();
const timer = window.setInterval(refresh, 3000);
return () => {
cancelled = true;
window.clearInterval(timer);
};
}, [planId]);
useEffect(() => {
setSessionMessages([]);
setSessionOpen(false);
setAdvancedOpen(false);
setSessionError(null);
}, [planId]);
const loadSessionMessages = async () => {
if (sessionLoading) return;
setSessionOpen(true);
setSessionLoading(true);
setSessionError(null);
try {
const data = await getPlanMessages(planId);
if (data.messages && data.messages.length > 0) {
setSessionMessages(data.messages);
} else {
const demoMission = getDemoMissions().find((item) => item.id === planId);
setSessionMessages(demoMission ? buildDemoPlanMessages(demoMission) : []);
}
} catch (err) {
const demoMission = getDemoMissions().find((item) => item.id === planId);
if (demoMission) {
setSessionMessages(buildDemoPlanMessages(demoMission));
setSessionError(null);
} else {
setSessionMessages([]);
setSessionError(err instanceof Error ? err.message : '读取会话失败');
}
} finally {
setSessionLoading(false);
}
};
const runsByTask = useMemo(() => {
const grouped = new Map<string, DebugRun[]>();
for (const run of runs) {
const list = grouped.get(run.task_id) || [];
list.push(run);
grouped.set(run.task_id, list);
}
for (const list of grouped.values()) {
list.sort((a, b) => (b.created_at || '').localeCompare(a.created_at || ''));
}
return grouped;
}, [runs]);
const eventTimeline = useMemo(
() => buildOperationalTimeline(mission, tasks, runs, sessionMessages, flow?.events),
[flow?.events, mission, runs, sessionMessages, tasks],
);
const businessTimeline = useMemo(
() => eventTimeline
.filter((event) => event.kind !== 'skill_tool')
.slice(-12),
[eventTimeline],
);
const resultEvents = useMemo(
() => {
const facts = eventTimeline.filter((event) => ['artifact', 'task_summary'].includes(event.kind));
const taskResults = eventTimeline.filter((event) => event.kind === 'task_done');
const runResults = latestByResultSubject(eventTimeline.filter((event) => event.kind === 'run_done'));
const terminal = taskResults.length > 0 ? taskResults : runResults;
return [...facts, ...terminal]
.sort((a, b) => eventTime(a).localeCompare(eventTime(b)))
.slice(-6);
},
[eventTimeline],
);
const latestEventAt = useMemo(
() => eventTimeline.reduce<string | null>((latest, event) => {
if (!event.occurred_at) return latest;
if (!latest || event.occurred_at.localeCompare(latest) > 0) return event.occurred_at;
return latest;
}, null),
[eventTimeline],
);
const displayUpdatedAt = mission.updatedAt || latestEventAt;
const openAdvanced = () => {
const nextOpen = !advancedOpen;
setAdvancedOpen(nextOpen);
if (nextOpen && !sessionOpen) {
void loadSessionMessages();
}
};
return (
<div className="de-drawer" role="dialog" aria-label={mission.title}>
<div className="de-drawer-head">
<div className="de-drawer-title-group">
<span className="de-drawer-eyebrow"></span>
<h3>{mission.title}</h3>
</div>
<button className="de-btn-secondary" onClick={onClose} aria-label="关闭"></button>
</div>
<div className="de-drawer-status-line">
<StatusBadge variant={stepVariant(mission.status)} label={displayStatus(mission.status)} />
<span className="de-drawer-meta">{missionPeriodLabel(mission)}</span>
<span className="de-drawer-meta">{progressText(mission)}</span>
</div>
<div className="de-drawer-timeline">
<section className="de-detail-section">
<div className="de-detail-section-head">
<h4></h4>
</div>
<div className="de-detail-kv">
<span></span><strong>{mission.objective || '未填写'}</strong>
<span></span><strong>{missionPeriodLabel(mission)}</strong>
<span></span><strong>{progressText(mission)}</strong>
<span></span><strong>{fmtTime(mission.createdAt)}</strong>
<span></span><strong>{displayStatus(mission.status)}</strong>
<span></span><strong>{fmtTime(displayUpdatedAt)}</strong>
</div>
</section>
<section className="de-detail-section de-execution-chain">
<div className="de-detail-section-head">
<h4></h4>
<span className="de-chain-plan-id">{fmtTime(displayUpdatedAt || mission.createdAt)}</span>
</div>
{loading && businessTimeline.length === 0 ? (
<p className="de-empty-text">...</p>
) : businessTimeline.length === 0 ? (
<p className="de-empty-text"></p>
) : (
<div className="de-chain-list">
{businessTimeline.map((event) => (
<div key={`chain-${event.event_id}`} className={`de-chain-row de-chain-row--${event.kind} ${isFailureMessage(event.message) ? 'de-chain-row--danger' : ''}`}>
<strong>{businessKindLabel(event.kind, event.message)}</strong>
<span>{businessTimelineMessage(event.message)}</span>
<time>{fmtTime(event.occurred_at)}</time>
</div>
))}
</div>
)}
</section>
<section className="de-detail-section">
<h4></h4>
{resultEvents.length > 0 ? (
<div className="de-event-list">
{[...resultEvents].reverse().map((event) => (
<div key={`result-${event.event_id}`} className={`de-event-row ${isFailureMessage(event.message) ? 'de-event-row--danger' : ''}`}>
<strong>{businessKindLabel(event.kind, event.message)}</strong>
<span>{businessTimelineMessage(event.message)}</span>
<time>{fmtTime(event.occurred_at)}</time>
</div>
))}
</div>
) : (
<p className="de-empty-text"></p>
)}
</section>
<section className="de-detail-section">
<h4></h4>
{loading ? <p className="de-empty-text">...</p> : null}
{mission.steps.length === 0 ? (
<p className="de-empty-text"></p>
) : (
mission.steps.map((step, i) => (
<div key={step.id} className="de-step-node">
<div className={`de-step-dot de-step-dot--${step.status}`} />
{i < mission.steps.length - 1 && <div className="de-step-line" />}
<div className="de-step-content">
<strong>{step.title}</strong>
<span>{displayStatus(step.status)}</span>
</div>
</div>
))
)}
</section>
<section className="de-detail-section">
<div className="de-detail-section-head">
<h4></h4>
<button className="de-btn-secondary de-btn-sm" onClick={openAdvanced} disabled={sessionLoading}>
{advancedOpen ? '收起' : '展开'}
</button>
</div>
{advancedOpen ? (
<>
<section className="de-detail-section">
<div className="de-detail-section-head">
<h4></h4>
<span className="de-chain-plan-id"></span>
</div>
{eventTimeline.length > 0 ? (
<div className="de-event-list">
{eventTimeline.slice(-20).reverse().map((event) => (
<div key={event.event_id} className={`de-event-row ${isFailureMessage(event.message) ? 'de-event-row--danger' : ''}`}>
<strong>{displayEventKind(event.kind)}</strong>
<span>{event.message}</span>
<time>{fmtTime(event.occurred_at)}</time>
</div>
))}
</div>
) : (
<p className="de-empty-text"></p>
)}
</section>
<section className="de-detail-section">
<div className="de-detail-section-head">
<h4></h4>
<button className="de-btn-secondary de-btn-sm" onClick={loadSessionMessages} disabled={sessionLoading}>
</button>
</div>
{sessionError ? (
<p className="de-empty-text">{sessionError}</p>
) : sessionLoading ? (
<p className="de-empty-text">...</p>
) : sessionMessages.length === 0 ? (
<p className="de-empty-text"></p>
) : (
<div className="de-session-message-list">
{sessionMessages.map((message, index) => (
<div key={`${message.role}-${index}`} className={`de-session-message de-session-message--${message.role === 'user' ? 'user' : 'agent'}`}>
<div className="de-session-message-role">{messageRoleLabel(message.role)}</div>
<p>{displayBackendMessage(message.content, message.content)}</p>
</div>
))}
</div>
)}
</section>
<section className="de-detail-section">
<h4></h4>
{tasks.length === 0 ? (
<p className="de-empty-text"></p>
) : (
tasks.map((task) => {
const taskRuns = runsByTask.get(task.task_id) || [];
const latestRun = taskRuns[0];
return (
<div key={task.task_id} className="de-task-run-card">
<div className="de-task-run-head">
<span>{task.title || '子任务'}</span>
<StatusBadge variant={stepVariant(task.status)} label={displayStatus(task.status)} />
</div>
<div className="de-task-run-meta">
<span>skill_id{task.assigned_skill_id || '未分配'}</span>
<span>{fmtTime(task.started_at)}</span>
<span>{fmtTime(task.finished_at)}</span>
</div>
<div className="de-task-run-meta">
<span>task_id{task.task_id}</span>
<span>{taskRuns.length} </span>
{latestRun ? <span>{displayStatus(latestRun.lifecycle)}</span> : null}
</div>
{isFailed(task.status) && task.error_message ? (
<div className="de-task-run-error">
<strong></strong>{task.error_message}
</div>
) : null}
</div>
);
})
)}
</section>
</>
) : (
<p className="de-empty-text"></p>
)}
</section>
</div>
</div>
);
}

View File

@@ -0,0 +1,7 @@
interface GemProps {
className?: string;
}
export default function Gem({ className = '' }: GemProps) {
return <div className={`de-gem ${className}`} aria-hidden="true" />;
}

View File

@@ -0,0 +1,15 @@
import type { ReactNode, CSSProperties } from 'react';
interface GlassCardProps {
children: ReactNode;
className?: string;
style?: CSSProperties;
}
export default function GlassCard({ children, className = '', style }: GlassCardProps) {
return (
<div className={`de-card ${className}`} style={style}>
{children}
</div>
);
}

View File

@@ -0,0 +1,238 @@
import StatusBadge from './StatusBadge';
import { displayStatus, type Mission } from '@/lib/consoleDataAdapter';
import { domSafeId } from '@/pages/digital/navigation';
interface MissionCardProps {
mission: Mission;
onSelect: (mission: Mission) => void;
onRun?: (mission: Mission) => void;
onRestart?: (mission: Mission) => void;
dateFilter?: string;
selected?: boolean;
focusLabel?: string | null;
running?: boolean;
}
function statusVariant(status: string): 'info' | 'success' | 'warning' | 'danger' {
switch (status) {
case 'succeeded': case 'completed': return 'success';
case 'failed': case 'error': return 'danger';
case 'running': case 'active': return 'info';
default: return 'warning';
}
}
function canRun(status: string): boolean {
return ['draft', 'reviewing', 'approved'].includes(status.toLowerCase());
}
function missionPeriodLabel(mission: Mission): string {
const text = `${mission.id} ${mission.title} ${mission.objective}`.toLowerCase();
if (text.includes('month') || text.includes('monthly') || text.includes('月')) return '月度任务';
if (text.includes('week') || text.includes('weekly') || text.includes('周')) return '周任务';
if (text.includes('临时') || text.includes('adhoc') || text.includes('hotlist') || text.includes('monitor')) return '临时任务';
return '日常任务';
}
function isDone(status: string): boolean {
return ['succeeded', 'success', 'finished', 'complete', 'completed'].includes(status.toLowerCase());
}
function isFailed(status: string): boolean {
return ['failed', 'error', 'cancelled', 'canceled', 'aborted', 'rejected', 'deadletter'].includes(status.toLowerCase());
}
function progressText(mission: Mission): string {
const total = mission.steps.length;
if (total === 0) return displayStatus(mission.status);
const done = mission.steps.filter((step) => isDone(step.status)).length;
const failed = mission.steps.filter((step) => isFailed(step.status)).length;
const running = mission.steps.filter((step) => ['running', 'active', 'queued', 'leased'].includes(step.status.toLowerCase())).length;
const parts = [`已完成 ${done}/${total}`];
if (failed > 0) parts.push(`失败 ${failed}`);
if (running > 0 && failed === 0) parts.push(`处理中 ${running}`);
return parts.join('');
}
function currentWorkText(mission: Mission): string {
const failedStep = mission.steps.find((step) => isFailed(step.status));
if (failedStep) return `执行失败:${failedStep.title}`;
const runningStep = mission.steps.find((step) => ['running', 'active'].includes(step.status.toLowerCase()));
if (runningStep) return `正在处理:${runningStep.title}`;
const nextStep = mission.steps.find((step) => !isDone(step.status));
if (nextStep) return `准备处理:${nextStep.title}`;
if (isDone(mission.status)) return '本轮任务已完成,等待你查看结果';
if (mission.status.toLowerCase() === 'failed') return '执行遇到异常,等待你检查后重新执行';
return '等待你委托数字员工开始处理';
}
function userNextActionText(mission: Mission, runnable: boolean): string {
const status = mission.status.toLowerCase();
if (status === 'failed' || status === 'error') return '查看详情后重新执行本任务';
if (['running', 'active'].includes(status)) return '等待完成或查看过程';
if (isDone(status)) return '查看产物,必要时重新执行';
if (runnable) return '确认目标后点击委托执行';
return '查看详情确认当前状态';
}
function artifactText(mission: Mission): string {
const text = `${mission.title} ${mission.objective}`.toLowerCase();
if (text.includes('excel') || text.includes('xlsx') || text.includes('导出') || text.includes('报表')) return '导出报表';
if (text.includes('brief') || text.includes('简报') || text.includes('日程')) return '工作简报';
if (text.includes('snapshot') || text.includes('快照') || text.includes('热榜')) return '结果快照';
if (isDone(mission.status)) return '查看完成结果';
return '完成后生成';
}
function dateKey(value?: string | null): string {
if (!value) return '';
const date = new Date(value);
if (Number.isNaN(date.getTime())) return '';
return [
date.getFullYear(),
String(date.getMonth() + 1).padStart(2, '0'),
String(date.getDate()).padStart(2, '0'),
].join('-');
}
function todayDateKey(): string {
return dateKey(new Date().toISOString());
}
function formatCardClock(value?: string | null): string {
if (!value) return '时间未记录';
const date = new Date(value);
if (Number.isNaN(date.getTime())) return '时间未记录';
return date.toLocaleTimeString('zh-CN', {
hour: '2-digit',
minute: '2-digit',
hour12: false,
});
}
function formatSyncTime(value?: string | null): string {
if (!value) return '';
const date = new Date(value);
if (Number.isNaN(date.getTime())) return '';
return date.toLocaleString('zh-CN', {
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
hour12: false,
});
}
function plannedClockFromTitle(mission: Mission): string | null {
const match = `${mission.title} ${mission.id}`.match(/(?:^|\D)(\d{1,2})[:](\d{2})(?:\D|$)/);
if (!match) return null;
const hour = Number(match[1]);
const minute = Number(match[2]);
if (!Number.isInteger(hour) || !Number.isInteger(minute) || hour > 23 || minute > 59) return null;
return `${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}`;
}
function taskExecutionTimes(mission: Mission): string[] {
return mission.steps.flatMap((step) => (
step.tasks.flatMap((task) => [task.startedAt, task.finishedAt])
)).filter((value): value is string => Boolean(value));
}
function actualExecutionTimeForDate(mission: Mission, selectedDate: string): string | null {
const times = taskExecutionTimes(mission)
.filter((value) => dateKey(value) === selectedDate)
.sort((left, right) => right.localeCompare(left));
return times[0] ?? null;
}
function missionTimeText(mission: Mission, selectedDate: string): string {
const actualTime = actualExecutionTimeForDate(mission, selectedDate);
if (actualTime) return `实际执行 ${formatCardClock(actualTime)}`;
const pendingText = selectedDate === todayDateKey() ? '今日未执行' : '当日未执行';
const plannedClock = plannedClockFromTitle(mission);
return `计划执行 ${plannedClock || '待确认'}${pendingText}`;
}
export default function MissionCard({
mission,
onSelect,
onRun,
onRestart,
dateFilter,
selected,
focusLabel,
running,
}: MissionCardProps) {
const runnable = canRun(mission.status);
const restartMayOverwrite = isDone(mission.status) || mission.steps.some((step) => isDone(step.status));
const selectedDate = dateFilter || todayDateKey();
const syncTime = formatSyncTime(mission.updatedAt || mission.createdAt);
return (
<div
id={`mission-card-${domSafeId(mission.id)}`}
className={`de-mission-card ${selected ? 'de-mission-card--selected' : ''}`}
onClick={() => onSelect(mission)}
onKeyDown={(event) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
onSelect(mission);
}
}}
role="button"
tabIndex={0}
>
<div className="de-mission-card-header">
<span className="de-mission-card-title">{mission.title}</span>
<div className="de-mission-card-badges">
{focusLabel && <span className="de-mission-card-focus-chip">{focusLabel}</span>}
<StatusBadge variant={statusVariant(mission.status)} label={displayStatus(mission.status)} />
</div>
</div>
{mission.objective && (
<p className="de-mission-card-desc">{mission.objective}</p>
)}
<div className="de-mission-card-meta">
<span>{missionPeriodLabel(mission)}</span>
<span>{progressText(mission)}</span>
{syncTime && <span> {syncTime}</span>}
</div>
<div className="de-mission-card-meta">
<span>{currentWorkText(mission)}</span>
</div>
<div className="de-mission-card-meta">
<span>{userNextActionText(mission, runnable)}</span>
<span>{artifactText(mission)}</span>
</div>
<div className="de-mission-card-actions">
<span className="de-mission-card-time">
{missionTimeText(mission, selectedDate)}
{restartMayOverwrite ? ';重新执行可能覆盖本任务结果' : ''}
</span>
<button
type="button"
className="de-btn-restart"
disabled={running}
onClick={(event) => {
event.stopPropagation();
if (!running) onRestart?.(mission);
}}
title={restartMayOverwrite ? '重新执行本任务,可能覆盖已有结果' : '重新执行本任务'}
>
</button>
<button
type="button"
className="de-btn-run"
disabled={!runnable || running}
onClick={(event) => {
event.stopPropagation();
if (runnable && !running) onRun?.(mission);
}}
>
{running ? '执行中...' : runnable ? '委托执行' : displayStatus(mission.status)}
</button>
</div>
</div>
);
}

View File

@@ -0,0 +1,15 @@
import type { ReactNode } from 'react';
interface NavSkewButtonProps {
active?: boolean;
onClick?: () => void;
children: ReactNode;
}
export default function NavSkewButton({ active, onClick, children }: NavSkewButtonProps) {
return (
<button className={`de-nav-btn ${active ? 'active' : ''}`} onClick={onClick}>
<span>{children}</span>
</button>
);
}

View File

@@ -0,0 +1,10 @@
export default function Stage() {
return (
<div className="de-stage" aria-hidden="true">
<div className="de-stage-glow" />
<div className="de-stage-layer de-stage-layer-1" />
<div className="de-stage-layer de-stage-layer-2" />
<div className="de-stage-layer de-stage-layer-3" />
</div>
);
}

View File

@@ -0,0 +1,17 @@
type BadgeVariant = 'info' | 'success' | 'warning' | 'danger';
interface StatusBadgeProps {
variant: BadgeVariant;
label: string;
}
const variantClass: Record<BadgeVariant, string> = {
info: 'de-badge-info',
success: 'de-badge-success',
warning: 'de-badge-warning',
danger: 'de-badge-danger',
};
export default function StatusBadge({ variant, label }: StatusBadgeProps) {
return <span className={`de-badge ${variantClass[variant]}`}>{label}</span>;
}

View File

@@ -0,0 +1,158 @@
import { createContext, useContext, useState, useCallback, type ReactNode } from 'react';
export type ConsoleRole = 'finance' | 'engineering' | 'executive' | 'hr' | 'sales';
export type MotionLevel = 'none' | 'minimal';
export type Density = 'compact' | 'balanced' | 'presentation';
export type Tone = 'strict' | 'technical' | 'executive' | 'warm-professional';
export interface RoleProfile {
id: ConsoleRole;
labelKey: string;
density: Density;
tone: Tone;
contentPriority: string[];
motion: {
defaultLevel: MotionLevel;
allowParticles: false;
allowCelebrations: false;
allowDecorativeBadges: false;
};
actionPolicy: {
requireConfirmationForDestructive: boolean;
showReversalHint: boolean;
preferOneClickApproval: boolean;
};
staleDataThresholdMs: number;
cssVars: Record<string, string>;
}
export const ROLE_PROFILES: Record<ConsoleRole, RoleProfile> = {
finance: {
id: 'finance',
labelKey: 'console.role.finance',
density: 'compact',
tone: 'strict',
contentPriority: ['audit', 'sources', 'exactNumbers'],
motion: { defaultLevel: 'none', allowParticles: false, allowCelebrations: false, allowDecorativeBadges: false },
actionPolicy: { requireConfirmationForDestructive: true, showReversalHint: true, preferOneClickApproval: false },
staleDataThresholdMs: 30000,
cssVars: {
'--role-accent': '#2563eb',
'--role-accent-soft': 'rgba(37,99,235,0.12)',
'--role-surface-tint': 'rgba(248,250,252,0.78)',
'--role-border': 'rgba(37,99,235,0.22)',
'--role-focus-ring': '#1d4ed8',
},
},
engineering: {
id: 'engineering',
labelKey: 'console.role.engineering',
density: 'compact',
tone: 'technical',
contentPriority: ['alarms', 'timestamps', 'reliability'],
motion: { defaultLevel: 'none', allowParticles: false, allowCelebrations: false, allowDecorativeBadges: false },
actionPolicy: { requireConfirmationForDestructive: true, showReversalHint: true, preferOneClickApproval: false },
staleDataThresholdMs: 15000,
cssVars: {
'--role-accent': '#0f766e',
'--role-accent-soft': 'rgba(15,118,110,0.12)',
'--role-surface-tint': 'rgba(240,253,250,0.72)',
'--role-border': 'rgba(15,118,110,0.24)',
'--role-focus-ring': '#0f766e',
},
},
executive: {
id: 'executive',
labelKey: 'console.role.executive',
density: 'presentation',
tone: 'executive',
contentPriority: ['kpi', 'approvals', 'exactNumbers'],
motion: { defaultLevel: 'minimal', allowParticles: false, allowCelebrations: false, allowDecorativeBadges: false },
actionPolicy: { requireConfirmationForDestructive: true, showReversalHint: true, preferOneClickApproval: true },
staleDataThresholdMs: 60000,
cssVars: {
'--role-accent': '#334155',
'--role-accent-soft': 'rgba(51,65,85,0.10)',
'--role-surface-tint': 'rgba(248,250,252,0.82)',
'--role-border': 'rgba(51,65,85,0.20)',
'--role-focus-ring': '#475569',
},
},
hr: {
id: 'hr',
labelKey: 'console.role.hr',
density: 'balanced',
tone: 'warm-professional',
contentPriority: ['queues', 'timestamps', 'approvals'],
motion: { defaultLevel: 'minimal', allowParticles: false, allowCelebrations: false, allowDecorativeBadges: false },
actionPolicy: { requireConfirmationForDestructive: true, showReversalHint: true, preferOneClickApproval: false },
staleDataThresholdMs: 45000,
cssVars: {
'--role-accent': '#7c3aed',
'--role-accent-soft': 'rgba(124,58,237,0.10)',
'--role-surface-tint': 'rgba(250,245,255,0.72)',
'--role-border': 'rgba(124,58,237,0.20)',
'--role-focus-ring': '#6d28d9',
},
},
sales: {
id: 'sales',
labelKey: 'console.role.sales',
density: 'balanced',
tone: 'warm-professional',
contentPriority: ['pipeline', 'sources', 'exactNumbers'],
motion: { defaultLevel: 'minimal', allowParticles: false, allowCelebrations: false, allowDecorativeBadges: false },
actionPolicy: { requireConfirmationForDestructive: true, showReversalHint: true, preferOneClickApproval: false },
staleDataThresholdMs: 45000,
cssVars: {
'--role-accent': '#b45309',
'--role-accent-soft': 'rgba(180,83,9,0.10)',
'--role-surface-tint': 'rgba(255,251,235,0.70)',
'--role-border': 'rgba(180,83,9,0.20)',
'--role-focus-ring': '#92400e',
},
},
};
interface RoleContextValue {
role: ConsoleRole;
profile: RoleProfile;
setRole: (role: ConsoleRole) => void;
}
const RoleContext = createContext<RoleContextValue | null>(null);
const STORAGE_KEY = 'sgrobot-console-role';
function getStoredRole(): ConsoleRole {
try {
const stored = localStorage.getItem(STORAGE_KEY);
if (stored && stored in ROLE_PROFILES) return stored as ConsoleRole;
} catch { /* localStorage blocked */ }
return 'sales';
}
export function RoleProvider({ children }: { children: ReactNode }) {
const [role, setRoleState] = useState<ConsoleRole>(getStoredRole);
const setRole = useCallback((newRole: ConsoleRole) => {
setRoleState(newRole);
try { localStorage.setItem(STORAGE_KEY, newRole); } catch { /* noop */ }
}, []);
const profile = ROLE_PROFILES[role];
return (
<RoleContext.Provider value={{ role, profile, setRole }}>
<div style={profile.cssVars as React.CSSProperties} data-role={role}>
{children}
</div>
</RoleContext.Provider>
);
}
export function useRole(): RoleContextValue {
const ctx = useContext(RoleContext);
if (!ctx) throw new Error('useRole must be used within RoleProvider');
return ctx;
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,706 @@
import type {
StatusResponse,
ToolSpec,
CronJob,
CronRun,
Integration,
DiagResult,
MemoryEntry,
CostSummary,
CliTool,
HealthSnapshot,
Session,
ChannelDetail,
BrowserSession,
UserNotification,
DebugStoreResponse,
DebugPlan,
DebugTask,
DebugRun,
DashboardResponse,
FlowResponse,
SkillSpec,
CreatePlanRequest,
RouteDecisionTimelineResponse,
IngressRouteRequest,
IngressRouteResponse,
GovernanceCommandRequest,
GovernanceCommandResponse,
DigitalEmployeeWorkdayProjection,
DigitalEmployeeWorkdayActionRequest,
PlanDispatchResponse,
} from '../types/api';
import { clearToken, getToken, setToken } from './auth';
import { apiOrigin, apiBase } from './basePath';
// ---------------------------------------------------------------------------
// Base fetch wrapper
// ---------------------------------------------------------------------------
const API_FETCH_TIMEOUT_MS = 9000;
const DIGITAL_EMPLOYEE_WORKDAY_TIMEOUT_MS = 20000;
type ApiFetchOptions = RequestInit & {
timeoutMs?: number;
};
export class UnauthorizedError extends Error {
constructor() {
super('Unauthorized');
this.name = 'UnauthorizedError';
}
}
export async function apiFetch<T = unknown>(
path: string,
options: ApiFetchOptions = {},
): Promise<T> {
const { timeoutMs = API_FETCH_TIMEOUT_MS, ...fetchOptions } = options;
const token = getToken();
const headers = new Headers(fetchOptions.headers);
const controller = new AbortController();
let timedOut = false;
const timeout = window.setTimeout(() => {
timedOut = true;
controller.abort();
}, timeoutMs);
const externalSignal = fetchOptions.signal;
const abortFromExternal = () => controller.abort();
if (externalSignal) {
if (externalSignal.aborted) {
controller.abort();
} else {
externalSignal.addEventListener('abort', abortFromExternal, { once: true });
}
}
if (token) {
headers.set('Authorization', `Bearer ${token}`);
}
if (
fetchOptions.body &&
typeof fetchOptions.body === 'string' &&
!headers.has('Content-Type')
) {
headers.set('Content-Type', 'application/json');
}
let response: Response;
try {
response = await fetch(`${apiOrigin()}${apiBase}${path}`, {
...fetchOptions,
headers,
signal: controller.signal,
});
} catch (error) {
if (error instanceof DOMException && error.name === 'AbortError' && timedOut) {
throw new Error(`API request timed out after ${timeoutMs / 1000}s: ${path}`);
}
throw error;
} finally {
window.clearTimeout(timeout);
externalSignal?.removeEventListener('abort', abortFromExternal);
}
if (response.status === 401) {
clearToken();
window.dispatchEvent(new Event('zeroclaw-unauthorized'));
throw new UnauthorizedError();
}
if (!response.ok) {
const text = await response.text().catch(() => '');
let message = text || response.statusText;
if (text) {
try {
const parsed = JSON.parse(text) as { message?: unknown; error?: unknown };
if (typeof parsed.message === 'string' && parsed.message.trim()) {
message = parsed.message.trim();
} else if (typeof parsed.error === 'string' && parsed.error.trim()) {
message = parsed.error.trim();
}
} catch {
message = text;
}
}
throw new Error(`API ${response.status}: ${message}`);
}
// Some endpoints may return 204 No Content
if (response.status === 204) {
return undefined as unknown as T;
}
return response.json() as Promise<T>;
}
function unwrapField<T>(value: T | Record<string, T>, key: string): T {
if (value !== null && typeof value === 'object' && !Array.isArray(value) && key in value) {
const unwrapped = (value as Record<string, T | undefined>)[key];
if (unwrapped !== undefined) {
return unwrapped;
}
}
return value as T;
}
// ---------------------------------------------------------------------------
// Pairing
// ---------------------------------------------------------------------------
export async function pair(code: string): Promise<{ token: string }> {
const response = await fetch(`${apiOrigin()}${apiBase}/pair`, {
method: 'POST',
headers: { 'X-Pairing-Code': code },
});
if (!response.ok) {
const text = await response.text().catch(() => '');
throw new Error(`Pairing failed (${response.status}): ${text || response.statusText}`);
}
const data = (await response.json()) as { token: string };
setToken(data.token);
return data;
}
export async function getAdminPairCode(): Promise<{ pairing_code: string | null; pairing_required: boolean }> {
const response = await fetch(`${apiOrigin()}${apiBase}/admin/paircode`);
if (!response.ok) {
throw new Error(`Failed to fetch pairing code (${response.status})`);
}
return response.json() as Promise<{ pairing_code: string | null; pairing_required: boolean }>;
}
// ---------------------------------------------------------------------------
// Public health (no auth required)
// ---------------------------------------------------------------------------
export async function getPublicHealth(): Promise<{ require_pairing: boolean; paired: boolean }> {
const response = await fetch(`${apiOrigin()}${apiBase}/health`);
if (!response.ok) {
throw new Error(`Health check failed (${response.status})`);
}
return response.json() as Promise<{ require_pairing: boolean; paired: boolean }>;
}
// ---------------------------------------------------------------------------
// Status / Health
// ---------------------------------------------------------------------------
export function getStatus(): Promise<StatusResponse> {
return apiFetch<StatusResponse>('/api/status');
}
export function getHealth(): Promise<HealthSnapshot> {
return apiFetch<HealthSnapshot | { health: HealthSnapshot }>('/api/health').then((data) =>
unwrapField(data, 'health'),
);
}
// ---------------------------------------------------------------------------
// Config
// ---------------------------------------------------------------------------
export function getConfig(): Promise<string> {
return apiFetch<string | { format?: string; content: string }>('/api/config').then((data) =>
typeof data === 'string' ? data : data.content,
);
}
export function putConfig(content: string, contentType = 'application/json'): Promise<void> {
return apiFetch<void>('/api/config', {
method: 'PUT',
headers: { 'Content-Type': contentType },
body: content,
});
}
// ---------------------------------------------------------------------------
// Tools
// ---------------------------------------------------------------------------
export function getTools(): Promise<ToolSpec[]> {
return apiFetch<ToolSpec[] | { tools: ToolSpec[] }>('/api/tools').then((data) =>
unwrapField(data, 'tools'),
);
}
// ---------------------------------------------------------------------------
// Cron
// ---------------------------------------------------------------------------
export function getCronJobs(): Promise<CronJob[]> {
return apiFetch<CronJob[] | { jobs: CronJob[] }>('/api/cron').then((data) =>
unwrapField(data, 'jobs'),
);
}
export function getSchedulerJobs(): Promise<CronJob[]> {
return apiFetch<CronJob[] | { jobs: CronJob[] }>('/api/scheduler/jobs').then((data) =>
unwrapField(data, 'jobs'),
);
}
export function addCronJob(body: {
name?: string;
command?: string;
prompt?: string;
job_type?: string;
schedule: string;
enabled?: boolean;
session_target?: string;
model?: string;
allowed_tools?: string[];
delete_after_run?: boolean;
}): Promise<CronJob> {
return apiFetch<CronJob | { status: string; job: CronJob }>('/api/cron', {
method: 'POST',
body: JSON.stringify(body),
}).then((data) => (typeof (data as { job?: CronJob }).job === 'object' ? (data as { job: CronJob }).job : (data as CronJob)));
}
export function deleteCronJob(id: string): Promise<void> {
return apiFetch<void>(`/api/cron/${encodeURIComponent(id)}`, {
method: 'DELETE',
});
}
export function patchCronJob(
id: string,
patch: { name?: string; schedule?: string; command?: string },
): Promise<CronJob> {
return apiFetch<CronJob | { status: string; job: CronJob }>(
`/api/cron/${encodeURIComponent(id)}`,
{
method: 'PATCH',
body: JSON.stringify(patch),
},
).then((data) => (typeof (data as { job?: CronJob }).job === 'object' ? (data as { job: CronJob }).job : (data as CronJob)));
}
export function getCronRuns(
jobId: string,
limit: number = 20,
): Promise<CronRun[]> {
const params = new URLSearchParams({ limit: String(limit) });
return apiFetch<CronRun[] | { runs: CronRun[] }>(
`/api/cron/${encodeURIComponent(jobId)}/runs?${params}`,
).then((data) => unwrapField(data, 'runs'));
}
export interface CronSettings {
enabled: boolean;
catch_up_on_startup: boolean;
max_run_history: number;
}
export function getCronSettings(): Promise<CronSettings> {
return apiFetch<CronSettings>('/api/cron/settings');
}
export function patchCronSettings(
patch: Partial<CronSettings>,
): Promise<CronSettings> {
return apiFetch<CronSettings & { status: string }>('/api/cron/settings', {
method: 'PATCH',
body: JSON.stringify(patch),
});
}
// ---------------------------------------------------------------------------
// Integrations
// ---------------------------------------------------------------------------
export function getIntegrations(): Promise<Integration[]> {
return apiFetch<Integration[] | { integrations: Integration[] }>('/api/integrations').then(
(data) => unwrapField(data, 'integrations'),
);
}
// ---------------------------------------------------------------------------
// Doctor / Diagnostics
// ---------------------------------------------------------------------------
export function runDoctor(): Promise<DiagResult[]> {
return apiFetch<DiagResult[] | { results: DiagResult[]; summary?: unknown }>('/api/doctor', {
method: 'POST',
body: JSON.stringify({}),
}).then((data) => (Array.isArray(data) ? data : data.results));
}
// ---------------------------------------------------------------------------
// Memory
// ---------------------------------------------------------------------------
export function getMemory(
query?: string,
category?: string,
): Promise<MemoryEntry[]> {
const params = new URLSearchParams();
if (query) params.set('query', query);
if (category) params.set('category', category);
const qs = params.toString();
return apiFetch<MemoryEntry[] | { entries: MemoryEntry[] }>(`/api/memory${qs ? `?${qs}` : ''}`).then(
(data) => unwrapField(data, 'entries'),
);
}
export function storeMemory(
key: string,
content: string,
category?: string,
): Promise<void> {
return apiFetch<unknown>('/api/memory', {
method: 'POST',
body: JSON.stringify({ key, content, category }),
}).then(() => undefined);
}
export function deleteMemory(key: string): Promise<void> {
return apiFetch<void>(`/api/memory/${encodeURIComponent(key)}`, {
method: 'DELETE',
});
}
// ---------------------------------------------------------------------------
// Cost
// ---------------------------------------------------------------------------
export function getCost(): Promise<CostSummary> {
return apiFetch<CostSummary | { cost: CostSummary }>('/api/cost').then((data) =>
unwrapField(data, 'cost'),
);
}
// ---------------------------------------------------------------------------
// Sessions
// ---------------------------------------------------------------------------
export function getSessions(): Promise<Session[]> {
return apiFetch<{ sessions: Session[] }>('/api/sessions').then((data) =>
data.sessions ?? [],
);
}
export function getSession(id: string): Promise<Session> {
return apiFetch<Session>(`/api/sessions/${encodeURIComponent(id)}`);
}
export function renameSession(id: string, name: string): Promise<{ session_id: string; name: string }> {
return apiFetch<{ session_id: string; name: string }>(`/api/sessions/${encodeURIComponent(id)}`, {
method: 'PUT',
body: JSON.stringify({ name }),
});
}
// ---------------------------------------------------------------------------
// Channels (detailed)
// ---------------------------------------------------------------------------
export function getChannels(): Promise<ChannelDetail[]> {
return apiFetch<ChannelDetail[] | { channels: ChannelDetail[] }>('/api/channels').then((data) =>
unwrapField(data, 'channels'),
);
}
// ---------------------------------------------------------------------------
// CLI Tools
// ---------------------------------------------------------------------------
export function getCliTools(): Promise<CliTool[]> {
return apiFetch<CliTool[] | { cli_tools: CliTool[] }>('/api/cli-tools').then((data) =>
unwrapField(data, 'cli_tools'),
);
}
// ---------------------------------------------------------------------------
// Browser / SuperRPA
// ---------------------------------------------------------------------------
export interface LaunchBrowserResponse {
session_id: string;
status: string;
}
export interface BrowserStatusResponse {
running: boolean;
sessions: BrowserSession[];
}
export function launchBrowser(url?: string, superrpaPath?: string): Promise<LaunchBrowserResponse> {
return apiFetch<LaunchBrowserResponse>('/api/browser/launch', {
method: 'POST',
body: JSON.stringify({ url: url || '', superrpa_path: superrpaPath || '' }),
});
}
export function getBrowserStatus(): Promise<BrowserStatusResponse> {
return apiFetch<BrowserStatusResponse>('/api/browser/status');
}
export function closeBrowserSession(sessionId: string): Promise<void> {
return apiFetch<void>(`/api/browser/session/${encodeURIComponent(sessionId)}/close`, {
method: 'POST',
});
}
// ---------------------------------------------------------------------------
// Plans
// ---------------------------------------------------------------------------
export function getPlans() {
return apiFetch<{ plans: import('../types/api').PlanSummary[] }>('/api/debug/plans');
}
export interface PlanMessage {
role: string;
content: string;
created_at?: string | null;
}
export function getSessionMessages(sessionId: string): Promise<{ messages: PlanMessage[] }> {
return apiFetch<{ session_id: string; messages: PlanMessage[] }>(
`/api/sessions/${encodeURIComponent(sessionId)}/messages`,
).then((d) => ({ messages: d.messages || [] }));
}
export function getPlanMessages(planId: string): Promise<{ messages: PlanMessage[] }> {
return apiFetch<{ plan_id: string; messages: PlanMessage[] }>(
`/api/plan/${encodeURIComponent(planId)}/messages`,
).then((d) => ({ messages: d.messages || [] }));
}
// ---------------------------------------------------------------------------
// Execution governance
// ---------------------------------------------------------------------------
export function getRouteDecisionTimeline(): Promise<RouteDecisionTimelineResponse> {
return apiFetch<RouteDecisionTimelineResponse>('/api/route-decisions');
}
export function routeIngress(
body: IngressRouteRequest,
): Promise<IngressRouteResponse> {
return apiFetch<IngressRouteResponse>('/api/ingress/route', {
method: 'POST',
body: JSON.stringify(body),
});
}
export function governanceCommand(
body: GovernanceCommandRequest,
): Promise<GovernanceCommandResponse> {
return apiFetch<GovernanceCommandResponse>('/api/governance/command', {
method: 'POST',
body: JSON.stringify(body),
});
}
export function runSchedulerNow(): Promise<GovernanceCommandResponse> {
const stamp = Date.now();
return governanceCommand({
command_kind: 'run_schedule_now',
context_refs: { schedule_id: 'scheduler-manual-check' },
payload: { source: 'scheduler-jobs-page' },
correlation_id: `ui-run-schedule-now-${stamp}`,
idempotency_key: `ui-run-schedule-now-${stamp}`,
});
}
export function getPlanRevisions(planId: string): Promise<any> {
return apiFetch(`/api/plan/${encodeURIComponent(planId)}/revisions`);
}
export function getEventLog(params: {
correlation_id?: string;
plan_id?: string;
task_id?: string;
run_id?: string;
limit?: number;
} = {}): Promise<any> {
const query = new URLSearchParams();
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined && value !== null && `${value}`.trim() !== '') {
query.set(key, `${value}`);
}
});
const suffix = query.toString();
return apiFetch(`/api/event-log${suffix ? `?${suffix}` : ''}`);
}
// ---------------------------------------------------------------------------
// Notifications
// ---------------------------------------------------------------------------
export { type UserNotification };
export function getNotifications(
status: string = 'unread',
): Promise<UserNotification[]> {
return apiFetch<{ notifications: UserNotification[] }>(
`/api/notifications?status=${encodeURIComponent(status)}`,
).then((d) => d.notifications ?? []);
}
export function getUnreadNotificationCount(): Promise<number> {
return apiFetch<{ count: number }>('/api/notifications/unread-count').then(
(d) => d.count ?? 0,
);
}
export function markNotificationRead(id: string): Promise<boolean> {
return apiFetch<{ ok: boolean }>(`/api/notifications/${encodeURIComponent(id)}/read`, {
method: 'POST',
}).then((d) => d.ok ?? false);
}
export function dismissNotification(id: string): Promise<boolean> {
return apiFetch<{ ok: boolean }>(
`/api/notifications/${encodeURIComponent(id)}/dismiss`,
{ method: 'POST' },
).then((d) => d.ok ?? false);
}
export function replyToNotification(
id: string,
content: string,
): Promise<{ ok: boolean; task_id?: string; status?: string }> {
return apiFetch<{ ok: boolean; task_id?: string; status?: string }>(
`/api/notifications/${encodeURIComponent(id)}/reply`,
{
method: 'POST',
body: JSON.stringify({ content }),
},
);
}
// ---------------------------------------------------------------------------
// Console: Debug Store, Plans, Tasks
// ---------------------------------------------------------------------------
export function getDebugStore(): Promise<DebugStoreResponse> {
return apiFetch<DebugStoreResponse>('/api/debug/store?limit=200');
}
export function getDashboardStats(): Promise<DashboardResponse> {
return apiFetch<DashboardResponse | { tasks: DashboardResponse['tasks'] }>('/api/dashboard')
.then((data) => ('tasks' in data && !('runtime' in data))
? { runtime: { active_tasks: 0, failed_tasks: 0, pending_confirmations: 0 }, tasks: data.tasks, approvals: { total: 0, pending: 0 } }
: data as DashboardResponse);
}
export function getDigitalEmployeeWorkday(date?: string): Promise<DigitalEmployeeWorkdayProjection> {
const query = date ? `?date=${encodeURIComponent(date)}` : '';
return apiFetch<DigitalEmployeeWorkdayProjection>(`/api/digital-employee/workday${query}`, {
timeoutMs: DIGITAL_EMPLOYEE_WORKDAY_TIMEOUT_MS,
});
}
export function postDigitalEmployeeWorkdayAction(
body: DigitalEmployeeWorkdayActionRequest,
): Promise<DigitalEmployeeWorkdayProjection> {
return apiFetch<DigitalEmployeeWorkdayProjection>('/api/digital-employee/workday/actions', {
method: 'POST',
body: JSON.stringify(body),
});
}
export function digitalEmployeeReportPdfUrl(date: string): string {
return `${apiOrigin()}${apiBase}/api/digital-employee/reports/${encodeURIComponent(date)}/pdf`;
}
export function dispatchPlanNow(planId: string): Promise<PlanDispatchResponse> {
return apiFetch<PlanDispatchResponse>(`/api/debug/plan/${encodeURIComponent(planId)}/dispatch`, {
method: 'POST',
timeoutMs: DIGITAL_EMPLOYEE_WORKDAY_TIMEOUT_MS,
});
}
export function getDebugPlans(): Promise<DebugPlan[]> {
return apiFetch<{ plans: DebugPlan[] }>('/api/debug/plans?limit=80')
.then((data) => data.plans ?? []);
}
export function searchDebugPlans(query: string, limit = 50): Promise<DebugPlan[]> {
const params = new URLSearchParams({ limit: String(limit), q: query });
return apiFetch<{ plans: DebugPlan[] }>(`/api/debug/plans?${params}`)
.then((data) => data.plans ?? []);
}
export function getDebugTasks(): Promise<{ tasks: DebugTask[]; runs: DebugRun[]; total?: number; truncated?: boolean }> {
return apiFetch<{ tasks: DebugTask[]; runs: DebugRun[]; total?: number; truncated?: boolean }>('/api/debug/tasks?limit=80')
.then((data) => ({ tasks: data.tasks ?? [], runs: data.runs ?? [], total: data.total, truncated: data.truncated }));
}
export function getDebugTasksForPlan(planId: string): Promise<{ tasks: DebugTask[]; runs: DebugRun[]; total?: number; truncated?: boolean }> {
const params = new URLSearchParams({ limit: '50', plan_id: planId });
return apiFetch<{ tasks: DebugTask[]; runs: DebugRun[]; total?: number; truncated?: boolean }>(`/api/debug/tasks?${params}`)
.then((data) => ({ tasks: data.tasks ?? [], runs: data.runs ?? [], total: data.total, truncated: data.truncated }));
}
export function getDebugPlanFlow(planId: string): Promise<FlowResponse> {
return apiFetch<FlowResponse>(`/api/debug/plan/${encodeURIComponent(planId)}/flow`);
}
export function getDebugTaskFlow(taskId: string): Promise<FlowResponse> {
return apiFetch<FlowResponse>(`/api/debug/task/${encodeURIComponent(taskId)}/flow`);
}
export function getSkills(): Promise<SkillSpec[]> {
return apiFetch<{ skills: SkillSpec[] }>('/api/skill')
.then((data) => data.skills ?? []);
}
export function setSkillEnabled(skillId: string, enabled: boolean): Promise<{ ok: boolean; skill_id: string; enabled: boolean; error?: string }> {
return apiFetch<{ ok: boolean; skill_id: string; enabled: boolean; error?: string }>(
`/api/skill/${encodeURIComponent(skillId)}/enabled`,
{
method: 'POST',
body: JSON.stringify({ enabled }),
},
);
}
export function createPlan(body: CreatePlanRequest): Promise<{ ok?: boolean; plan_id: string; task_count?: number }> {
return apiFetch('/api/debug/plan/create', {
method: 'POST',
body: JSON.stringify(body),
});
}
export function planAction(planId: string, action: string): Promise<any> {
const endpoint = action === 'submit'
? `/api/plan/${encodeURIComponent(planId)}/submit`
: `/api/debug/plan/${encodeURIComponent(planId)}/${action}`;
return apiFetch(endpoint, { method: 'POST' });
}
export function taskAction(taskId: string, action: string): Promise<any> {
return apiFetch(`/api/task/${encodeURIComponent(taskId)}/${action}`, { method: 'POST' });
}
/**
* Legacy compatibility only.
* Current React pages must trigger scheduler checks through runSchedulerNow()
* so backend governance rejection and command errors are surfaced correctly.
*/
export function triggerCronCheck(): Promise<{ ok?: boolean; activated: number }> {
return apiFetch<{ ok?: boolean; activated: number }>('/api/cron/check', { method: 'POST' });
}
export function getAuditTrace(kind: string, id: string): Promise<any> {
const params = new URLSearchParams({ kind, id });
return apiFetch(`/api/audit?${params}`);
}
export function getArtifacts(): Promise<any[]> {
return apiFetch<{ artifacts: any[] }>('/api/artifact')
.then((data) => data.artifacts ?? []);
}
export function getDebugConversations(): Promise<any[]> {
return apiFetch<{ conversations: any[] }>('/api/debug/conversations')
.then((data) => data.conversations ?? []);
}

View File

@@ -0,0 +1,42 @@
const TOKEN_KEY = 'zeroclaw_token';
/**
* Retrieve the stored authentication token.
*/
export function getToken(): string | null {
try {
return localStorage.getItem(TOKEN_KEY);
} catch {
return null;
}
}
/**
* Store an authentication token.
*/
export function setToken(token: string): void {
try {
localStorage.setItem(TOKEN_KEY, token);
} catch {
// localStorage may be unavailable (e.g. in some private browsing modes)
}
}
/**
* Remove the stored authentication token.
*/
export function clearToken(): void {
try {
localStorage.removeItem(TOKEN_KEY);
} catch {
// Ignore
}
}
/**
* Returns true if a token is currently stored.
*/
export function isAuthenticated(): boolean {
const token = getToken();
return token !== null && token.length > 0;
}

View File

@@ -0,0 +1,15 @@
declare global {
interface Window {
__SGROBOT_BASE__?: string;
__QIMINGCLAW_DIGITAL_API_BASE__?: string;
}
}
export const basePath = '.';
export const apiBase: string = (() => {
const configured = window.__QIMINGCLAW_DIGITAL_API_BASE__?.replace(/\/+$/, '');
return configured ?? '';
})();
export const apiOrigin = (): string => '';

View File

@@ -0,0 +1,488 @@
// consoleDataAdapter.ts — API 类型 → 业务 ViewModel 映射
// API 类型参考: src/types/api.ts (DebugStoreResponse, DebugPlan, SkillSpec, etc.)
// ── View Models ──
export interface DashboardOverview {
generatedAt: string;
isPartial: boolean;
sources: DataSourceStatus[];
taskTotals: { total: number; queued: number; running: number; succeeded: number; failed: number };
approvals: { total: number; pending: number };
health: { activeTasks: number; failedTasks: number; pendingConfirmations: number };
}
export interface Mission {
id: string;
title: string;
objective: string;
status: string;
createdAt: string | null;
updatedAt: string | null;
steps: MissionStep[];
sourceIds: { planId?: string; taskIds: string[] };
}
export interface MissionStep {
id: string;
title: string;
description?: string;
status: string;
preferredSkills: string[];
tasks: Array<{
id: string;
title: string;
description?: string;
status: string;
assignedSkillId: string | null;
startedAt: string | null;
finishedAt: string | null;
}>;
}
export interface ApprovalItem {
id: string;
missionId: string | null;
taskId: string | null;
status: string;
title: string;
createdAt: string | null;
reversible: boolean;
}
export interface JournalEntry {
id: string;
kind: string;
source: string;
message: string;
occurredAt: string | null;
planId: string | null;
taskId: string | null;
sourceLink?: string;
}
export interface SkillSummary {
id: string;
name: string;
version: string | null;
enabled: boolean;
description: string;
category: string;
capabilityLevel: 'core' | 'standard' | 'specialized' | 'experimental';
source: 'skill-api';
}
export interface DataSourceStatus {
endpoint: string;
ok: boolean;
loadedAt?: string;
errorMessage?: string;
}
// ── 内部 helper ──
type Obj = Record<string, unknown>;
function arr(v: unknown): Obj[] {
return Array.isArray(v) ? (v as Obj[]) : [];
}
function str(v: unknown, fallback = ''): string {
if (typeof v === 'string') return v;
if (typeof v === 'number') return String(v);
return fallback;
}
function hasChinese(v: string): boolean {
return /[\u4e00-\u9fff]/.test(v);
}
const STATUS_ZH: Record<string, string> = {
queued: '排队中',
ready: '就绪',
idle: '待执行',
pending: '待处理',
pending_authorization: '待授权',
waiting_input: '待输入',
draft: '草稿',
reviewing: '审核中',
approved: '已批准',
preparing: '准备中',
running: '执行中',
active: '运行中',
blocked: '已阻塞',
succeeded: '已完成',
success: '成功',
finished: '已完成',
complete: '已完成',
completed: '已完成',
failed: '失败',
error: '错误',
cancelled: '已取消',
canceled: '已取消',
aborted: '已终止',
rejected: '已拒绝',
deferred: '已暂缓',
dispatched: '已下发',
scheduled: '已设置定时',
enabled: '已启用',
disabled: '已停用',
paused: '已暂停',
unknown: '未知',
};
const EVENT_KIND_ZH: Record<string, string> = {
plan: '计划',
task: '任务',
task_running: '处理中',
task_done: '任务完成',
run: '执行开始',
run_done: '执行完成',
skill_tool: '技能调用',
artifact: '产物',
task_summary: '结果',
entered_session: '会话',
start: '目标',
RunStarted: '开始调用',
RequestDispatched: '请求已发出',
ResponseReceived: '收到返回',
RunSkipped: '本次跳过',
RunCompleted: '执行完成',
waiting_for_authorization: '等待授权',
pending_authorization: '等待授权',
authorization_approved: '授权通过',
authorization_rejected: '授权拒绝',
authorization_deferred: '稍后处理',
failed: '执行失败',
completed: '执行完成',
};
export function displayStatus(status: unknown, fallback = '未知'): string {
const raw = str(status).trim();
if (!raw) return fallback;
const mapped = STATUS_ZH[raw] ?? STATUS_ZH[raw.toLowerCase()];
if (mapped) return mapped;
return hasChinese(raw) ? raw : fallback;
}
export function displayEventKind(kind: unknown): string {
const raw = str(kind).trim();
if (!raw) return '事件';
return EVENT_KIND_ZH[raw] ?? EVENT_KIND_ZH[raw.toLowerCase()] ?? (hasChinese(raw) ? raw : '事件');
}
export function displayBackendMessage(message: unknown, fallback = '暂无事件内容'): string {
const raw = str(message).trim();
if (!raw) return fallback;
if (hasChinese(raw)) return raw;
const status = displayStatus(raw, '');
if (status) return status;
const attempt = raw.match(/^(Succeeded|Failed|Running|Completed)\s*\(attempt\s*#(\d+)\)$/i);
if (attempt) {
return `${displayStatus(attempt[1])}(第 ${attempt[2]} 次)`;
}
if (/request failed/i.test(raw)) return '请求失败';
if (/backend unavailable/i.test(raw)) return '后端服务不可达';
if (/not found/i.test(raw)) return '未找到对应资源';
return fallback;
}
export function displayBusinessText(value: unknown, fallback = '未命名内容'): string {
const raw = str(value).trim();
if (!raw) return fallback;
if (/unnamed/i.test(raw)) return fallback;
if (hasChinese(raw)) return raw;
const status = displayStatus(raw, '');
if (status) return status;
const skillName = knownSkillName(raw);
if (skillName) return skillName;
return raw;
}
function isTerminalDoneStatus(status: string): boolean {
return ['succeeded', 'success', 'finished', 'complete', 'completed'].includes(status.toLowerCase());
}
function isTerminalFailedStatus(status: string): boolean {
return ['failed', 'error', 'cancelled', 'canceled', 'aborted', 'rejected', 'deadletter'].includes(status.toLowerCase());
}
function deriveStepStatus(rawStatus: unknown, tasks: MissionStep['tasks']): string {
const status = str(rawStatus).trim().toLowerCase();
if (tasks.length === 0) return status || 'pending';
if (tasks.some((task) => isTerminalFailedStatus(task.status))) return 'failed';
if (tasks.some((task) => ['running', 'active', 'leased'].includes(task.status.toLowerCase()))) return 'running';
if (tasks.some((task) => ['queued', 'ready', 'pending'].includes(task.status.toLowerCase()))) return 'queued';
if (tasks.every((task) => isTerminalDoneStatus(task.status))) return 'succeeded';
return status || 'pending';
}
function deriveMissionStatus(rawStatus: unknown, steps: MissionStep[]): string {
const status = str(rawStatus, 'unknown').trim().toLowerCase();
if (isTerminalDoneStatus(status) || isTerminalFailedStatus(status)) return status;
if (steps.some((step) => isTerminalFailedStatus(step.status))) return 'failed';
if (steps.some((step) => ['running', 'active', 'queued', 'leased'].includes(step.status.toLowerCase()))) return 'running';
if (steps.length > 0 && steps.every((step) => isTerminalDoneStatus(step.status))) return 'succeeded';
return status || 'unknown';
}
// ── 适配函数 ──
/**
* @param storeData — getDebugStore() 返回值 (DebugStoreResponse)
* @param skillsData — getSkills() 返回值 (SkillSpec[])
*
* DebugStoreResponse 形状: { tables: { tasks, runs, approvals, ... } }
* SkillSpec 形状: { skill_id, name, version, enabled, description }
* DebugPlan 形状: { plan_id, title, objective, status, created_at, steps }
*/
export function adaptDashboardOverview(
storeData: Obj | null
): DashboardOverview {
const now = new Date().toISOString();
const tables = (storeData?.tables ?? {}) as Obj;
const tasks = arr(tables.tasks);
const approvals = arr(tables.approvals);
const taskTotals = {
total: tasks.length,
queued: tasks.filter(t => t.status === 'queued').length,
running: tasks.filter(t => t.status === 'running').length,
succeeded: tasks.filter(t => t.status === 'succeeded').length,
failed: tasks.filter(t => t.status === 'failed').length,
};
return {
generatedAt: now,
isPartial: !storeData,
sources: [
{ endpoint: '/api/debug/store', ok: storeData !== null, loadedAt: now },
],
taskTotals,
approvals: {
total: approvals.length,
pending: approvals.filter(a => a.status === 'pending').length,
},
health: {
activeTasks: taskTotals.running,
failedTasks: taskTotals.failed,
pendingConfirmations: approvals.filter(a => a.status === 'pending').length,
},
};
}
export function adaptMissions(plansData: Obj[] | null): Mission[] {
if (!plansData) return [];
return plansData.map((p: Obj) => {
const planId = str(p.plan_id);
const stepsRaw = arr(p.steps);
const steps: MissionStep[] = stepsRaw.map((s: Obj) => {
const taskList = arr(s.tasks);
const tasks = taskList.map((t: Obj) => ({
id: str(t.task_id),
title: displayBusinessText(t.title, '子任务'),
description: str(t.description),
status: str(t.status, 'pending'),
assignedSkillId: t.assigned_skill_id ? str(t.assigned_skill_id) : null,
startedAt: t.started_at ? str(t.started_at) : null,
finishedAt: t.finished_at ? str(t.finished_at) : null,
}));
return {
id: str(s.step_id),
title: displayBusinessText(s.title, '任务步骤'),
description: str(s.description),
status: deriveStepStatus(s.status, tasks),
preferredSkills: Array.isArray(s.preferred_skills) ? (s.preferred_skills as string[]) : [],
tasks,
};
});
const taskIds: string[] = [];
for (const s of steps) {
for (const t of s.tasks) {
taskIds.push(t.id);
}
}
return {
id: planId,
title: displayBusinessText(p.title, '未命名任务'),
objective: displayBusinessText(p.objective || p.description, '暂无任务说明'),
status: deriveMissionStatus(p.status, steps),
createdAt: p.created_at ? str(p.created_at) : null,
updatedAt: p.updated_at ? str(p.updated_at) : null,
steps,
sourceIds: { planId: planId || undefined, taskIds },
};
});
}
const SKILL_ZH: Record<string, string> = {
'decision-analyzer': '决策分析',
'git-log-collector': 'Git 日志采集',
'number-generator': '数据生成',
'office-export-xlsx': 'Excel 导出',
'task-tracker': '任务跟踪',
'weekly-report-writer': '周报撰写',
'zhihu-hotlist-detail': '知乎热榜明细',
'zhihu-hotlist': '知乎热榜',
'tq-lineloss-report': '台区线损报表',
'archive-workorder-grid-push-monitor': '归档工单推送监控',
'available-balance-below-zero-monitor': '可用余额监控',
'command-center-fee-control-monitor': '费用管控监控',
'sgcc-todo-crawler': 'SGCC 待办爬取',
'appointment-workorder-monitor': '约时工单监控',
'fault-location-monitor': '故障定位监控',
'fudian-failure-monitor': '复电失败监控',
'important-service-expiry-monitor': '服务到期预警',
'by-95598-customer-satisfaction-report': '95598 满意率日报',
'by-daily-report-statistics': '日报统计',
'by-fire-fault-monthly-report': '火警故障月报',
'by-risk-control-next-day-plan-report': '风控次日计划',
'by-risk-control-week-plan-report': '风控周计划',
'by-supervision-workorder-detail-report': '督办工单明细',
'age-file-encryption': '文件加解密',
'anonymous-file-upload': '匿名文件上传',
'browser-automation-agent': '浏览器自动化',
'bulk-github-star': '批量仓库标星',
'changelog-generator': '更新日志生成',
'code-reviewer': '代码审查',
'commit-automation': '自动提交',
'crypto-trading-signal': '交易信号分析',
'csv-to-json': 'CSV 转 JSON',
'data-analysis': '数据分析',
'deep-research': '深度调研',
'dependency-updater': '依赖更新',
'deploy-automation': '自动部署',
'dns-lookup': 'DNS 查询',
'document-generator': '文档生成',
'email-automation': '邮件自动化',
'file-converter': '文件格式转换',
'git-history-analyzer': 'Git 历史分析',
'github-issue-manager': 'Issue 管理',
'image-optimizer': '图片优化',
'json-schema-validator': 'JSON Schema 校验',
'log-analyzer': '日志分析',
'markdown-to-pdf': 'Markdown 转 PDF',
'meeting-summarizer': '会议纪要',
'news-aggregator': '新闻聚合',
'pdf-extractor': 'PDF 提取',
'performance-profiler': '性能分析',
'pr-review-bot': 'PR 审查',
'release-notes-generator': '发布说明生成',
'screenshot-automation': '截图自动化',
'search-engine-scraper': '搜索引擎抓取',
'sentiment-analysis': '情感分析',
'shell-command-runner': '命令执行',
'slack-notification': 'Slack 通知',
'spreadsheet-generator': '电子表格生成',
'text-summarizer': '文本摘要',
'torrent-search': '资源搜索',
'translate-text': '文本翻译',
'web-scraper': '网页抓取',
'website-monitor': '网站监控',
'whitepaper-analyzer': '白皮书分析',
'xml-to-json': 'XML 转 JSON',
};
function knownSkillName(skillId: string): string | null {
if (SKILL_ZH[skillId]) return SKILL_ZH[skillId];
for (const [key, val] of Object.entries(SKILL_ZH)) {
if (skillId.includes(key) || key.includes(skillId)) return val;
}
return null;
}
function zhName(skillId: string, fallback: string): string {
const known = knownSkillName(skillId);
if (known) return known;
return hasChinese(fallback) ? fallback : '自动化技能';
}
const SKILL_CATEGORY: Record<string, string> = {
'decision-analyzer': '数字化', 'git-log-collector': '数字化', 'number-generator': '数字化',
'office-export-xlsx': '办公', 'task-tracker': '办公', 'weekly-report-writer': '报表',
'zhihu-hotlist-detail': '数字化', 'zhihu-hotlist': '数字化', 'tq-lineloss-report': '报表',
'archive-workorder-grid-push-monitor': '监控巡检', 'available-balance-below-zero-monitor': '监控巡检',
'command-center-fee-control-monitor': '监控巡检', 'sgcc-todo-crawler': '监控巡检',
'appointment-workorder-monitor': '监控巡检', 'fault-location-monitor': '监控巡检',
'fudian-failure-monitor': '监控巡检', 'important-service-expiry-monitor': '监控巡检',
'by-95598-customer-satisfaction-report': '报表', 'by-daily-report-statistics': '报表',
'by-fire-fault-monthly-report': '报表', 'by-risk-control-next-day-plan-report': '报表',
'by-risk-control-week-plan-report': '报表', 'by-supervision-workorder-detail-report': '报表',
'age-file-encryption': '数字化', 'anonymous-file-upload': '通信', 'browser-automation-agent': '数字化',
'bulk-github-star': '发展', 'changelog-generator': '发展', 'chat-logger': '通信',
'check-crypto-address-balance': '财务', 'city-distance': '发展', 'city-tourism-website-builder': '营销',
'crawl-websites-at-scale': '数字化', 'csv-data-summarizer': '财务', 'd3js-data-visualization': '数字化',
'database-query-and-export': '数字化', 'file-tracker': '综合', 'free-geocoding-and-maps': '发展',
'free-translation-api': '综合', 'free-weather-data': '综合', 'generate-asset-price-chart': '财务',
'generate-qr-code-natively': '数字化', 'get-crypto-price': '财务', 'git-worktree-cli': '发展',
'github-stats': '发展', 'html-to-markdown': '数字化', 'image-compressor': '综合',
'json-to-csv': '数字化', 'llm-council': '数字化', 'markdown-table-generator': '综合',
'multi-format-converter': '综合', 'npm-package-info': '发展', 'pdf-generator': '综合',
'random-number-generator': '综合', 'readability-score': '综合', 'rss-feed-reader': '通信',
'sql-formatter': '数字化', 'text-diff': '综合', 'torrent-search': '通信',
'unit-converter': '综合', 'url-shortener': '通信', 'youtube-transcript': '通信', 'zip-extractor': '综合',
};
function categoryFor(skillId: string): string {
if (SKILL_CATEGORY[skillId]) return SKILL_CATEGORY[skillId];
return '综合';
}
function zhDescription(skillId: string, rawDescription: unknown, name: string): string {
const description = str(rawDescription).trim();
if (description && hasChinese(description)) return description;
if (skillId.includes('monitor')) return `${name}能力,用于按排班自动巡检并提示异常。`;
if (skillId.includes('report')) return `${name}能力,用于自动生成和整理业务报表。`;
return `${name}能力,可由数字员工按需执行或定时处理。`;
}
export function adaptSkillSummaries(skillsData: Obj[] | null): SkillSummary[] {
if (!skillsData) return [];
return skillsData.map((s: Obj) => {
const sid = str(s.skill_id, str(s.id));
return {
id: sid,
name: zhName(sid, str(s.name, sid)),
version: s.version ? str(s.version) : null,
enabled: Boolean(s.enabled),
description: zhDescription(sid, s.description, zhName(sid, str(s.name, sid))),
category: categoryFor(sid),
capabilityLevel: 'standard' as const,
source: 'skill-api' as const,
};
});
}
export function adaptJournalEntries(storeData: Obj | null): JournalEntry[] {
if (!storeData) return [];
const tables = (storeData.tables ?? {}) as Obj;
const events = arr(tables.canonical_events);
return events.map((e: Obj) => ({
id: str(e.event_id, str(e.id)),
kind: str(e.event_type ?? e.kind, 'unknown'),
source: 'debug-store',
message: str(e.message, ''),
occurredAt: e.occurred_at ? str(e.occurred_at) : e.timestamp ? str(e.timestamp) : null,
planId: e.plan_id ? str(e.plan_id) : null,
taskId: e.task_id ? str(e.task_id) : null,
}));
}
export function adaptApprovals(storeData: Obj | null): ApprovalItem[] {
if (!storeData) return [];
const tables = (storeData.tables ?? {}) as Obj;
const approvals = arr(tables.approvals);
return approvals.map((a: Obj) => ({
id: str(a.approval_id, str(a.id)),
missionId: a.plan_id ? str(a.plan_id) : null,
taskId: a.task_id ? str(a.task_id) : null,
status: str(a.status, 'pending'),
title: displayBusinessText(a.title, '待处理授权'),
createdAt: a.created_at ? str(a.created_at) : null,
reversible: Boolean(a.reversible),
}));
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,11 @@
declare global {
interface Window {
__TAURI__?: unknown;
__ZEROCLAW_GATEWAY__?: string;
__TAURI_INTERNALS__?: unknown;
}
}
export const isTauri = (): boolean => false;
export const tauriGatewayUrl = (): string => '';

View File

@@ -0,0 +1,22 @@
import React from 'react';
import { createRoot } from 'react-dom/client';
import { HashRouter, Navigate, Route, Routes } from 'react-router-dom';
import DigitalEmployeeShell from './pages/digital/DigitalEmployeeShell';
import './index.css';
function App() {
return (
<HashRouter>
<Routes>
<Route path="/digital" element={<DigitalEmployeeShell />} />
<Route path="*" element={<Navigate to="/digital" replace />} />
</Routes>
</HashRouter>
);
}
createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>,
);

View File

@@ -0,0 +1,322 @@
import { useCallback, useEffect, useMemo, useRef, useState, type CSSProperties } from 'react';
import { Download } from 'lucide-react';
import GlassCard from '@/components/digital/GlassCard';
import CardTitleBar from '@/components/digital/CardTitleBar';
import {
digitalEmployeeReportPdfUrl,
getDigitalEmployeeWorkday,
postDigitalEmployeeWorkdayAction,
} from '@/lib/api';
import type {
DigitalEmployeeDailyReport,
DigitalEmployeeRunDetail,
DigitalEmployeeTaskExecutionSummary,
DigitalEmployeeWorkdayProjection,
} from '@/types/api';
const pageGridStyle: CSSProperties = {
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(260px, 1fr))',
gap: 12,
};
const panelStyle: CSSProperties = {
border: '1px solid var(--de-glass-border-lighter)',
borderRadius: 10,
padding: '12px 14px',
background: 'linear-gradient(180deg, rgba(244,250,255,0.82), rgba(235,246,255,0.58))',
};
type BusyAction = 'update_report_schedule' | 'generate_daily_report' | null;
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 reportStatusLabel(report?: DigitalEmployeeDailyReport | null): string {
if (report?.status === 'ready') return '已生成';
if (report?.status === 'generating') return '生成中';
if (report?.status === 'failed') return '生成失败';
return '待生成';
}
function reportStatusClass(report?: DigitalEmployeeDailyReport | null): string {
return report?.status === 'ready' ? 'de-report-badge--ready' : 'de-report-badge--waiting';
}
function reportSourceText(report?: DigitalEmployeeDailyReport | null): string {
if (!report) return 'AI 将基于当天任务和运行明细生成日报';
if (report.generated_by === 'ai') return `AI 已生成日报${report.model ? `${report.model}` : ''}`;
return 'AI 不可用时已使用规则兜底生成日报';
}
function selectedDutyIds(projection: DigitalEmployeeWorkdayProjection | null): string[] {
if (!projection) return [];
const selected = projection.duties.filter((duty) => duty.selected).map((duty) => duty.id);
if (selected.length > 0) return selected;
return projection.duties.filter((duty) => duty.implemented).map((duty) => duty.id);
}
function formatGeneratedAt(value?: string | null): string {
if (!value) return '暂未生成';
const date = new Date(value);
if (Number.isNaN(date.getTime())) return value;
return date.toLocaleString('zh-CN', {
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
});
}
function TaskSectionList({ sections }: { sections: DigitalEmployeeTaskExecutionSummary[] }) {
if (sections.length === 0) {
return <p className="de-empty-text" style={{ padding: '18px 0 6px' }}></p>;
}
return (
<div className="de-task-summary-grid" aria-label="日报任务汇总">
{sections.map((section) => (
<div key={section.id} className={`de-task-summary-card de-live-feed-row--${section.latest_status_tone || 'info'}`}>
<span className="de-task-summary-title">{section.title}</span>
<span className="de-badge de-badge-info">{section.latest_status_label}</span>
<strong> {section.total_count}</strong>
<small> {section.success_count} · {section.failure_count}</small>
<em>{section.ai_summary || section.business_summary}</em>
</div>
))}
</div>
);
}
function DetailList({ details }: { details: DigitalEmployeeRunDetail[] }) {
if (details.length === 0) {
return <p className="de-empty-text" style={{ padding: '18px 0 6px' }}></p>;
}
return (
<div className="de-run-detail-list de-run-detail-list-scroll" aria-label="日报运行明细">
<div className="de-run-detail-row de-run-detail-row--head">
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
</div>
{details.map((detail) => (
<div key={detail.id} className={`de-run-detail-row de-live-feed-row--${detail.status_tone || 'info'}`}>
<span>{detail.time || '--:--'}</span>
<strong>{detail.task_name}</strong>
<em>{detail.step_name || detail.operation_name || '任务执行'}</em>
<i className="de-badge de-badge-info">{detail.status_label}</i>
<span>{detail.business_detail}</span>
</div>
))}
</div>
);
}
export default function DailyReport({ focusSection }: { focusSection?: string | null }) {
const today = useMemo(() => todayInputDate(), []);
const [projection, setProjection] = useState<DigitalEmployeeWorkdayProjection | null>(null);
const [reportScheduleTime, setReportScheduleTime] = useState('18:00');
const [loading, setLoading] = useState(true);
const [busyAction, setBusyAction] = useState<BusyAction>(null);
const [message, setMessage] = useState<string | null>(null);
const mountedRef = useRef(false);
const isLiveProjection = projection?.source === 'live';
const report = isLiveProjection ? (projection?.daily_report ?? null) : null;
const taskSections = isLiveProjection ? (report?.task_sections ?? projection?.execution_summaries ?? []) : [];
const runDetails = isLiveProjection ? (projection?.run_details ?? []) : [];
const sourceNotice = isLiveProjection ? null : (projection?.demo_reason ?? '当前没有真实运行投影,日报区域暂不展示兜底数据。');
const totals = report?.totals ?? {
task_count: taskSections.length,
execution_count: taskSections.reduce((sum, item) => sum + item.total_count, 0),
success_count: taskSections.reduce((sum, item) => sum + item.success_count, 0),
failure_count: taskSections.reduce((sum, item) => sum + item.failure_count, 0),
running_count: taskSections.reduce((sum, item) => sum + item.running_count, 0),
};
const fetchProjection = useCallback(async () => {
setLoading(true);
setMessage(null);
try {
const data = await getDigitalEmployeeWorkday(today);
if (!mountedRef.current) return;
setProjection(data);
setReportScheduleTime(data.source === 'live' ? (data.daily_report?.report_schedule_time || '18:00') : '18:00');
} catch (error) {
if (!mountedRef.current) return;
setMessage(error instanceof Error ? error.message : '日报数据加载失败');
} finally {
if (mountedRef.current) setLoading(false);
}
}, [today]);
useEffect(() => {
mountedRef.current = true;
void fetchProjection();
return () => { mountedRef.current = false; };
}, [fetchProjection]);
useEffect(() => {
if (!focusSection || loading) return;
window.setTimeout(() => {
document.getElementById(`report-section-${focusSection}`)?.scrollIntoView({
block: 'center',
behavior: 'smooth',
});
}, 80);
}, [focusSection, loading]);
const saveReportSchedule = useCallback(async () => {
setBusyAction('update_report_schedule');
setMessage(null);
try {
const data = await postDigitalEmployeeWorkdayAction({
action: 'update_report_schedule',
date: today,
report_schedule_time: reportScheduleTime,
});
if (!mountedRef.current) return;
setProjection(data);
setReportScheduleTime(data.source === 'live' ? (data.daily_report?.report_schedule_time || reportScheduleTime) : reportScheduleTime);
setMessage('日报生成时间已更新。');
} catch (error) {
if (!mountedRef.current) return;
setMessage(error instanceof Error ? error.message : '日报生成时间设置失败');
} finally {
if (mountedRef.current) setBusyAction(null);
}
}, [reportScheduleTime, today]);
const generateDailyReport = useCallback(async () => {
setBusyAction('generate_daily_report');
setMessage(null);
try {
const data = await postDigitalEmployeeWorkdayAction({
action: 'generate_daily_report',
date: today,
duty_ids: isLiveProjection ? selectedDutyIds(projection) : [],
report_schedule_time: reportScheduleTime,
});
if (!mountedRef.current) return;
setProjection(data);
setReportScheduleTime(data.source === 'live' ? (data.daily_report?.report_schedule_time || reportScheduleTime) : reportScheduleTime);
setMessage('日报已生成,可下载 PDF。');
} catch (error) {
if (!mountedRef.current) return;
setMessage(error instanceof Error ? error.message : '日报生成失败');
} finally {
if (mountedRef.current) setBusyAction(null);
}
}, [isLiveProjection, projection, reportScheduleTime, today]);
const downloadDailyReportPdf = useCallback(() => {
window.open(digitalEmployeeReportPdfUrl(today), '_blank', 'noopener,noreferrer');
}, [today]);
return (
<div style={{ padding: '14px 18px' }}>
<GlassCard>
<CardTitleBar title="日报生成">
<span className="de-card-meta-text">{today}</span>
</CardTitleBar>
<div className="de-card-body">
{loading ? (
<div className="de-loading-skeleton" style={{ height: 420 }} />
) : (
<>
<section id="report-section-today" className="de-report-summary" style={{ marginBottom: 12 }}>
<div className="de-report-summary-head">
<div className="de-report-summary-title-row">
<h3>{report?.title || '数字员工日报'}</h3>
<p>{reportSourceText(report)}</p>
</div>
<span className={`de-report-badge ${reportStatusClass(report)}`}>{reportStatusLabel(report)}</span>
</div>
<div className="de-report-summary-body">
<div className="de-report-schedule-row">
<label>
<span></span>
<input
type="time"
value={reportScheduleTime}
onChange={(event) => setReportScheduleTime(event.target.value || '18:00')}
/>
</label>
<button
type="button"
className="de-btn-secondary de-btn-sm"
disabled={busyAction === 'update_report_schedule'}
onClick={() => { void saveReportSchedule(); }}
>
</button>
<button
type="button"
className="de-btn-primary de-btn-sm"
disabled={busyAction === 'generate_daily_report'}
onClick={() => { void generateDailyReport(); }}
>
{busyAction === 'generate_daily_report' ? '生成中...' : '生成日报'}
</button>
<button
type="button"
className="de-btn-secondary de-btn-sm de-report-download-btn"
disabled={!report?.pdf_available}
onClick={downloadDailyReportPdf}
title={report?.pdf_available ? '下载日报 PDF' : '生成日报后可下载日报 PDF'}
>
<Download size={14} aria-hidden="true" />
</button>
</div>
<div className="de-report-metrics">
<div className="de-report-metric"><span></span><strong>{totals.task_count}</strong></div>
<div className="de-report-metric"><span></span><strong>{totals.execution_count}</strong></div>
<div className="de-report-metric"><span></span><strong>{totals.success_count}</strong></div>
<div className="de-report-metric"><span></span><strong>{totals.failure_count}</strong></div>
</div>
<div className="de-report-meta">
<p>{report?.summary || '点击生成日报后AI 会基于当天所有任务写出总分结构日报。'}</p>
{sourceNotice ? <p>{sourceNotice}</p> : null}
<p>{reportScheduleTime}{formatGeneratedAt(report?.generated_at)}</p>
{message ? <p>{message}</p> : null}
</div>
</div>
</section>
<div style={pageGridStyle}>
<section id="report-section-tasks" style={panelStyle}>
<h3 className="de-settings-heading" style={{ marginTop: 0 }}></h3>
<TaskSectionList sections={taskSections} />
</section>
<section id="report-section-details" style={panelStyle}>
<h3 className="de-settings-heading" style={{ marginTop: 0 }}></h3>
<DetailList details={runDetails} />
</section>
<section id="report-section-summary" style={panelStyle}>
<h3 className="de-settings-heading" style={{ marginTop: 0 }}></h3>
{(report?.detail_lines ?? []).length === 0 ? (
<p className="de-empty-text" style={{ padding: '18px 0 6px' }}> AI </p>
) : (
<div className="de-report-meta">
{(report?.detail_lines ?? []).map((line) => <p key={line}>{line}</p>)}
</div>
)}
</section>
</div>
</>
)}
</div>
</GlassCard>
</div>
);
}

View File

@@ -0,0 +1,138 @@
import { useCallback, useEffect, useState } from 'react';
import { Bell, Mail, X } from 'lucide-react';
import { useLocation, useNavigate } from 'react-router-dom';
import { RoleProvider } from '@/contexts/RoleContext';
import CommandDeck from './CommandDeck';
import MissionCenter from './MissionCenter';
import SkillLibrary from './SkillLibrary';
import DailyReport from './DailyReport';
import OpsSettings from './OpsSettings';
import type { DigitalNavigationTarget, DigitalTab } from './navigation';
const TABS: { id: DigitalTab; label: string }[] = [
{ id: 'home', label: '首页' },
{ id: 'missions', label: '任务中心' },
{ id: 'skills', label: '技能库' },
{ id: 'reports', label: '日报' },
{ id: 'settings', label: '设置' },
];
const DIGITAL_EMPLOYEE_CLOSE_MESSAGE = 'sgrobot:digital-close';
function isDigitalTab(value: string | null): value is DigitalTab {
return Boolean(value && TABS.some(tab => tab.id === value));
}
function tabFromPathname(pathname: string): DigitalTab | null {
const parts = pathname.split('/').filter(Boolean);
const last = parts.length > 0 ? parts[parts.length - 1]! : null;
return isDigitalTab(last) ? last : null;
}
function targetFromLocation(pathname: string, search: string): DigitalNavigationTarget {
const params = new URLSearchParams(search);
const queryTab = params.get('tab');
const tab = isDigitalTab(queryTab) ? queryTab : tabFromPathname(pathname) ?? 'home';
return {
tab,
date: params.get('date'),
missionId: params.get('mission'),
taskId: params.get('task'),
skillId: params.get('skill'),
reportSection: params.get('section'),
};
}
function TabContent({
target,
onNavigate,
}: {
target: DigitalNavigationTarget;
onNavigate: (target: DigitalNavigationTarget) => void;
}) {
const tab = target.tab;
switch (tab) {
case 'home': return <CommandDeck onNavigate={onNavigate} />;
case 'missions': return <MissionCenter focusMissionId={target.missionId} focusTaskId={target.taskId} focusDate={target.date} />;
case 'skills': return <SkillLibrary focusSkillId={target.skillId} />;
case 'reports': return <DailyReport focusSection={target.reportSection} />;
case 'settings': return <OpsSettings />;
}
}
function closeDigitalWindow() {
if (window.parent && window.parent !== window) {
const targetOrigin = window.location.origin === 'null' ? '*' : window.location.origin;
window.parent.postMessage({ type: DIGITAL_EMPLOYEE_CLOSE_MESSAGE }, targetOrigin);
return;
}
window.close();
}
export default function DigitalEmployeeShell() {
const location = useLocation();
const navigate = useNavigate();
const [target, setTarget] = useState<DigitalNavigationTarget>(() => targetFromLocation(location.pathname, location.search));
useEffect(() => {
setTarget(targetFromLocation(location.pathname, location.search));
}, [location.pathname, location.search]);
const navigateDigital = useCallback((nextTarget: DigitalNavigationTarget) => {
setTarget(nextTarget);
const params = new URLSearchParams();
if (nextTarget.tab !== 'home') params.set('tab', nextTarget.tab);
if (nextTarget.date) params.set('date', nextTarget.date);
if (nextTarget.missionId) params.set('mission', nextTarget.missionId);
if (nextTarget.taskId) params.set('task', nextTarget.taskId);
if (nextTarget.skillId) params.set('skill', nextTarget.skillId);
if (nextTarget.reportSection) params.set('section', nextTarget.reportSection);
navigate({
pathname: location.pathname,
search: params.toString() ? `?${params.toString()}` : '',
}, { replace: false });
}, [location.pathname, navigate]);
return (
<RoleProvider>
<div className="de-shell">
<header className="de-header">
<div className="de-header-content">
<div className="de-header-left">
<div className="de-header-accent-bar" />
<h1 className="de-header-title"></h1>
</div>
<div className="de-header-center">
{TABS.map(tab => (
<button
key={tab.id}
className={`de-nav-btn ${target.tab === tab.id ? 'active' : ''}`}
onClick={() => navigateDigital({ tab: tab.id })}
>
<span>{tab.label}</span>
</button>
))}
</div>
<div className="de-header-right">
<span className="de-header-welcome"></span>
<span className="de-header-username"></span>
<button type="button" className="de-header-icon" title="消息">
<Mail size={16} />
</button>
<button type="button" className="de-header-icon" title="通知">
<Bell size={16} />
</button>
<button type="button" className="de-header-icon de-header-close" title="关闭" onClick={closeDigitalWindow}>
<X size={17} />
</button>
</div>
</div>
</header>
{/* Content */}
<div className="de-shell-body">
<TabContent target={target} onNavigate={navigateDigital} />
</div>
</div>
</RoleProvider>
);
}

View File

@@ -0,0 +1,729 @@
import { useState, useEffect, useCallback, useRef } from 'react';
import GlassCard from '@/components/digital/GlassCard';
import CardTitleBar from '@/components/digital/CardTitleBar';
import MissionCard from '@/components/digital/MissionCard';
import FlowDetailPanel from '@/components/digital/FlowDetailPanel';
import { adaptMissions, type Mission } from '@/lib/consoleDataAdapter';
import { getDebugPlans, governanceCommand, searchDebugPlans } from '@/lib/api';
import type { GovernanceCommandKind, GovernanceCommandResponse } from '@/types/api';
import { t } from '@/lib/i18n';
import {
completeDemoMissionOnce,
getDemoMissions,
runDemoMissionOnce,
} from './demoScenario';
import { domSafeId } from './navigation';
const REQUIRED_MISSION_IDS = [
'whitecollar-day',
'whitecollar-day-0830-browser-portal-check',
'whitecollar-day-0900-browser-inbox-triage',
'whitecollar-day-0930-calendar-brief',
'whitecollar-day-1030-browser-crm-followup',
'whitecollar-day-1130-browser-expense-approval',
'whitecollar-day-1330-sales-snapshot',
'whitecollar-day-1430-report-export',
'whitecollar-day-1530-browser-knowledge-check',
'whitecollar-day-1630-task-board-sync',
'whitecollar-day-1730-browser-day-close',
'zhihu-hotlist-monitor',
];
const DELEGATION_FILTERS = [
{ key: 'all', label: '全部委托' },
{ key: 'daily', label: '日常任务' },
{ key: 'weekly', label: '周任务' },
{ key: 'monthly', label: '月度任务' },
{ key: 'adhoc', label: '临时任务' },
];
type MissionNotice = {
kind: 'selected' | 'created' | 'dispatched';
missionId: string;
title: string;
planId?: string;
};
function planSortTime(plan: Record<string, unknown>): number {
const value = typeof plan.updated_at === 'string'
? plan.updated_at
: typeof plan.created_at === 'string'
? plan.created_at
: '';
const time = value ? new Date(value).getTime() : 0;
return Number.isFinite(time) ? time : 0;
}
function dateKey(value?: string | null): string {
if (!value) return '';
const date = new Date(value);
if (Number.isNaN(date.getTime())) return '';
return [
date.getFullYear(),
String(date.getMonth() + 1).padStart(2, '0'),
String(date.getDate()).padStart(2, '0'),
].join('-');
}
function todayDateKey(): string {
return dateKey(new Date().toISOString());
}
function shiftDateKey(value: string, days: number): string {
const date = value ? new Date(`${value}T12:00:00`) : new Date();
if (Number.isNaN(date.getTime())) return todayDateKey();
date.setDate(date.getDate() + days);
return dateKey(date.toISOString());
}
function plannedClockFromMission(mission: Mission): { text: string; minutes: number } | null {
const match = `${mission.title} ${mission.id}`.match(/(?:^|\D)(\d{1,2})[:](\d{2})(?:\D|$)/);
if (!match) return null;
const hour = Number(match[1]);
const minute = Number(match[2]);
if (!Number.isInteger(hour) || !Number.isInteger(minute) || hour > 23 || minute > 59) return null;
return {
text: `${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}`,
minutes: hour * 60 + minute,
};
}
function missionTaskTimes(mission: Mission): string[] {
return mission.steps.flatMap((step) => (
step.tasks.flatMap((task) => [task.startedAt, task.finishedAt])
)).filter((value): value is string => Boolean(value));
}
function missionMatchesDate(mission: Mission, selectedDate: string): boolean {
const taskDates = missionTaskTimes(mission).map(dateKey).filter(Boolean);
if (taskDates.includes(selectedDate)) return true;
if (plannedClockFromMission(mission)) return true;
return [mission.updatedAt, mission.createdAt].map(dateKey).includes(selectedDate);
}
function missionDateKey(mission: Mission): string {
const planned = plannedClockFromMission(mission);
if (!planned) return mission.id;
return `${mission.title}`.trim().toLowerCase() || mission.id;
}
function missionHasActualWorkOnDate(mission: Mission, selectedDate: string): boolean {
return missionTaskTimes(mission).some((value) => dateKey(value) === selectedDate);
}
function missionDedupScore(mission: Mission, selectedDate: string): number {
const actualScore = missionHasActualWorkOnDate(mission, selectedDate) ? 10_000 : 0;
const requiredScore = REQUIRED_MISSION_IDS.includes(mission.id) ? 1_000 : 0;
const selectedSyncScore = [mission.updatedAt, mission.createdAt].map(dateKey).includes(selectedDate) ? 100 : 0;
return actualScore + requiredScore + selectedSyncScore + planSortTime({
updated_at: mission.updatedAt || '',
created_at: mission.createdAt || '',
}) / 10_000_000_000_000;
}
function dedupeMissionsForDate(missions: Mission[], selectedDate: string): Mission[] {
const byKey = new Map<string, Mission>();
for (const mission of missions) {
const key = missionDateKey(mission);
const previous = byKey.get(key);
if (!previous || missionDedupScore(mission, selectedDate) > missionDedupScore(previous, selectedDate)) {
byKey.set(key, mission);
}
}
return Array.from(byKey.values());
}
function missionDateSortValue(mission: Mission, selectedDate: string): number {
const planned = plannedClockFromMission(mission);
if (planned) return planned.minutes;
const taskTimes = missionTaskTimes(mission)
.filter((value) => dateKey(value) === selectedDate)
.sort();
if (taskTimes[0]) {
const date = new Date(taskTimes[0]);
return date.getHours() * 60 + date.getMinutes();
}
return 24 * 60;
}
function missionDelegationGroup(mission: Mission): string {
const text = `${mission.id} ${mission.title} ${mission.objective}`.toLowerCase();
if (text.includes('month') || text.includes('monthly') || text.includes('月')) return 'monthly';
if (text.includes('week') || text.includes('weekly') || text.includes('周')) return 'weekly';
if (text.includes('临时') || text.includes('adhoc') || text.includes('hotlist') || text.includes('monitor')) return 'adhoc';
return 'daily';
}
async function sendMissionGovernanceCommand(
commandKind: GovernanceCommandKind,
contextRefs: Record<string, unknown>,
payload: Record<string, unknown> = {},
): Promise<GovernanceCommandResponse> {
const stamp = Date.now();
const response = await governanceCommand({
command_kind: commandKind,
context_refs: contextRefs,
payload: { source: 'digital-mission-center', ...payload },
correlation_id: `mission-${commandKind}-${stamp}`,
idempotency_key: `mission-${commandKind}-${Object.values(contextRefs).join('-') || 'new'}-${stamp}`,
});
if (!response.accepted) {
throw new Error(response.message || `command 未被接受:${response.command_id || commandKind}`);
}
return response;
}
async function sendMissionPlanCommand(
planId: string,
commandKind: 'submit_plan' | 'approve_plan' | 'activate_plan' | 'amend_plan',
payload: Record<string, unknown> = {},
): Promise<GovernanceCommandResponse> {
return sendMissionGovernanceCommand(commandKind, { plan_id: planId }, payload);
}
async function dispatchMissionPlan(mission: Mission): Promise<string | null> {
const planId = mission.sourceIds.planId;
if (!planId) return null;
const status = mission.status.toLowerCase();
if (status === 'draft') {
await sendMissionPlanCommand(planId, 'submit_plan', { requested_action: 'mission_dispatch' });
await sendMissionPlanCommand(planId, 'approve_plan', { requested_action: 'mission_dispatch' });
} else if (status === 'reviewing') {
await sendMissionPlanCommand(planId, 'approve_plan', { requested_action: 'mission_dispatch' });
} else if (!['approved', 'active', 'running'].includes(status)) {
await sendMissionPlanCommand(planId, 'amend_plan', {
requested_action: 'reopen_for_execution',
summary: '数字员工任务中心请求重新执行任务',
});
return null;
}
await sendMissionPlanCommand(planId, 'activate_plan', { requested_action: 'mission_dispatch' });
return planId;
}
function restartStepsForMission(mission: Mission) {
const steps = mission.steps.map((step, index) => {
const skills = new Set<string>();
step.preferredSkills.forEach((skill) => {
if (skill.trim()) skills.add(skill);
});
step.tasks.forEach((task) => {
if (task.assignedSkillId?.trim()) skills.add(task.assignedSkillId);
});
const description = [
step.description,
...step.tasks.map((task) => task.description),
step.title,
mission.objective,
mission.title,
].find((value) => typeof value === 'string' && value.trim());
return {
title: step.title || `${mission.title}${index + 1}`,
description: description || step.title || mission.objective || mission.title,
skills: Array.from(skills),
depends_on: index === 0 ? [] : [`step-${index}`],
};
}).filter((step) => step.skills.length > 0);
if (steps.length === 0) {
throw new Error('当前任务没有可复用的 skill不能创建真实重新执行计划。');
}
return steps;
}
function restartMissionTitle(title: string): string {
const base = title.replace(/(\s*)+$/u, '').trim();
return `${base || title} 重新执行`;
}
async function createRestartPlan(mission: Mission): Promise<string> {
const response = await sendMissionGovernanceCommand('create_plan', {
session_id: 'digital-mission-center',
plan_id: mission.sourceIds.planId || mission.id,
}, {
requested_action: 'mission_restart',
title: restartMissionTitle(mission.title),
description: mission.objective || mission.title,
steps: restartStepsForMission(mission),
original_plan_id: mission.sourceIds.planId || mission.id,
});
const planId = typeof response.result?.plan_id === 'string' ? response.result.plan_id : '';
if (!planId) {
throw new Error('重新执行计划已提交,但后端没有返回 plan_id。');
}
const status = typeof response.result?.status === 'string' ? response.result.status.toLowerCase() : '';
if (status === 'draft') {
await sendMissionPlanCommand(planId, 'submit_plan', { requested_action: 'mission_restart' });
await sendMissionPlanCommand(planId, 'approve_plan', { requested_action: 'mission_restart' });
await sendMissionPlanCommand(planId, 'activate_plan', { requested_action: 'mission_restart' });
} else if (status === 'reviewing') {
await sendMissionPlanCommand(planId, 'approve_plan', { requested_action: 'mission_restart' });
await sendMissionPlanCommand(planId, 'activate_plan', { requested_action: 'mission_restart' });
} else if (status === 'approved') {
await sendMissionPlanCommand(planId, 'activate_plan', { requested_action: 'mission_restart' });
}
return planId;
}
export default function MissionCenter({
focusMissionId,
focusTaskId,
focusDate,
}: {
focusMissionId?: string | null;
focusTaskId?: string | null;
focusDate?: string | null;
}) {
const initialMissionDate = focusDate || todayDateKey();
const [missions, setMissions] = useState<Mission[]>([]);
const [selected, setSelected] = useState<Mission | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(false);
const [filter, setFilter] = useState<string>('all');
const [dateFilter, setDateFilter] = useState<string>(() => initialMissionDate);
const [showAllDates, setShowAllDates] = useState(false);
const [runningMissionId, setRunningMissionId] = useState<string | null>(null);
const [runningAll, setRunningAll] = useState(false);
const [demoMode, setDemoMode] = useState(false);
const [actionError, setActionError] = useState<string | null>(null);
const [recentMissionId, setRecentMissionId] = useState<string | null>(null);
const [missionNotice, setMissionNotice] = useState<MissionNotice | null>(null);
const detailRef = useRef<HTMLDivElement | null>(null);
const mountedRef = useRef(false);
useEffect(() => {
mountedRef.current = true;
return () => { mountedRef.current = false; };
}, []);
const fetchMissions = useCallback(async (): Promise<Mission[]> => {
setLoading(true); setError(false);
try {
const data = await getDebugPlans();
if (!mountedRef.current) return [];
const planMap = new Map<string, Record<string, unknown>>();
const addPlan = (plan: Record<string, unknown>) => {
const id = typeof plan.plan_id === 'string' ? plan.plan_id : '';
if (id && !planMap.has(id)) planMap.set(id, plan);
};
(Array.isArray(data) ? data as Record<string, unknown>[] : []).forEach(addPlan);
const missing = REQUIRED_MISSION_IDS.filter((id) => !planMap.has(id));
if (missing.length > 0) {
const extraResults = await Promise.allSettled(
missing.map(async (id) => ({ id, plans: await searchDebugPlans(id, 20) })),
);
for (const result of extraResults) {
if (result.status === 'fulfilled') {
const exact = result.value.plans.find((plan) => plan.plan_id === result.value.id);
if (exact) {
addPlan(exact as Record<string, unknown>);
}
}
}
}
const requiredPlans = REQUIRED_MISSION_IDS
.map((id) => planMap.get(id))
.filter((plan): plan is Record<string, unknown> => Boolean(plan));
const recentPlans = [...planMap.entries()]
.filter(([id]) => !REQUIRED_MISSION_IDS.includes(id))
.map(([, plan]) => plan)
.sort((a, b) => planSortTime(b) - planSortTime(a));
const orderedPlans = [...recentPlans, ...requiredPlans];
if (orderedPlans.length > 0) {
const existingIds = new Set(orderedPlans.map((plan) => typeof plan.plan_id === 'string' ? plan.plan_id : ''));
const demoSupplement = getDemoMissions()
.filter((mission) => !existingIds.has(mission.id))
.map((mission) => ({
plan_id: mission.id,
title: mission.title,
objective: mission.objective,
status: mission.status,
created_at: mission.createdAt || new Date().toISOString(),
updated_at: mission.updatedAt || mission.createdAt || new Date().toISOString(),
steps: mission.steps.map((step) => ({
step_id: step.id,
title: step.title,
description: step.description,
status: step.status,
preferred_skills: step.preferredSkills,
tasks: step.tasks.map((task) => ({
task_id: task.id,
title: task.title,
description: task.description,
status: task.status,
assigned_skill_id: task.assignedSkillId,
started_at: task.startedAt,
finished_at: task.finishedAt,
})),
})),
}));
const nextMissions = adaptMissions([...orderedPlans, ...demoSupplement]);
setDemoMode(false);
setMissions(nextMissions);
return nextMissions;
} else {
const demoMissions = getDemoMissions();
setDemoMode(true);
setMissions(demoMissions);
return demoMissions;
}
} catch (err) {
if (!mountedRef.current) return [];
console.error('[MissionCenter] fetch failed:', err);
const demoMissions = getDemoMissions();
setDemoMode(true);
setMissions(demoMissions);
setError(false);
return demoMissions;
} finally {
if (mountedRef.current) setLoading(false);
}
}, []);
useEffect(() => { fetchMissions(); }, [fetchMissions]);
useEffect(() => {
setDateFilter(focusDate || todayDateKey());
setShowAllDates(false);
}, [focusDate]);
const focusMission = useCallback((mission: Mission) => {
setSelected(mission);
setRecentMissionId(mission.id);
window.setTimeout(() => {
detailRef.current?.scrollIntoView({ block: 'start', inline: 'nearest', behavior: 'smooth' });
}, 40);
}, []);
const selectMission = useCallback((mission: Mission) => {
focusMission(mission);
setMissionNotice({
kind: 'selected',
missionId: mission.id,
title: mission.title,
planId: mission.sourceIds.planId,
});
}, [focusMission]);
useEffect(() => {
if (!focusMissionId && !focusTaskId) return;
const matched = missions.find((mission) => (
mission.id === focusMissionId
|| mission.sourceIds.planId === focusMissionId
|| (focusTaskId ? mission.sourceIds.taskIds.includes(focusTaskId) : false)
));
if (!matched) return;
setFilter('all');
selectMission(matched);
window.setTimeout(() => {
document.getElementById(`mission-card-${domSafeId(matched.id)}`)?.scrollIntoView({
block: 'center',
behavior: 'smooth',
});
}, 80);
}, [focusMissionId, focusTaskId, missions, selectMission]);
const runMission = useCallback(async (mission: Mission) => {
setRunningMissionId(mission.id);
setError(false);
setActionError(null);
if (demoMode || !mission.sourceIds.planId) {
setMissions((current) => runDemoMissionOnce(current, mission.id));
setSelected((current) => {
if (current?.id !== mission.id) return current;
const next = runDemoMissionOnce([current], mission.id);
return next[0] ?? current;
});
window.setTimeout(() => {
if (!mountedRef.current) return;
setMissions((current) => completeDemoMissionOnce(current, mission.id));
setSelected((current) => {
if (current?.id !== mission.id) return current;
const next = completeDemoMissionOnce([current], mission.id);
return next[0] ?? current;
});
setRunningMissionId(null);
}, 1200);
return;
}
try {
const newPlanId = await dispatchMissionPlan(mission);
const nextMissions = await fetchMissions();
setFilter('all');
if (newPlanId) {
const selectedMission = nextMissions.find((item) => item.id === newPlanId || item.sourceIds.planId === newPlanId);
if (selectedMission) {
focusMission(selectedMission);
setMissionNotice({
kind: 'dispatched',
missionId: selectedMission.id,
title: selectedMission.title,
planId: newPlanId,
});
} else {
const plans = await searchDebugPlans(newPlanId, 5);
const fallback = adaptMissions(plans as unknown as Record<string, unknown>[]).find((item) => item.id === newPlanId);
if (fallback) {
focusMission(fallback);
setMissionNotice({
kind: 'dispatched',
missionId: fallback.id,
title: fallback.title,
planId: newPlanId,
});
}
else setSelected(null);
}
} else {
setSelected((current) => current?.id === mission.id ? null : current);
}
} catch (err) {
console.error('[MissionCenter] dispatch failed:', err);
setError(false);
setActionError(err instanceof Error ? err.message : '任务委托失败');
} finally {
setRunningMissionId(null);
}
}, [demoMode, fetchMissions, focusMission]);
const restartMission = useCallback(async (mission: Mission) => {
setRunningMissionId(mission.id);
setError(false);
setActionError(null);
if (demoMode || !mission.sourceIds.planId) {
await runMission(mission);
return;
}
try {
const newPlanId = await createRestartPlan(mission);
const nextMissions = await fetchMissions();
setFilter('all');
const selectedMission = nextMissions.find((item) => item.id === newPlanId || item.sourceIds.planId === newPlanId);
if (selectedMission) {
focusMission(selectedMission);
setMissionNotice({
kind: 'created',
missionId: selectedMission.id,
title: selectedMission.title,
planId: newPlanId,
});
} else {
setRecentMissionId(newPlanId);
setMissionNotice({
kind: 'created',
missionId: newPlanId,
title: restartMissionTitle(mission.title),
planId: newPlanId,
});
}
} catch (err) {
console.error('[MissionCenter] restart failed:', err);
setError(false);
setActionError(err instanceof Error ? err.message : '重新执行失败');
} finally {
setRunningMissionId(null);
}
}, [demoMode, fetchMissions, focusMission, runMission]);
const selectedDate = dateFilter || todayDateKey();
const filteredByDate = dedupeMissionsForDate(
showAllDates ? missions : missions.filter((mission) => missionMatchesDate(mission, selectedDate)),
selectedDate,
);
const runnableMissions = filteredByDate.filter((mission) => (
mission.sourceIds.planId
&& ['draft', 'reviewing', 'approved'].includes(mission.status.toLowerCase())
));
const runAllMissions = useCallback(async () => {
if (runnableMissions.length === 0) return;
setRunningAll(true);
setError(false);
if (demoMode) {
let delay = 0;
for (const mission of runnableMissions.slice(0, 4)) {
window.setTimeout(() => {
if (!mountedRef.current) return;
setRunningMissionId(mission.id);
setMissions((current) => runDemoMissionOnce(current, mission.id));
}, delay);
delay += 400;
window.setTimeout(() => {
if (!mountedRef.current) return;
setMissions((current) => completeDemoMissionOnce(current, mission.id));
}, delay + 900);
}
window.setTimeout(() => {
if (!mountedRef.current) return;
setRunningMissionId(null);
setRunningAll(false);
}, delay + 1200);
return;
}
try {
for (const mission of runnableMissions) {
setRunningMissionId(mission.id);
await dispatchMissionPlan(mission);
}
await fetchMissions();
setSelected(null);
} catch (err) {
console.error('[MissionCenter] batch dispatch failed:', err);
setError(false);
setActionError(err instanceof Error ? err.message : '批量委托失败');
} finally {
setRunningMissionId(null);
setRunningAll(false);
}
}, [demoMode, fetchMissions, runnableMissions]);
const filtered = filter === 'all'
? filteredByDate
: filteredByDate.filter(m => missionDelegationGroup(m) === filter);
const displayMissions = filtered
.map((mission, index) => ({ mission, index }))
.sort((a, b) => {
const priority = (mission: Mission) => (
(mission.id === recentMissionId ? 2 : 0)
+ (mission.id === selected?.id ? 1 : 0)
);
return priority(b.mission) - priority(a.mission)
|| missionDateSortValue(a.mission, selectedDate) - missionDateSortValue(b.mission, selectedDate)
|| a.index - b.index;
})
.map(({ mission }) => mission);
const noticeTitle = missionNotice?.kind === 'created'
? '刚创建重新执行任务'
: missionNotice?.kind === 'dispatched'
? '刚委托执行任务'
: '当前查看任务';
const focusLabelForMission = (mission: Mission): string | null => {
if (mission.id !== recentMissionId) return null;
if (missionNotice?.missionId === mission.id && missionNotice.kind === 'created') return '刚创建';
if (missionNotice?.missionId === mission.id && missionNotice.kind === 'dispatched') return '刚委托';
return '当前查看';
};
return (
<div className="de-mission-center-page">
<div className="de-mission-center-layout">
<div className="de-mc-list">
<GlassCard>
<CardTitleBar title="任务委托">
<button
onClick={runAllMissions}
className="de-btn-primary de-btn-sm"
disabled={runningAll || runnableMissions.length === 0}
>
{runningAll ? '委托中...' : `委托可执行任务(${runnableMissions.length})`}
</button>
<button onClick={fetchMissions} className="de-btn-secondary de-btn-sm">{t('console.action.refresh')}</button>
</CardTitleBar>
<div className="de-card-body">
{demoMode && (
<div className="de-schedule-result de-schedule-result--success" style={{ marginBottom: 12 }}>
使
</div>
)}
{actionError && (
<div className="de-schedule-result de-schedule-result--error" style={{ marginBottom: 12 }}>
{actionError}
</div>
)}
{missionNotice && (
<div className="de-mission-focus-notice">
<strong>{noticeTitle}</strong>
<span>{missionNotice.title}</span>
{missionNotice.planId && <code>{missionNotice.planId}</code>}
</div>
)}
<div className="de-date-filter-row">
<label className="de-date-filter-field">
<span></span>
<input
type="date"
value={selectedDate}
onChange={(event) => {
setShowAllDates(false);
setDateFilter(event.target.value || todayDateKey());
}}
/>
</label>
<div className="de-date-filter-actions" aria-label="日期切换">
<button type="button" className="de-filter-chip" onClick={() => { setShowAllDates(false); setDateFilter(shiftDateKey(selectedDate, -1)); }}></button>
<button type="button" className="de-filter-chip active" onClick={() => { setShowAllDates(false); setDateFilter(todayDateKey()); }}></button>
<button type="button" className="de-filter-chip" onClick={() => { setShowAllDates(false); setDateFilter(shiftDateKey(selectedDate, 1)); }}></button>
</div>
<span className="de-date-filter-summary">
{showAllDates ? `全部 ${filteredByDate.length} 项任务` : `${filteredByDate.length} 项任务`}
</span>
</div>
<div className="de-filter-row">
{DELEGATION_FILTERS.map((item) => (
<button
key={item.key}
className={`de-filter-chip ${filter === item.key ? 'active' : ''}`}
onClick={() => setFilter(item.key)}
>
{item.label}
</button>
))}
</div>
{loading ? (
<div className="de-loading-skeleton" style={{ height: 300 }} />
) : error ? (
<p className="de-empty-text">{t('console.state.backendDown')}</p>
) : displayMissions.length === 0 ? (
<div className="de-mission-empty-panel">
<strong></strong>
<p></p>
<div className="de-mission-empty-actions">
<button type="button" className="de-btn-primary de-btn-sm" onClick={() => { setShowAllDates(true); setFilter('all'); }}>
</button>
<button type="button" className="de-btn-secondary de-btn-sm" onClick={() => { setShowAllDates(false); setDateFilter(focusDate || todayDateKey()); }}>
</button>
</div>
</div>
) : (
<div className="de-mc-card-list">
{displayMissions.map(m => (
<MissionCard
key={m.id}
mission={m}
selected={selected?.id === m.id}
focusLabel={focusLabelForMission(m)}
dateFilter={selectedDate}
running={runningMissionId === m.id}
onSelect={selectMission}
onRun={runMission}
onRestart={restartMission}
/>
))}
</div>
)}
</div>
</GlassCard>
</div>
<div className="de-mc-detail" ref={detailRef}>
{selected ? (
<FlowDetailPanel mission={selected} onClose={() => setSelected(null)} />
) : (
<GlassCard>
<div className="de-card-body">
<div className="de-mission-empty-panel de-mission-empty-panel--detail">
<strong></strong>
<p></p>
</div>
</div>
</GlassCard>
)}
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,170 @@
import { useState, useEffect, useRef, type CSSProperties } from 'react';
import GlassCard from '@/components/digital/GlassCard';
import CardTitleBar from '@/components/digital/CardTitleBar';
import { useRole, ROLE_PROFILES, type ConsoleRole } from '@/contexts/RoleContext';
import { t } from '@/lib/i18n';
const ROLES: ConsoleRole[] = ['sales', 'engineering', 'hr', 'finance', 'executive'];
const STORAGE_KEY = 'sgrobot-role-preferences';
type RolePreferences = {
workStart: string;
workEnd: string;
notification: 'system' | 'email' | 'silent';
reportTime: string;
avatarStyle: 'professional' | 'friendly' | 'concise';
reminderLevel: 'important' | 'all' | 'quiet';
};
const defaultPreferences: RolePreferences = {
workStart: '09:00',
workEnd: '18:00',
notification: 'system',
reportTime: '17:30',
avatarStyle: 'professional',
reminderLevel: 'important',
};
const densityLabel: Record<string, string> = {
compact: '紧凑',
balanced: '均衡',
presentation: '汇报',
};
const fieldGridStyle: CSSProperties = {
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(220px, 1fr))',
gap: 12,
};
const fieldStyle: CSSProperties = {
display: 'flex',
flexDirection: 'column',
gap: 6,
border: '1px solid var(--de-glass-border-lighter)',
borderRadius: 10,
padding: '12px 14px',
background: 'linear-gradient(180deg, rgba(244,250,255,0.82), rgba(235,246,255,0.58))',
};
const inputStyle: CSSProperties = {
minHeight: 34,
borderRadius: 8,
border: '1px solid var(--de-glass-border-light)',
background: 'rgba(255,255,255,0.72)',
color: 'var(--de-text-primary)',
padding: '0 10px',
};
function loadPreferences(): RolePreferences {
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) return defaultPreferences;
return { ...defaultPreferences, ...JSON.parse(raw) };
} catch {
return defaultPreferences;
}
}
export default function OpsSettings() {
const { role, profile, setRole } = useRole();
const [preferences, setPreferences] = useState<RolePreferences>(loadPreferences);
const [saved, setSaved] = useState(false);
const savedTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
useEffect(() => {
return () => { if (savedTimerRef.current) clearTimeout(savedTimerRef.current); };
}, []);
const markSaved = () => {
setSaved(true);
if (savedTimerRef.current) clearTimeout(savedTimerRef.current);
savedTimerRef.current = setTimeout(() => setSaved(false), 2000);
};
const savePreferences = (next: RolePreferences) => {
setPreferences(next);
try { localStorage.setItem(STORAGE_KEY, JSON.stringify(next)); } catch { /* localStorage blocked */ }
markSaved();
};
const updatePreference = <K extends keyof RolePreferences>(key: K, value: RolePreferences[K]) => {
savePreferences({ ...preferences, [key]: value });
};
const handleRoleChange = (nextRole: ConsoleRole) => {
setRole(nextRole);
markSaved();
};
return (
<div style={{ padding: '14px 18px' }}>
<GlassCard>
<CardTitleBar title="角色偏好">
{saved && <span className="de-card-meta-text"></span>}
</CardTitleBar>
<div className="de-card-body">
<section className="de-settings-section">
<h3 className="de-settings-heading"></h3>
<p className="de-settings-desc"></p>
<div className="de-role-grid">
{ROLES.map((item) => (
<button key={item} className={`de-role-card ${role === item ? 'active' : ''}`} onClick={() => handleRoleChange(item)}>
<span className="de-role-label">{t(ROLE_PROFILES[item].labelKey)}</span>
<span className="de-role-density">{densityLabel[ROLE_PROFILES[item].density] ?? '均衡'}</span>
</button>
))}
</div>
</section>
<section className="de-settings-section">
<h3 className="de-settings-heading"></h3>
<div style={fieldGridStyle}>
<label style={fieldStyle}>
<span className="de-role-label"></span>
<input style={inputStyle} type="time" value={preferences.workStart} onChange={(event) => updatePreference('workStart', event.target.value)} />
</label>
<label style={fieldStyle}>
<span className="de-role-label"></span>
<input style={inputStyle} type="time" value={preferences.workEnd} onChange={(event) => updatePreference('workEnd', event.target.value)} />
</label>
<label style={fieldStyle}>
<span className="de-role-label"></span>
<input style={inputStyle} type="time" value={preferences.reportTime} onChange={(event) => updatePreference('reportTime', event.target.value)} />
</label>
<label style={fieldStyle}>
<span className="de-role-label"></span>
<select style={inputStyle} value={preferences.notification} onChange={(event) => updatePreference('notification', event.target.value as RolePreferences['notification'])}>
<option value="system"></option>
<option value="email"></option>
<option value="silent"></option>
</select>
</label>
<label style={fieldStyle}>
<span className="de-role-label"></span>
<select style={inputStyle} value={preferences.avatarStyle} onChange={(event) => updatePreference('avatarStyle', event.target.value as RolePreferences['avatarStyle'])}>
<option value="professional"></option>
<option value="friendly"></option>
<option value="concise"></option>
</select>
</label>
<label style={fieldStyle}>
<span className="de-role-label"></span>
<select style={inputStyle} value={preferences.reminderLevel} onChange={(event) => updatePreference('reminderLevel', event.target.value as RolePreferences['reminderLevel'])}>
<option value="important"></option>
<option value="all"></option>
<option value="quiet"></option>
</select>
</label>
</div>
</section>
<details className="de-settings-section" style={{ marginBottom: 0 }}>
<summary className="de-settings-heading" style={{ cursor: 'pointer' }}></summary>
<p className="de-settings-desc">使{Math.round(profile.staleDataThresholdMs / 1000)} </p>
</details>
</div>
</GlassCard>
</div>
);
}

View File

@@ -0,0 +1,318 @@
import { useState, useEffect, useCallback, useRef } from 'react';
import GlassCard from '@/components/digital/GlassCard';
import CardTitleBar from '@/components/digital/CardTitleBar';
import { adaptSkillSummaries, type SkillSummary } from '@/lib/consoleDataAdapter';
import { getSkills, setSkillEnabled } from '@/lib/api';
import { getDemoSkills } from './demoScenario';
import { domSafeId } from './navigation';
type TaskCadence = 'daily' | 'weekly' | 'monthly' | 'adhoc';
type SkillSection = {
key: string;
title: string;
hint: string;
skills: SkillSummary[];
emptyText: string;
};
const cadenceOptions: Array<{ value: 'all' | TaskCadence; label: string }> = [
{ value: 'all', label: '全部任务' },
{ value: 'daily', label: '日常任务' },
{ value: 'weekly', label: '周任务' },
{ value: 'monthly', label: '月度任务' },
{ value: 'adhoc', label: '临时任务' },
];
const cadenceLabels: Record<TaskCadence, string> = {
daily: '日常',
weekly: '周',
monthly: '月度',
adhoc: '临时',
};
function getCadence(skill: SkillSummary): TaskCadence {
const text = `${skill.id} ${skill.name} ${skill.description} ${skill.category}`.toLowerCase();
if (/weekly|week|周/.test(text)) return 'weekly';
if (/monthly|month|月报|月度/.test(text)) return 'monthly';
if (/monitor|crawler|巡检|监控|待办|日程|日报|日终|收件箱|工作台|客户跟进/.test(text)) return 'daily';
return 'adhoc';
}
function isAdvancedSkill(skill: SkillSummary): boolean {
const text = `${skill.id} ${skill.name} ${skill.description} ${skill.category}`.toLowerCase();
return (
skill.capabilityLevel === 'specialized' ||
skill.capabilityLevel === 'experimental' ||
/加密|深度|分析|自动化|部署|代码|审查|调研|批量|converter|crypto|github|deploy|review|research/.test(text)
);
}
function isRecommendedSkill(skill: SkillSummary): boolean {
const text = `${skill.id} ${skill.name} ${skill.description} ${skill.category}`.toLowerCase();
return /日报|周报|报表|待办|日程|巡检|客户|知识库|经营|费用|收件箱|工作台/.test(text);
}
function getOutcome(skill: SkillSummary): string {
const name = skill.name;
const text = `${skill.id} ${skill.name} ${skill.description} ${skill.category}`.toLowerCase();
if (/report|报表|日报|周报|月报|汇报/.test(text)) return `自动整理${name}材料,减少手工汇总和重复填报。`;
if (/monitor|巡检|监控|预警/.test(text)) return `按约定频率完成${name},及时提示异常和遗漏。`;
if (/todo|待办|task|看板|日程/.test(text)) return `帮你梳理${name},把后续处理事项集中呈现。`;
if (/客户|crm|回访|满意/.test(text)) return `汇总客户相关信息,形成可直接跟进的处理建议。`;
if (/知识|文档|制度|模板/.test(text)) return `查找和核对资料内容,减少人工翻找和版本确认。`;
return skill.description;
}
function learningStatus(skill: SkillSummary, recommended: boolean, advanced: boolean): string {
if (skill.enabled) return '已学习';
if (!skill.enabled) return '暂不使用';
if (advanced) return '需确认后学习';
if (recommended) return '建议学习';
return '可学习';
}
function SkillCard({
skill,
recommended = false,
advanced = false,
selected = false,
updating = false,
onSelect,
onToggle,
}: {
skill: SkillSummary;
recommended?: boolean;
advanced?: boolean;
selected?: boolean;
updating?: boolean;
onSelect?: (skill: SkillSummary) => void;
onToggle?: (skill: SkillSummary) => void;
}) {
const cadence = getCadence(skill);
const status = learningStatus(skill, recommended, advanced);
return (
<div
id={`skill-card-${domSafeId(skill.id)}`}
className={`de-skill-card ${selected ? 'de-skill-card--selected' : ''}`}
role="button"
tabIndex={0}
onClick={() => onSelect?.(skill)}
onKeyDown={(event) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
onSelect?.(skill);
}
}}
>
<div className="de-skill-card-head">
<span className="de-skill-name">{skill.name}</span>
<span className={`de-skill-status ${skill.enabled ? 'enabled' : 'disabled'}`}>
{status}
</span>
</div>
<p className="de-skill-desc">{getOutcome(skill)}</p>
<div className="de-skill-card-foot">
<span className="de-skill-version">
{cadenceLabels[cadence]}
</span>
<button
className={`de-skill-btn ${skill.enabled ? 'de-skill-btn--danger' : ''}`}
disabled={updating}
title={skill.enabled ? '暂时停用此技能,后续路由不会选择它。' : '恢复使用此技能。'}
onClick={(event) => {
event.stopPropagation();
if (!updating) onToggle?.(skill);
}}
>
{updating ? '处理中...' : skill.enabled ? '暂不使用' : '恢复使用'}
</button>
</div>
</div>
);
}
export default function SkillLibrary({ focusSkillId }: { focusSkillId?: string | null }) {
const [skills, setSkills] = useState<SkillSummary[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(false);
const [search, setSearch] = useState('');
const [cadenceFilter, setCadenceFilter] = useState<'all' | TaskCadence>('all');
const [selectedSkillId, setSelectedSkillId] = useState<string | null>(focusSkillId ?? null);
const [updatingSkillId, setUpdatingSkillId] = useState<string | null>(null);
const [actionError, setActionError] = useState<string | null>(null);
const mountedRef = useRef(false);
useEffect(() => {
mountedRef.current = true;
return () => { mountedRef.current = false; };
}, []);
const fetchSkills = useCallback(async () => {
setLoading(true); setError(false);
try {
const data = await getSkills();
if (!mountedRef.current) return;
setSkills(
Array.isArray(data) && data.length > 0
? adaptSkillSummaries(data as Record<string, unknown>[])
: getDemoSkills(),
);
} catch (err) {
if (!mountedRef.current) return;
console.error('[SkillLibrary] fetch failed:', err);
setSkills(getDemoSkills());
setError(false);
} finally {
if (mountedRef.current) setLoading(false);
}
}, []);
useEffect(() => { fetchSkills(); }, [fetchSkills]);
useEffect(() => {
if (!focusSkillId) return;
setSelectedSkillId(focusSkillId);
setCadenceFilter('all');
setSearch('');
window.setTimeout(() => {
document.getElementById(`skill-card-${domSafeId(focusSkillId)}`)?.scrollIntoView({
block: 'center',
behavior: 'smooth',
});
}, 80);
}, [focusSkillId, skills.length]);
const toggleSkill = useCallback(async (skill: SkillSummary) => {
const nextEnabled = !skill.enabled;
setUpdatingSkillId(skill.id);
setActionError(null);
try {
const result = await setSkillEnabled(skill.id, nextEnabled);
if (!result.ok) {
throw new Error(result.error || '技能状态变更失败');
}
setSkills((current) => current.map((item) => (
item.id === skill.id ? { ...item, enabled: nextEnabled } : item
)));
setSelectedSkillId(skill.id);
} catch (err) {
setActionError(err instanceof Error ? err.message : '技能状态变更失败');
} finally {
setUpdatingSkillId(null);
}
}, []);
const filtered = skills
.filter(s => cadenceFilter === 'all' || getCadence(s) === cadenceFilter)
.filter(s => !search || s.name.includes(search) || s.description.includes(search) || s.category.includes(search));
const advancedSkills = filtered.filter(isAdvancedSkill);
const advancedIds = new Set(advancedSkills.map(s => s.id));
const learnedSkills = filtered.filter(s => s.enabled && !advancedIds.has(s.id));
const recommendedPool = filtered.filter(s => !s.enabled && isRecommendedSkill(s) && !isAdvancedSkill(s));
const baseLearnableSkills = filtered.filter(s => !s.enabled && !isAdvancedSkill(s));
const recommendedSkills = (recommendedPool.length > 0 ? recommendedPool : baseLearnableSkills).slice(0, 4);
const recommendedIds = new Set(recommendedSkills.map(s => s.id));
const learnableSkills = baseLearnableSkills.filter(s => !recommendedIds.has(s.id));
const sections: SkillSection[] = [
{
key: 'recommended',
title: '推荐学习',
hint: '优先补齐高频工作能力,适合让数字员工尽快接手重复事务。',
skills: recommendedSkills,
emptyText: '暂无推荐学习项。',
},
{
key: 'learnable',
title: '可学习技能',
hint: '可纳入数字员工日常工作的业务能力,学习入口待接入后启用。',
skills: learnableSkills,
emptyText: '当前没有可学习技能。',
},
{
key: 'learned',
title: '已学技能',
hint: '数字员工已经掌握并可被业务场景调用的能力。',
skills: learnedSkills,
emptyText: '当前还没有已学习技能。',
},
{
key: 'advanced',
title: '高级技能',
hint: '适合有明确边界或需要确认后再交给数字员工处理的能力。',
skills: advancedSkills,
emptyText: '暂无高级技能。',
},
];
const renderSection = (section: SkillSection) => (
<section key={section.key} style={{ display: 'grid', gap: 10 }}>
<div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', gap: 12 }}>
<div>
<h3 style={{ margin: 0, fontSize: 16, color: 'var(--de-text-primary)' }}>{section.title}</h3>
<p style={{ margin: '4px 0 0', fontSize: 12, color: 'var(--de-text-muted)' }}>{section.hint}</p>
</div>
<span className="de-capability-badge de-capability-standard">{section.skills.length} </span>
</div>
{section.skills.length === 0 ? (
<p className="de-empty-text" style={{ margin: 0 }}>{section.emptyText}</p>
) : (
<div className="de-skill-grid">
{section.skills.map(s => (
<SkillCard
key={`${section.key}-${s.id}`}
skill={s}
recommended={section.key === 'recommended'}
advanced={section.key === 'advanced'}
selected={selectedSkillId === s.id}
updating={updatingSkillId === s.id}
onSelect={(skill) => setSelectedSkillId(skill.id)}
onToggle={toggleSkill}
/>
))}
</div>
)}
</section>
);
return (
<div style={{ padding: '14px 18px' }}>
<GlassCard>
<CardTitleBar title="学习技能">
<button onClick={fetchSkills} className="de-btn-secondary de-btn-sm"></button>
</CardTitleBar>
<div className="de-card-body">
{actionError && (
<div className="de-schedule-result de-schedule-result--error" style={{ marginBottom: 12 }}>
{actionError}
</div>
)}
<div className="de-skill-toolbar">
<input type="text" className="de-input" placeholder="搜索业务能力..." value={search} onChange={e => setSearch(e.target.value)} />
<div className="de-filter-row">
{cadenceOptions.map(option => (
<button key={option.value} className={`de-filter-chip ${cadenceFilter === option.value ? 'active' : ''}`} onClick={() => setCadenceFilter(option.value)}>
{option.label}
</button>
))}
</div>
</div>
{loading ? (
<div className="de-loading-skeleton" style={{ height: 300 }} />
) : error ? (
<p className="de-empty-text"></p>
) : filtered.length === 0 && skills.length > 0 ? (
<p className="de-empty-text"></p>
) : filtered.length === 0 ? (
<p className="de-empty-text"></p>
) : (
<div style={{ display: 'grid', gap: 18 }}>
{sections.map(renderSection)}
</div>
)}
</div>
</GlassCard>
</div>
);
}

View File

@@ -0,0 +1,310 @@
import type { PlanMessage } from '@/lib/api';
import type {
DashboardOverview,
JournalEntry,
Mission,
MissionStep,
SkillSummary,
} from '@/lib/consoleDataAdapter';
import type { DebugPlan, DebugRun, DebugTask, FlowResponse } from '@/types/api';
function todayAt(hour: number, minute: number): string {
const date = new Date();
date.setHours(hour, minute, 0, 0);
return date.toISOString();
}
function makeTask(
missionId: string,
stepId: string,
title: string,
status: string,
skillId: string,
startedAt: string | null,
finishedAt: string | null,
): MissionStep['tasks'][number] {
return {
id: `${missionId}-${stepId}-task`,
title,
status,
assignedSkillId: skillId,
startedAt,
finishedAt,
};
}
function makeMission(
id: string,
title: string,
objective: string,
status: string,
updatedAt: string,
steps: MissionStep[],
): Mission {
return {
id,
title,
objective,
status,
createdAt: todayAt(8, 0),
updatedAt,
steps,
sourceIds: { planId: id, taskIds: steps.flatMap((step) => step.tasks.map((task) => task.id)) },
};
}
const DEMO_SKILLS: SkillSummary[] = [
{ id: 'whitecollar-day-browser-portal-check', name: '工作台巡检', version: '1.0.0', enabled: true, description: '检查工作台首页、待办数量和重点入口是否可用。', category: '办公', capabilityLevel: 'standard', source: 'skill-api' },
{ id: 'whitecollar-day-browser-inbox-triage', name: '收件箱分拣', version: '1.0.0', enabled: true, description: '读取邮件和通知列表,按紧急程度整理待办。', category: '通信', capabilityLevel: 'standard', source: 'skill-api' },
{ id: 'whitecollar-day-calendar-brief', name: '日程简报', version: '1.0.0', enabled: true, description: '生成当日会议、回访和报表节点的时间提醒。', category: '办公', capabilityLevel: 'standard', source: 'skill-api' },
{ id: 'whitecollar-day-browser-crm-followup', name: '客户跟进', version: '1.0.0', enabled: true, description: '读取客户跟进页面,形成回访摘要和下一步建议。', category: '营销', capabilityLevel: 'specialized', source: 'skill-api' },
{ id: 'whitecollar-day-browser-expense-approval', name: '费用审批核对', version: '1.0.0', enabled: true, description: '检查待审批费用单,筛出异常和超限项。', category: '财务', capabilityLevel: 'specialized', source: 'skill-api' },
{ id: 'whitecollar-day-sales-snapshot', name: '经营快照', version: '1.0.0', enabled: true, description: '汇总客户、项目和当日经营看板。', category: '报表', capabilityLevel: 'standard', source: 'skill-api' },
{ id: 'whitecollar-day-report-export', name: '报表导出', version: '1.0.0', enabled: true, description: '整理并导出面向领导汇报的简版报表。', category: '报表', capabilityLevel: 'standard', source: 'skill-api' },
{ id: 'whitecollar-day-browser-knowledge-check', name: '知识库核查', version: '1.0.0', enabled: true, description: '检索知识库资料,确认制度和模板是否最新。', category: '数字化', capabilityLevel: 'standard', source: 'skill-api' },
{ id: 'whitecollar-day-task-board-sync', name: '任务看板同步', version: '1.0.0', enabled: true, description: '把今日处理结果同步到任务看板和协同清单。', category: '办公', capabilityLevel: 'standard', source: 'skill-api' },
{ id: 'whitecollar-day-browser-day-close', name: '日终收口', version: '1.0.0', enabled: true, description: '汇总今日动作、风险和明日重点。', category: '办公', capabilityLevel: 'specialized', source: 'skill-api' },
];
const DEMO_MISSIONS: Mission[] = [
makeMission(
'whitecollar-day',
'白领一天端到端验收',
'模拟业务人员一天内通过数字员工完成网页采集、客户回访、资料整理和日报输出。',
'active',
todayAt(10, 35),
[
{ id: 'contact', title: '读取供应商联系人', status: 'completed', preferredSkills: ['whitecollar-day-legacy-contact'], tasks: [makeTask('whitecollar-day', 'contact', '整理联系人名单', 'completed', 'whitecollar-day-legacy-contact', todayAt(8, 30), todayAt(8, 33))] },
{ id: 'news', title: '读取行业动态', status: 'completed', preferredSkills: ['whitecollar-day-legacy-news'], tasks: [makeTask('whitecollar-day', 'news', '整理行业动态摘要', 'completed', 'whitecollar-day-legacy-news', todayAt(8, 35), todayAt(8, 41))] },
{ id: 'form', title: '预填客户回访登记', status: 'completed', preferredSkills: ['whitecollar-day-legacy-form'], tasks: [makeTask('whitecollar-day', 'form', '填充回访登记表', 'completed', 'whitecollar-day-legacy-form', todayAt(8, 45), todayAt(8, 51))] },
{ id: 'compare', title: '比对设备方案', status: 'running', preferredSkills: ['whitecollar-day-legacy-compare'], tasks: [makeTask('whitecollar-day', 'compare', '生成方案差异结论', 'running', 'whitecollar-day-legacy-compare', todayAt(10, 28), null)] },
{ id: 'todos', title: '读取待办系统', status: 'pending', preferredSkills: ['whitecollar-day-legacy-todos'], tasks: [makeTask('whitecollar-day', 'todos', '汇总个人待办', 'pending', 'whitecollar-day-legacy-todos', null, null)] },
{ id: 'customers', title: '读取客户清单', status: 'pending', preferredSkills: ['whitecollar-day-legacy-customers'], tasks: [makeTask('whitecollar-day', 'customers', '提取重点客户', 'pending', 'whitecollar-day-legacy-customers', null, null)] },
{ id: 'download', title: '识别最新资料下载链接', status: 'pending', preferredSkills: ['whitecollar-day-legacy-download'], tasks: [makeTask('whitecollar-day', 'download', '识别最新制度材料', 'pending', 'whitecollar-day-legacy-download', null, null)] },
{ id: 'email', title: '生成客户跟进邮件', status: 'pending', preferredSkills: ['whitecollar-day-legacy-email'], tasks: [makeTask('whitecollar-day', 'email', '生成回访邮件草稿', 'pending', 'whitecollar-day-legacy-email', null, null)] },
{ id: 'daily-report', title: '生成工作日报', status: 'pending', preferredSkills: ['whitecollar-day-legacy-daily-report'], tasks: [makeTask('whitecollar-day', 'daily-report', '生成工作日报', 'pending', 'whitecollar-day-legacy-daily-report', null, null)] },
{ id: 'tomorrow-plan', title: '生成明日计划', status: 'pending', preferredSkills: ['whitecollar-day-legacy-tomorrow-plan'], tasks: [makeTask('whitecollar-day', 'tomorrow-plan', '整理明日计划', 'pending', 'whitecollar-day-legacy-tomorrow-plan', null, null)] },
],
),
makeMission('whitecollar-day-0830-browser-portal-check', '白领一天 08:30 工作台巡检', '巡检工作台首页、待办数量和关键入口状态。', 'completed', todayAt(8, 34), [
{ id: 'browser-portal-check', title: '浏览器工作台巡检', status: 'completed', preferredSkills: ['whitecollar-day-browser-portal-check'], tasks: [makeTask('whitecollar-day-0830-browser-portal-check', 'browser-portal-check', '检查门户首页与入口', 'completed', 'whitecollar-day-browser-portal-check', todayAt(8, 30), todayAt(8, 34))] },
]),
makeMission('whitecollar-day-0900-browser-inbox-triage', '白领一天 09:00 收件箱分拣', '识别高优先级通知并形成今日待办。', 'completed', todayAt(9, 6), [
{ id: 'browser-inbox-triage', title: '浏览器收件箱分拣', status: 'completed', preferredSkills: ['whitecollar-day-browser-inbox-triage'], tasks: [makeTask('whitecollar-day-0900-browser-inbox-triage', 'browser-inbox-triage', '分拣通知与邮件', 'completed', 'whitecollar-day-browser-inbox-triage', todayAt(9, 0), todayAt(9, 6))] },
]),
makeMission('whitecollar-day-0930-calendar-brief', '白领一天 09:30 日程简报', '整理会议、回访与汇报节点,形成上午排程。', 'completed', todayAt(9, 32), [
{ id: 'calendar-brief', title: '生成日程简报', status: 'completed', preferredSkills: ['whitecollar-day-calendar-brief'], tasks: [makeTask('whitecollar-day-0930-calendar-brief', 'calendar-brief', '整理今日日程', 'completed', 'whitecollar-day-calendar-brief', todayAt(9, 30), todayAt(9, 32))] },
]),
makeMission('whitecollar-day-1030-browser-crm-followup', '白领一天 10:30 客户跟进', '汇总重点客户情况,生成下一步回访建议。', 'running', todayAt(10, 35), [
{ id: 'browser-crm-followup', title: '浏览器客户跟进', status: 'running', preferredSkills: ['whitecollar-day-browser-crm-followup'], tasks: [makeTask('whitecollar-day-1030-browser-crm-followup', 'browser-crm-followup', '提取客户回访摘要', 'running', 'whitecollar-day-browser-crm-followup', todayAt(10, 30), null)] },
]),
makeMission('whitecollar-day-1130-browser-expense-approval', '白领一天 11:30 费用审批核对', '检查待审批单据并输出异常说明。', 'approved', todayAt(11, 20), [
{ id: 'browser-expense-approval', title: '浏览器费用审批核对', status: 'pending', preferredSkills: ['whitecollar-day-browser-expense-approval'], tasks: [makeTask('whitecollar-day-1130-browser-expense-approval', 'browser-expense-approval', '核对费用审批单', 'pending', 'whitecollar-day-browser-expense-approval', null, null)] },
]),
makeMission('whitecollar-day-1330-sales-snapshot', '白领一天 13:30 经营快照', '汇总客户、项目和经营看板,形成午后快照。', 'draft', todayAt(13, 20), [
{ id: 'sales-snapshot', title: '生成经营快照', status: 'pending', preferredSkills: ['whitecollar-day-sales-snapshot'], tasks: [makeTask('whitecollar-day-1330-sales-snapshot', 'sales-snapshot', '整理经营快照', 'pending', 'whitecollar-day-sales-snapshot', null, null)] },
]),
makeMission('whitecollar-day-1430-report-export', '白领一天 14:30 报表导出', '导出领导汇报所需的简版统计报表。', 'draft', todayAt(14, 10), [
{ id: 'report-export', title: '导出汇报报表', status: 'pending', preferredSkills: ['whitecollar-day-report-export'], tasks: [makeTask('whitecollar-day-1430-report-export', 'report-export', '导出统计报表', 'pending', 'whitecollar-day-report-export', null, null)] },
]),
makeMission('whitecollar-day-1530-browser-knowledge-check', '白领一天 15:30 知识库核查', '核查制度、模板与常见问题资料是否最新。', 'draft', todayAt(15, 15), [
{ id: 'browser-knowledge-check', title: '浏览器知识库核查', status: 'pending', preferredSkills: ['whitecollar-day-browser-knowledge-check'], tasks: [makeTask('whitecollar-day-1530-browser-knowledge-check', 'browser-knowledge-check', '核查知识库资料', 'pending', 'whitecollar-day-browser-knowledge-check', null, null)] },
]),
makeMission('whitecollar-day-1630-task-board-sync', '白领一天 16:30 任务看板同步', '把今日处理结果同步到看板和协同清单。', 'draft', todayAt(16, 10), [
{ id: 'task-board-sync', title: '同步任务看板', status: 'pending', preferredSkills: ['whitecollar-day-task-board-sync'], tasks: [makeTask('whitecollar-day-1630-task-board-sync', 'task-board-sync', '同步任务状态', 'pending', 'whitecollar-day-task-board-sync', null, null)] },
]),
makeMission('whitecollar-day-1730-browser-day-close', '白领一天 17:30 日终收口', '汇总今日成果、风险和明日重点,形成日终总结。', 'draft', todayAt(17, 10), [
{ id: 'task-board-sync', title: '同步任务看板', status: 'pending', preferredSkills: ['whitecollar-day-task-board-sync'], tasks: [makeTask('whitecollar-day-1730-browser-day-close', 'task-board-sync', '同步看板结果', 'pending', 'whitecollar-day-task-board-sync', null, null)] },
{ id: 'browser-day-close', title: '浏览器日终收口', status: 'pending', preferredSkills: ['whitecollar-day-browser-day-close'], tasks: [makeTask('whitecollar-day-1730-browser-day-close', 'browser-day-close', '整理日终收口', 'pending', 'whitecollar-day-browser-day-close', null, null)] },
]),
makeMission('zhihu-hotlist-monitor', '知乎热榜采集', '通过计划模板调用知乎热榜采集技能,导出表格文件并生成当天热点摘要。', 'approved', todayAt(10, 12), [
{ id: 'collect-zhihu', title: '采集知乎热榜', status: 'pending', preferredSkills: ['zhihu-hotlist'], tasks: [makeTask('zhihu-hotlist-monitor', 'collect-zhihu', '采集知乎热榜', 'pending', 'zhihu-hotlist', null, null)] },
{ id: 'export-xlsx', title: '导出表格文件', status: 'pending', preferredSkills: ['office-export-xlsx'], tasks: [makeTask('zhihu-hotlist-monitor', 'export-xlsx', '导出热榜表格', 'pending', 'office-export-xlsx', null, null)] },
]),
];
function cloneMission(mission: Mission): Mission {
return {
...mission,
steps: mission.steps.map((step) => ({
...step,
preferredSkills: [...step.preferredSkills],
tasks: step.tasks.map((task) => ({ ...task })),
})),
sourceIds: { planId: mission.sourceIds.planId, taskIds: [...mission.sourceIds.taskIds] },
};
}
export function getDemoMissions(): Mission[] {
return DEMO_MISSIONS.map(cloneMission);
}
export function getDemoSkills(): SkillSummary[] {
return DEMO_SKILLS.map((skill) => ({ ...skill }));
}
export function buildDemoTasks(missions: Mission[]): DebugTask[] {
return missions.flatMap((mission) => mission.steps.flatMap((step) => step.tasks.map((task) => ({
task_id: task.id,
title: task.title,
status: task.status,
plan_id: mission.id,
step_id: step.id,
assigned_skill_id: task.assignedSkillId || '',
description: `${mission.title} · ${step.title}`,
created_at: mission.createdAt || todayAt(8, 0),
updated_at: mission.updatedAt || mission.createdAt || todayAt(8, 0),
started_at: task.startedAt,
finished_at: task.finishedAt,
}))));
}
export function buildDemoRuns(tasks: DebugTask[]): DebugRun[] {
return tasks
.filter((task) => ['completed', 'running'].includes(task.status))
.map((task, index) => ({
run_id: `demo-run-${index + 1}`,
task_id: task.task_id,
plan_id: task.plan_id,
attempt_no: 1,
lifecycle: task.status === 'running' ? 'Running' : 'Succeeded',
created_at: task.started_at || task.created_at,
completed_at: task.finished_at,
}));
}
export function buildDemoOverview(tasks: DebugTask[]): DashboardOverview {
return {
generatedAt: new Date().toISOString(),
isPartial: true,
sources: [{ endpoint: 'digital-demo', ok: true, loadedAt: new Date().toISOString() }],
taskTotals: {
total: tasks.length,
queued: tasks.filter((task) => task.status === 'pending').length,
running: tasks.filter((task) => task.status === 'running').length,
succeeded: tasks.filter((task) => task.status === 'completed').length,
failed: 0,
},
approvals: { total: 1, pending: 1 },
health: { activeTasks: tasks.filter((task) => task.status === 'running').length, failedTasks: 0, pendingConfirmations: 1 },
};
}
export function buildDemoPlans(missions: Mission[]): DebugPlan[] {
return missions.map((mission) => ({
plan_id: mission.id,
title: mission.title,
objective: mission.objective,
status: mission.status,
created_at: mission.createdAt || todayAt(8, 0),
steps: mission.steps.map((step) => ({
step_id: step.id,
title: step.title,
depends_on: [],
preferred_skills: step.preferredSkills,
tasks: step.tasks.map((task) => ({
task_id: task.id,
title: task.title,
status: task.status,
})),
})),
}));
}
export function buildDemoJournalEntries(missions: Mission[]): JournalEntry[] {
return missions
.slice(0, 8)
.map((mission, index) => ({
id: `demo-journal-${index + 1}`,
kind: mission.status === 'completed' ? 'completed' : mission.status === 'active' || mission.status === 'running' ? 'start' : 'pending_authorization',
source: 'digital-demo',
message: `${mission.title}${mission.objective}`,
occurredAt: mission.updatedAt,
planId: mission.id,
taskId: mission.sourceIds.taskIds[0] || null,
}));
}
export function buildDemoFlowEvents(mission: Mission): NonNullable<FlowResponse['events']> {
const activeStep = mission.steps.find((step) => step.status === 'running') || mission.steps.find((step) => step.status === 'pending') || mission.steps[0];
const events: NonNullable<FlowResponse['events']> = [
{ event_id: `${mission.id}-event-1`, kind: 'entered_session', occurred_at: mission.createdAt || todayAt(8, 0), message: `已接收场景:${mission.title}` },
{ event_id: `${mission.id}-event-2`, kind: 'start', occurred_at: mission.updatedAt || mission.createdAt || todayAt(8, 0), message: `当前目标:${mission.objective}` },
];
if (activeStep) {
events.push({
event_id: `${mission.id}-event-3`,
kind: mission.status === 'completed' ? 'completed' : mission.status === 'approved' || mission.status === 'draft' ? 'pending_authorization' : 'RunStarted',
occurred_at: mission.updatedAt || mission.createdAt || todayAt(8, 0),
message: `${activeStep.title}`,
});
}
if (mission.status === 'completed') {
events.push({
event_id: `${mission.id}-event-4`,
kind: 'RunCompleted',
occurred_at: mission.updatedAt || mission.createdAt || todayAt(8, 0),
message: '已完成本场景执行并回写结果。',
});
}
return events;
}
export function buildDemoPlanMessages(mission: Mission): PlanMessage[] {
const completedSteps = mission.steps.filter((step) => step.status === 'completed').length;
const runningStep = mission.steps.find((step) => step.status === 'running');
const closing =
mission.status === 'completed'
? `当前已完成全部 ${mission.steps.length} 个步骤,结果已整理完毕。`
: runningStep
? `当前正在处理“${runningStep.title}”,已完成 ${completedSteps} 个步骤。`
: `当前场景已准备就绪,可直接发起执行。`;
return [
{ role: 'user', content: `请执行场景:${mission.title}` },
{ role: 'assistant', content: `已接收。业务目标:${mission.objective}` },
{ role: 'assistant', content: closing },
];
}
export function runDemoMissionOnce(missions: Mission[], missionId: string): Mission[] {
return missions.map((mission) => {
if (mission.id !== missionId) return mission;
return {
...mission,
status: 'running',
updatedAt: new Date().toISOString(),
steps: mission.steps.map((step, index) => ({
...step,
status: index === 0 ? 'running' : step.status === 'completed' ? 'completed' : 'pending',
tasks: step.tasks.map((task) => ({
...task,
status: index === 0 ? 'running' : task.status === 'completed' ? 'completed' : 'pending',
startedAt: index === 0 ? new Date().toISOString() : task.startedAt,
finishedAt: index === 0 ? null : task.finishedAt,
})),
})),
};
});
}
export function completeDemoMissionOnce(missions: Mission[], missionId: string): Mission[] {
return missions.map((mission) => {
if (mission.id !== missionId) return mission;
return {
...mission,
status: 'completed',
updatedAt: new Date().toISOString(),
steps: mission.steps.map((step) => ({
...step,
status: 'completed',
tasks: step.tasks.map((task) => ({
...task,
status: 'completed',
startedAt: task.startedAt || new Date().toISOString(),
finishedAt: new Date().toISOString(),
})),
})),
};
});
}

View File

@@ -0,0 +1,14 @@
export type DigitalTab = 'home' | 'missions' | 'skills' | 'reports' | 'settings';
export type DigitalNavigationTarget = {
tab: DigitalTab;
date?: string | null;
missionId?: string | null;
taskId?: string | null;
skillId?: string | null;
reportSection?: string | null;
};
export function domSafeId(value: string): string {
return value.replace(/[^a-zA-Z0-9_-]/g, '_');
}

View File

@@ -0,0 +1,744 @@
export interface StatusResponse {
provider: string | null;
model: string;
temperature: number;
uptime_seconds: number;
gateway_port: number;
locale: string;
memory_backend: string;
paired: boolean;
channels: Record<string, boolean>;
health: HealthSnapshot;
}
export interface HealthSnapshot {
pid: number;
updated_at: string;
uptime_seconds: number;
components: Record<string, ComponentHealth>;
}
export interface ComponentHealth {
status: string;
updated_at: string;
last_ok: string | null;
last_error: string | null;
restart_count: number;
}
export interface ToolSpec {
name: string;
description: string;
parameters: any;
}
export interface CronJob {
id: string;
name: string | null;
expression: string;
command: string;
prompt: string | null;
job_type: string;
schedule: unknown;
enabled: boolean;
delivery: unknown;
template_id?: string | null;
trigger_kind?: string | null;
step_count?: number;
skill_ids?: string[];
delete_after_run: boolean;
created_at: string;
next_run: string;
last_run: string | null;
last_status: string | null;
last_output: string | null;
}
export interface CronRun {
id: number;
job_id: string;
started_at: string;
finished_at: string;
status: string;
output: string | null;
duration_ms: number | null;
}
export interface Integration {
name: string;
description: string;
category: string;
status: 'Available' | 'Active' | 'ComingSoon';
}
export interface DiagResult {
severity: 'ok' | 'warn' | 'error';
category: string;
message: string;
}
export interface MemoryEntry {
id: string;
key: string;
content: string;
category: string;
timestamp: string;
session_id: string | null;
score: number | null;
}
export interface CostSummary {
session_cost_usd: number;
daily_cost_usd: number;
monthly_cost_usd: number;
total_tokens: number;
request_count: number;
by_model: Record<string, ModelStats>;
}
export interface ModelStats {
model: string;
cost_usd: number;
total_tokens: number;
request_count: number;
}
export interface CliTool {
name: string;
path: string;
version: string | null;
category: string;
}
export interface Session {
session_id: string;
kind: 'chat' | 'plan';
created_at: string;
last_activity: string;
message_count: number;
plan_id?: string | null;
plan_title?: string | null;
name?: string | null;
id?: string;
started_at?: string;
channel?: string;
status?: 'active' | 'idle' | 'closed';
}
export interface SessionListResponse {
sessions: Session[];
}
export interface ChannelDetail {
name: string;
type: string;
enabled: boolean;
status: 'active' | 'inactive' | 'error';
message_count: number;
last_message_at: string | null;
health: 'healthy' | 'degraded' | 'down';
}
export interface SSEEvent {
type: string;
timestamp?: string;
[key: string]: any;
}
export interface BrowserSession {
session_id: string;
target_url: string;
status: 'launching' | 'running' | 'idle' | 'stopped' | 'error';
started_at: string;
pid?: number;
error_message?: string;
}
export interface UserNotification {
notification_id: string;
plan_id: string;
task_id: string;
kind: 'user_message' | 'need_input' | 'need_approval'
| 'task_progress' | 'task_failed' | 'task_finished'
| 'confirmation_pending' | 'confirmation_resolved';
title: string;
body: string;
level: 'info' | 'warn' | 'error' | 'action';
status: 'unread' | 'read' | 'dismissed' | 'resolved';
correlation_id: string;
created_at: string;
read_at?: string | null;
dismissed_at?: string | null;
}
export type ChatRouteMode = 'auto' | 'chat' | 'direct' | 'plan';
export interface WsMessage {
type: 'message' | 'chunk' | 'chunk_reset' | 'thinking' | 'tool_call' | 'tool_result' | 'done' | 'error' | 'plan_msg_ack' | 'plan_notification' | 'plan_need_input' | 'plan_need_approval' | 'notification' | 'session_start' | 'session_renamed' | 'connected' | 'plan_created' | 'direct_task_created' | 'ask' | 'governance';
content?: string;
full_response?: string;
name?: string;
args?: any;
output?: string;
message?: string;
code?: string;
level?: string;
question?: string;
task_id?: string;
approval_id?: string;
action?: string;
plan_id?: string;
session_id?: string;
provisional?: boolean;
instruction?: string;
status?: string;
title?: string;
task_count?: number;
notification?: UserNotification;
response_kind?: string;
route_decision?: RouteDecisionRecord;
command?: unknown;
created_plan_id?: string | null;
created_schedule_id?: string | null;
created_task_ids?: string[];
created_run_ids?: string[];
projection_name?: string | null;
projection_payload?: Record<string, unknown> | null;
}
export interface PlanTaskSummary {
task_id: string;
title: string;
status: string;
}
export interface PlanStepSummary {
step_id: string;
title: string;
depends_on: string[];
preferred_skills: string[];
not_before?: string;
tasks: PlanTaskSummary[];
}
export interface PlanSummary {
plan_id: string;
title: string;
objective: string;
status: string;
created_at?: string;
steps: PlanStepSummary[];
}
// ---------------------------------------------------------------------------
// Execution governance
// ---------------------------------------------------------------------------
export type RouteDecisionRouteKind =
| 'ChatOnly'
| 'AskClarification'
| 'ProjectionQuery'
| 'PlanControl'
| 'DirectRun'
| 'PlanExecution'
| 'RejectedByPolicy'
| (string & {});
export type RouteDecisionIntentKind =
| 'chat'
| 'query'
| 'control'
| 'execute'
| 'schedule'
| 'amend'
| 'approve'
| 'recover'
| 'reject'
| 'Chat'
| 'Query'
| 'Control'
| 'Execute'
| 'Schedule'
| 'Amend'
| 'Approve'
| 'Recover'
| 'Reject'
| (string & {});
export interface RouteDecisionRecord {
id: string;
route_kind: RouteDecisionRouteKind;
intent_kind: RouteDecisionIntentKind;
reason_codes: string[];
candidate_skills: string[];
context_refs: Record<string, unknown>;
created_command_id: string | null;
correlation_id: string;
creates_plan: boolean;
creates_task_run: boolean;
approval_mode: string | null;
policy_result: string | null;
recorded_at: string;
entrypoint?: string | null;
actor?: string | null;
}
export interface RouteDecisionTimelineResponse {
has_route_decision_state: boolean;
decisions: RouteDecisionRecord[];
}
export interface IngressRouteRequest {
entrypoint?: 'chat' | 'task_center' | 'cron' | 'webhook' | 'legacy_api' | 'browser_event' | 'file_event';
actor?: string;
text?: string;
payload?: Record<string, unknown>;
attachments?: string[];
timestamp?: string;
correlation_id?: string;
idempotency_key?: string;
context_refs?: Record<string, unknown>;
}
export interface IngressRouteResponse {
accepted: boolean;
envelope?: Record<string, unknown>;
response_kind?: string;
route_decision?: RouteDecisionRecord;
command?: unknown;
created_plan_id?: string | null;
created_schedule_id?: string | null;
created_task_ids?: string[];
created_run_ids?: string[];
projection_name?: string | null;
projection_payload?: Record<string, unknown> | null;
message?: string;
error?: GovernanceCommandErrorCode;
}
export type GovernanceCommandKind =
| 'create_goal'
| 'create_direct_task'
| 'create_plan'
| 'submit_plan'
| 'approve_plan'
| 'reject_plan'
| 'activate_plan'
| 'pause_plan'
| 'resume_plan'
| 'amend_plan'
| 'cancel_plan'
| 'retry_task'
| 'skip_task'
| 'pause_task'
| 'resume_task'
| 'provide_task_input'
| 'cancel_task'
| 'start_run'
| 'cancel_run'
| 'request_approval'
| 'approve_approval'
| 'reject_approval'
| 'create_schedule'
| 'pause_schedule'
| 'run_schedule_now';
export type GovernanceCommandErrorCode =
| 'invalid_context'
| 'missing_capability'
| 'policy_blocked'
| 'conflict'
| 'execution_failed'
| 'unauthorized'
| 'invalid_state'
| (string & {});
export interface GovernanceCommandRequest {
command_kind: GovernanceCommandKind;
context_refs: Record<string, unknown>;
payload?: Record<string, unknown>;
idempotency_key?: string;
correlation_id?: string;
}
export interface GovernanceCommandResponse {
command_id: string;
accepted: boolean;
correlation_id: string;
result?: Record<string, unknown>;
error?: GovernanceCommandErrorCode;
message?: string;
}
// ═══════════════════════════════════════════════════════════════════════
// Console types — Debug API responses, Plan/Task/Run, Swimlane
// ═══════════════════════════════════════════════════════════════════════
export interface DashboardStats {
total: number;
queued: number;
running: number;
succeeded: number;
failed: number;
paused?: number;
}
export interface DebugTask {
task_id: string;
title: string;
status: string;
plan_id: string | null;
step_id: string;
assigned_skill_id: string;
description?: string;
created_at: string;
updated_at: string;
started_at: string | null;
finished_at: string | null;
trigger_source?: string | null;
scheduler_identity?: string | null;
cron_job_id?: string | null;
timeout_at?: string | null;
error_message?: string | null;
[key: string]: any;
}
export interface DebugRun {
run_id: string;
task_id: string;
plan_id: string | null;
skill_id?: string;
attempt_no: number;
lifecycle: string;
created_at: string;
completed_at: string | null;
events?: Array<{
event_id?: string;
kind?: string;
occurred_at?: string;
message?: string | null;
}>;
artifacts?: Array<Record<string, unknown>>;
[key: string]: any;
}
export interface DebugPlan {
plan_id: string;
title: string;
objective?: string;
status: string;
created_at: string;
steps?: PlanStepSummary[];
[key: string]: any;
}
export interface ApprovalEntry {
approval_id: string;
task_id: string;
status: string;
created_at?: string;
[key: string]: any;
}
export interface ArtifactEntry {
artifact_id?: string;
subject_id: string;
kind?: string;
name?: string;
uri?: string;
value?: string;
[key: string]: any;
}
export interface CanonicalEvent {
event_id: string;
aggregate_type?: string;
aggregate_id?: string;
event_type?: string;
kind?: string;
message?: string;
occurred_at?: string;
timestamp?: string;
plan_id: string | null;
task_id: string | null;
[key: string]: any;
}
export interface DebugStoreResponse {
tables: {
canonical_events: CanonicalEvent[];
tasks: DebugTask[];
runs: DebugRun[];
plans: DebugPlan[];
plan_steps: PlanStepSummary[];
approvals: ApprovalEntry[];
artifacts: ArtifactEntry[];
};
}
export interface FlowStep {
seq?: number;
lane: string;
label: string;
sub_label?: string;
timestamp: string;
color?: string;
source_event_id?: string;
source_type?: string;
payload?: Record<string, unknown>;
}
export interface LaneInfo {
key: string;
label: string;
color: string;
}
export interface FlowResponse {
steps: FlowStep[];
lanes: LaneInfo[];
task?: Record<string, unknown>;
plan_id?: string;
plan_title?: string;
plan_status?: string;
plan_objective?: string;
events?: Array<{ event_id: string; kind: string; occurred_at: string; message: string }>;
}
export interface SkillSpec {
skill_id: string;
name: string;
version: string;
enabled: boolean;
description: string;
[key: string]: any;
}
export interface CreatePlanRequest {
title: string;
description: string;
steps: Array<{
title: string;
description: string;
skills: string[];
depends_on: string[];
}>;
}
export interface DashboardResponse {
runtime: {
active_tasks: number;
failed_tasks: number;
pending_confirmations: number;
};
tasks: DashboardStats;
approvals: {
total: number;
pending: number;
};
}
export type DigitalEmployeeProjectionSource = 'live' | 'demo';
export interface DigitalEmployeeProfile {
name: string;
title: string;
major: string;
company: string;
position?: string;
current_status?: 'working' | 'resting' | string;
current_status_label?: string;
motto: string;
}
export interface DigitalEmployeeMetric {
label: string;
value: string;
}
export interface DigitalEmployeeDuty {
id: string;
title: string;
summary: string;
implemented: boolean;
selectable: boolean;
selected: boolean;
status: string;
status_label: string;
status_tone: 'running' | 'success' | 'danger' | 'warning' | 'waiting' | 'info' | string;
schedule_text: string;
result_text: string;
conversation_id?: string | null;
}
export interface DigitalEmployeeWorkdayState {
confirmed: boolean;
confirmed_at?: string | null;
current_status?: 'working' | 'resting' | string;
current_status_label?: string;
status_label: string;
status_tone: string;
summary: string;
bubble_text: string;
}
export interface DigitalEmployeeOverview {
title: string;
status_label: string;
status_tone: string;
metrics: DigitalEmployeeMetric[];
}
export interface DigitalEmployeeWorkEvent {
id: string;
time: string;
plan_title: string;
task_title: string;
step_title?: string | null;
detail: string;
status: string;
status_label: string;
status_tone: string;
updated_at?: string | null;
conversation_id?: string | null;
}
export interface DigitalEmployeeTaskExecutionSummary {
id: string;
plan_id?: string | null;
title: string;
description: string;
selected: boolean;
total_count: number;
success_count: number;
failure_count: number;
running_count: number;
waiting_count: number;
latest_status_label: string;
latest_status_tone: string;
business_summary: string;
ai_summary?: string;
}
export interface DigitalEmployeeRunDetail {
id: string;
time: string;
task_name: string;
step_name?: string | null;
operation_name?: string | null;
status_label: string;
status_tone: string;
business_detail: string;
raw_detail?: string;
updated_at?: string | null;
conversation_id?: string | null;
}
export interface DigitalEmployeeDecisionAction {
id: string;
label: string;
tone: 'primary' | 'secondary' | string;
}
export interface DigitalEmployeeDecision {
id: string;
title: string;
scenario: string;
status: string;
status_label: string;
plan_title: string;
task_title: string;
actions: DigitalEmployeeDecisionAction[];
}
export interface DigitalEmployeeDeliveryArtifact {
name: string;
status: string;
}
export interface DigitalEmployeeDelivery {
headline: string;
summary: string;
lines: string[];
artifacts: DigitalEmployeeDeliveryArtifact[];
}
export interface DigitalEmployeeDailyReport {
date: string;
title: string;
report_schedule_time: string;
status: 'not_generated' | 'generating' | 'ready' | 'failed' | string;
generated_at?: string | null;
generated_by: 'ai' | 'fallback' | string;
model?: string | null;
source?: string;
summary: string;
totals: {
task_count: number;
execution_count: number;
success_count: number;
failure_count: number;
running_count?: number;
};
task_sections: DigitalEmployeeTaskExecutionSummary[];
detail_lines: string[];
pdf_available: boolean;
pdf_url?: string | null;
}
export interface DigitalEmployeeWorkdayProjection {
source: DigitalEmployeeProjectionSource;
demo_reason?: string | null;
date: string;
employee: DigitalEmployeeProfile;
workday: DigitalEmployeeWorkdayState;
overview: DigitalEmployeeOverview;
duties: DigitalEmployeeDuty[];
events: DigitalEmployeeWorkEvent[];
execution_summaries?: DigitalEmployeeTaskExecutionSummary[];
run_details?: DigitalEmployeeRunDetail[];
decisions: DigitalEmployeeDecision[];
delivery: DigitalEmployeeDelivery;
daily_report?: DigitalEmployeeDailyReport;
}
export interface DigitalEmployeeWorkdayActionRequest {
action: 'confirm_workday' | 'decide_approval' | 'update_profile' | 'update_report_schedule' | 'generate_daily_report';
date?: string;
duty_ids?: string[];
decision_id?: string;
decision?: string;
name?: string;
company?: string;
position?: string;
current_status?: 'working' | 'resting' | string;
report_schedule_time?: string;
}
export interface PlanDispatchTask {
task_id: string;
title: string;
status: string;
}
export interface PlanDispatchResponse {
ok?: boolean;
plan_id?: string;
source_plan_id?: string | null;
dispatched_tasks?: PlanDispatchTask[];
error?: string;
}
/** Shared type used by PlanSidebar and PlanChat for the selected plan state. */
export interface PlanChatPlan {
plan_id: string;
title: string;
objective: string;
status: string;
created_at?: string | null;
steps: Array<{
step_id: string;
title: string;
tasks: Array<{ task_id: string; title: string; status: string }>;
}>;
}

View File

@@ -0,0 +1,27 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsBuildInfoFile",
"incremental": true,
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src"]
}

View File

@@ -0,0 +1,13 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
],
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}

View File

@@ -0,0 +1,21 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsBuildInfoFile",
"incremental": true,
"target": "ES2022",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true
},
"include": ["vite.config.ts"]
}

View File

@@ -0,0 +1,18 @@
import path from "node:path";
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import tailwindcss from "@tailwindcss/vite";
export default defineConfig({
base: "./",
plugins: [tailwindcss(), react()],
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
build: {
outDir: "../../public/sgrobot-digital",
emptyOutDir: true,
},
});

View File

@@ -13,6 +13,8 @@
"build:main:dev": "dotenv -e .env.development -- cross-env NODE_ENV=development tsc -p tsconfig.main.json && tsc-alias -p tsconfig.main.json",
"build:main": "dotenv -e .env.production -- cross-env NODE_ENV=production tsc -p tsconfig.main.json && tsc-alias -p tsconfig.main.json",
"build:renderer": "vite build",
"sync:sgrobot-digital": "node scripts/sync-sgrobot-digital.js",
"build:sgrobot-digital": "node scripts/build-sgrobot-digital.js",
"build": "dotenv -e .env.production -- cross-env NODE_ENV=production npm run build:main && npm run build:renderer",
"test": "vitest",
"test:run": "vitest run",

View File

@@ -1 +0,0 @@
function t(n,e,i,s){if(typeof e=="function"?n!==e||!s:!e.has(n))throw new TypeError("Cannot read private member from an object whose class did not declare it");return i==="m"?s:i==="a"?s.call(n):s?s.value:e.get(n)}function c(n,e,i,s,_){if(typeof e=="function"?n!==e||!0:!e.has(n))throw new TypeError("Cannot write private member to an object whose class did not declare it");return e.set(n,i),i}var a,r,o,l,u;const d="__TAURI_TO_IPC_KEY__";function w(n,e=!1){return window.__TAURI_INTERNALS__.transformCallback(n,e)}class g{constructor(e){a.set(this,void 0),r.set(this,0),o.set(this,[]),l.set(this,void 0),c(this,a,e||(()=>{})),this.id=w(i=>{const s=i.index;if("end"in i){s==t(this,r,"f")?this.cleanupCallback():c(this,l,s);return}const _=i.message;if(s==t(this,r,"f")){for(t(this,a,"f").call(this,_),c(this,r,t(this,r,"f")+1);t(this,r,"f")in t(this,o,"f");){const p=t(this,o,"f")[t(this,r,"f")];t(this,a,"f").call(this,p),delete t(this,o,"f")[t(this,r,"f")],c(this,r,t(this,r,"f")+1)}t(this,r,"f")===t(this,l,"f")&&this.cleanupCallback()}else t(this,o,"f")[s]=_})}cleanupCallback(){window.__TAURI_INTERNALS__.unregisterCallback(this.id)}set onmessage(e){c(this,a,e)}get onmessage(){return t(this,a,"f")}[(a=new WeakMap,r=new WeakMap,o=new WeakMap,l=new WeakMap,d)](){return`__CHANNEL__:${this.id}`}toJSON(){return this[d]()}}class f{constructor(e,i,s){this.plugin=e,this.event=i,this.channelId=s}async unregister(){return h(`plugin:${this.plugin}|remove_listener`,{event:this.event,channelId:this.channelId})}}async function m(n,e,i){const s=new g(i);try{return await h(`plugin:${n}|register_listener`,{event:e,handler:s}),new f(n,e,s.id)}catch{return await h(`plugin:${n}|registerListener`,{event:e,handler:s}),new f(n,e,s.id)}}async function I(n){return h(`plugin:${n}|check_permissions`)}async function C(n){return h(`plugin:${n}|request_permissions`)}async function h(n,e={},i){return window.__TAURI_INTERNALS__.invoke(n,e,i)}function T(n,e="asset"){return window.__TAURI_INTERNALS__.convertFileSrc(n,e)}class k{get rid(){return t(this,u,"f")}constructor(e){u.set(this,void 0),c(this,u,e)}async close(){return h("plugin:resources|close",{rid:this.rid})}}u=new WeakMap;function E(){return!!(globalThis||window).isTauri}export{g as Channel,f as PluginListener,k as Resource,d as SERIALIZE_TO_IPC_FN,m as addPluginListener,I as checkPermissions,T as convertFileSrc,h as invoke,E as isTauri,C as requestPermissions,w as transformCallback};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,13 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="color-scheme" content="dark" />
<link rel="icon" type="image/png" href="/sgrobot-digital/logo.png" />
<title>sgRobot</title>
<title>数字员工</title>
<script>
window.__TAURI__ = window.__TAURI__ || {};
window.__QIMINGCLAW_EMBEDDED_DIGITAL__ = true;
try {
window.localStorage.setItem("zeroclaw_token", "qimingclaw_embedded_digital_employee");
window.sessionStorage.setItem("sgrobot_logged_in", "1");
@@ -15,8 +13,8 @@
console.warn("[qimingclaw] digital employee auth bootstrap skipped", error);
}
</script>
<script type="module" crossorigin src="/sgrobot-digital/assets/index-DwDxQhF4.js"></script>
<link rel="stylesheet" crossorigin href="/sgrobot-digital/assets/index-BTJFqpbJ.css">
<script type="module" crossorigin src="./assets/index-DX0h8EMp.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-DqV9Vw1H.css">
</head>
<body>
<div id="root"></div>

View File

@@ -1,660 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>sgRobot 业务流程与架构模块映射</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
.theme-dark-blue { background-color: #0d2758; }
.theme-text-dark-blue { color: #0d2758; }
.theme-light-blue-bg { background-color: #f0f6fc; }
.theme-border-blue { border-color: #aec3db; }
.theme-red { color: #d92d20; }
body {
font-family: "PingFang SC", "Microsoft YaHei", "Noto Sans SC", "Source Han Sans SC", Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
font-size: 18px;
}
.text-xs { font-size: 0.92rem !important; }
.text-sm { font-size: 1.08rem !important; }
.text-base { font-size: 1.24rem !important; }
.text-lg { font-size: 1.4rem !important; }
.text-xl { font-size: 1.62rem !important; }
.text-3xl { font-size: 2.75rem !important; }
.text-\[13px\] { font-size: 1.14rem !important; }
[data-tip] {
cursor: help;
}
.module-map {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 12px;
}
.module-card {
border: 1px solid #aec3db;
border-radius: 8px;
background: white;
box-shadow: 0 6px 16px rgba(13, 39, 88, 0.07);
overflow: hidden;
}
.module-title {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 12px;
color: white;
background: #0d2758;
font-weight: 800;
line-height: 1.25;
}
.module-list {
display: flex;
flex-wrap: wrap;
gap: 6px;
padding: 10px;
background: #f0f6fc;
}
.module-pill {
border: 1px solid #c8d7e8;
border-radius: 999px;
padding: 3px 9px;
background: white;
color: #0d2758;
font-size: 0.9rem;
font-weight: 700;
white-space: nowrap;
}
.flow-card {
border: 1px solid #aec3db;
border-radius: 8px;
background: white;
box-shadow: 0 8px 20px rgba(13, 39, 88, 0.08);
overflow: hidden;
}
.flow-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 14px 16px 10px 16px;
border-bottom: 1px solid #d8e4f2;
}
.flow-title {
display: flex;
align-items: center;
gap: 10px;
color: #0d2758;
font-weight: 800;
font-size: 1.22rem;
line-height: 1.25;
}
.flow-tag {
border: 1px solid #c8d7e8;
border-radius: 999px;
padding: 3px 10px;
color: #5f7082;
background: #f8fbff;
font-weight: 700;
white-space: nowrap;
font-size: 0.92rem;
}
.flow-note {
padding: 9px 16px;
background: #f8fbff;
color: #5f7082;
border-bottom: 1px solid #d8e4f2;
font-weight: 700;
line-height: 1.45;
}
.swimlane-grid {
display: flex;
flex-direction: column;
gap: 0;
overflow-x: auto;
}
.lane {
min-width: 1080px;
display: grid;
grid-template-columns: 220px minmax(0, 1fr);
border-bottom: 1px solid #d8e4f2;
background: #f0f6fc;
}
.lane:last-child {
border-bottom: none;
}
.lane-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
min-height: 54px;
padding: 10px 12px;
background: #0d2758;
color: white;
font-weight: 800;
line-height: 1.25;
}
.lane-body {
display: flex;
flex-direction: row;
align-items: stretch;
gap: 10px;
min-height: 118px;
padding: 12px;
}
.module-node {
min-width: 180px;
flex: 1 1 0;
border: 1px solid #c8d7e8;
border-left: 5px solid #0d2758;
border-radius: 7px;
background: white;
color: #0d2758;
padding: 9px 10px 10px 10px;
box-shadow: 0 2px 8px rgba(13, 39, 88, 0.06);
}
.module-node.risk {
border-left-color: #d92d20;
}
.module-node.memory {
border-left-color: #7a5af8;
}
.module-node.service {
border-left-color: #00856f;
}
.node-top {
display: flex;
align-items: center;
gap: 7px;
margin-bottom: 7px;
}
.node-number {
display: inline-flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
border-radius: 999px;
color: white;
background: #0d2758;
font-weight: 800;
font-size: 0.8rem;
flex: 0 0 auto;
}
.module-node.risk .node-number {
background: #d92d20;
}
.module-node.memory .node-number {
background: #7a5af8;
}
.module-node.service .node-number {
background: #00856f;
}
.node-module {
display: inline-flex;
align-items: center;
max-width: 100%;
border: 1px solid #c8d7e8;
border-radius: 999px;
background: #f8fbff;
padding: 2px 8px;
color: #5f7082;
font-size: 0.82rem;
font-weight: 800;
line-height: 1.35;
}
.node-title {
display: block;
font-weight: 900;
line-height: 1.25;
margin-bottom: 4px;
}
.node-desc {
display: block;
color: #5f7082;
font-size: 0.9rem;
line-height: 1.35;
}
.legend {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.legend-item {
display: inline-flex;
align-items: center;
gap: 7px;
border: 1px solid #c8d7e8;
border-radius: 999px;
background: white;
color: #0d2758;
padding: 5px 11px;
font-weight: 700;
}
.legend-dot {
width: 10px;
height: 10px;
border-radius: 999px;
background: #0d2758;
flex: 0 0 auto;
}
.legend-dot.risk {
background: #d92d20;
}
.legend-dot.memory {
background: #7a5af8;
}
.legend-dot.service {
background: #00856f;
}
#sg-tooltip {
position: fixed;
z-index: 9999;
max-width: 440px;
padding: 10px 12px;
border-radius: 6px;
background: #0d2758;
color: white;
font-size: 0.95rem;
line-height: 1.45;
box-shadow: 0 12px 32px rgba(13, 39, 88, 0.22);
pointer-events: none;
opacity: 0;
transform: translateY(4px);
transition: opacity 0.12s ease, transform 0.12s ease;
}
#sg-tooltip.is-visible {
opacity: 1;
transform: translateY(0);
}
@media (max-width: 1100px) {
.module-map {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 720px) {
.module-map {
grid-template-columns: 1fr;
}
.flow-head {
align-items: flex-start;
flex-direction: column;
}
.lane {
min-width: 0;
display: block;
}
.lane-body {
flex-direction: column;
}
.module-node {
min-width: 0;
}
}
</style>
</head>
<body class="bg-white p-4 md:p-8 min-h-screen text-sm select-none">
<nav class="max-w-[1780px] mx-auto mb-5 flex flex-wrap items-center justify-between gap-3 border border-blue-200 rounded-lg px-4 py-3 shadow-sm bg-white">
<div class="flex items-center gap-2 theme-text-dark-blue font-bold text-base" data-tip="汇报专用静态网站,可直接双击 index.html 打开,不需要启动本地服务。">
<i class="fa-solid fa-display"></i>
<span>sgRobot 汇报静态网站</span>
</div>
<div class="flex items-center gap-2">
<a href="index.html" class="border theme-border-blue theme-text-dark-blue rounded px-4 py-2 font-bold bg-white hover:bg-blue-50" data-tip="返回 sgRobot 的目标态业务架构总览。">业务架构总览</a>
<a href="flows.html" class="theme-dark-blue text-white rounded px-4 py-2 font-bold shadow-sm" data-tip="当前页面展示业务流程如何落到架构模块。">业务流程图</a>
<a href="roadmap.html" class="border theme-border-blue theme-text-dark-blue rounded px-4 py-2 font-bold bg-white hover:bg-blue-50" data-tip="查看五月底上线倒排的里程碑、分工边界和验收口径。">里程碑与分工</a>
</div>
</nav>
<header class="max-w-[1780px] mx-auto mb-6">
<div class="flex items-center mb-2" data-tip="本页展示业务流程与架构模块的对应关系,不再只画孤立步骤。">
<div class="w-10 h-10 theme-dark-blue text-white flex items-center justify-center rounded shadow-sm mr-3">
<i class="fa-solid fa-route text-xl"></i>
</div>
<div>
<h1 class="text-3xl font-bold theme-text-dark-blue tracking-wide">sgRobot <span class="font-bold">业务流程与架构模块映射</span></h1>
</div>
</div>
<div class="ml-14 text-[13px] font-medium text-gray-700 leading-relaxed" data-tip="每条流程按架构图的上下分层来摆放,节点上的小标签标明它落到哪个具体模块。">
读图方式:每张流程图按<span class="font-bold">四个架构分区</span>自上而下展开,节点编号表示业务先后顺序,节点标签表示承接该动作的<span class="theme-red font-bold">具体架构模块</span>
</div>
</header>
<main class="max-w-[1780px] mx-auto flex flex-col gap-5">
<section class="border-2 theme-border-blue rounded-lg p-4 bg-white shadow-sm" data-tip="这里先把架构图中的四个分区和模块列出来,后续每个流程节点都会引用这些模块。">
<div class="flex flex-wrap items-center justify-between gap-3 mb-4">
<div class="font-bold theme-text-dark-blue text-lg flex items-center gap-2">
<i class="fa-solid fa-layer-group"></i>
<span>架构模块对照</span>
</div>
<div class="legend">
<span class="legend-item" data-tip="蓝色节点表示常规业务动作。"><span class="legend-dot"></span>常规动作</span>
<span class="legend-item" data-tip="红色节点表示需要人工判断、风险确认或异常处置。"><span class="legend-dot risk"></span>风险决策</span>
<span class="legend-item" data-tip="绿色节点表示按需后台服务、设备连接或外部系统协作。"><span class="legend-dot service"></span>服务协作</span>
<span class="legend-item" data-tip="紫色节点表示结果、经验、规则或过程记录沉淀。"><span class="legend-dot memory"></span>记录沉淀</span>
</div>
</div>
<div class="module-map">
<div class="module-card" data-tip="业务入口与协同负责把人的需求、系统事件和审批动作接进系统。">
<div class="module-title"><i class="fa-solid fa-right-to-bracket"></i><span>业务入口与协同</span></div>
<div class="module-list">
<span class="module-pill" data-tip="接收人工提交、系统接口、计划任务和事件触发。">业务需求入口</span>
<span class="module-pill" data-tip="让用户、运维和审批人查看、操作、审批、回放和验收成果。">协同工作台</span>
</div>
</div>
<div class="module-card" data-tip="任务治理与风控负责计划拆解、任务执行管理、人工治理和风险规则。">
<div class="module-title"><i class="fa-solid fa-shield-halved"></i><span>任务治理与风控</span></div>
<div class="module-list">
<span class="module-pill" data-tip="把业务目标拆成执行计划和计划步骤。">目标计划管理</span>
<span class="module-pill" data-tip="管理工作任务、执行记录、重试、恢复点和超时。">任务执行管理</span>
<span class="module-pill" data-tip="承接审批、暂停、恢复、取消、人工接管和重新规划。">人工治理</span>
<span class="module-pill" data-tip="管理记忆规则、服务占用、风险等级和健康状态。">风险与规则</span>
</div>
</div>
<div class="module-card" data-tip="自动执行与能力负责真正完成业务动作,并管理外部系统、设备和后台服务。">
<div class="module-title"><i class="fa-solid fa-gears"></i><span>自动执行与能力</span></div>
<div class="module-list">
<span class="module-pill" data-tip="登记可被调度使用的自动化能力。">能力目录</span>
<span class="module-pill" data-tip="检查前置条件、匹配能力、安排顺序和失败恢复。">调度中枢</span>
<span class="module-pill" data-tip="执行规则清楚、结果稳定、可重复的任务。">自动脚本</span>
<span class="module-pill" data-tip="把多步骤工作组织成可治理流程。">流程编排</span>
<span class="module-pill" data-tip="辅助规划、解释、诊断和修复建议。">智能辅助</span>
<span class="module-pill" data-tip="对接网页、业务系统、设备和机器人。">外部系统适配</span>
<span class="module-pill" data-tip="按需启动和回收邮件读取、监听器、浏览器环境等后台服务。">按需后台服务</span>
</div>
</div>
<div class="module-card" data-tip="运营记录与看板沉淀过程、成果、经验和指标,让管理者看到进度、风险和结果。">
<div class="module-title"><i class="fa-solid fa-chart-column"></i><span>运营记录与看板</span></div>
<div class="module-list">
<span class="module-pill" data-tip="保存计划、任务、执行和审批的当前状态。">业务状态台账</span>
<span class="module-pill" data-tip="记录关键过程变化,用于回放、审计和问题追踪。">过程日志</span>
<span class="module-pill" data-tip="保存文件、报告、截图和数据结果。">成果资料库</span>
<span class="module-pill" data-tip="沉淀项目事实、偏好、关键决策、处理经验和观察摘要。">经验与记忆库</span>
<span class="module-pill" data-tip="展示任务看板、审批队列、记忆索引和服务健康。">运营看板</span>
<span class="module-pill" data-tip="按时间顺序展示过程回放、诊断和追责。">全程追踪</span>
<span class="module-pill" data-tip="统计服务时效、成功率、响应耗时和人工介入率。">运营指标</span>
</div>
</div>
</div>
</section>
<div id="flow-root" class="flex flex-col gap-5"></div>
</main>
<script>
const lanes = [
{ key: "entry", icon: "fa-right-to-bracket", name: "业务入口与协同", tip: "对应架构图第一分区:业务需求入口和协同工作台。" },
{ key: "governance", icon: "fa-shield-halved", name: "任务治理与风控", tip: "对应架构图第二分区:目标计划管理、任务执行管理、人工治理、风险与规则。" },
{ key: "execution", icon: "fa-gears", name: "自动执行与能力", tip: "对应架构图第三分区:能力目录、调度中枢、自动脚本、流程编排、智能辅助、外部系统适配、按需后台服务。" },
{ key: "records", icon: "fa-chart-column", name: "运营记录与看板", tip: "对应架构图第四分区:业务状态台账、过程日志、成果资料库、经验与记忆库、运营看板、全程追踪、运营指标。" },
];
const flows = [
{
title: "人工交办任务处理",
icon: "fa-user-pen",
tag: "用户主动发起",
tip: "用户主动提交目标后,流程依次经过需求入口、计划治理、自动执行、成果验收和经验沉淀。",
nodes: [
{ lane: "entry", step: 1, module: "协同工作台", title: "接收交办", desc: "目标、材料、期望结果", tip: "用户在协同工作台提交业务目标、背景材料和验收要求。" },
{ lane: "entry", step: 2, module: "业务需求入口", title: "登记目标", desc: "形成可追踪入口", tip: "业务需求入口把原始需求登记成可追踪的业务目标。" },
{ lane: "governance", step: 3, module: "目标计划管理", title: "拆解计划", desc: "步骤、依赖、验收标准", tip: "目标计划管理把业务目标拆成执行计划、计划步骤、依赖和验收标准。" },
{ lane: "governance", step: 4, module: "任务执行管理", title: "生成任务", desc: "排队、优先级、恢复点", tip: "任务执行管理把计划步骤转成工作任务,并设置优先级、重试和恢复点。" },
{ lane: "execution", step: 5, module: "调度中枢", title: "匹配能力", desc: "脚本、流程、智能辅助", tip: "调度中枢为工作任务选择合适能力,并检查前置条件。" },
{ lane: "execution", step: 6, module: "自动脚本 / 流程编排", title: "执行任务", desc: "自动处理并持续回写", tip: "自动执行模块完成实际业务动作,并持续回写过程状态和中间成果。" },
{ lane: "governance", step: 7, module: "人工治理", title: "成果验收", desc: "确认、退回、接管", type: "risk", tip: "人工治理负责成果确认、退回重做、暂停或人工接管。" },
{ lane: "records", step: 8, module: "成果资料库 / 运营看板", title: "成果呈现", desc: "报告、截图、任务看板", type: "memory", tip: "成果资料库保存交付物,运营看板展示任务状态和验收结果。" },
{ lane: "records", step: 9, module: "经验与记忆库", title: "经验沉淀", desc: "偏好、决策、处理经验", type: "memory", tip: "经验与记忆库沉淀用户偏好、关键决策和处理经验,供后续任务复用。" },
],
},
{
title: "邮件与消息触发处理",
icon: "fa-envelope-open-text",
tag: "事件自动触发",
tip: "邮件、消息或业务通知到达后,按需服务先把事件读入,再由治理模块判断是否生成任务。",
nodes: [
{ lane: "execution", step: 1, module: "按需后台服务", title: "启动读取服务", desc: "邮件、监听器、消息", type: "service", tip: "按需后台服务在需要时启动邮件读取或消息监听,不要求所有服务随主程序常驻。" },
{ lane: "entry", step: 2, module: "业务需求入口", title: "接收事件", desc: "通知、邮件、业务提醒", tip: "业务需求入口把读取到的消息统一登记为事件或待判断请求。" },
{ lane: "governance", step: 3, module: "风险与规则", title: "分类判断", desc: "优先级、风险、是否自动处理", type: "risk", tip: "风险与规则判断消息类别、优先级、影响范围,以及是否需要人工确认。" },
{ lane: "governance", step: 4, module: "目标计划管理", title: "生成目标或任务", desc: "纳入统一治理", tip: "符合条件的消息会被转成业务目标、执行计划或工作任务。" },
{ lane: "execution", step: 5, module: "调度中枢 / 自动脚本", title: "自动处理", desc: "回复、归档、提取、触发后续流程", tip: "调度中枢选择处理能力,自动完成回复、归档、数据提取或后续流程触发。" },
{ lane: "governance", step: 6, module: "人工治理", title: "异常转人工", desc: "审批、补充、接管", type: "risk", tip: "遇到权限、金额、外部影响或识别不确定时,进入人工审批或人工接管。" },
{ lane: "records", step: 7, module: "过程日志 / 运营看板", title: "记录处理结果", desc: "结果、风险、处理人", type: "memory", tip: "过程日志记录处理过程,运营看板展示处理结果、风险状态和责任人。" },
{ lane: "records", step: 8, module: "经验与记忆库", title: "更新规则经验", desc: "分类经验、处理偏好", type: "memory", tip: "消息分类、人工判断和处理偏好沉淀为后续自动判断依据。" },
],
},
{
title: "定时巡检与报告",
icon: "fa-calendar-check",
tag: "周期运营任务",
tip: "周期巡检从计划任务入口进入,经过巡检计划、外部采集、报告生成和运营指标沉淀。",
nodes: [
{ lane: "entry", step: 1, module: "业务需求入口", title: "排程触发", desc: "日常、周报、月报", tip: "业务需求入口接收计划任务触发,启动周期巡检流程。" },
{ lane: "governance", step: 2, module: "目标计划管理", title: "生成巡检计划", desc: "对象、范围、标准", tip: "目标计划管理根据巡检对象、范围、责任人和验收标准生成计划。" },
{ lane: "governance", step: 3, module: "任务执行管理", title: "安排巡检任务", desc: "批次、优先级、超时", tip: "任务执行管理把巡检计划拆成工作任务,并配置批次、优先级和超时管控。" },
{ lane: "execution", step: 4, module: "外部系统适配", title: "连接巡检对象", desc: "系统、网页、设备", type: "service", tip: "外部系统适配连接业务系统、网页、设备或机器人,准备采集信息。" },
{ lane: "execution", step: 5, module: "自动脚本 / 智能辅助", title: "采集并生成报告", desc: "指标、截图、结论", tip: "自动脚本采集数据,智能辅助整理结论、异常和建议。" },
{ lane: "records", step: 6, module: "成果资料库", title: "归档报告证据", desc: "报告、截图、数据结果", type: "memory", tip: "成果资料库保存巡检报告、截图和数据结果,支撑验收和复盘。" },
{ lane: "records", step: 7, module: "运营指标", title: "更新运营指标", desc: "趋势、成功率、耗时", type: "memory", tip: "运营指标沉淀巡检趋势、执行成功率、响应耗时和服务时效。" },
{ lane: "governance", step: 8, module: "人工治理", title: "异常升级", desc: "告警、确认、复盘", type: "risk", tip: "发现异常、超时或失败率升高时,转人工确认、升级和复盘。" },
],
},
{
title: "高风险动作审批",
icon: "fa-shield-halved",
tag: "人控关键节点",
tip: "高风险动作通过风险规则识别,在执行前转入人工治理,并在全程追踪中留痕。",
nodes: [
{ lane: "governance", step: 1, module: "风险与规则", title: "识别风险动作", desc: "权限、影响、不可逆", type: "risk", tip: "风险与规则识别删除、发布、付款、发信、改配置、控制设备等高风险动作。" },
{ lane: "governance", step: 2, module: "任务执行管理", title: "暂停在风险点", desc: "停止继续执行", type: "risk", tip: "任务执行管理把流程暂停在风险节点,避免未经确认继续执行。" },
{ lane: "entry", step: 3, module: "协同工作台", title: "呈现审批材料", desc: "原因、影响、建议", type: "risk", tip: "协同工作台向审批人展示目标、原因、影响范围、可选方案和建议动作。" },
{ lane: "governance", step: 4, module: "人工治理", title: "审批决策", desc: "通过、拒绝、接管、重规划", type: "risk", tip: "人工治理承接审批通过、拒绝、补充材料、人工接管或重新规划。" },
{ lane: "execution", step: 5, module: "调度中枢", title: "执行分支", desc: "继续、取消、转人工", tip: "调度中枢根据审批结论继续执行、取消任务或转人工处理。" },
{ lane: "records", step: 6, module: "全程追踪", title: "审批留痕", desc: "谁、何时、为何", type: "memory", tip: "全程追踪记录审批人、审批时间、审批理由和最终结果。" },
{ lane: "records", step: 7, module: "经验与记忆库", title: "沉淀审批边界", desc: "规则、案例、判断依据", type: "memory", tip: "审批案例会沉淀为后续风险判断、审批边界和培训材料。" },
],
},
{
title: "外部系统与设备协作",
icon: "fa-plug-circle-bolt",
tag: "跨系统协作",
tip: "跨系统任务通过服务准备、外部适配、状态回写和成果归档,把网页、设备和机器人纳入统一治理。",
nodes: [
{ lane: "entry", step: 1, module: "业务需求入口", title: "协作请求进入", desc: "业务系统、人、计划任务", tip: "业务需求入口接收需要外部系统、设备或机器人参与的协作请求。" },
{ lane: "governance", step: 2, module: "风险与规则", title: "检查权限与服务占用", desc: "账号、设备、占用关系", type: "risk", tip: "风险与规则检查权限、账号、设备状态和后台服务占用关系。" },
{ lane: "execution", step: 3, module: "按需后台服务", title: "准备运行环境", desc: "浏览器环境、连接、服务实例", type: "service", tip: "按需后台服务准备浏览器环境、设备连接或外部服务实例。" },
{ lane: "execution", step: 4, module: "外部系统适配", title: "执行外部动作", desc: "网页、设备、机器人", type: "service", tip: "外部系统适配执行网页操作、设备控制或机器人动作。" },
{ lane: "records", step: 5, module: "业务状态台账", title: "状态回写", desc: "进度、异常、服务健康", type: "memory", tip: "业务状态台账记录进度、异常和服务健康,供治理流程继续判断。" },
{ lane: "governance", step: 6, module: "任务执行管理", title: "异常恢复", desc: "重试、替代、接管", type: "risk", tip: "外部系统不可用、设备异常或页面变化时,任务执行管理负责重试、替代路径或人工接管。" },
{ lane: "records", step: 7, module: "成果资料库 / 全程追踪", title: "成果归档", desc: "证据、状态、经验", type: "memory", tip: "最终成果、截图证据、设备状态和处理过程统一归档。" },
],
},
{
title: "失败恢复与复盘改进",
icon: "fa-screwdriver-wrench",
tag: "持续改进闭环",
tip: "失败不是只停在报错,而是由记录层提供证据,治理层决策恢复路径,执行层重试,最后沉淀规则。",
nodes: [
{ lane: "records", step: 1, module: "过程日志", title: "发现失败事实", desc: "失败、超时、不达标", type: "risk", tip: "过程日志记录任务失败、超时、外部异常或成果不达标。" },
{ lane: "records", step: 2, module: "全程追踪", title: "汇总证据", desc: "过程、截图、状态", type: "memory", tip: "全程追踪汇总过程记录、截图、服务状态和最近变更。" },
{ lane: "execution", step: 3, module: "智能辅助", title: "自动诊断", desc: "原因、影响、建议", tip: "智能辅助根据证据给出可能原因、影响范围和修复建议。" },
{ lane: "governance", step: 4, module: "任务执行管理", title: "选择恢复路径", desc: "重试、替代、回退", type: "risk", tip: "任务执行管理根据风险和预算选择自动重试、替代能力或回退路径。" },
{ lane: "execution", step: 5, module: "调度中枢", title: "执行恢复方案", desc: "重新调度、替代执行", tip: "调度中枢安排重试、替代能力或补偿步骤。" },
{ lane: "governance", step: 6, module: "人工治理", title: "人工介入", desc: "补充、确认、接管", type: "risk", tip: "自动恢复仍不可靠或风险较高时,人工治理负责补充信息、确认方案或接管执行。" },
{ lane: "records", step: 7, module: "经验与记忆库", title: "复盘沉淀", desc: "原因、方法、预防", type: "memory", tip: "处理完成后沉淀失败原因、修复方法、适用条件和预防建议。" },
{ lane: "governance", step: 8, module: "风险与规则", title: "优化规则", desc: "减少重复失败", type: "memory", tip: "复盘结果转化为规则、能力说明、巡检项或审批边界优化。" },
],
},
];
function escapeHtml(value) {
return String(value)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
function renderNode(node) {
const type = node.type ? ` ${escapeHtml(node.type)}` : "";
return `
<div class="module-node${type}" data-tip="${escapeHtml(node.tip)}">
<div class="node-top">
<span class="node-number">${escapeHtml(node.step)}</span>
<span class="node-module">${escapeHtml(node.module)}</span>
</div>
<span class="node-title">${escapeHtml(node.title)}</span>
<span class="node-desc">${escapeHtml(node.desc)}</span>
</div>
`;
}
function renderFlow(flow) {
const lanesHtml = lanes.map((lane) => {
const nodes = flow.nodes
.filter((node) => node.lane === lane.key)
.sort((a, b) => a.step - b.step)
.map(renderNode)
.join("");
return `
<div class="lane" data-tip="${escapeHtml(lane.tip)}">
<div class="lane-head">
<span><i class="fa-solid ${escapeHtml(lane.icon)} mr-1"></i>${escapeHtml(lane.name)}</span>
</div>
<div class="lane-body">${nodes}</div>
</div>
`;
}).join("");
return `
<section class="flow-card" data-tip="${escapeHtml(flow.tip)}">
<div class="flow-head">
<div class="flow-title">
<i class="fa-solid ${escapeHtml(flow.icon)}"></i>
<span>${escapeHtml(flow.title)}</span>
</div>
<span class="flow-tag" data-tip="${escapeHtml(flow.tip)}">${escapeHtml(flow.tag)}</span>
</div>
<div class="flow-note" data-tip="节点按数字读,四层顺序与架构图一致;有些流程会在治理、执行和记录之间回流。">
节点按数字读;每一层就是架构图中的一个分区,节点标签就是承接该步骤的具体模块。
</div>
<div class="swimlane-grid">${lanesHtml}</div>
</section>
`;
}
document.getElementById("flow-root").innerHTML = flows.map(renderFlow).join("");
const tooltip = document.createElement("div");
tooltip.id = "sg-tooltip";
document.body.appendChild(tooltip);
let activeTipTarget = null;
function positionTooltip(event) {
const margin = 14;
const offset = 16;
const rect = tooltip.getBoundingClientRect();
let left = event.clientX + offset;
let top = event.clientY + offset;
if (left + rect.width + margin > window.innerWidth) {
left = Math.max(margin, event.clientX - rect.width - offset);
}
if (top + rect.height + margin > window.innerHeight) {
top = Math.max(margin, event.clientY - rect.height - offset);
}
tooltip.style.left = `${left}px`;
tooltip.style.top = `${top}px`;
}
function showTooltip(target, event) {
const text = target.dataset.tip;
if (!text) return;
activeTipTarget = target;
tooltip.textContent = text;
tooltip.classList.add("is-visible");
positionTooltip(event);
}
function hideTooltip() {
activeTipTarget = null;
tooltip.classList.remove("is-visible");
}
document.addEventListener("pointerover", (event) => {
const target = event.target.closest("[data-tip]");
if (target) showTooltip(target, event);
});
document.addEventListener("pointermove", (event) => {
if (activeTipTarget) positionTooltip(event);
});
document.addEventListener("pointerout", (event) => {
if (!activeTipTarget) return;
const next = event.relatedTarget;
if (!next || !activeTipTarget.contains(next)) hideTooltip();
});
document.querySelectorAll("[data-tip]").forEach((element) => {
element.setAttribute("tabindex", "0");
element.setAttribute("aria-label", element.dataset.tip);
element.addEventListener("focus", (event) => {
const rect = event.currentTarget.getBoundingClientRect();
showTooltip(event.currentTarget, {
clientX: rect.left + rect.width / 2,
clientY: rect.top + rect.height / 2,
});
});
element.addEventListener("blur", hideTooltip);
});
</script>
</body>
</html>

View File

@@ -1,650 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>sgRobot 目标态业务架构</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
/* 自定义一些特定颜色以更贴近原图 */
.theme-dark-blue { background-color: #0d2758; }
.theme-text-dark-blue { color: #0d2758; }
.theme-light-blue-bg { background-color: #f0f6fc; }
.theme-border-blue { border-color: #aec3db; }
.theme-text-gray { color: #5f7082; }
.theme-red { color: #d92d20; }
/* 连接线箭头样式辅助 */
.arrow-right-solid {
width: 0;
height: 0;
border-top: 5px solid transparent;
border-bottom: 5px solid transparent;
border-left: 8px solid #0d2758;
}
.arrow-down-solid {
width: 0;
height: 0;
border-left: 5px solid transparent;
border-right: 5px solid transparent;
border-top: 8px solid #0d2758;
}
/* 用于核心闭环的虚线箭头 */
.dashed-arrow-line {
background-image: linear-gradient(to right, #d92d20 50%, transparent 50%);
background-size: 10px 2px;
background-repeat: repeat-x;
}
.arrow-right-red {
width: 0;
height: 0;
border-top: 4px solid transparent;
border-bottom: 4px solid transparent;
border-left: 6px solid #d92d20;
}
.arrow-left-red {
width: 0;
height: 0;
border-top: 4px solid transparent;
border-bottom: 4px solid transparent;
border-right: 6px solid #d92d20;
}
body {
font-family: "PingFang SC", "Microsoft YaHei", "Noto Sans SC", "Source Han Sans SC", Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
font-size: 18px;
}
/* 汇报场景下整体字号再上调一档,兼顾投屏可读性 */
.text-xs { font-size: 0.92rem !important; }
.text-sm { font-size: 1.08rem !important; }
.text-base { font-size: 1.24rem !important; }
.text-lg { font-size: 1.4rem !important; }
.text-xl { font-size: 1.62rem !important; }
.text-3xl { font-size: 2.75rem !important; }
.text-\[9px\] { font-size: 0.82rem !important; }
.text-\[10px\] { font-size: 0.92rem !important; }
.text-\[11px\] { font-size: 1.02rem !important; }
.text-\[13px\] { font-size: 1.14rem !important; }
/* 放大后给各层卡片更多高度,避免文字贴边 */
.min-h-\[60px\] { min-height: 86px !important; }
.min-h-\[65px\] { min-height: 92px !important; }
.min-h-\[70px\] { min-height: 104px !important; }
[data-tip] {
cursor: help;
}
.term-list {
display: flex;
flex-wrap: wrap;
gap: 5px 6px;
margin-top: 5px;
}
.term {
display: inline-flex;
align-items: center;
border: 1px solid #c8d7e8;
border-radius: 999px;
background: #f8fbff;
padding: 2px 8px;
color: #5f7082;
font-size: 0.86rem;
line-height: 1.35;
white-space: nowrap;
}
#sg-tooltip {
position: fixed;
z-index: 9999;
max-width: 520px;
padding: 10px 12px;
border-radius: 6px;
background: #0d2758;
color: white;
font-size: 0.95rem;
line-height: 1.45;
white-space: pre-line;
box-shadow: 0 12px 32px rgba(13, 39, 88, 0.22);
pointer-events: none;
opacity: 0;
transform: translateY(4px);
transition: opacity 0.12s ease, transform 0.12s ease;
}
#sg-tooltip.is-visible {
opacity: 1;
transform: translateY(0);
}
</style>
</head>
<body class="bg-white p-4 md:p-8 min-h-screen text-sm select-none">
<nav class="max-w-[1780px] mx-auto mb-5 flex flex-wrap items-center justify-between gap-3 border border-blue-200 rounded-lg px-4 py-3 shadow-sm bg-white">
<div class="flex items-center gap-2 theme-text-dark-blue font-bold text-base" data-tip="汇报专用静态网站,可直接双击 index.html 打开,不需要启动本地服务。">
<i class="fa-solid fa-display"></i>
<span>sgRobot 汇报静态网站</span>
</div>
<div class="flex items-center gap-2">
<a href="index.html" class="theme-dark-blue text-white rounded px-4 py-2 font-bold shadow-sm" data-tip="查看 sgRobot 的目标态业务架构总览。">业务架构总览</a>
<a href="flows.html" class="border theme-border-blue theme-text-dark-blue rounded px-4 py-2 font-bold bg-white hover:bg-blue-50" data-tip="查看几类典型业务如何进入、执行、沉淀和被人工治理。">业务流程图</a>
<a href="roadmap.html" class="border theme-border-blue theme-text-dark-blue rounded px-4 py-2 font-bold bg-white hover:bg-blue-50" data-tip="查看五月底上线倒排的里程碑、分工边界和验收口径。">里程碑与分工</a>
</div>
</nav>
<div class="max-w-[1780px] mx-auto mb-6">
<div class="flex items-center mb-2">
<div class="w-10 h-10 theme-dark-blue text-white flex items-center justify-center rounded shadow-sm mr-3">
<i class="fa-solid fa-cube text-xl"></i>
</div>
<div>
<h1 class="text-3xl font-bold theme-text-dark-blue tracking-wide">sgRobot <span class="font-bold">目标态业务架构</span></h1>
</div>
</div>
<div class="ml-14 text-[13px] font-medium text-gray-700 leading-relaxed">
围绕<span class="font-bold">业务入口</span><span class="font-bold">任务治理</span><span class="font-bold">自动执行</span><span class="theme-red font-bold">运营看板</span>四条主线,把目标接收、计划拆解、执行过程、人工决策、经验沉淀和后台服务统一纳入可管理的业务闭环。
</div>
</div>
<div class="max-w-[1780px] mx-auto relative">
<!-- 顶层用户 -->
<div class="flex justify-center mb-[-12px] relative z-10">
<div class="bg-white border-2 theme-border-blue rounded-full px-10 py-2 flex items-center gap-3 shadow-sm" data-tip="系统的外部参与者。用户提交目标,运维关注健康与风险,审批人处理治理节点,外部业务系统可通过系统接口或业务事件触发任务。">
<i class="fa-solid fa-user-group text-blue-600 text-lg"></i>
<span class="font-bold theme-text-dark-blue text-base">
<span data-tip="用户是目标和需求的主要提出者,可以发起任务、补充输入和接受结果。">用户</span> /
<span data-tip="运维关注系统健康、服务状态、告警、失败恢复和上线风险。">运维</span> /
<span data-tip="审批人负责处理高风险动作、外部副作用、权限边界和人工治理节点。">审批人</span> /
<span data-tip="外部业务系统通过系统接口、消息或业务事件触发 sgRobot 工作流。">外部业务系统</span>
</span>
</div>
</div>
<div class="flex justify-center mb-2 text-blue-600 text-lg">
<i class="fa-solid fa-arrow-down"></i>
</div>
<!-- 主结构外框 -->
<div class="border-2 theme-border-blue rounded-lg p-3 pt-6 flex flex-col gap-2 relative bg-white">
<div class="absolute right-[-10px] top-[15%] bottom-[15%] w-[20px] border-r-2 border-t-2 border-b-2 theme-border-blue rounded-r-lg z-0 opacity-50 hidden lg:block"></div>
<!-- Plane 1: 业务入口与协同 -->
<div class="flex items-stretch border theme-border-blue rounded bg-white relative z-10">
<div class="w-12 md:w-40 theme-dark-blue text-white flex flex-col items-center justify-center p-2 rounded-l flex-shrink-0" data-tip="业务入口与协同负责把人的需求、系统事件和业务审批统一接入,并把任务进展反馈给相关人员。">
<div class="bg-white/20 px-2 py-1 rounded text-xs mb-1 font-mono">1</div>
<span class="font-bold hidden md:block text-center leading-tight">业务入口与协同</span>
<span class="font-bold md:hidden text-center writing-vertical-lr text-xs">入口协同</span>
</div>
<div class="flex-1 theme-light-blue-bg p-3 grid grid-cols-1 md:grid-cols-2 gap-4 rounded-r">
<div class="bg-white border theme-border-blue rounded p-2 flex items-center gap-3 shadow-sm min-h-[60px] w-full" data-tip="业务需求入口把人工提交、接口调用、计划任务和事件触发统一登记为可跟踪的业务目标或处理请求。">
<i class="fa-solid fa-arrow-right-to-bracket text-2xl theme-text-dark-blue ml-2"></i>
<div>
<div class="font-bold theme-text-dark-blue text-sm">业务需求入口</div>
<div class="term-list">
<span class="term" data-tip="人工提交是用户在控制台或对话入口主动发起的目标。">人工提交</span>
<span class="term" data-tip="系统接口让外部业务系统以结构化请求提交目标、状态或审批动作。">系统接口</span>
<span class="term" data-tip="计划任务是按时间、周期或排程触发的目标入口。">计划任务</span>
<span class="term" data-tip="事件触发来自邮件、系统通知、消息、文件变化或内部业务事件。">事件触发</span>
</div>
</div>
</div>
<div class="bg-white border theme-border-blue rounded p-2 flex items-center gap-3 shadow-sm min-h-[60px] w-full" data-tip="协同工作台让用户、运维和审批人查看进度、处理审批、回放过程和验收成果。">
<i class="fa-solid fa-desktop text-2xl theme-text-dark-blue ml-2"></i>
<div>
<div class="font-bold theme-text-dark-blue text-sm">协同工作台</div>
<div class="term-list">
<span class="term" data-tip="查看用于读取目标、计划、任务、执行记录、服务状态和经验记忆。">查看</span>
<span class="term" data-tip="操作是用户对任务、服务、参数和视图发起的显式动作。">操作</span>
<span class="term" data-tip="审批用于确认高风险动作、外部副作用或权限敏感步骤。">审批</span>
<span class="term" data-tip="回放按事件时间线复原过程,用于诊断、复盘和追责。">回放</span>
<span class="term" data-tip="成果浏览用于查看报告、截图、文件、日志快照和结构化结果。">成果浏览</span>
</div>
</div>
</div>
</div>
</div>
<!-- Plane 2: 任务治理与风控 -->
<div class="flex items-stretch border theme-border-blue rounded bg-white relative z-10">
<div class="w-12 md:w-40 theme-dark-blue text-white flex flex-col items-center justify-center p-2 rounded-l flex-shrink-0" data-tip="任务治理与风控负责把业务目标拆成计划和任务,并统一管理审批、风险、暂停、恢复、重试和服务占用规则。">
<i class="fa-solid fa-sliders text-xl mb-1"></i>
<span class="font-bold hidden md:block text-center leading-tight">任务治理与风控</span>
<span class="font-bold md:hidden text-center writing-vertical-lr text-xs mt-2">任务风控</span>
</div>
<div class="flex-1 theme-light-blue-bg p-3 grid grid-cols-1 md:grid-cols-4 gap-3 rounded-r">
<div class="bg-white border theme-border-blue rounded p-2 shadow-sm min-h-[70px]" data-tip="目标与计划管理把原始目标转成可审查、可冻结、可重规划的计划结构。">
<div class="flex items-center gap-2 mb-1">
<i class="fa-solid fa-bullseye theme-text-dark-blue text-lg"></i>
<span class="font-bold theme-text-dark-blue text-[13px]">目标计划管理</span>
</div>
<div class="term-list">
<span class="term" data-tip="业务目标是用户或外部系统想达成的结果,也是后续计划拆解的源头。">业务目标</span>
<span class="term" data-tip="执行计划描述成功标准、风险、依赖和执行步骤。">执行计划</span>
<span class="term" data-tip="计划步骤是计划中的单个动作,用于生成可调度的工作任务。">计划步骤</span>
</div>
</div>
<div class="bg-white border theme-border-blue rounded p-2 shadow-sm min-h-[70px]" data-tip="任务执行管理负责从步骤生成任务,并管理执行尝试、失败恢复、服务占用和最终状态。">
<div class="flex items-center gap-2 mb-1">
<i class="fa-solid fa-list-ul theme-text-dark-blue text-lg"></i>
<span class="font-bold theme-text-dark-blue text-[13px]">任务执行管理</span>
</div>
<div class="term-list">
<span class="term" data-tip="工作任务是可调度的工作单元,绑定能力、输入、依赖和优先级。">工作任务</span>
<span class="term" data-tip="执行记录代表一次任务执行尝试,重试会产生新的执行记录。">执行记录</span>
<span class="term" data-tip="自动重试是失败或中断后的再执行策略,受预算和风险约束。">自动重试</span>
<span class="term" data-tip="恢复点是长任务的过程锚点,用于失败后恢复和问题诊断。">恢复点</span>
<span class="term" data-tip="超时管控防止任务无限运行,超时后进入恢复或人工介入路径。">超时管控</span>
</div>
</div>
<div class="bg-white border theme-border-blue rounded p-2 shadow-sm min-h-[70px]" data-tip="人工治理把人的介入变成可记录、可回放、可追责的业务操作,而不是后台直接改结果。">
<div class="flex items-center gap-2 mb-1">
<i class="fa-solid fa-user-check theme-text-dark-blue text-lg"></i>
<span class="font-bold theme-text-dark-blue text-[13px]">人工治理</span>
</div>
<div class="term-list">
<span class="term" data-tip="审批通过允许高风险步骤、外部副作用或权限敏感动作继续执行。">审批通过</span>
<span class="term" data-tip="暂停让人检查任务状态、参数、风险或依赖。">暂停</span>
<span class="term" data-tip="恢复从暂停点继续执行,通常需要保留上下文和服务占用状态。">恢复</span>
<span class="term" data-tip="取消终止任务或计划,并释放相关服务占用。">取消</span>
<span class="term" data-tip="人工接管把自动执行转交给人工或指定操作者处理。">人工接管</span>
<span class="term" data-tip="重新规划在现有目标下重新生成或修改执行路径。">重新规划</span>
</div>
</div>
<div class="bg-white border theme-border-blue rounded p-2 shadow-sm min-h-[70px]" data-tip="风险与规则规定经验记忆怎么用、后台服务怎么占用、哪些动作必须审批、哪些状态必须进入看板。">
<div class="flex items-center gap-2 mb-1">
<i class="fa-solid fa-shield-halved theme-text-dark-blue text-lg"></i>
<span class="font-bold theme-text-dark-blue text-[13px]">风险与规则</span>
</div>
<div class="term-list">
<span class="term" data-tip="记忆规则规定经验何时召回、写入、遗忘、替代或标记冲突。">记忆规则</span>
<span class="term" data-tip="服务占用记录任务对后台服务的使用关系,防止误停和资源泄漏。">服务占用</span>
<span class="term" data-tip="风险等级表示动作、任务、服务或记忆使用的风险级别和审批要求。">风险等级</span>
<span class="term" data-tip="健康状态汇总任务、服务、通道和看板等关键组件是否正常。">健康状态</span>
</div>
</div>
</div>
</div>
<!-- Plane 3: 自动执行与能力 -->
<div class="flex items-stretch border theme-border-blue rounded bg-white relative z-10">
<div class="w-12 md:w-40 theme-dark-blue text-white flex flex-col items-center justify-center p-2 rounded-l flex-shrink-0" data-tip="自动执行与能力负责真正完成业务动作,包括脚本、流程、智能辅助、外部系统适配和按需后台服务。">
<i class="fa-solid fa-gears text-xl mb-1"></i>
<span class="font-bold hidden md:block text-center leading-tight">自动执行与能力</span>
<span class="font-bold md:hidden text-center writing-vertical-lr text-xs mt-2">自动执行</span>
</div>
<div class="flex-1 theme-light-blue-bg p-3 grid grid-cols-1 md:grid-cols-7 gap-3 rounded-r">
<div class="bg-white border theme-border-blue rounded p-2 shadow-sm min-h-[60px]" data-tip="能力目录管理可治理自动化能力的登记、版本、使用约定、策略和健康。">
<div class="font-bold theme-text-dark-blue text-[13px]"><i class="fa-solid fa-box mr-1"></i>能力目录</div>
<div class="term-list">
<span class="term" data-tip="能力登记把脚本、流程、智能辅助或外部适配纳入统一目录。">能力登记</span>
<span class="term" data-tip="版本管理用于区分能力说明、实现和兼容边界。">版本管理</span>
<span class="term" data-tip="使用约定说明输入、输出、成果和过程记录要求。">使用约定</span>
</div>
</div>
<div class="bg-white border theme-border-blue rounded p-2 shadow-sm min-h-[60px]" data-tip="调度中枢负责检查前置条件、匹配自动化能力、安排执行顺序并处理失败恢复。">
<div class="font-bold theme-text-dark-blue text-[13px]"><i class="fa-solid fa-sitemap mr-1"></i>调度中枢</div>
<div class="term-list">
<span class="term" data-tip="前置检查判断任务、服务、记忆、成果和审批之间的依赖是否满足。">前置检查</span>
<span class="term" data-tip="能力匹配根据使用约定、风险、可用性和上下文选择合适能力。">能力匹配</span>
<span class="term" data-tip="失败恢复处理回滚、替代路径、重试和人工兜底。">失败恢复</span>
</div>
</div>
<div class="bg-white border theme-border-blue rounded p-2 shadow-sm min-h-[60px]" data-tip="自动脚本适合规则清楚、结果稳定、可重试、可审计的任务。">
<div class="font-bold theme-text-dark-blue text-[13px]"><i class="fa-solid fa-code mr-1"></i>自动脚本</div>
<div class="term-list"><span class="term" data-tip="确定性任务适合明确输入输出,结果更容易测试和审计。">确定性任务</span></div>
</div>
<div class="bg-white border theme-border-blue rounded p-2 shadow-sm min-h-[60px]" data-tip="流程编排承载组合能力和子流程,把多步骤操作纳入统一治理。">
<div class="font-bold theme-text-dark-blue text-[13px]"><i class="fa-solid fa-diagram-project mr-1"></i>流程编排</div>
<div class="term-list">
<span class="term" data-tip="组合能力把多个原子能力组合为一个可治理流程。">组合能力</span>
<span class="term" data-tip="子流程是在一个任务内部继续拆分的执行片段。">子流程</span>
</div>
</div>
<div class="bg-white border theme-border-blue rounded p-2 shadow-sm min-h-[60px]" data-tip="智能辅助用于规划、选择、解释和修复建议,但任务状态仍由治理流程统一管理。">
<div class="font-bold theme-text-dark-blue text-[13px]"><i class="fa-solid fa-brain mr-1"></i>智能辅助</div>
<div class="term-list">
<span class="term" data-tip="规划用于把目标拆成可审查的计划和步骤。">规划</span>
<span class="term" data-tip="修复建议用于失败诊断、替代能力和补偿路径建议。">修复建议</span>
<span class="term" data-tip="解释用于说明计划、风险、失败原因和人工介入建议。">解释</span>
</div>
</div>
<div class="bg-white border theme-border-blue rounded p-2 shadow-sm min-h-[60px]" data-tip="外部系统适配把网页、设备和机器人能力接入统一业务流程,避免各自独立运行、难以管理。">
<div class="font-bold theme-text-dark-blue text-[13px]"><i class="fa-solid fa-robot mr-1"></i>外部系统适配</div>
<div class="term-list">
<span class="term" data-tip="网页操作用于网页、控制台和浏览器自动化能力。">网页操作</span>
<span class="term" data-tip="设备接入用于连接硬件、串口、设备接口或外设能力。">设备接入</span>
<span class="term" data-tip="机器人控制用于机器人本体能力,如感知、运动和语音。">机器人控制</span>
</div>
</div>
<div class="bg-white border theme-border-blue rounded p-2 shadow-sm min-h-[60px]" data-tip="按需后台服务负责邮件读取、监听器、浏览器环境、配套后台程序和外部服务进程的启动、占用、健康、日志和空闲回收。">
<div class="font-bold theme-text-dark-blue text-[13px]"><i class="fa-solid fa-server mr-1"></i>按需后台服务</div>
<div class="term-list">
<span class="term" data-tip="服务定义描述后台服务如何启动、停止、授权、检查健康和对外提供能力。">服务定义</span>
<span class="term" data-tip="运行实例是服务的一次实际运行,记录访问地址、进程和状态。">运行实例</span>
<span class="term" data-tip="占用关系表示某个任务正在依赖服务实例,决定服务是否能回收。">占用关系</span>
</div>
</div>
</div>
</div>
<!-- Plane 4: 运营记录与看板 -->
<div class="flex items-stretch border theme-border-blue rounded bg-white relative z-10">
<div class="w-12 md:w-40 theme-dark-blue text-white flex flex-col items-center justify-center p-2 rounded-l flex-shrink-0" data-tip="运营记录与看板沉淀业务过程、成果、经验和服务状态,让管理者能看进度、看风险、看结果、看责任。">
<i class="fa-solid fa-database text-xl mb-1"></i>
<span class="font-bold hidden md:block text-center leading-tight">运营记录与看板</span>
<span class="font-bold md:hidden text-center writing-vertical-lr text-xs mt-2">运营看板</span>
</div>
<div class="flex-1 theme-light-blue-bg p-3 grid grid-cols-1 md:grid-cols-7 gap-3 rounded-r">
<div class="bg-white border theme-border-blue rounded p-2 shadow-sm min-h-[65px]" data-tip="业务状态台账保存计划、任务、执行记录和审批记录的当前状态,是进度和责任判断的依据。">
<div class="font-bold theme-text-dark-blue text-[13px]"><i class="fa-solid fa-lock mr-1"></i>业务状态台账</div>
<div class="term-list">
<span class="term" data-tip="执行计划状态表示计划处于草稿、审核、激活、阻塞、完成或取消等阶段。">执行计划</span>
<span class="term" data-tip="工作任务状态表示任务是否待处理、排队、执行中、暂停、失败或完成。">工作任务</span>
<span class="term" data-tip="执行记录状态表示一次执行尝试的生命周期和健康信息。">执行记录</span>
<span class="term" data-tip="审批记录状态表示审批请求是否待处理、已通过、已拒绝、已过期或已取消。">审批记录</span>
</div>
</div>
<div class="bg-white border theme-border-blue rounded p-2 shadow-sm min-h-[65px]" data-tip="过程日志记录所有关键变化,是审计、回放、看板生成和问题追踪的基础。">
<div class="font-bold theme-text-dark-blue text-[13px]"><i class="fa-regular fa-file-lines mr-1"></i>过程日志</div>
<div class="term-list"><span class="term" data-tip="关键过程记录是已经发生的业务变化,如任务开始、执行失败、服务启动或经验更新。">关键过程记录</span></div>
</div>
<div class="bg-white border theme-border-blue rounded p-2 shadow-sm min-h-[65px]" data-tip="成果资料库存放执行生成的文件、报告、截图和结构化数据结果。">
<div class="font-bold theme-text-dark-blue text-[13px]"><i class="fa-solid fa-folder-open mr-1"></i>成果资料库</div>
<div class="term-list">
<span class="term" data-tip="文件是执行生成或引用的本地或远端文件产物。">文件</span>
<span class="term" data-tip="报告是面向人或验收的总结性产物。">报告</span>
<span class="term" data-tip="截图用于证明界面、网页、浏览器或视觉状态。">截图</span>
<span class="term" data-tip="数据结果是可被系统继续读取的结构化数据、表格、记录或索引。">数据结果</span>
</div>
</div>
<div class="bg-white border theme-border-blue rounded p-2 shadow-sm min-h-[65px]" data-tip="经验与记忆库存放可复用知识,不代表当前任务状态,必须受来源、置信度、保留和遗忘规则约束。">
<div class="font-bold theme-text-dark-blue text-[13px]"><i class="fa-solid fa-brain mr-1"></i>经验与记忆库</div>
<div class="term-list">
<span class="term" data-tip="项目事实是相对稳定、可引用的信息,但仍应有来源和置信度。">项目事实</span>
<span class="term" data-tip="使用偏好记录用户、项目或团队的长期选择倾向。">使用偏好</span>
<span class="term" data-tip="关键决策记录已经确定的架构、流程、策略或边界选择。">关键决策</span>
<span class="term" data-tip="处理经验记录过去执行中的成功路径、失败模式和修复方法。">处理经验</span>
<span class="term" data-tip="观察摘要从邮件、消息、工单、服务日志或运行结果中提炼可复用结论。">观察摘要</span>
</div>
</div>
<div class="bg-white border theme-border-blue rounded p-2 shadow-sm min-h-[65px]" data-tip="运营看板面向管理、查询和协同,由状态、过程日志、成果、经验记忆和服务健康汇总生成。">
<div class="font-bold theme-text-dark-blue text-[13px]"><i class="fa-solid fa-chart-column mr-1"></i>运营看板</div>
<div class="term-list">
<span class="term" data-tip="任务看板展示任务状态、阻塞、风险和下一步动作。">任务看板</span>
<span class="term" data-tip="审批队列集中展示等待人工处理的治理节点。">审批队列</span>
<span class="term" data-tip="记忆索引让用户查看、修正、替代或遗忘长期记忆。">记忆索引</span>
<span class="term" data-tip="服务健康展示服务实例、占用关系、重启和最近错误。">服务健康</span>
</div>
</div>
<div class="bg-white border theme-border-blue rounded p-2 shadow-sm min-h-[65px]" data-tip="全程追踪按时间顺序展示计划、任务、执行、审批、服务和经验记忆变化。">
<div class="font-bold theme-text-dark-blue text-[13px]"><i class="fa-solid fa-clock-rotate-left mr-1"></i>全程追踪</div>
<div class="term-list">
<span class="term" data-tip="过程回放按时间顺序复原业务过程和人工操作。">过程回放</span>
<span class="term" data-tip="诊断用于定位失败原因、依赖问题、服务退化或策略阻塞。">诊断</span>
<span class="term" data-tip="追责用于确认谁在什么时候通过什么操作改变了什么状态。">追责</span>
</div>
</div>
<div class="bg-white border theme-border-blue rounded p-2 shadow-sm min-h-[65px]" data-tip="运营指标用于验收、运营和持续改进,衡量服务时效、成功率、响应耗时和人工介入率。">
<div class="font-bold theme-text-dark-blue text-[13px]"><i class="fa-solid fa-chart-simple mr-1"></i>运营指标</div>
<div class="term-list">
<span class="term" data-tip="服务时效衡量任务、服务或工作流的服务水平和完成时限。">服务时效</span>
<span class="term" data-tip="成功率衡量任务、能力、服务或计划执行成功的比例。">成功率</span>
<span class="term" data-tip="响应耗时衡量目标接入、任务排队、执行、看板生成和审批等待时间。">响应耗时</span>
<span class="term" data-tip="人工介入率衡量有多少任务需要审批、暂停、接管或人工修复。">人工介入率</span>
</div>
</div>
</div>
</div>
</div> <!-- End of Main Architecture Container -->
<!-- 核心闭环 -->
<div class="mt-4 border border-blue-200 rounded flex overflow-hidden shadow-sm">
<div class="w-16 md:w-24 theme-dark-blue text-white flex flex-col items-center justify-center p-2 flex-shrink-0" data-tip="核心闭环展示一次业务目标从接入、计划、执行、沉淀、看板到人工决策的完整路径。">
<i class="fa-solid fa-rotate text-xl mb-1"></i>
<span class="font-bold text-center text-xs md:text-sm">核心闭环</span>
</div>
<div class="flex-1 bg-white p-4 relative overflow-x-auto">
<div class="flex items-center justify-between min-w-[1260px] mb-4 mt-2 px-4">
<div class="flex items-center gap-1 font-bold theme-text-dark-blue" data-tip="业务目标是用户或外部系统想达成的结果,进入系统后会先转成可治理的执行计划。"><i class="fa-solid fa-bullseye mr-1"></i>业务目标</div>
<i class="fa-solid fa-arrow-right text-blue-300 text-sm"></i>
<div class="flex items-center gap-1 border border-blue-200 px-2 py-1 rounded theme-text-dark-blue" data-tip="执行计划描述为什么做、做成什么、如何拆,并支持人工审查、冻结和重新规划。"><i class="fa-regular fa-file-lines mr-1"></i>执行计划</div>
<i class="fa-solid fa-arrow-right text-blue-300 text-sm"></i>
<div class="flex items-center gap-1 border border-blue-200 px-2 py-1 rounded theme-text-dark-blue" data-tip="工作任务是由计划步骤拆出来的可调度工作单元,负责绑定能力、输入、优先级和依赖。"><i class="fa-solid fa-list-ul mr-1"></i>工作任务</div>
<i class="fa-solid fa-arrow-right text-blue-300 text-sm"></i>
<div class="flex items-center gap-1 border border-blue-200 px-2 py-1 rounded theme-text-dark-blue" data-tip="能力匹配为当前任务选择正式自动化能力或业务流程,必须满足版本、策略、权限和输入输出约定。"><i class="fa-solid fa-link mr-1"></i>能力匹配</div>
<i class="fa-solid fa-arrow-right text-blue-300 text-sm"></i>
<div class="flex items-center gap-1 border border-blue-200 px-2 py-1 rounded theme-text-dark-blue" data-tip="执行前准备需要召回的经验记忆、需要启动或占用的后台服务,例如邮件读取、浏览器环境或外部服务进程。"><i class="fa-solid fa-server mr-1"></i>经验与服务准备</div>
<i class="fa-solid fa-arrow-right text-blue-300 text-sm"></i>
<div class="flex items-center gap-1 border border-blue-200 px-2 py-1 rounded theme-text-dark-blue" data-tip="执行记录代表任务的一次执行尝试,记录执行人、服务占用、运行状态、恢复点和最终状态。"><i class="fa-regular fa-circle-play mr-1"></i>执行记录</div>
<i class="fa-solid fa-arrow-right text-blue-300 text-sm"></i>
<div class="flex items-center gap-1 font-bold theme-text-dark-blue" data-tip="自动执行阶段由脚本、流程、智能辅助或外部系统适配真正调用资源,并持续回写过程记录和状态。"><i class="fa-solid fa-gear mr-1"></i>自动执行</div>
<i class="fa-solid fa-arrow-right text-blue-300 text-sm"></i>
<div class="flex items-center gap-1 border border-blue-200 px-2 py-1 rounded theme-text-dark-blue bg-gray-50" data-tip="执行结果沉淀为业务状态、过程日志、成果资料和可复用经验,后续看板和审计都从这里生成。"><i class="fa-solid fa-database mr-1"></i>过程记录与成果沉淀</div>
<i class="fa-solid fa-arrow-right text-blue-300 text-sm"></i>
<div class="flex items-center gap-1 font-bold theme-text-dark-blue" data-tip="运营看板面向管理和查询,包括任务看板、全程追踪、审批队列、记忆索引和服务健康。"><i class="fa-solid fa-chart-column mr-1"></i>运营看板</div>
<i class="fa-solid fa-arrow-right text-blue-300 text-sm"></i>
<div class="flex items-center gap-1 font-bold theme-red border border-red-200 px-2 py-1 rounded" data-tip="人工决策允许审批、暂停、恢复、取消、接管和重新规划,并把每次干预记录下来。"><i class="fa-regular fa-user mr-1"></i>人工决策</div>
</div>
<!-- 底部虚线回环 -->
<div class="relative mt-6 min-w-[1260px] h-4">
<div class="absolute w-[80%] left-[10%] border-t border-dashed border-red-500 top-1/2"></div>
<div class="absolute left-[10%] top-1/2 -mt-1"><div class="arrow-left-red"></div></div>
<div class="absolute w-full flex justify-center -top-3">
<span class="bg-white px-2 text-xs font-bold theme-red" data-tip="人工决策会回到计划或任务层,改变后续执行路径,而不是直接绕过业务流程。">审批 / 暂停 / 恢复 / 取消 / 人工接管 / 重新规划</span>
</div>
<!-- 垂直连接线 -->
<div class="absolute left-[10%] bottom-1/2 h-4 border-l border-dashed border-red-500"></div>
<div class="absolute right-[10%] bottom-1/2 h-6 border-r border-dashed border-red-500"></div>
</div>
</div>
</div>
</div>
<script>
const tooltip = document.createElement("div");
tooltip.id = "sg-tooltip";
document.body.appendChild(tooltip);
let activeTipTarget = null;
const exampleRules = [
{ keys: ["汇报专用静态网站"], example: "双击 report-site/index.html 打开后,可在业务架构总览和业务流程图之间切换。" },
{ keys: ["业务架构总览"], example: "汇报开场先用这一页讲清楚 sgRobot 的四个业务分区。" },
{ keys: ["业务流程图"], example: "讲邮件触发流程时,可以切到流程页看它经过哪些架构模块。" },
{ keys: ["外部参与者"], example: "业务人员提交目标,运维看服务状态,审批人处理高风险动作。" },
{ keys: ["用户是目标"], example: "用户提出“把今天收到的客户邮件整理成日报”。" },
{ keys: ["运维关注"], example: "运维发现邮件读取服务连续失败后,查看健康状态并重启服务。" },
{ keys: ["审批人负责"], example: "系统要自动发送外部邮件前,审批人确认内容和收件人。" },
{ keys: ["外部业务系统"], example: "工单系统产生新工单后,通过系统接口触发 sgRobot 处理。" },
{ keys: ["业务入口与协同"], example: "用户提交巡检需求,外部系统推送消息,审批人在工作台确认风险动作。" },
{ keys: ["业务需求入口"], example: "每天 9 点的巡检计划、人工提交的日报需求、邮件到达事件都先进入这里。" },
{ keys: ["人工提交"], example: "负责人在工作台提交“生成本周设备巡检报告”。" },
{ keys: ["系统接口"], example: "CRM 系统把新客户资料同步过来,请求自动补全客户画像。" },
{ keys: ["计划任务"], example: "每个工作日 18 点自动汇总当天处理结果。" },
{ keys: ["事件触发"], example: "指定邮箱收到带附件的邮件后,自动触发读取和归档流程。" },
{ keys: ["协同工作台"], example: "审批人打开工作台查看待审批队列,运维查看失败任务。" },
{ keys: ["查看"], example: "查看某个任务当前是排队中、执行中还是等待审批。" },
{ keys: ["操作"], example: "用户手动暂停一个执行中的网页采集任务。" },
{ keys: ["审批"], example: "自动发送客户邮件前,需要审批人确认内容。" },
{ keys: ["回放"], example: "复盘昨天失败的巡检任务,查看它在哪一步超时。" },
{ keys: ["成果浏览"], example: "打开自动生成的报告、截图和数据表进行验收。" },
{ keys: ["任务治理与风控"], example: "把“处理客户邮件”拆成读取、分类、回复、归档,并在发信前要求审批。" },
{ keys: ["目标计划管理"], example: "把“生成月度巡检报告”拆成采集数据、生成报告、提交验收。" },
{ keys: ["业务目标"], example: "目标可以是“完成今日邮件分类并输出异常清单”。" },
{ keys: ["执行计划"], example: "计划中写明先读取邮件,再分类,再生成日报,最后等待验收。" },
{ keys: ["计划步骤"], example: "“登录系统下载报表”就是计划里的一个步骤。" },
{ keys: ["任务执行管理"], example: "同一计划下的多个采集任务可以排队、重试、暂停或取消。" },
{ keys: ["工作任务"], example: "“读取 inbox 中未处理邮件”是一项可调度工作任务。" },
{ keys: ["执行记录"], example: "第一次读取邮件失败后,第二次重试会形成新的执行记录。" },
{ keys: ["自动重试"], example: "网页临时超时后,系统在 3 分钟后自动再试一次。" },
{ keys: ["恢复点"], example: "长流程已经完成数据下载,失败后从报告生成环节继续。" },
{ keys: ["超时管控"], example: "浏览器操作超过 10 分钟未完成,就转入失败恢复或人工介入。" },
{ keys: ["人工治理"], example: "任务可以被审批、暂停、恢复、取消、接管或重新规划。" },
{ keys: ["审批通过"], example: "审批人确认发信内容无误后,允许流程继续发送。" },
{ keys: ["暂停"], example: "发现输入资料不完整时,先暂停任务等待补充。" },
{ keys: ["恢复"], example: "资料补齐后,从暂停点继续执行,不必从头开始。" },
{ keys: ["取消"], example: "业务目标已经撤销,取消计划并释放后台服务。" },
{ keys: ["人工接管"], example: "网页验证码无法自动处理时,转给人工完成后再继续。" },
{ keys: ["重新规划"], example: "原接口不可用,改走网页下载报表的替代路径。" },
{ keys: ["风险与规则"], example: "涉及外部发信、删除文件、控制设备的动作必须进入审批。" },
{ keys: ["记忆规则"], example: "用户偏好可以长期保留,但临时验证码不允许写入记忆。" },
{ keys: ["服务占用"], example: "某个任务正在使用邮件读取服务时,系统不会把该服务提前回收。" },
{ keys: ["风险等级"], example: "读取邮件是低风险,批量发信是高风险,需要审批。" },
{ keys: ["健康状态"], example: "邮件服务正常、浏览器环境异常、任务队列积压都能被展示出来。" },
{ keys: ["自动执行与能力"], example: "系统调用脚本、流程、智能辅助和外部系统适配来完成真实业务动作。" },
{ keys: ["能力目录"], example: "“读取邮件”“生成报告”“网页下载报表”都登记成可复用能力。" },
{ keys: ["能力登记"], example: "新增一个“登录后台下载销售报表”的自动化能力。" },
{ keys: ["版本管理"], example: "报表下载流程改版后,保留 v1 和 v2避免影响旧任务。" },
{ keys: ["使用约定"], example: "规定输入必须包含日期范围,输出必须包含报告文件和数据结果。" },
{ keys: ["调度中枢"], example: "先检查账号和服务是否可用,再安排读取邮件、分类和生成报告。" },
{ keys: ["前置检查"], example: "执行前确认邮箱权限、浏览器环境、审批状态都满足条件。" },
{ keys: ["能力匹配"], example: "同样是取报表,优先走接口,接口失败再走网页自动化。" },
{ keys: ["失败恢复"], example: "下载失败后先重试,仍失败则换备用路径或转人工处理。" },
{ keys: ["自动脚本"], example: "每天固定导出报表、清洗数据、生成 CSV 文件。" },
{ keys: ["确定性任务"], example: "把固定格式表格转换成统一字段就是确定性任务。" },
{ keys: ["流程编排"], example: "把邮件读取、附件解析、报告生成、审批发送组合成一条流程。" },
{ keys: ["组合能力"], example: "“客户邮件处理”由读取、分类、回复草稿、归档四个能力组合。" },
{ keys: ["子流程"], example: "报告生成任务内部再拆成取数、制图、写摘要三个子流程。" },
{ keys: ["智能辅助"], example: "根据失败日志判断可能原因,并给出下一步处理建议。" },
{ keys: ["规划用于"], example: "把“做一份巡检报告”拆成采集、分析、生成、验收。" },
{ keys: ["修复建议"], example: "系统提示“登录页结构变化,建议改用备用选择器或人工接管”。" },
{ keys: ["解释用于"], example: "向审批人说明为什么这一步被判定为高风险。" },
{ keys: ["外部系统适配"], example: "同一业务流程可以同时操作网页后台、设备接口和机器人。" },
{ keys: ["网页操作"], example: "自动登录后台,下载当天订单报表。" },
{ keys: ["设备接入"], example: "读取设备状态或通过设备接口下发巡检命令。" },
{ keys: ["机器人控制"], example: "让机器人执行移动、拍照、语音播报等动作。" },
{ keys: ["按需后台服务"], example: "只有处理邮件任务时才启动邮件读取服务,空闲后自动回收。" },
{ keys: ["服务定义"], example: "定义邮件读取服务如何启动、检查健康、停止和授权。" },
{ keys: ["运行实例"], example: "当前启动了一个邮件读取实例,状态为运行中。" },
{ keys: ["占用关系"], example: "任务 A 正在使用浏览器环境,因此该环境不能被其他任务关闭。" },
{ keys: ["运营记录与看板"], example: "领导可以看到任务进度、风险节点、成果报告和人工介入情况。" },
{ keys: ["业务状态台账"], example: "某计划已激活,三个任务成功,一个任务等待审批。" },
{ keys: ["执行计划状态"], example: "计划当前处于“等待审批”或“执行中”。" },
{ keys: ["工作任务状态"], example: "任务当前是排队中、执行中、暂停、失败或完成。" },
{ keys: ["执行记录状态"], example: "第 2 次执行成功,第 1 次执行因网页超时失败。" },
{ keys: ["审批记录状态"], example: "发信审批已通过,删除文件审批被拒绝。" },
{ keys: ["过程日志"], example: "记录任务开始、服务启动、审批通过、执行失败等关键变化。" },
{ keys: ["关键过程记录"], example: "“09:03 邮件读取服务启动”“09:05 附件解析失败”。" },
{ keys: ["成果资料库"], example: "保存巡检报告、页面截图、附件解析结果和导出的表格。" },
{ keys: ["文件是"], example: "保存下载的 Excel、PDF 报告或邮件附件。" },
{ keys: ["报告是"], example: "自动生成日报、巡检报告或异常汇总。" },
{ keys: ["截图用于"], example: "保留网页提交成功页面,作为验收证据。" },
{ keys: ["数据结果"], example: "把邮件分类结果保存成结构化表格,供后续统计。" },
{ keys: ["经验与记忆库"], example: "记录某项目常用账号、报告格式偏好、失败处理经验。" },
{ keys: ["项目事实"], example: "某系统的报表入口固定在“运营中心 / 日报”菜单下。" },
{ keys: ["使用偏好"], example: "用户偏好报告用中文摘要,并保留原始数据表。" },
{ keys: ["关键决策"], example: "决定所有外部发信必须先进入人工审批。" },
{ keys: ["处理经验"], example: "某网页下载失败时,先刷新登录态再重试。" },
{ keys: ["观察摘要"], example: "最近三次失败都集中在同一个外部系统登录环节。" },
{ keys: ["运营看板"], example: "展示今日任务成功率、待审批数量、失败任务和服务健康。" },
{ keys: ["任务看板"], example: "看到哪些任务已完成、哪些阻塞、哪些需要人工介入。" },
{ keys: ["审批队列"], example: "所有等待确认的发信、删除、设备控制动作集中展示。" },
{ keys: ["记忆索引"], example: "查看并修正“用户偏好报告格式”的长期记忆。" },
{ keys: ["服务健康"], example: "邮件读取服务正常,浏览器环境重启 1 次,设备连接异常。" },
{ keys: ["全程追踪"], example: "按时间线查看一次任务从提交到验收的完整过程。" },
{ keys: ["过程回放"], example: "复盘某次失败,看到它在附件解析步骤出错。" },
{ keys: ["诊断"], example: "定位失败原因是账号过期、网页变化还是服务不可用。" },
{ keys: ["追责"], example: "确认是谁在什么时间批准了批量发信动作。" },
{ keys: ["运营指标"], example: "每周统计成功率、平均耗时和人工介入率。" },
{ keys: ["服务时效"], example: "巡检报告要求 30 分钟内完成并提交。" },
{ keys: ["成功率"], example: "本周邮件处理任务 100 次,成功 96 次。" },
{ keys: ["响应耗时"], example: "从邮件到达到生成处理结果平均耗时 2 分钟。" },
{ keys: ["人工介入率"], example: "本月 12% 的任务因审批或异常需要人工介入。" },
{ keys: ["核心闭环"], example: "一封客户邮件进入系统后,生成任务、自动处理、沉淀成果,再进入看板和复盘。" },
{ keys: ["经验与服务准备"], example: "执行前先读取用户偏好,并启动邮件读取或浏览器服务。" },
{ keys: ["过程记录与成果沉淀"], example: "任务完成后保存报告、截图、日志和可复用处理经验。" },
{ keys: ["人工决策"], example: "审批人确认批量发信、暂停异常任务或要求重新规划。" },
{ keys: ["审批 / 暂停"], example: "审批不通过时,流程回到计划层重新规划。" },
];
function normalizeText(value) {
return (value || "").replace(/\s+/g, " ").trim();
}
function tooltipExample(target) {
const label = normalizeText(target.textContent);
const base = normalizeText(target.dataset.tip);
const haystack = `${label} ${base}`;
const rule = exampleRules.find((item) => item.keys.every((key) => haystack.includes(key)));
if (rule) return rule.example;
return "领导查看该节点时,可以把它对应到一次人工交办、邮件触发、定时巡检或高风险审批中的具体动作。";
}
function tooltipText(target) {
const text = normalizeText(target.dataset.tip);
if (!text) return "";
if (text.includes("例:")) return text;
return `${text}\n例:${tooltipExample(target)}`;
}
function positionTooltip(event) {
const margin = 14;
const offset = 16;
const rect = tooltip.getBoundingClientRect();
let left = event.clientX + offset;
let top = event.clientY + offset;
if (left + rect.width + margin > window.innerWidth) {
left = Math.max(margin, event.clientX - rect.width - offset);
}
if (top + rect.height + margin > window.innerHeight) {
top = Math.max(margin, event.clientY - rect.height - offset);
}
tooltip.style.left = `${left}px`;
tooltip.style.top = `${top}px`;
}
function showTooltip(target, event) {
const text = tooltipText(target);
if (!text) return;
activeTipTarget = target;
tooltip.textContent = text;
tooltip.classList.add("is-visible");
positionTooltip(event);
}
function hideTooltip() {
activeTipTarget = null;
tooltip.classList.remove("is-visible");
}
document.addEventListener("pointerover", (event) => {
const target = event.target.closest("[data-tip]");
if (target) showTooltip(target, event);
});
document.addEventListener("pointermove", (event) => {
if (activeTipTarget) positionTooltip(event);
});
document.addEventListener("pointerout", (event) => {
if (!activeTipTarget) return;
const next = event.relatedTarget;
if (!next || !activeTipTarget.contains(next)) hideTooltip();
});
document.querySelectorAll("[data-tip]").forEach((element) => {
element.setAttribute("tabindex", "0");
element.setAttribute("aria-label", tooltipText(element));
element.addEventListener("focus", (event) => {
const rect = event.currentTarget.getBoundingClientRect();
showTooltip(event.currentTarget, {
clientX: rect.left + rect.width / 2,
clientY: rect.top + rect.height / 2,
});
});
element.addEventListener("blur", hideTooltip);
});
</script>
</body>
</html>

View File

@@ -1,626 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>sgRobot 五月底上线任务分工表</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
.theme-dark-blue { background-color: #0d2758; }
.theme-text-dark-blue { color: #0d2758; }
.theme-light-blue-bg { background-color: #f0f6fc; }
.theme-border-blue { border-color: #aec3db; }
.theme-red { color: #d92d20; }
body {
font-family: "PingFang SC", "Microsoft YaHei", "Noto Sans SC", "Source Han Sans SC", Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
font-size: 18px;
}
.text-sm { font-size: 1.08rem !important; }
.text-base { font-size: 1.24rem !important; }
.text-lg { font-size: 1.4rem !important; }
.text-xl { font-size: 1.62rem !important; }
.text-3xl { font-size: 2.75rem !important; }
.text-\[13px\] { font-size: 1.14rem !important; }
[data-tip] { cursor: help; }
.panel {
border: 1px solid #aec3db;
border-radius: 8px;
background: white;
box-shadow: 0 8px 20px rgba(13, 39, 88, 0.08);
overflow: hidden;
}
.panel-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 14px 16px 10px 16px;
border-bottom: 1px solid #d8e4f2;
}
.panel-title {
display: flex;
align-items: center;
gap: 10px;
color: #0d2758;
font-weight: 900;
font-size: 1.3rem;
line-height: 1.25;
}
.panel-tag {
border: 1px solid #c8d7e8;
border-radius: 999px;
padding: 3px 10px;
color: #5f7082;
background: #f8fbff;
font-weight: 800;
white-space: nowrap;
font-size: 0.92rem;
}
.summary-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 12px;
padding: 14px;
background: #f0f6fc;
}
.summary-card {
border: 1px solid #aec3db;
border-radius: 8px;
background: white;
padding: 12px;
color: #0d2758;
}
.summary-card strong {
display: block;
font-size: 1.5rem;
line-height: 1;
margin-bottom: 6px;
}
.summary-card span {
display: block;
color: #5f7082;
font-weight: 800;
line-height: 1.32;
}
.task-table-wrap {
overflow-x: auto;
background: #f0f6fc;
}
.task-table {
width: 100%;
min-width: 1320px;
border-collapse: collapse;
color: #0d2758;
}
.task-table th {
background: #0d2758;
color: white;
text-align: left;
padding: 11px 12px;
font-weight: 900;
border-right: 1px solid #375078;
white-space: nowrap;
}
.task-table td {
background: white;
padding: 11px 12px;
border-right: 1px solid #d8e4f2;
border-bottom: 1px solid #d8e4f2;
vertical-align: top;
font-weight: 760;
line-height: 1.38;
}
.task-table tr:nth-child(even) td {
background: #f8fbff;
}
.task-name {
font-weight: 900;
color: #0d2758;
}
.date-pill {
display: inline-flex;
align-items: center;
border-radius: 999px;
background: #0d2758;
color: white;
padding: 4px 10px;
font-weight: 900;
white-space: nowrap;
}
.owner-pill {
display: inline-flex;
align-items: center;
border-radius: 999px;
border: 1px solid #c8d7e8;
background: white;
color: #0d2758;
padding: 4px 10px;
font-weight: 900;
white-space: nowrap;
}
.boundary-ok {
color: #0d2758;
}
.boundary-no {
color: #9f1239;
}
.interface-map-wrap {
overflow-x: auto;
background: #f0f6fc;
padding: 14px;
}
.interface-map {
min-width: 1320px;
width: 100%;
height: auto;
display: block;
border: 1px solid #d8e4f2;
border-radius: 8px;
background: white;
}
.map-note {
padding: 10px 14px 14px 14px;
background: #f0f6fc;
color: #5f7082;
font-weight: 800;
line-height: 1.4;
}
.timeline {
display: grid;
grid-template-columns: repeat(6, minmax(160px, 1fr));
gap: 10px;
padding: 14px;
background: #f0f6fc;
overflow-x: auto;
}
.time-card {
min-width: 170px;
border: 1px solid #aec3db;
border-radius: 8px;
background: white;
padding: 12px;
color: #0d2758;
}
.time-date {
display: inline-flex;
background: #0d2758;
color: white;
border-radius: 999px;
padding: 4px 10px;
font-weight: 900;
margin-bottom: 8px;
}
.time-title {
font-weight: 900;
line-height: 1.25;
margin-bottom: 5px;
}
.time-text {
color: #5f7082;
font-weight: 760;
line-height: 1.35;
}
#sg-tooltip {
position: fixed;
z-index: 9999;
max-width: 460px;
padding: 10px 12px;
border-radius: 6px;
background: #0d2758;
color: white;
font-size: 0.95rem;
line-height: 1.45;
box-shadow: 0 12px 32px rgba(13, 39, 88, 0.22);
pointer-events: none;
opacity: 0;
transform: translateY(4px);
transition: opacity 0.12s ease, transform 0.12s ease;
}
#sg-tooltip.is-visible {
opacity: 1;
transform: translateY(0);
}
@media (max-width: 1100px) {
.summary-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.timeline { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 720px) {
.summary-grid { grid-template-columns: 1fr; }
.timeline { grid-template-columns: 1fr; }
.panel-head { align-items: flex-start; flex-direction: column; }
}
</style>
</head>
<body class="bg-white p-4 md:p-8 min-h-screen text-sm select-none">
<div class="max-w-[1780px] mx-auto mb-4 border border-amber-300 bg-amber-50 text-amber-900 rounded-lg px-4 py-3 shadow-sm">
<div class="font-black text-base">历史汇报版</div>
<div class="text-sm leading-relaxed mt-1">
本页面保留为早期 10 任务包汇报视图,不再作为团队分工和接口冻结源头。当前分工、接口 owner、状态、Mock 和验收证据以
<code>plan/2026-04-26-architecture-team-handoff/</code>
及其中的 <code>interfaces/interface-freeze-register.md</code> 为准。
</div>
</div>
<nav class="max-w-[1780px] mx-auto mb-5 flex flex-wrap items-center justify-between gap-3 border border-blue-200 rounded-lg px-4 py-3 shadow-sm bg-white">
<div class="flex items-center gap-2 theme-text-dark-blue font-bold text-base" data-tip="汇报专用静态网站,可直接双击 index.html 打开,不需要启动本地服务。">
<i class="fa-solid fa-display"></i>
<span>sgRobot 汇报静态网站</span>
</div>
<div class="flex items-center gap-2">
<a href="index.html" class="border theme-border-blue theme-text-dark-blue rounded px-4 py-2 font-bold bg-white hover:bg-blue-50" data-tip="返回业务架构总览。">业务架构总览</a>
<a href="flows.html" class="border theme-border-blue theme-text-dark-blue rounded px-4 py-2 font-bold bg-white hover:bg-blue-50" data-tip="查看业务流程如何落到架构模块。">业务流程图</a>
<a href="roadmap.html" class="theme-dark-blue text-white rounded px-4 py-2 font-bold shadow-sm" data-tip="当前页面只看任务、负责人、日期和边界。">里程碑与分工</a>
</div>
</nav>
<header class="max-w-[1780px] mx-auto mb-6">
<div class="flex items-center mb-2" data-tip="这一页只保留领导最需要看的信息:哪个任务、谁负责、什么时候完成、边界是什么。">
<div class="w-10 h-10 theme-dark-blue text-white flex items-center justify-center rounded shadow-sm mr-3">
<i class="fa-solid fa-list-check text-xl"></i>
</div>
<div>
<h1 class="text-3xl font-bold theme-text-dark-blue tracking-wide">sgRobot <span class="font-bold">五月底上线任务分工表</span></h1>
</div>
</div>
<div class="ml-14 text-[13px] font-medium text-gray-700 leading-relaxed" data-tip="负责人先按角色写,后续可以替换成具体姓名。">
原则:<span class="font-bold">一个任务,一个负责人,一个完成日期,一个边界</span>。各模块只按公共接口交付不互相改代码5 月 31 日统一验收。
</div>
</header>
<main class="max-w-[1780px] mx-auto flex flex-col gap-5">
<section class="panel" data-tip="这四个数字是领导快速判断范围用的。">
<div class="panel-head">
<div class="panel-title"><i class="fa-solid fa-circle-info"></i><span>一页结论</span></div>
<span class="panel-tag" data-tip="如需具体人名,直接把负责人角色替换为姓名即可。">负责人先按角色占位</span>
</div>
<div class="summary-grid">
<div class="summary-card" data-tip="共 10 个任务包;公共接口只做口径冻结,核心控制另行实现,避免一个负责人背所有责任。"><strong>10 个</strong><span>任务包</span></div>
<div class="summary-card" data-tip="只设一个最终日期2026 年 5 月 31 日完成上线验收。"><strong>5/31</strong><span>上线验收收口</span></div>
<div class="summary-card" data-tip="模块之间只通过公共接口、测试样例和交付物对接。"><strong>互不改代码</strong><span>只按接口交付</span></div>
<div class="summary-card" data-tip="测试和发布有否决权,没证据就不能算完成。"><strong>有证据</strong><span>才算完成</span></div>
</div>
</section>
<section class="panel" data-tip="这是核心表。领导看这一张表就能知道谁负责什么、哪天完成、边界在哪里。">
<div class="panel-head">
<div class="panel-title"><i class="fa-solid fa-table"></i><span>任务分工总表</span></div>
<span class="panel-tag" data-tip="完成日期是建议倒排,可根据真实人员情况微调。">按完成日期倒排</span>
</div>
<div class="task-table-wrap">
<table class="task-table">
<thead>
<tr>
<th>任务</th>
<th>负责人</th>
<th>完成日期</th>
<th>交付物</th>
<th>边界:只负责</th>
<th>边界:不负责</th>
</tr>
</thead>
<tbody>
<tr data-tip="公共接口不要求 4 月 30 日全部实现只要求各负责人把字段、调用方向、Mock 数据和验收口径确认下来。">
<td><span class="task-name">公共接口口径冻结</span></td>
<td><span class="owner-pill">总负责人 + 各模块负责人</span></td>
<td><span class="date-pill">4/30</span></td>
<td>接口目录、字段草案、Mock 数据、对接方向、验收口径</td>
<td class="boundary-ok">只冻结模块之间怎么对接,以及每个模块交付什么数据</td>
<td class="boundary-no">不要求一次性完成所有后端实现;不替各模块写内部代码</td>
</tr>
<tr data-tip="核心后端只负责把主链路跑起来,不再同时承担所有适配模块的接口设计责任。">
<td><span class="task-name">核心控制主链路</span></td>
<td><span class="owner-pill">核心后端负责人</span></td>
<td><span class="date-pill">5/08</span></td>
<td>计划、任务、执行记录、审批、服务占用、能力目录最小实现</td>
<td class="boundary-ok">核心状态推进、公共接口实现、主链路冒烟</td>
<td class="boundary-no">不写 UI不写浏览器、硬件、本地服务实现</td>
</tr>
<tr data-tip="解决新增 Skill 时自动写控制脚本的问题。">
<td><span class="task-name">Skill 编写助手</span></td>
<td><span class="owner-pill">Skill 工具负责人</span></td>
<td><span class="date-pill">5/08</span></td>
<td>控制脚本生成器、模板、校验器、3 个示例 Skill</td>
<td class="boundary-ok">自动生成新 Skill 的控制脚本和校验规则</td>
<td class="boundary-no">不做浏览器、硬件、本地服务底层适配</td>
</tr>
<tr data-tip="把已有自动化脚本批量纳入 Skill 体系。">
<td><span class="task-name">自动化脚本批量转 Skill</span></td>
<td><span class="owner-pill">脚本迁移负责人</span></td>
<td><span class="date-pill">5/12</span></td>
<td>转换工具、迁移清单、已转换 Skill 包、回归报告</td>
<td class="boundary-ok">旧脚本包装、补输入输出、补日志、登记能力目录</td>
<td class="boundary-no">不重写业务逻辑;不改适配器底层能力</td>
</tr>
<tr data-tip="浏览器适配独立成包,不和 UI 混在一起。">
<td><span class="task-name">浏览器适配</span></td>
<td><span class="owner-pill">浏览器适配负责人</span></td>
<td><span class="date-pill">5/12</span></td>
<td>浏览器适配器、示例 Skill、截图证据、失败说明</td>
<td class="boundary-ok">打开页面、点击、输入、下载、截图、会话和错误回写</td>
<td class="boundary-no">不决定业务审批;不做 UI 页面;不写硬件接口</td>
</tr>
<tr data-tip="子程序和本地服务单独管理,避免主程序和后台服务绑死。">
<td><span class="task-name">本地服务与子程序管理</span></td>
<td><span class="owner-pill">本地服务负责人</span></td>
<td><span class="date-pill">5/14</span></td>
<td>服务管理器、服务清单、健康数据、运维命令</td>
<td class="boundary-ok">邮件读取、监听器、后台进程启动停止、健康、日志、回收</td>
<td class="boundary-no">不定义业务流程;不直接修改任务结果</td>
</tr>
<tr data-tip="硬件适配要有安全边界,不能绕过人工治理。">
<td><span class="task-name">硬件与机器人适配</span></td>
<td><span class="owner-pill">硬件适配负责人</span></td>
<td><span class="date-pill">5/14</span></td>
<td>硬件适配器、模拟测试、状态回写、安全风险清单</td>
<td class="boundary-ok">设备连接、状态读取、基础动作、机器人控制、异常停止</td>
<td class="boundary-no">不绕过审批执行高风险动作;不负责浏览器自动化</td>
</tr>
<tr data-tip="UI 只做操作台,不能自己发明另一套后端状态。">
<td><span class="task-name">UI 操作台</span></td>
<td><span class="owner-pill">前端负责人</span></td>
<td><span class="date-pill">5/18</span></td>
<td>任务看板、审批队列、服务健康、成果浏览、过程回放</td>
<td class="boundary-ok">用户操作入口和可视化验收页面</td>
<td class="boundary-no">不绕过后端改状态;不自建任务模型</td>
</tr>
<tr data-tip="编译打包独立负责 Windows、信创、ARM不和业务开发绑在一起。">
<td><span class="task-name">浏览器编译与平台打包</span></td>
<td><span class="owner-pill">构建发布负责人</span></td>
<td><span class="date-pill">5/22</span></td>
<td>Windows / 信创 / ARM 构建产物、依赖清单、安装说明</td>
<td class="boundary-ok">平台构建、依赖整理、打包、失败归因</td>
<td class="boundary-no">不改业务逻辑;不写 Skill不做 UI 功能</td>
</tr>
<tr data-tip="测试发布负责最终收口和上线判断。">
<td><span class="task-name">测试验收与发布收口</span></td>
<td><span class="owner-pill">测试发布负责人</span></td>
<td><span class="date-pill">5/31</span></td>
<td>测试报告、上线风险、已知问题、发布包、回滚方案</td>
<td class="boundary-ok">模块测试、端到端测试、兼容验证、发布门禁</td>
<td class="boundary-no">不替功能模块补实现,但可判定是否阻断上线</td>
</tr>
</tbody>
</table>
</div>
</section>
<section class="panel" data-tip="这张图只表达接口关系:每个负责人都通过中间的公共接口对接,模块之间不互相改代码。">
<div class="panel-head">
<div class="panel-title"><i class="fa-solid fa-diagram-project"></i><span>分工接口关系图</span></div>
<span class="panel-tag" data-tip="线上的文字就是这个负责人和核心接口之间的交付接口。">看线上的接口名</span>
</div>
<div class="interface-map-wrap">
<svg class="interface-map" viewBox="0 0 1500 720" role="img" aria-label="sgRobot 五月底上线分工接口关系图">
<defs>
<marker id="arrow" markerWidth="10" markerHeight="10" refX="8" refY="3" orient="auto" markerUnits="strokeWidth">
<path d="M0,0 L0,6 L9,3 z" fill="#6f8faf"></path>
</marker>
<filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="0" dy="4" stdDeviation="4" flood-color="#0d2758" flood-opacity="0.12"></feDropShadow>
</filter>
</defs>
<rect x="40" y="30" width="1420" height="660" rx="10" fill="#f8fbff" stroke="#d8e4f2"></rect>
<rect x="555" y="64" width="390" height="92" rx="10" fill="#0d2758" filter="url(#shadow)"></rect>
<text x="750" y="102" text-anchor="middle" fill="#ffffff" font-size="25" font-weight="900">核心控制主链路</text>
<text x="750" y="132" text-anchor="middle" fill="#dbeafe" font-size="18" font-weight="800">核心后端负责人5/08 实现</text>
<rect x="565" y="285" width="370" height="118" rx="10" fill="#ffffff" stroke="#aec3db" stroke-width="2" filter="url(#shadow)"></rect>
<text x="750" y="325" text-anchor="middle" fill="#0d2758" font-size="24" font-weight="900">公共接口合同</text>
<text x="750" y="356" text-anchor="middle" fill="#5f7082" font-size="17" font-weight="800">总负责人 + 各模块负责人共同确认4/30</text>
<text x="750" y="383" text-anchor="middle" fill="#d92d20" font-size="17" font-weight="900">核心后端只实现主链路,不替各模块兜底</text>
<line x1="750" y1="156" x2="750" y2="285" stroke="#6f8faf" stroke-width="3" marker-end="url(#arrow)"></line>
<text x="766" y="226" fill="#5f7082" font-size="16" font-weight="800">核心接口实现</text>
<rect x="75" y="75" width="290" height="72" rx="8" fill="#ffffff" stroke="#aec3db" stroke-width="2" filter="url(#shadow)"></rect>
<text x="220" y="106" text-anchor="middle" fill="#0d2758" font-size="20" font-weight="900">Skill 编写助手</text>
<text x="220" y="131" text-anchor="middle" fill="#5f7082" font-size="16" font-weight="800">Skill 工具负责人</text>
<line x1="365" y1="111" x2="565" y2="302" stroke="#6f8faf" stroke-width="3" marker-end="url(#arrow)"></line>
<rect x="376" y="178" width="172" height="32" rx="16" fill="#ffffff" stroke="#c8d7e8"></rect>
<text x="462" y="200" text-anchor="middle" fill="#0d2758" font-size="15" font-weight="900">Skill 生成合同</text>
<rect x="75" y="185" width="290" height="72" rx="8" fill="#ffffff" stroke="#aec3db" stroke-width="2" filter="url(#shadow)"></rect>
<text x="220" y="216" text-anchor="middle" fill="#0d2758" font-size="20" font-weight="900">脚本批量转 Skill</text>
<text x="220" y="241" text-anchor="middle" fill="#5f7082" font-size="16" font-weight="800">脚本迁移负责人</text>
<line x1="365" y1="221" x2="565" y2="327" stroke="#6f8faf" stroke-width="3" marker-end="url(#arrow)"></line>
<rect x="386" y="253" width="160" height="32" rx="16" fill="#ffffff" stroke="#c8d7e8"></rect>
<text x="466" y="275" text-anchor="middle" fill="#0d2758" font-size="15" font-weight="900">Skill 包接口</text>
<rect x="75" y="295" width="290" height="72" rx="8" fill="#ffffff" stroke="#aec3db" stroke-width="2" filter="url(#shadow)"></rect>
<text x="220" y="326" text-anchor="middle" fill="#0d2758" font-size="20" font-weight="900">浏览器适配</text>
<text x="220" y="351" text-anchor="middle" fill="#5f7082" font-size="16" font-weight="800">浏览器适配负责人</text>
<line x1="365" y1="331" x2="565" y2="344" stroke="#6f8faf" stroke-width="3" marker-end="url(#arrow)"></line>
<rect x="384" y="333" width="158" height="32" rx="16" fill="#ffffff" stroke="#c8d7e8"></rect>
<text x="463" y="355" text-anchor="middle" fill="#0d2758" font-size="15" font-weight="900">浏览器接口</text>
<rect x="75" y="405" width="290" height="72" rx="8" fill="#ffffff" stroke="#aec3db" stroke-width="2" filter="url(#shadow)"></rect>
<text x="220" y="436" text-anchor="middle" fill="#0d2758" font-size="20" font-weight="900">本地服务管理</text>
<text x="220" y="461" text-anchor="middle" fill="#5f7082" font-size="16" font-weight="800">本地服务负责人</text>
<line x1="365" y1="441" x2="565" y2="366" stroke="#6f8faf" stroke-width="3" marker-end="url(#arrow)"></line>
<rect x="392" y="397" width="146" height="32" rx="16" fill="#ffffff" stroke="#c8d7e8"></rect>
<text x="465" y="419" text-anchor="middle" fill="#0d2758" font-size="15" font-weight="900">服务合同</text>
<rect x="75" y="515" width="290" height="72" rx="8" fill="#ffffff" stroke="#aec3db" stroke-width="2" filter="url(#shadow)"></rect>
<text x="220" y="546" text-anchor="middle" fill="#0d2758" font-size="20" font-weight="900">硬件与机器人适配</text>
<text x="220" y="571" text-anchor="middle" fill="#5f7082" font-size="16" font-weight="800">硬件适配负责人</text>
<line x1="365" y1="551" x2="565" y2="388" stroke="#6f8faf" stroke-width="3" marker-end="url(#arrow)"></line>
<rect x="382" y="474" width="168" height="32" rx="16" fill="#ffffff" stroke="#c8d7e8"></rect>
<text x="466" y="496" text-anchor="middle" fill="#0d2758" font-size="15" font-weight="900">硬件适配接口</text>
<rect x="1135" y="120" width="290" height="72" rx="8" fill="#ffffff" stroke="#aec3db" stroke-width="2" filter="url(#shadow)"></rect>
<text x="1280" y="151" text-anchor="middle" fill="#0d2758" font-size="20" font-weight="900">UI 操作台</text>
<text x="1280" y="176" text-anchor="middle" fill="#5f7082" font-size="16" font-weight="800">前端负责人</text>
<line x1="1135" y1="156" x2="935" y2="310" stroke="#6f8faf" stroke-width="3" marker-end="url(#arrow)"></line>
<rect x="960" y="220" width="152" height="32" rx="16" fill="#ffffff" stroke="#c8d7e8"></rect>
<text x="1036" y="242" text-anchor="middle" fill="#0d2758" font-size="15" font-weight="900">UI 数据接口</text>
<rect x="1135" y="285" width="290" height="72" rx="8" fill="#ffffff" stroke="#aec3db" stroke-width="2" filter="url(#shadow)"></rect>
<text x="1280" y="316" text-anchor="middle" fill="#0d2758" font-size="20" font-weight="900">浏览器编译与打包</text>
<text x="1280" y="341" text-anchor="middle" fill="#5f7082" font-size="16" font-weight="800">构建发布负责人</text>
<line x1="1135" y1="321" x2="935" y2="344" stroke="#6f8faf" stroke-width="3" marker-end="url(#arrow)"></line>
<rect x="954" y="311" width="170" height="32" rx="16" fill="#ffffff" stroke="#c8d7e8"></rect>
<text x="1039" y="333" text-anchor="middle" fill="#0d2758" font-size="15" font-weight="900">构建产物接口</text>
<rect x="1135" y="450" width="290" height="72" rx="8" fill="#ffffff" stroke="#aec3db" stroke-width="2" filter="url(#shadow)"></rect>
<text x="1280" y="481" text-anchor="middle" fill="#0d2758" font-size="20" font-weight="900">测试验收与发布</text>
<text x="1280" y="506" text-anchor="middle" fill="#5f7082" font-size="16" font-weight="800">测试发布负责人</text>
<line x1="1135" y1="486" x2="935" y2="378" stroke="#6f8faf" stroke-width="3" marker-end="url(#arrow)"></line>
<rect x="958" y="412" width="158" height="32" rx="16" fill="#ffffff" stroke="#c8d7e8"></rect>
<text x="1037" y="434" text-anchor="middle" fill="#0d2758" font-size="15" font-weight="900">验收门禁</text>
<rect x="520" y="560" width="460" height="76" rx="10" fill="#fff7f7" stroke="#fecaca" stroke-width="2"></rect>
<text x="750" y="592" text-anchor="middle" fill="#9f1239" font-size="21" font-weight="900">规则:负责人之间不直接改对方模块</text>
<text x="750" y="620" text-anchor="middle" fill="#9f1239" font-size="17" font-weight="800">所有协作都落到接口合同、交付包、测试报告</text>
</svg>
</div>
<div class="map-note" data-tip="这句话是给领导看的解释口径。">
读图方式:每个白框是一个负责人,线上的文字是他交给核心控制或从核心控制读取的接口。除测试发布外,其他负责人不直接验收别人内部实现,只按接口和交付物对接。
</div>
</section>
<section class="panel" data-tip="把里程碑压缩到几个关键日期,领导只需要知道每个日期要看到什么。">
<div class="panel-head">
<div class="panel-title"><i class="fa-solid fa-calendar-days"></i><span>简版里程碑</span></div>
<span class="panel-tag" data-tip="不再画复杂甘特图,只保留关键日期。">哪个任务哪天完成</span>
</div>
<div class="timeline">
<div class="time-card" data-tip="这里只冻结口径,不要求完成全部实现。目标是让各模块能并行开工。">
<span class="time-date">4/30</span>
<div class="time-title">公共接口口径冻结</div>
<div class="time-text">接口目录、字段草案、Mock 数据、对接方向确认。</div>
</div>
<div class="time-card" data-tip="核心后端在这个日期前交付主链路最小实现,不再背所有适配模块的实现。">
<span class="time-date">5/08</span>
<div class="time-title">核心主链路完成</div>
<div class="time-text">计划、任务、执行记录、审批、服务占用可冒烟。</div>
</div>
<div class="time-card" data-tip="这一阶段拿到 Skill 生产、脚本迁移和浏览器适配首批能力。">
<span class="time-date">5/12</span>
<div class="time-title">Skill 与浏览器首批完成</div>
<div class="time-text">Skill 编写助手、脚本转 Skill、浏览器适配可用。</div>
</div>
<div class="time-card" data-tip="本地服务和硬件适配要给出可调用版本。">
<span class="time-date">5/14</span>
<div class="time-title">服务与硬件完成</div>
<div class="time-text">本地服务管理、硬件与机器人适配完成首版验收。</div>
</div>
<div class="time-card" data-tip="UI 在 5 月 18 日前必须从粗略页面变成可操作台。">
<span class="time-date">5/18</span>
<div class="time-title">UI 操作台完成</div>
<div class="time-text">任务、审批、服务、成果、回放都能操作或查看。</div>
</div>
<div class="time-card" data-tip="五月底只做验收和发布,不再扩功能。">
<span class="time-date">5/31</span>
<div class="time-title">上线验收完成</div>
<div class="time-text">发布包、测试报告、回滚方案、演示材料齐备。</div>
</div>
</div>
</section>
</main>
<script>
const tooltip = document.createElement("div");
tooltip.id = "sg-tooltip";
document.body.appendChild(tooltip);
let activeTipTarget = null;
function positionTooltip(event) {
const margin = 14;
const offset = 16;
const rect = tooltip.getBoundingClientRect();
let left = event.clientX + offset;
let top = event.clientY + offset;
if (left + rect.width + margin > window.innerWidth) {
left = Math.max(margin, event.clientX - rect.width - offset);
}
if (top + rect.height + margin > window.innerHeight) {
top = Math.max(margin, event.clientY - rect.height - offset);
}
tooltip.style.left = `${left}px`;
tooltip.style.top = `${top}px`;
}
function showTooltip(target, event) {
const text = target.dataset.tip;
if (!text) return;
activeTipTarget = target;
tooltip.textContent = text;
tooltip.classList.add("is-visible");
positionTooltip(event);
}
function hideTooltip() {
activeTipTarget = null;
tooltip.classList.remove("is-visible");
}
document.addEventListener("pointerover", (event) => {
const target = event.target.closest("[data-tip]");
if (target) showTooltip(target, event);
});
document.addEventListener("pointermove", (event) => {
if (activeTipTarget) positionTooltip(event);
});
document.addEventListener("pointerout", (event) => {
if (!activeTipTarget) return;
const next = event.relatedTarget;
if (!next || !activeTipTarget.contains(next)) hideTooltip();
});
document.querySelectorAll("[data-tip]").forEach((element) => {
element.setAttribute("tabindex", "0");
element.setAttribute("aria-label", element.dataset.tip);
element.addEventListener("focus", (event) => {
const rect = event.currentTarget.getBoundingClientRect();
showTooltip(event.currentTarget, {
clientX: rect.left + rect.width / 2,
clientY: rect.top + rect.height / 2,
});
});
element.addEventListener("blur", hideTooltip);
});
</script>
</body>
</html>

View File

@@ -1,176 +0,0 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>白领一天业务测试页</title>
<style>
body {
margin: 0;
font-family: "Microsoft YaHei", Arial, sans-serif;
color: #1f2937;
background: #f6f8fb;
}
header {
padding: 20px 28px;
background: #0f766e;
color: white;
}
main {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 16px;
padding: 20px;
}
section {
min-height: 180px;
padding: 16px;
border: 1px solid #d8dee8;
border-radius: 8px;
background: white;
}
h1,
h2 {
margin: 0 0 12px;
}
label,
input,
select,
textarea,
button {
display: block;
width: 100%;
box-sizing: border-box;
margin: 8px 0;
font-size: 14px;
}
input,
select,
textarea {
padding: 8px;
border: 1px solid #cbd5e1;
border-radius: 6px;
}
button {
padding: 9px 12px;
border: 0;
border-radius: 6px;
color: white;
background: #2563eb;
cursor: pointer;
}
table {
width: 100%;
border-collapse: collapse;
font-size: 14px;
}
th,
td {
padding: 8px;
border-bottom: 1px solid #e5e7eb;
text-align: left;
}
.badge {
display: inline-block;
padding: 2px 8px;
border-radius: 999px;
background: #e0f2fe;
color: #0369a1;
}
.result {
min-height: 22px;
color: #166534;
font-weight: 600;
}
</style>
</head>
<body>
<header>
<h1>白领一天业务测试页</h1>
<p>用于验证数字员工通过 sgBrowser 完成网页采集、填写、比对和下载确认。</p>
</header>
<main>
<section id="company-contact">
<h2>供应商联系人</h2>
<p data-field="company">白银智维科技有限公司</p>
<p data-field="contact">联系人:李敏</p>
<p data-field="phone">电话0931-6201888</p>
<p data-field="email">邮箱limin@example.local</p>
</section>
<section id="industry-news">
<h2>行业动态</h2>
<ol>
<li data-score="96">内网终端安全巡检纳入月度闭环</li>
<li data-score="88">营销系统工单响应时限压缩至 2 小时</li>
<li data-score="81">国产化办公终端批量适配完成</li>
</ol>
</section>
<section id="customer-form">
<h2>客户回访登记</h2>
<label>客户名称<input id="customer-name" /></label>
<label>联系电话<input id="customer-phone" /></label>
<label>回访结论<select id="visit-result"><option>请选择</option><option>已解决</option><option>需跟进</option></select></label>
<textarea id="visit-note" rows="3" placeholder="回访备注"></textarea>
<button id="prepare-form" type="button">检查表单</button>
<p id="form-status" class="result"></p>
</section>
<section id="product-compare">
<h2>设备方案比对</h2>
<table>
<thead>
<tr><th>方案</th><th>价格</th><th>交付天数</th><th>风险</th></tr>
</thead>
<tbody>
<tr><td>基础巡检包</td><td>68000</td><td>12</td><td></td></tr>
<tr><td>增强巡检包</td><td>82000</td><td>9</td><td></td></tr>
<tr><td>全量管控包</td><td>98000</td><td>7</td><td></td></tr>
</tbody>
</table>
</section>
<section id="todo-login">
<h2>待办系统</h2>
<label>用户名<input id="login-user" /></label>
<label>口令<input id="login-pass" type="password" /></label>
<button id="login-button" type="button">登录</button>
<ul id="todo-list">
<li><span class="badge">紧急</span> 完成安全巡检日报</li>
<li><span class="badge">普通</span> 核对客户回访清单</li>
<li><span class="badge">普通</span> 整理明日会议材料</li>
</ul>
<p id="login-status" class="result"></p>
</section>
<section id="customer-list">
<h2>客户清单</h2>
<table>
<tbody>
<tr><td>兰州新区供电服务站</td><td>待回访</td><td>王磊</td></tr>
<tr><td>白银银光营业厅</td><td>已完成</td><td>张琴</td></tr>
<tr><td>景泰调度中心</td><td>需协同</td><td>马宁</td></tr>
</tbody>
</table>
</section>
<section id="download-center">
<h2>资料下载</h2>
<p>最新资料2026 年 5 月内网终端巡检模板</p>
<a id="latest-download" href="daily-template-202605.xlsx" download>下载巡检模板</a>
</section>
</main>
<script>
document.getElementById("prepare-form").addEventListener("click", function () {
const name = document.getElementById("customer-name").value.trim();
const result = document.getElementById("visit-result").value;
document.getElementById("form-status").textContent =
name && result !== "请选择" ? "表单已检查,等待人工确认提交" : "表单信息不完整";
});
document.getElementById("login-button").addEventListener("click", function () {
document.getElementById("login-status").textContent = "已读取待办,不执行真实登录";
});
</script>
</body>
</html>

View File

@@ -0,0 +1,41 @@
#!/usr/bin/env node
const { spawnSync } = require('node:child_process');
const fs = require('node:fs');
const path = require('node:path');
const clientRoot = path.resolve(__dirname, '..');
const embeddedRoot = path.join(clientRoot, 'embedded/sgrobot-digital');
const outputRoot = path.join(clientRoot, 'public/sgrobot-digital');
function run(command, args, options = {}) {
const result = spawnSync(command, args, {
cwd: options.cwd,
stdio: 'inherit',
shell: false,
});
if (result.status !== 0) {
process.exit(result.status || 1);
}
}
function main() {
const packageJson = path.join(embeddedRoot, 'package.json');
const nodeModules = path.join(embeddedRoot, 'node_modules');
if (!fs.existsSync(packageJson)) {
throw new Error(`Missing embedded package: ${packageJson}`);
}
if (!fs.existsSync(nodeModules)) {
console.error('Missing embedded dependencies.');
console.error(`Run: npm --prefix ${path.relative(clientRoot, embeddedRoot)} install`);
process.exit(1);
}
run('npm', ['run', 'build'], { cwd: embeddedRoot });
if (!fs.existsSync(path.join(outputRoot, 'index.html'))) {
throw new Error(`Build did not produce ${path.join(outputRoot, 'index.html')}`);
}
}
main();

View File

@@ -0,0 +1,65 @@
#!/usr/bin/env node
const fs = require('node:fs');
const path = require('node:path');
const clientRoot = path.resolve(__dirname, '..');
const repoRoot = path.resolve(clientRoot, '../..');
const defaultSgRobotWeb = path.resolve(repoRoot, '../../sgRobot/crates/sgclaw-core/web');
const sourceRoot = path.resolve(process.env.SGROBOT_WEB_DIR || defaultSgRobotWeb);
const targetRoot = path.resolve(clientRoot, 'embedded/sgrobot-digital');
const copies = [
['src/pages/digital', 'src/pages/digital'],
['src/components/digital', 'src/components/digital'],
['src/contexts/RoleContext.tsx', 'src/contexts/RoleContext.tsx'],
['src/lib/api.ts', 'src/lib/api.ts'],
['src/lib/auth.ts', 'src/lib/auth.ts'],
['src/lib/consoleDataAdapter.ts', 'src/lib/consoleDataAdapter.ts'],
['src/lib/i18n.ts', 'src/lib/i18n.ts'],
['src/types/api.ts', 'src/types/api.ts'],
['src/index.css', 'src/index.css'],
['public/digital-assets', 'public/digital-assets'],
['public/logo.png', 'public/logo.png'],
];
function assertExists(location) {
if (!fs.existsSync(location)) {
throw new Error(`Missing required path: ${location}`);
}
}
function copyPath(from, to) {
assertExists(from);
fs.rmSync(to, { recursive: true, force: true });
fs.mkdirSync(path.dirname(to), { recursive: true });
fs.cpSync(from, to, { recursive: true });
}
function stripTauriWindowImport() {
const shellPath = path.join(targetRoot, 'src/pages/digital/DigitalEmployeeShell.tsx');
let source = fs.readFileSync(shellPath, 'utf8');
source = source.replace("import { isTauri } from '@/lib/tauri';\n", '');
source = source.replace(
/\n if \(isTauri\(\)\) \{\n void import\('@tauri-apps\/api\/window'\)[\s\S]*?\n return;\n \}/,
'',
);
fs.writeFileSync(shellPath, source);
}
function main() {
assertExists(sourceRoot);
assertExists(targetRoot);
for (const [fromRel, toRel] of copies) {
copyPath(path.join(sourceRoot, fromRel), path.join(targetRoot, toRel));
}
stripTauriWindowImport();
console.log(`Synced sgRobot digital UI from ${sourceRoot}`);
console.log(`Target: ${targetRoot}`);
console.log('qimingclaw embedded adapters were preserved.');
}
main();

View File

@@ -0,0 +1,722 @@
# sgRobot 核心能力融合计划
## 定位
本计划不是单纯的“前端页面嵌入计划”。
真正目标是:
```text
以 qimingclaw 为客户端底座,
以数字员工页面为主要产品载体,
逐步吸收 sgRobot 的核心任务操作系统能力。
```
数字员工页面不是一个展示壳,而是后续承载以下能力的主入口:
- 今日工作台
- 任务中心
- 技能库
- 执行记录
- 审批与人工介入
- 运行时间线
- 产物与日报
- 计划模板与业务流程
当前 qimingclaw 已经完成一个过渡版本:在“概览”菜单内嵌 sgRobot 数字员工静态页面,不启动 sgRobot 后端也能展示页面。但这只是第一步,后续要把 sgRobot 的核心模型和执行治理能力融合进 qimingclaw。
## 总体原则
### 1. qimingclaw 是底座
保留 qimingclaw 当前已有优势:
- Electron 客户端
- 登录与注册链路
- lanproxy 远程接入
- `/computer/chat` 本地执行入口
- ACP 引擎
- MCP 工具接入
- 本地 SQLite 配置与状态存储
不把 sgRobot Rust daemon 原样搬进来,不让 qimingclaw 启动依赖 sgRobot 进程。
### 2. 数字员工页面是产品载体
sgRobot 的能力不是以后台模块裸露给用户,而是通过数字员工页面呈现:
- 用户看到的是数字员工的任务、状态、汇报和介入点。
- 后台融合的是 Plan / Task / Run / Skill / Event / Artifact / Approval。
- 页面最终从 demo fallback 过渡到 qimingclaw 真实数据。
### 3. 源码融合,运行隔离
数字员工前端采用源码级融合,而不是长期维护 `dist` 快照。
但第一阶段仍保持独立构建,不直接混入 qimingclaw 主 renderer bundle避免
- React 18 / React 19 冲突
- Tailwind / Ant Design 样式污染
- 路由体系互相影响
推荐方式:
```text
源码融合
-> 独立构建
-> 输出静态资源
-> qimingclaw webview 加载
-> 后续通过 qimingclaw API 接真实数据
```
## 目标架构
最终目标链路:
```text
用户目标 / 后端远程任务 / 定时任务
-> Plan 或 Direct Task
-> Task
-> Run
-> qimingclaw ACP / MCP 执行
-> Event / Artifact / Approval
-> Projection
-> 数字员工页面展示与人工介入
```
核心对象:
```text
Plan
PlanStep
Task
Run
Skill
Event
Artifact
Approval
MemoryEntry
ManagedService
```
第一轮不要求一次性实现全部对象,但数据库和 API 设计要为这些对象预留边界。
## 当前状态
已完成的过渡实现:
```text
crates/agent-electron-client/public/sgrobot-digital/
crates/agent-electron-client/src/renderer/components/pages/OverviewPage.tsx
```
当前链路:
```text
qimingclaw 左侧菜单“概览”
-> OverviewPage
-> Electron webview
-> sgrobot-digital/index.html#/digital
-> sgRobot 数字员工静态页面
-> demo fallback 数据
```
当前问题:
- 数字员工前端还是构建产物快照。
- 页面数据主要来自 sgRobot demo fallback。
- qimingclaw 没有 Plan / Task / Run / Event / Artifact 的正式状态模型。
- qimingclaw 的 ACP 执行链路尚未映射到数字员工任务中心。
- Skill、Approval、日报、执行时间线还不是 qimingclaw 真实能力。
## 分阶段融合计划
## Phase 1数字员工前端源码融合已开始
目标:把现在的静态快照变成可维护源码集成。
### 做法
在 qimingclaw 中新增独立 embedded 前端源码目录:
```text
crates/agent-electron-client/
embedded/
sgrobot-digital/
package.json
vite.config.ts
tsconfig.json
src/
pages/digital/
components/digital/
contexts/
lib/
types/
index.css
main.tsx
public/
digital-assets/
logo.png
patches/
README.md
public/
sgrobot-digital/
```
新增脚本:
```text
scripts/sync-sgrobot-digital.js
scripts/build-sgrobot-digital.js
```
当前 qimingclaw 已新增:
```text
crates/agent-electron-client/embedded/sgrobot-digital/
crates/agent-electron-client/scripts/sync-sgrobot-digital.js
crates/agent-electron-client/scripts/build-sgrobot-digital.js
```
常用命令:
```bash
cd crates/agent-electron-client
npm run sync:sgrobot-digital
npm --prefix embedded/sgrobot-digital install
npm run build:sgrobot-digital
npm run build:renderer
```
其中:
- `sync:sgrobot-digital` 从 sgRobot 的 web 源码同步数字员工相关源码和资源。
- `build:sgrobot-digital` 从 qimingclaw 内的 embedded 源码构建到 `public/sgrobot-digital`
- `build:renderer` 验证 qimingclaw 主客户端构建。
同步范围优先包含:
```text
sgRobot/crates/sgclaw-core/web/src/pages/digital/
sgRobot/crates/sgclaw-core/web/src/components/digital/
sgRobot/crates/sgclaw-core/web/src/contexts/RoleContext.tsx
sgRobot/crates/sgclaw-core/web/src/lib/api.ts
sgRobot/crates/sgclaw-core/web/src/lib/basePath.ts
sgRobot/crates/sgclaw-core/web/src/lib/i18n.ts
sgRobot/crates/sgclaw-core/web/src/lib/tauri.ts
sgRobot/crates/sgclaw-core/web/src/types/api.ts
sgRobot/crates/sgclaw-core/web/src/index.css
sgRobot/crates/sgclaw-core/web/public/digital-assets/
```
qimingclaw 专用 patch
- 直达 `#/digital`
- 禁用 sgRobot 配对页
- 静态资源使用相对 base path兼容 Vite dev server 和 Electron `file://` 打包态
- API 暂时允许 fallback 到 demo 数据
- 关闭 sgRobot 独立窗口行为,交给 qimingclaw 容器处理
当前边界:
- 前端已从“纯构建产物快照”推进为“源码同步 + 独立构建 + 静态嵌入”。
- 数字员工页面仍主要使用 sgRobot 的 demo fallback 和同源 API 探测。
- qimingclaw 还没有正式落地 Plan / Task / Run / Event / Artifact / Approval 数据模型。
- 下一阶段应开始建设 qimingclaw 本地数字员工 API 适配层。
### 产出
- 数字员工页面可以从源码重新构建。
- `public/sgrobot-digital` 不再手工维护。
- qimingclaw 不启动 sgRobot 也能打开数字员工页面。
### 验收
- `npm run sync:sgrobot-digital`
- `npm run build:sgrobot-digital`
- `npm run build:renderer`
- qimingclaw“概览”直接显示数字员工首页不出现配对页。
## Phase 2数字员工 API 适配层
目标:让数字员工页面开始读取 qimingclaw 真实数据。
### 做法
在 qimingclaw main process 或本地 HTTP 服务中新增数字员工 API 适配层。
先实现这些 sgRobot 兼容 API
```text
GET /api/digital-employee/workday
POST /api/digital-employee/workday/actions
GET /api/debug/plans
GET /api/debug/tasks
GET /api/skill
GET /api/debug/store
```
数据来源先映射 qimingclaw 当前已有能力:
- 当前用户
- 登录状态
- 服务状态
- Agent 状态
- 会话列表
- 最近 `/computer/chat` 请求
- ACP session 状态
- MCP tool 列表
此阶段不追求完整 Plan/Task/Run只先让页面从 qimingclaw 取真实状态。
### 产出
- 数字员工首页展示 qimingclaw 的真实运行状态。
- 技能库可以显示 qimingclaw 当前 MCP/ACP 工具能力。
- 任务中心开始展示 qimingclaw 最近执行记录。
### 验收
- 停止 sgRobot 后端,数字员工页面仍然工作。
- 页面 API 请求命中 qimingclaw。
- demo fallback 只作为无数据兜底,不再是主数据源。
## Phase 3Run / Event / Artifact 轻量控制平面
目标:把 qimingclaw 当前执行链路正式记录为可展示、可审计的运行对象。
### 当前 qimingclaw 执行链路
```text
backend
-> lanproxy
-> computerServer:60006 /computer/chat
-> agentService.ensureEngineForRequest
-> AcpEngine.chat
-> ACP session / prompt
-> /computer/progress/{session_id} SSE
```
### 需要新增的核心表
先落地最小集合:
```text
runs
events
artifacts
approvals
```
推荐语义:
- `Run`:一次 qimingclaw 执行尝试。
- `Event`:执行过程中的事实记录。
- `Artifact`:执行结果、文件、报告、截图、日志摘要。
- `Approval`:权限请求、人工确认、风险操作确认。
### 映射方式
```text
/computer/chat request
-> create Run
ACP progress event
-> append Event
permission request
-> create Approval
tool result / final message
-> create Artifact 或 Event
session complete
-> finish Run
```
### 产出
- 每次执行都有 Run ID。
- 数字员工“今日执行”“运行明细”读取真实 Run/Event。
- 后续日报和审计有数据基础。
### 验收
- 发起一次远程 `/computer/chat` 后,数字员工页面能看到执行记录。
- Run 状态能从 running 进入 succeeded / failed / cancelled。
- Event 时间线可追踪主要执行过程。
## Phase 4Task / PlanStep / Plan
目标:吸收 sgRobot 的任务操作系统骨架。
### 先做 Task
把一次业务执行抽象成 Task
```text
Task
-> Run attempt 1
-> Run attempt 2
```
Task 管业务目标Run 管执行尝试。
### 再做 Plan / PlanStep
当 Task 稳定后,引入:
```text
Plan
PlanStep
```
Plan 用于承载多步骤业务流程。
### 数据关系
```text
Plan
-> PlanStep[]
-> Task[]
-> Run[]
-> Event[]
-> Artifact[]
-> Approval[]
```
### 产出
- 数字员工任务中心从“执行记录列表”升级为“计划 / 任务 / 步骤”视图。
- 支持多步骤任务展示。
- 支持失败隔离和重试。
### 验收
- 单步任务能以 Task 展示。
- 多步任务能以 Plan 展示。
- 每个 Task 能看到 Run 历史。
## Phase 5PlanTemplate 融合
目标:吸收 sgRobot 的业务流程模板能力。
### 新增目录
```text
~/.qimingclaw/plan_templates/
```
支持文件:
```text
PlanTemplate.toml
```
### API
```text
GET /api/plan-templates
GET /api/plan-templates/:id
POST /api/plan-templates/:id/activate
```
### 与数字员工页面关系
数字员工“任务设置”区域读取 PlanTemplate。
用户可以:
- 查看模板
- 选择今日任务
- 一键执行
- 设置定时
### 产出
- qimingclaw 具备业务流程沉淀能力。
- 数字员工不只是临时聊天入口,而是可选择业务模板执行。
### 验收
- 放入一个 `PlanTemplate.toml` 后,数字员工页面能展示。
- 点击执行后生成 Plan / Task / Run。
## Phase 6Skill Catalog 融合
目标:把 qimingclaw 的 MCP / ACP 工具治理成 sgRobot 风格 Skill。
### 数据模型
```text
Skill
skill_id
name
description
version
kind
input_schema
output_schema
timeout_policy
retry_policy
approval_policy
sandbox_policy
enabled
```
### 来源映射
```text
MCP tools
ACP commands
qimingclaw built-in tools
GUI agent tools
file server tools
```
### 与数字员工页面关系
数字员工“技能库”展示 qimingclaw Skill Catalog。
用户可以:
- 查看技能
- 启用 / 禁用
- 查看所属 MCP server
- 查看输入输出说明
- 查看最近调用记录
### 产出
- 工具从“技术配置”变成“数字员工能力”。
- PlanTemplate 可以绑定 Skill。
### 验收
- MCP tool 能出现在技能库。
- 禁用技能后PlanTemplate 或任务执行不会再选择它。
## Phase 7Approval / Human-in-the-loop
目标:吸收 sgRobot 的人工治理机制。
### 融合对象
qimingclaw 已有 ACP permission 和 sandbox permission 概念。
需要统一成:
```text
Approval
```
来源包括:
- 文件写入
- 命令执行
- 外部服务调用
- 高风险工具
- Plan 执行确认
- Task 重试 / 跳过 / 取消
### 与数字员工页面关系
数字员工页面展示:
- 待审批
- 已批准
- 已拒绝
- 等待输入
- 风险说明
用户可操作:
- approve
- reject
- approve once
- approve always
- pause
- resume
- cancel
### 产出
- 人工介入成为主路径能力。
- 数字员工不是黑箱自动执行。
### 验收
- ACP permission request 能进入数字员工审批区。
- 用户在数字员工页面审批后ACP 执行继续。
## Phase 8Artifact / 日报 / 汇报
目标:把执行结果正式沉淀为产物,并由数字员工生成日报和汇报视图。
### Artifact 类型
```text
file
report
screenshot
log
json
markdown
diff
summary
```
### 日报数据来源
```text
Run
Event
Artifact
Approval
Task status
Plan status
```
### 与数字员工页面关系
数字员工“日报”区域展示:
- 今日执行
- 成功 / 失败 / 异常
- 人工介入次数
- 关键产物
- 风险提示
- 可下载报告
### 产出
- qimingclaw 的执行结果可沉淀、可回看、可汇报。
### 验收
- 完成任务后能在日报区域看到执行摘要。
- Artifact 可从任务详情进入查看。
## Phase 9ManagedService / 常驻能力治理
目标:吸收 sgRobot 对长期服务能力的治理方式。
qimingclaw 当前已有多个服务:
```text
computerServer
fileServer
lanproxy
mcpProxy
agent
guiServer
adminServer
```
需要把它们建模为:
```text
ManagedService
ServiceInstance
ServiceLease
```
### 与数字员工页面关系
数字员工页面可以展示:
- 服务健康
- 最近错误
- 重启次数
- 当前依赖它的任务
- 是否需要人工处理
### 产出
- 服务状态不只在客户端设置页展示,也进入数字员工的运行上下文。
### 验收
- 服务异常时,数字员工首页能提示。
- 服务恢复后,事件和状态同步更新。
## Phase 10Plan Agent / Task Agent
目标:在 qimingclaw 内形成 sgRobot 的双层 Agent 架构。
这是后期能力,不建议提前做。
### Plan Agent
负责:
- 计划生命周期
- 用户对话
- 异常决策
- 审批转发
- 重试 / 跳过 / 重规划
### Task Agent
负责:
- 单个 Task 的执行
- ACP session 调用
- MCP tool 调用
- 进度上报
- 产物回写
### 推荐链路
```text
User
-> Digital Employee
-> Plan Agent
-> Task Agent
-> ACP / MCP
-> Event / Artifact / Approval
-> Digital Employee Projection
```
### 产出
- qimingclaw 从“Agent 客户端”升级为“数字员工任务操作系统”。
## 推荐实施顺序
按风险和收益排序:
```text
1. 数字员工前端源码融合
2. 数字员工 API 适配层
3. Run / Event / Artifact 轻量控制平面
4. Task / Plan 基础模型
5. PlanTemplate
6. Skill Catalog
7. Approval / Human-in-the-loop
8. 日报与 Artifact
9. ManagedService
10. Plan Agent / Task Agent
```
## 第一阶段验收清单
第一阶段完成后,应满足:
- qimingclaw 内有数字员工前端源码目录。
- `public/sgrobot-digital` 可由脚本生成。
- 不启动 sgRobot 后端也能打开“概览”。
- “概览”直接显示数字员工页面。
- 不出现 sgRobot 配对页。
- 不手工维护压缩后的 `dist/assets/*.js`
- sgRobot 仓库不参与 qimingclaw 提交。
- `npm run build:renderer` 通过。
## 最终验收标准
整个融合完成后,应满足:
- 数字员工页面显示 qimingclaw 真实数据。
- 远程 `/computer/chat` 请求能生成 Run 和 Event。
- 用户能在数字员工页面看到任务进度、失败、产物和审批。
- PlanTemplate 能创建可执行计划。
- Skill Catalog 能治理 MCP/ACP 工具。
- 日报能基于真实 Run/Event/Artifact 生成。
- qimingclaw 不依赖 sgRobot daemon。
- sgRobot 的核心理念和对象模型被 qimingclaw 吸收,而不是简单嵌入另一个应用。