Files
app/tauri/src/runtime/inventory/client-inventory.ts
T

64 lines
3.2 KiB
TypeScript

/**
* 面向 Agent 的客户端能力清单生成器。
*
* 根据当前公开的 Surface 和 Capability 计算带 revision 的 Inventory;只有可见能力变化时
* 才更新,避免重试或内部状态抖动错误扩大 Agent 的可调用范围。
*/
import type { CapabilityDefinition } from "@/runtime/capabilities/capability-registry";
import type { SurfaceRegistrySnapshot } from "@/runtime/surfaces/surface-registry";
import type { CoreAppRecordInput } from "@/runtime/app-management/app-registry";
export type ClientInventory = Readonly<{
revision: string;
standard_components: readonly Readonly<{ id: "notice" | "choice" | "confirm" | "input"; version: "1" }>[];
applications: readonly Readonly<{
id: string;
version: string;
surfaces: readonly Readonly<{ id: string; events: readonly string[] }>[];
tools?: readonly Readonly<{ name: string; handling: string }>[];
}>[];
capabilities: readonly CapabilityDefinition[];
}>;
/**
* Builds the only inventory the Agent may receive. It is an observation of
* host-local enablement and policy, not an install operation nor a grant of
* authority. The model deliberately has no access to bundle IDs, code, local
* paths, enabled timestamps, user content, cookies, or device identifiers.
*/
export class ClientInventoryPublisher {
private revision = 0;
private previousFingerprint = "";
private current: ClientInventory | undefined;
public sync(surfaces: SurfaceRegistrySnapshot, capabilities: readonly CapabilityDefinition[], apps: readonly CoreAppRecordInput[] = []): { changed: boolean; inventory: ClientInventory } {
const surfaceApplications = surfaces.enabled
.map(enabled => ({ id: enabled.app_id, version: enabled.version, surfaces: [{ id: "task-dashboard" as const, events: ["cancel"] as ["cancel"] }] }))
const manifestApplications = apps.filter(app => app.enabled).map(app => ({
id: app.app_scope, version: app.manifest.version, surfaces: [],
tools: app.manifest.tools.map(tool => ({ name: tool.name, handling: tool.handling })),
}));
const applications = [...surfaceApplications, ...manifestApplications]
.sort((left, right) => `${left.id}@${left.version}`.localeCompare(`${right.id}@${right.version}`));
const allowedCapabilities = [...capabilities]
.sort((left, right) => left.name.localeCompare(right.name))
.map(definition => ({ ...definition, input_schema: { ...definition.input_schema, required: [...definition.input_schema.required], properties: { ...definition.input_schema.properties } } }));
const fingerprint = JSON.stringify({ applications, capabilities: allowedCapabilities });
if (this.current && fingerprint === this.previousFingerprint) return { changed: false, inventory: this.current };
this.revision += 1;
this.previousFingerprint = fingerprint;
this.current = {
revision: `catalog-${this.revision}`,
standard_components: [
{ id: "notice", version: "1" },
{ id: "choice", version: "1" },
{ id: "confirm", version: "1" },
{ id: "input", version: "1" },
],
applications,
capabilities: allowedCapabilities,
};
return { changed: true, inventory: this.current };
}
}