Z ZPI 文档 v0.3.8
介绍页

源码

packages/desktop/src/main/task-state.ts

typescript103 行查看原文
import type { SQLOutputValue } from "node:sqlite";
import { validPerformanceTotals } from "@zpi-dev/coding-agent";
import type { TaskState } from "./state-store.ts";

const object = (value: unknown): value is Record<string, unknown> =>
  value !== null && typeof value === "object" && !Array.isArray(value);
const string = (value: unknown) => typeof value === "string";
const nonempty = (value: unknown) => string(value) && (value as string).length > 0;
const number = (value: unknown) => typeof value === "number" && Number.isFinite(value);
const integer = (value: unknown) => Number.isSafeInteger(value) && Number(value) >= 0;
const nullableId = (value: unknown) => value === null || nonempty(value);

/** Validate before use. Diagnostics name fields, never include task text or other payload values. */
export function validateTask(value: unknown, id: string): asserts value is TaskState {
  const fail = (field: string): never => {
    throw new Error(`storage: invalid task ${JSON.stringify(id)}: ${field}`);
  };
  if (!object(value)) fail("data must be an object");
  const data = value as Record<string, unknown>;
  const required: Record<string, (value: unknown) => boolean> = {
    id: (v) => string(v) && /^[a-zA-Z0-9_-]+$/.test(String(v)),
    projectId: nullableId,
    title: string,
    createdAt: integer,
    updatedAt: integer,
  };
  for (const [field, check] of Object.entries(required))
    if (!check(data[field])) fail(`${field} missing or invalid type`);
  if (data.id !== id) fail("id does not match SQL primary key");
  const optional: Record<string, (value: unknown) => boolean> = {
    modelPerformanceTotals: validPerformanceTotals,
    sideChatInheritance: (v) =>
      object(v) &&
      v.version === 1 &&
      nonempty(v.childGeneration) &&
      integer(v.hiddenEndSeq) &&
      (v.parentGeneration === undefined || nonempty(v.parentGeneration)) &&
      (v.parentEndSeq === undefined || integer(v.parentEndSeq)) &&
      validPerformanceTotals(v.totals),
    parentSessionId: nonempty,
    groupId: nullableId,
    sortKey: number,
    revision: integer,
    cwd: nonempty,
    sessionFile: nonempty,
    titleState: string,
    worktreeBaseRef: string,
    diagnostic: string,
    newWorktree: (v) => typeof v === "boolean",
    draft: (v) => typeof v === "boolean",
    queuePaused: (v) => typeof v === "boolean",
    activitySeq: integer,
    unreadAt: integer,
    pinnedAt: (v) => v === null || integer(v),
    archivedAt: (v) => v === null || integer(v),
    status: (v) =>
      ["running", "completed", "aborted", "error", "interrupted"].includes(String(v)) && string(v),
    lastThinkingLevel: nonempty,
    currentSelection: (v) => object(v) && nonempty(v.provider) && nonempty(v.modelId) && string(v.reasoning),
    fork: (v) => object(v) && nonempty(v.sessionId) && nonempty(v.runId),
    worktree: (v) =>
      object(v) &&
      nonempty(v.path) &&
      nonempty(v.sourceCwd) &&
      (v.id === undefined || string(v.id)) &&
      (v.branch === undefined || string(v.branch)),
    // The host validates the configuration's capability/template contract before restoring execution.
    frozenConfiguration: object,
  };
  for (const [field, check] of Object.entries(optional))
    if (data[field] !== undefined && !check(data[field])) fail(`${field} invalid type or value`);
}

export function taskFromRow(row: Record<string, SQLOutputValue>): TaskState {
  const id = String(row.id);
  let data: unknown;
  try {
    data = JSON.parse(String(row.data));
  } catch {
    throw new Error(`storage: invalid task ${JSON.stringify(id)}: data is not JSON`);
  }
  validateTask(data, id);
  // Legacy JSON duplicates may be stale after FK/group changes. Only the SQL columns own these fields.
  const task = {
    ...data,
    id,
    title: row.title,
    createdAt: row.created_at,
    updatedAt: row.updated_at,
    revision: row.revision,
    groupId: row.group_id,
    sortKey: row.sort_key,
    projectId: row.project_id,
    parentSessionId: row.parent_task_id ?? undefined,
  };
  validateTask(task, id);
  if (!nonempty(row.id) || !integer(row.revision) || Number(row.revision) < 1)
    throw new Error(`storage: invalid task ${JSON.stringify(id)}: SQL id/revision`);
  delete task.running;
  delete task.pendingQuestion;
  return task;
}