feat(client): vendor digital employee source
This commit is contained in:
@@ -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 ?? []);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 => '';
|
||||
@@ -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
@@ -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 => '';
|
||||
Reference in New Issue
Block a user