mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-02 10:31:35 +00:00
* fix(gateway): declare viewer presence explicitly instead of deriving it from subscriptions * refactor(ui): isolate viewer presence lifecycle ownership * chore(protocol): register viewer presence schema owner * fix(ui): keep viewer presence cleared after detach * chore: drop changelog edits from this PR CHANGELOG.md is release-owned; release generation derives entries from merged PRs. Release-note context stays in the PR body.
155 lines
5.7 KiB
JavaScript
155 lines
5.7 KiB
JavaScript
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
|
|
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
const schemaDir = path.join(repoRoot, "packages/gateway-protocol/src/schema");
|
|
const failures = [];
|
|
const read = (relativePath) => fs.readFileSync(path.join(repoRoot, relativePath), "utf8");
|
|
const check = (condition, message) => {
|
|
if (!condition) {
|
|
failures.push(message);
|
|
}
|
|
};
|
|
|
|
const registryPath = "packages/gateway-protocol/src/schema/protocol-schemas.ts";
|
|
const registrySource = read(registryPath);
|
|
const fragmentImports = [
|
|
...registrySource.matchAll(
|
|
/^import \{ ([A-Za-z0-9_]+) \} from "(\.\/protocol-schema-fragment-[^"]+\.js)";$/gmu,
|
|
),
|
|
].map((match) => ({ binding: match[1], specifier: match[2] }));
|
|
const importSpecifiers = [...registrySource.matchAll(/^import .* from "([^"]+)";$/gmu)].map(
|
|
(match) => match[1],
|
|
);
|
|
check(
|
|
importSpecifiers.every(
|
|
(specifier) =>
|
|
specifier === "./protocol-schema-composer.js" ||
|
|
specifier.startsWith("./protocol-schema-fragment-"),
|
|
),
|
|
`${registryPath} may import only the composer and schema fragments`,
|
|
);
|
|
check(
|
|
!/\b[A-Z][A-Za-z0-9]*Schema\b/u.test(registrySource),
|
|
`${registryPath} contains a direct *Schema inventory`,
|
|
);
|
|
|
|
const composition = registrySource.match(
|
|
/export const ProtocolSchemas = composeProtocolSchemaFragments\(\[([\s\S]*?)\]\s+as const\);/u,
|
|
);
|
|
const composedBindings = (composition?.[1] ?? "")
|
|
.split("\n")
|
|
.map((line) => line.trim().replace(/,$/u, ""))
|
|
.filter(Boolean);
|
|
const importedBindings = fragmentImports.map(({ binding }) => binding);
|
|
check(Boolean(composition), `${registryPath} must explicitly compose an ordered fragment array`);
|
|
check(
|
|
composedBindings.length === importedBindings.length &&
|
|
new Set(composedBindings).size === composedBindings.length &&
|
|
importedBindings.every((binding) => composedBindings.includes(binding)),
|
|
`${registryPath} must compose every imported fragment exactly once`,
|
|
);
|
|
|
|
const fragmentFiles = fs
|
|
.readdirSync(schemaDir)
|
|
.filter((name) => /^protocol-schema-fragment-.+\.ts$/u.test(name));
|
|
const importedFiles = fragmentImports.map(({ specifier }) => `${specifier.slice(2, -3)}.ts`);
|
|
check(
|
|
fragmentFiles.length === importedFiles.length &&
|
|
fragmentFiles.every((name) => importedFiles.includes(name)),
|
|
`${registryPath} must explicitly import every protocol schema fragment`,
|
|
);
|
|
|
|
const importsByBinding = new Map(
|
|
fragmentImports.map((fragmentImport) => [fragmentImport.binding, fragmentImport]),
|
|
);
|
|
const seenKeys = new Set();
|
|
const orderedKeys = [];
|
|
for (const binding of composedBindings) {
|
|
const { specifier } = importsByBinding.get(binding) ?? {};
|
|
if (!specifier) {
|
|
continue;
|
|
}
|
|
const moduleUrl = new URL(specifier.replace(/\.js$/u, ".ts"), pathToFileURL(registryPath));
|
|
const fragment = (await import(moduleUrl.href))[binding];
|
|
check(fragment && typeof fragment === "object", `${specifier} must export object ${binding}`);
|
|
if (!fragment || typeof fragment !== "object") {
|
|
continue;
|
|
}
|
|
for (const key of Object.keys(fragment)) {
|
|
check(!seenKeys.has(key), `duplicate protocol schema key ${key}`);
|
|
seenKeys.add(key);
|
|
orderedKeys.push(key);
|
|
}
|
|
}
|
|
const { ProtocolSchemas } = await import(
|
|
pathToFileURL(path.join(schemaDir, "protocol-schemas.ts"))
|
|
);
|
|
check(
|
|
JSON.stringify(Object.keys(ProtocolSchemas)) === JSON.stringify(orderedKeys),
|
|
"ProtocolSchemas must preserve explicit fragment/key order",
|
|
);
|
|
|
|
const composerSource = read("packages/gateway-protocol/src/schema/protocol-schema-composer.ts");
|
|
check(!/\.(?:sort|toSorted)\s*\(/u.test(composerSource), "schema composer must not sort");
|
|
check(
|
|
composerSource.includes("Object.hasOwn(registry, key)"),
|
|
"schema composer must reject duplicate fragment keys",
|
|
);
|
|
|
|
const withoutComments = (source) =>
|
|
source
|
|
.replace(/\r\n?/gu, "\n")
|
|
.replace(/\/\*[\s\S]*?\*\//gu, "")
|
|
.replace(/^\s*\/\/.*$/gmu, "")
|
|
.trim();
|
|
const schemaModulesSource = withoutComments(
|
|
read("packages/gateway-protocol/src/schema-modules.ts"),
|
|
);
|
|
const ownerModules = [
|
|
...schemaModulesSource.matchAll(/^export \* from "\.\/schema\/([^"]+)\.js";$/gmu),
|
|
].map((match) => match[1]);
|
|
check(
|
|
ownerModules.length === 52 && new Set(ownerModules).size === ownerModules.length,
|
|
"schema-modules.ts must contain one unique 52-module owner list",
|
|
);
|
|
check(
|
|
schemaModulesSource.split("\n").filter(Boolean).length === ownerModules.length,
|
|
"schema-modules.ts may contain only owner-module exports",
|
|
);
|
|
check(
|
|
withoutComments(read("packages/gateway-protocol/src/schema.ts")) ===
|
|
'export * from "./schema-modules.js";\nexport * from "./schema/protocol-schemas.js";',
|
|
"schema.ts must remain a schema-modules/protocol-schemas wrapper",
|
|
);
|
|
check(
|
|
withoutComments(read("packages/gateway-protocol/src/schema-types.ts")) ===
|
|
'export type * from "./schema-modules.js";',
|
|
"schema-types.ts must remain a registry-free schema-modules wrapper",
|
|
);
|
|
|
|
for (const relativePath of [
|
|
"packages/gateway-protocol/src/index.ts",
|
|
"packages/gateway-protocol/src/schema-export-registry.ts",
|
|
"packages/gateway-protocol/src/validator-registry.ts",
|
|
]) {
|
|
check(
|
|
!read(relativePath).includes('from "./schema.js"'),
|
|
`${relativePath} must not cross the registry through schema.ts`,
|
|
);
|
|
}
|
|
const pluginSdkGuard = read("scripts/check-plugin-sdk-exports.mjs");
|
|
check(
|
|
pluginSdkGuard.includes("FORBIDDEN_PUBLIC_PROTOCOL_REGISTRY_RE") &&
|
|
pluginSdkGuard.includes("FORBIDDEN PUBLIC DTS REGISTRY"),
|
|
"plugin SDK declaration checks must reject leaked ProtocolSchemas declarations",
|
|
);
|
|
|
|
if (failures.length) {
|
|
throw new Error(
|
|
failures.map((failure) => `protocol registry check failed: ${failure}`).join("\n"),
|
|
);
|
|
}
|
|
console.log("protocol registry check passed");
|