feat(client): load local digital plan templates
This commit is contained in:
@@ -31,6 +31,7 @@ declare global {
|
||||
getRuntimeRecords?: () => Promise<QimingclawRuntimeRecords>;
|
||||
getUiState?: () => Promise<AdapterState>;
|
||||
getSkillCapabilities?: () => Promise<QimingclawSkillCapabilities>;
|
||||
getPlanTemplates?: () => Promise<QimingclawPlanTemplateList>;
|
||||
saveUiState?: (update: { state: AdapterState; action?: string; date?: string; metadata?: Record<string, unknown> }) => Promise<AdapterState>;
|
||||
getSyncStatus?: () => Promise<QimingclawSyncStatus>;
|
||||
flushSync?: () => Promise<QimingclawSyncStatus>;
|
||||
@@ -123,6 +124,19 @@ interface QimingclawSnapshot {
|
||||
sync?: QimingclawSyncStatus | null;
|
||||
runtime?: QimingclawRuntimeRecords | null;
|
||||
uiState?: AdapterState | null;
|
||||
planTemplates?: QimingclawPlanTemplateRecord[] | null;
|
||||
}
|
||||
|
||||
interface QimingclawPlanTemplateList {
|
||||
generatedAt: string;
|
||||
templateDir: string;
|
||||
templates: QimingclawPlanTemplateRecord[];
|
||||
}
|
||||
|
||||
interface QimingclawPlanTemplateRecord extends DebugPlan {
|
||||
source: 'qimingclaw-plan-template';
|
||||
template_path: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
interface QimingclawGovernanceCommandRecord {
|
||||
@@ -475,12 +489,13 @@ async function readQimingclawSnapshot(): Promise<QimingclawSnapshot | null> {
|
||||
try {
|
||||
const snapshot = await window.QimingClawBridge?.digital?.getSnapshot?.() ?? null;
|
||||
if (!snapshot) return null;
|
||||
const [sync, runtime, uiState] = await Promise.all([
|
||||
const [sync, runtime, uiState, planTemplates] = await Promise.all([
|
||||
window.QimingClawBridge?.digital?.getSyncStatus?.().catch(() => null) ?? null,
|
||||
window.QimingClawBridge?.digital?.getRuntimeRecords?.().catch(() => null) ?? null,
|
||||
readBridgeUiState(),
|
||||
window.QimingClawBridge?.digital?.getPlanTemplates?.().catch(() => null) ?? null,
|
||||
]);
|
||||
return { ...snapshot, sync, runtime, uiState };
|
||||
return { ...snapshot, sync, runtime, uiState, planTemplates: planTemplates?.templates ?? [] };
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
@@ -842,9 +857,53 @@ function runtimeDebugPlans(snapshot: QimingclawSnapshot | null): DebugPlan[] {
|
||||
}));
|
||||
}
|
||||
|
||||
function templatePlans(snapshot: QimingclawSnapshot | null): DebugPlan[] {
|
||||
const customTemplates = snapshot?.planTemplates ?? [];
|
||||
const byId = new Map<string, DebugPlan>();
|
||||
for (const plan of [...customTemplates, ...PLANS]) {
|
||||
if (!byId.has(plan.plan_id)) byId.set(plan.plan_id, plan);
|
||||
}
|
||||
return Array.from(byId.values());
|
||||
}
|
||||
|
||||
function mergedDebugPlans(snapshot: QimingclawSnapshot | null): DebugPlan[] {
|
||||
const byId = new Map<string, DebugPlan>();
|
||||
for (const plan of [...runtimeDebugPlans(snapshot), ...templatePlans(snapshot).map((plan) => withRuntimePlan(plan, snapshot))]) {
|
||||
if (!byId.has(plan.plan_id)) byId.set(plan.plan_id, plan);
|
||||
}
|
||||
return Array.from(byId.values());
|
||||
}
|
||||
|
||||
function planTemplateJob(plan: DebugPlan, snapshot: QimingclawSnapshot | null): CronJob {
|
||||
const normalizedPlan = withRuntimePlan(plan, snapshot);
|
||||
const schedule = PLAN_SCHEDULES[plan.plan_id] ?? PLAN_SCHEDULES['qimingclaw-client-readiness']!;
|
||||
return {
|
||||
id: `schedule-${plan.plan_id}`,
|
||||
name: plan.title,
|
||||
expression: schedule.expression,
|
||||
command: `digital-employee:${plan.plan_id}`,
|
||||
prompt: plan.objective ?? null,
|
||||
job_type: 'plan_template',
|
||||
schedule: { expression: schedule.expression, template_id: plan.plan_id },
|
||||
enabled: true,
|
||||
delivery: { mode: 'plan_template', template_id: plan.plan_id },
|
||||
template_id: plan.plan_id,
|
||||
trigger_kind: 'scheduled',
|
||||
step_count: plan.steps?.length ?? 0,
|
||||
skill_ids: [...new Set((plan.steps ?? []).flatMap((step) => step.preferred_skills ?? []))],
|
||||
delete_after_run: false,
|
||||
created_at: plan.created_at,
|
||||
next_run: schedule.nextRun,
|
||||
last_run: normalizedPlan.status === 'running' ? todayAt(9, 5) : null,
|
||||
last_status: normalizedPlan.status === 'running' ? 'running' : null,
|
||||
last_output: null,
|
||||
};
|
||||
}
|
||||
|
||||
function buildSchedulerJobs(snapshot: QimingclawSnapshot | null = null): CronJob[] {
|
||||
const templateJobs = templatePlans(snapshot).map((plan) => planTemplateJob(plan, snapshot));
|
||||
if (hasRuntimeRecords(snapshot)) {
|
||||
return runtimeDebugPlans(snapshot).map((plan) => ({
|
||||
const runtimeJobs = runtimeDebugPlans(snapshot).map((plan) => ({
|
||||
id: `runtime-${plan.plan_id}`,
|
||||
name: plan.title,
|
||||
expression: '',
|
||||
@@ -865,31 +924,13 @@ function buildSchedulerJobs(snapshot: QimingclawSnapshot | null = null): CronJob
|
||||
last_status: plan.status,
|
||||
last_output: null,
|
||||
}));
|
||||
const runtimeIds = new Set<string | null | undefined>(runtimeJobs.map((job) => job.template_id));
|
||||
return [
|
||||
...runtimeJobs,
|
||||
...templateJobs.filter((job) => !runtimeIds.has(job.template_id)),
|
||||
];
|
||||
}
|
||||
return PLANS.map((plan) => {
|
||||
const schedule = PLAN_SCHEDULES[plan.plan_id] ?? PLAN_SCHEDULES['qimingclaw-client-readiness']!;
|
||||
return {
|
||||
id: `schedule-${plan.plan_id}`,
|
||||
name: plan.title,
|
||||
expression: schedule.expression,
|
||||
command: `digital-employee:${plan.plan_id}`,
|
||||
prompt: plan.objective ?? null,
|
||||
job_type: 'plan_template',
|
||||
schedule: { expression: schedule.expression, template_id: plan.plan_id },
|
||||
enabled: true,
|
||||
delivery: { mode: 'plan_template', template_id: plan.plan_id },
|
||||
template_id: plan.plan_id,
|
||||
trigger_kind: 'scheduled',
|
||||
step_count: plan.steps?.length ?? 0,
|
||||
skill_ids: [...new Set((plan.steps ?? []).flatMap((step) => step.preferred_skills ?? []))],
|
||||
delete_after_run: false,
|
||||
created_at: plan.created_at,
|
||||
next_run: schedule.nextRun,
|
||||
last_run: plan.status === 'running' ? todayAt(9, 5) : null,
|
||||
last_status: plan.status === 'running' ? 'running' : null,
|
||||
last_output: null,
|
||||
};
|
||||
});
|
||||
return templateJobs;
|
||||
}
|
||||
|
||||
function buildDuties(state: AdapterState, date: string, snapshot: QimingclawSnapshot | null): DigitalEmployeeDuty[] {
|
||||
@@ -934,9 +975,23 @@ function buildDuties(state: AdapterState, date: string, snapshot: QimingclawSnap
|
||||
result_text: task.syncError || `同步状态:${task.syncStatus}`,
|
||||
conversation_id: task.id,
|
||||
}));
|
||||
return [...planDuties, ...standaloneDuties];
|
||||
const runtimeDutyIds = new Set([...planDuties, ...standaloneDuties].map((duty) => duty.id));
|
||||
return [
|
||||
...planDuties,
|
||||
...standaloneDuties,
|
||||
...templateDuties(state, date, snapshot).filter((duty) => !runtimeDutyIds.has(duty.id)),
|
||||
];
|
||||
}
|
||||
return PLANS.map((rawPlan) => {
|
||||
return templateDuties(state, date, snapshot);
|
||||
}
|
||||
|
||||
function templateDuties(
|
||||
state: AdapterState,
|
||||
date: string,
|
||||
snapshot: QimingclawSnapshot | null,
|
||||
): DigitalEmployeeDuty[] {
|
||||
const selected = selectedSetForDate(state, date);
|
||||
return templatePlans(snapshot).map((rawPlan) => {
|
||||
const plan = withRuntimePlan(rawPlan, snapshot);
|
||||
const schedule = PLAN_SCHEDULES[plan.plan_id] ?? PLAN_SCHEDULES['qimingclaw-client-readiness']!;
|
||||
return {
|
||||
@@ -975,15 +1030,25 @@ function planResultText(plan: DebugPlan, snapshot: QimingclawSnapshot | null): s
|
||||
}
|
||||
|
||||
function buildTasks(planId?: string | null, snapshot: QimingclawSnapshot | null = null): DebugTask[] {
|
||||
if (hasRuntimeRecords(snapshot)) {
|
||||
return runtimeTasks(snapshot)
|
||||
.filter((task) => !planId || task.planId === planId || task.id === planId)
|
||||
.map(runtimeTaskToDebugTask);
|
||||
}
|
||||
return PLANS
|
||||
const templateTasks = templatePlans(snapshot)
|
||||
.filter((plan) => !planId || plan.plan_id === planId)
|
||||
.map((plan) => withRuntimePlan(plan, snapshot))
|
||||
.flatMap((plan) => (plan.steps ?? []).flatMap((step) => (step.tasks ?? []).map((task) => ({
|
||||
.flatMap(templatePlanTasks);
|
||||
if (hasRuntimeRecords(snapshot)) {
|
||||
const runtimeResult = runtimeTasks(snapshot)
|
||||
.filter((task) => !planId || task.planId === planId || task.id === planId)
|
||||
.map(runtimeTaskToDebugTask);
|
||||
const runtimeTaskIds = new Set(runtimeResult.map((task) => task.task_id));
|
||||
return [
|
||||
...runtimeResult,
|
||||
...templateTasks.filter((task) => !runtimeTaskIds.has(task.task_id)),
|
||||
];
|
||||
}
|
||||
return templateTasks;
|
||||
}
|
||||
|
||||
function templatePlanTasks(plan: DebugPlan): DebugTask[] {
|
||||
return (plan.steps ?? []).flatMap((step) => (step.tasks ?? []).map((task) => ({
|
||||
task_id: task.task_id,
|
||||
title: task.title,
|
||||
status: plan.status === 'running' && task.status === 'pending' ? 'running' : task.status,
|
||||
@@ -995,7 +1060,7 @@ function buildTasks(planId?: string | null, snapshot: QimingclawSnapshot | null
|
||||
updated_at: plan.status === 'running' ? new Date().toISOString() : plan.created_at,
|
||||
started_at: task.status === 'running' ? todayAt(9, 5) : null,
|
||||
finished_at: task.status === 'completed' ? todayAt(9, 10) : null,
|
||||
}))));
|
||||
})));
|
||||
}
|
||||
|
||||
function buildRuns(tasks: DebugTask[]): DebugRun[] {
|
||||
@@ -1392,9 +1457,7 @@ function slugifyIdPart(value: string): string {
|
||||
|
||||
function filterPlans(query?: string | null, snapshot: QimingclawSnapshot | null = null): DebugPlan[] {
|
||||
const normalized = (query ?? '').trim().toLowerCase();
|
||||
const plans = hasRuntimeRecords(snapshot)
|
||||
? runtimeDebugPlans(snapshot)
|
||||
: PLANS.map((plan) => withRuntimePlan(plan, snapshot));
|
||||
const plans = mergedDebugPlans(snapshot);
|
||||
if (!normalized) return plans;
|
||||
return plans.filter((plan) => (
|
||||
plan.title.toLowerCase().includes(normalized)
|
||||
@@ -1743,7 +1806,7 @@ async function handleGovernanceCommand(
|
||||
}
|
||||
|
||||
if (body.command_kind === 'activate_plan' || body.command_kind === 'run_schedule_now') {
|
||||
const targetPlanId = planId || runtimeDebugPlans(snapshot)[0]?.plan_id || PLANS[0]?.plan_id || 'qimingclaw-client-readiness';
|
||||
const targetPlanId = planId || runtimeDebugPlans(snapshot)[0]?.plan_id || templatePlans(snapshot)[0]?.plan_id || 'qimingclaw-client-readiness';
|
||||
const dispatch = await dispatchPlan(targetPlanId, snapshot);
|
||||
const response = governanceAccepted(commandId, correlationId, '已映射为 qimingclaw 本地任务激活。', {
|
||||
plan_id: targetPlanId,
|
||||
@@ -1865,7 +1928,7 @@ function buildPlanFlow(planId: string, snapshot: QimingclawSnapshot | null = nul
|
||||
})),
|
||||
};
|
||||
}
|
||||
const plan = withRuntimePlan(PLANS.find((candidate) => candidate.plan_id === planId) ?? PLANS[0]!, snapshot);
|
||||
const plan = withRuntimePlan(templatePlans(snapshot).find((candidate) => candidate.plan_id === planId) ?? templatePlans(snapshot)[0]!, snapshot);
|
||||
const timestamp = new Date().toISOString();
|
||||
return {
|
||||
plan_id: plan.plan_id,
|
||||
@@ -1976,21 +2039,49 @@ function buildTaskFlow(taskId: string, snapshot: QimingclawSnapshot | null = nul
|
||||
async function dispatchPlan(planId: string, snapshot: QimingclawSnapshot | null = null): Promise<PlanDispatchResponse> {
|
||||
const state = snapshot?.uiState ?? await readBridgeUiState() ?? readState();
|
||||
const date = todayInputDate();
|
||||
const plan = mergedDebugPlans(snapshot).find((candidate) => candidate.plan_id === planId);
|
||||
const activatedAt = Date.now();
|
||||
const correlationId = `dispatch-${planId}-${activatedAt}`;
|
||||
const selected = new Set(state.selectedDutyIdsByDate[date] ?? []);
|
||||
selected.add(planId);
|
||||
state.selectedDutyIdsByDate[date] = [...selected];
|
||||
state.confirmedDates[date] = state.confirmedDates[date] ?? new Date().toISOString();
|
||||
await saveState(state, 'confirm_workday', date);
|
||||
const dispatchedTasks = buildTasks(planId, snapshot).map((task) => ({
|
||||
task_id: task.task_id,
|
||||
title: task.title,
|
||||
status: task.status === 'pending' ? 'running' : task.status,
|
||||
}));
|
||||
await recordGovernanceCommand(
|
||||
{
|
||||
command_kind: 'activate_plan',
|
||||
context_refs: { plan_id: planId },
|
||||
payload: {
|
||||
plan_id: planId,
|
||||
title: plan?.title,
|
||||
objective: plan?.objective,
|
||||
requested_action: 'dispatch_plan',
|
||||
},
|
||||
correlation_id: correlationId,
|
||||
},
|
||||
governanceAccepted(
|
||||
`qimingclaw-digital-dispatch-${slugifyIdPart(planId)}-${activatedAt}`,
|
||||
correlationId,
|
||||
'已将数字员工模板执行写入 qimingclaw 本地运行记录。',
|
||||
{
|
||||
plan_id: planId,
|
||||
status: 'active',
|
||||
dispatched_tasks: dispatchedTasks,
|
||||
source: 'qimingclaw-adapter',
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
plan_id: planId,
|
||||
source_plan_id: planId,
|
||||
dispatched_tasks: buildTasks(planId, snapshot).map((task) => ({
|
||||
task_id: task.task_id,
|
||||
title: task.title,
|
||||
status: task.status === 'pending' ? 'running' : task.status,
|
||||
})),
|
||||
dispatched_tasks: dispatchedTasks,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2011,7 +2102,7 @@ function buildPlanMessages(planId: string, snapshot: QimingclawSnapshot | null =
|
||||
})),
|
||||
];
|
||||
}
|
||||
const plan = PLANS.find((candidate) => candidate.plan_id === planId) ?? PLANS[0]!;
|
||||
const plan = templatePlans(snapshot).find((candidate) => candidate.plan_id === planId) ?? templatePlans(snapshot)[0]!;
|
||||
return [
|
||||
{
|
||||
role: 'user',
|
||||
|
||||
Reference in New Issue
Block a user