Files
tutti/tools/scripts/generate-agent-gui-provider-catalog.mjs
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

333 lines
11 KiB
JavaScript

import { execFileSync } from "node:child_process";
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { dirname, resolve } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { format } from "prettier";
import YAML from "yaml";
const scriptDirectory = dirname(fileURLToPath(import.meta.url));
const repoRoot = resolve(scriptDirectory, "../..");
const daemonRoot = resolve(repoRoot, "packages/agent/daemon");
const outputPath = resolve(
repoRoot,
"packages/agent/gui/generated/providerIdentityCatalog.ts"
);
const capabilityOutputPath = resolve(
repoRoot,
"packages/agent/activity-core/src/generated/agentCapabilityKeys.ts"
);
const openapiPath = resolve(
repoRoot,
"services/tuttid/api/openapi/tuttid.v1.yaml"
);
const prettierConfigPath = resolve(
repoRoot,
"packages/configs/prettier/base.mjs"
);
const checkOnly = process.argv.includes("--check");
const { default: prettierConfig } = await import(
pathToFileURL(prettierConfigPath).href
);
const openapiProviderSchemaNames = [
"DesktopAgentComposerDefaultsByProvider",
"DesktopAgentGuiConversationRailCollapsedByProvider"
];
const openProviderIDPattern = "^[a-z][a-z0-9._:-]*$";
const providerStatusKinds = [
"codex_cli",
"claude_cli",
"opencode_cli",
"generic_cli"
];
if (isMainModule()) {
const catalog = readRegistryCatalog();
const capabilityKeys = readRegistryCapabilityKeys();
const openapi = YAML.parse(readFileSync(openapiPath, "utf8"));
validateRegistryCatalogAgainstOpenAPI(catalog, openapi);
validateCapabilityKeysAgainstOpenAPI(capabilityKeys, openapi);
const source = await renderProviderIdentityCatalog(catalog);
const capabilitySource = await renderAgentCapabilityKeys(capabilityKeys);
if (checkOnly) {
const existing = readFileSync(outputPath, "utf8");
if (existing !== source) {
throw new Error(
`Generated AgentGUI provider identity catalog is out of date: ${outputPath}`
);
}
const existingCapabilitySource = readFileSync(capabilityOutputPath, "utf8");
if (existingCapabilitySource !== capabilitySource) {
throw new Error(
`Generated agent capability keys are out of date: ${capabilityOutputPath}`
);
}
} else {
mkdirSync(dirname(outputPath), { recursive: true });
writeFileSync(outputPath, source, "utf8");
mkdirSync(dirname(capabilityOutputPath), { recursive: true });
writeFileSync(capabilityOutputPath, capabilitySource, "utf8");
}
}
function readRegistryCapabilityKeys() {
const output = execFileSync(
"go",
["run", "./providerregistry/cmd/providercapabilitycatalog"],
{ cwd: daemonRoot, encoding: "utf8" }
);
const keys = JSON.parse(output);
if (
!Array.isArray(keys) ||
keys.length === 0 ||
keys.some((key) => typeof key !== "string" || key.trim() === "") ||
new Set(keys).size !== keys.length
) {
throw new TypeError("provider capability catalog must be unique strings");
}
return keys;
}
export async function renderAgentCapabilityKeys(keys) {
return await format(
`// Code generated by tools/scripts/generate-agent-gui-provider-catalog.mjs. DO NOT EDIT.\n\nexport const AGENT_CAPABILITY_KEYS = ${JSON.stringify(keys, null, 2)} as const;\nexport type AgentCapabilityKey = (typeof AGENT_CAPABILITY_KEYS)[number];\n`,
{ ...prettierConfig, parser: "typescript" }
);
}
function isMainModule() {
return Boolean(
process.argv[1] &&
resolve(process.argv[1]) === fileURLToPath(import.meta.url)
);
}
function readRegistryCatalog() {
const output = execFileSync(
"go",
["run", "./providerregistry/cmd/provideridentitycatalog"],
{
cwd: daemonRoot,
encoding: "utf8"
}
);
const catalog = JSON.parse(output);
validateRegistryCatalog(catalog);
return catalog;
}
export async function renderProviderIdentityCatalog(catalog) {
validateRegistryCatalog(catalog);
return await format(
`// Code generated by tools/scripts/generate-agent-gui-provider-catalog.mjs. DO NOT EDIT.\n\nexport const generatedProviderIdentityCatalog = ${JSON.stringify(catalog, null, 2)} as const;\n`,
{
...prettierConfig,
parser: "typescript"
}
);
}
export function validateRegistryCatalog(catalog) {
if (!Array.isArray(catalog)) {
throw new TypeError("provider identity catalog must be an array");
}
const providerIds = new Set();
const targetIds = new Set();
for (const entry of catalog) {
if (!entry || typeof entry !== "object") {
throw new TypeError("provider identity catalog entry must be an object");
}
for (const key of ["providerId", "displayName", "iconKey", "localeKey"]) {
if (typeof entry[key] !== "string" || entry[key].trim() === "") {
throw new TypeError(`provider identity catalog ${key} is required`);
}
}
if (!["", "anthropic", "openai"].includes(entry.modelPlanProtocol)) {
throw new TypeError(
`invalid model plan protocol for provider: ${entry.providerId}`
);
}
if (!providerStatusKinds.includes(entry.statusKind)) {
throw new TypeError(
`invalid status kind for provider: ${entry.providerId}`
);
}
if (!Array.isArray(entry.aliases)) {
throw new TypeError("provider identity catalog aliases must be an array");
}
if (providerIds.has(entry.providerId)) {
throw new TypeError(`duplicate provider id: ${entry.providerId}`);
}
providerIds.add(entry.providerId);
const target = entry.target;
if (
!target ||
typeof target !== "object" ||
typeof target.id !== "string" ||
target.id.trim() === "" ||
typeof target.launchRefType !== "string" ||
target.launchRefType.trim() === "" ||
typeof target.enabled !== "boolean" ||
!Number.isInteger(target.sortOrder)
) {
throw new TypeError(`invalid target for provider: ${entry.providerId}`);
}
if (targetIds.has(target.id)) {
throw new TypeError(`duplicate target id: ${target.id}`);
}
targetIds.add(target.id);
const desktop = entry.desktop;
if (
!desktop ||
typeof desktop !== "object" ||
typeof desktop.managed !== "boolean" ||
!Number.isInteger(desktop.managedOrder) ||
desktop.managedOrder < 0 ||
desktop.managed !== desktop.managedOrder > 0 ||
!Number.isInteger(desktop.statusProbePriority) ||
desktop.statusProbePriority < 0 ||
desktop.managed !== desktop.statusProbePriority > 0 ||
typeof desktop.usageProbeKind !== "string" ||
typeof desktop.visibilityGate !== "string" ||
typeof desktop.runtimeProbeFallback !== "string" ||
typeof desktop.installBootstrap !== "boolean" ||
typeof desktop.refreshOnAccountChange !== "boolean" ||
(!desktop.managed && desktop.installBootstrap) ||
(!desktop.managed && desktop.refreshOnAccountChange) ||
typeof desktop.developerLogs !== "boolean" ||
typeof desktop.defaultProviderEligible !== "boolean" ||
!Number.isInteger(desktop.defaultProviderPriority) ||
desktop.defaultProviderPriority < 0 ||
desktop.defaultProviderEligible !== desktop.defaultProviderPriority > 0 ||
!Number.isInteger(desktop.unavailableDockOrderOffset) ||
desktop.unavailableDockOrderOffset < 0
) {
throw new TypeError(
`invalid desktop integration for provider: ${entry.providerId}`
);
}
}
}
export function validateRegistryCatalogAgainstOpenAPI(catalog, openapi) {
validateRegistryCatalog(catalog);
const migratedProviderIds = catalog.map((entry) => entry.providerId);
validateOpenProviderSchema(
openapi,
"AgentTargetProvider",
migratedProviderIds
);
validateOpenProviderSchema(
openapi,
"WorkspaceAgentProvider",
migratedProviderIds
);
for (const schemaName of openapiProviderSchemaNames) {
const schema = openapi?.components?.schemas?.[schemaName];
if (
!schema ||
typeof schema !== "object" ||
schema.type !== "object" ||
schema.additionalProperties !== false ||
!schema.properties ||
typeof schema.properties !== "object" ||
Array.isArray(schema.properties)
) {
throw new TypeError(
`openapi provider-keyed schema must remain a closed object: ${schemaName}`
);
}
const actualProviderIds = Object.keys(schema.properties);
assertSameStringSet(
actualProviderIds,
migratedProviderIds,
`openapi provider schema drift: ${schemaName}`
);
}
const defaultProviderIds =
openapi?.components?.schemas?.DesktopDefaultAgentProvider?.enum;
if (!Array.isArray(defaultProviderIds)) {
throw new TypeError(
"openapi provider schema is missing: DesktopDefaultAgentProvider"
);
}
const supportedProviderIds = new Set(migratedProviderIds);
for (const providerId of defaultProviderIds) {
if (!supportedProviderIds.has(providerId)) {
throw new TypeError(
`openapi default provider is not registered: ${providerId}`
);
}
}
assertSameStringSet(
catalog
.filter((entry) => entry.desktop.defaultProviderEligible)
.map((entry) => entry.providerId),
defaultProviderIds,
"desktop default provider descriptor drift"
);
}
function validateOpenProviderSchema(openapi, schemaName, migratedProviderIds) {
const schema = openapi?.components?.schemas?.[schemaName];
if (
!schema ||
typeof schema !== "object" ||
schema.type !== "string" ||
schema.enum !== undefined ||
schema.minLength !== 1 ||
schema.maxLength !== 128 ||
schema.pattern !== openProviderIDPattern
) {
throw new TypeError(
`openapi ${schemaName} must remain an open, bounded string schema`
);
}
const pattern = new RegExp(schema.pattern, "u");
for (const providerId of [...migratedProviderIds, "acp:extension-example"]) {
if (!pattern.test(providerId)) {
throw new TypeError(
`openapi ${schemaName} rejects valid provider id: ${providerId}`
);
}
}
for (const invalid of ["BadProvider", "bad provider", "$(provider)"]) {
if (pattern.test(invalid)) {
throw new TypeError(
`openapi ${schemaName} accepts unsafe provider id: ${invalid}`
);
}
}
}
export function validateCapabilityKeysAgainstOpenAPI(keys, openapi) {
const schema = openapi?.components?.schemas?.WorkspaceAgentCapabilities;
if (!schema || typeof schema !== "object") {
throw new TypeError("openapi WorkspaceAgentCapabilities schema is missing");
}
assertSameStringSet(
Object.keys(schema.properties ?? {}),
keys,
"openapi agent capability vocabulary drift"
);
}
function assertSameStringSet(actual, expected, label) {
const normalizedActual = [...new Set(actual)].sort();
const normalizedExpected = [...new Set(expected)].sort();
if (
normalizedActual.length !== actual.length ||
normalizedExpected.length !== expected.length ||
JSON.stringify(normalizedActual) !== JSON.stringify(normalizedExpected)
) {
throw new TypeError(
`${label}: actual=${JSON.stringify(normalizedActual)} expected=${JSON.stringify(normalizedExpected)}`
);
}
}