mirror of
https://github.com/aiclientproxy/proxycast.git
synced 2026-09-24 23:10:56 +08:00
release: v1.7.0
This commit is contained in:
@@ -13,6 +13,10 @@ const DEFAULTS = {
|
||||
streamMode: "both",
|
||||
};
|
||||
|
||||
const INVOKE_TIMEOUT_MS = 60_000;
|
||||
const INVOKE_RETRY_COUNT = 3;
|
||||
const INVOKE_RETRY_DELAY_MS = 500;
|
||||
|
||||
function printHelp() {
|
||||
console.log(`
|
||||
Lime Browser Runtime Smoke
|
||||
@@ -111,25 +115,55 @@ function assert(condition, message) {
|
||||
}
|
||||
}
|
||||
|
||||
async function invoke(invokeUrl, cmd, args) {
|
||||
const response = await fetch(invokeUrl, {
|
||||
function isTransientInvokeError(error) {
|
||||
return (
|
||||
error?.name === "TimeoutError" ||
|
||||
(error instanceof TypeError && error.message === "fetch failed")
|
||||
);
|
||||
}
|
||||
|
||||
async function invoke(options, cmd, args) {
|
||||
const requestInit = {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ cmd, args }),
|
||||
});
|
||||
signal: AbortSignal.timeout(Math.min(options.timeoutMs, INVOKE_TIMEOUT_MS)),
|
||||
};
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
for (let attempt = 1; attempt <= INVOKE_RETRY_COUNT; attempt += 1) {
|
||||
try {
|
||||
const response = await fetch(options.invokeUrl, requestInit);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const payload = await response.json();
|
||||
if (payload?.error) {
|
||||
throw new Error(String(payload.error));
|
||||
}
|
||||
|
||||
return payload?.result;
|
||||
} catch (error) {
|
||||
const detail = error instanceof Error ? error.message : String(error);
|
||||
if (!isTransientInvokeError(error) || attempt >= INVOKE_RETRY_COUNT) {
|
||||
if (error?.name === "TimeoutError") {
|
||||
throw new Error(
|
||||
`[smoke:browser-runtime] ${cmd} 超时,${Math.min(options.timeoutMs, INVOKE_TIMEOUT_MS)}ms 内未收到 DevBridge 响应`,
|
||||
);
|
||||
}
|
||||
throw new Error(`[smoke:browser-runtime] ${cmd} 请求失败: ${detail}`);
|
||||
}
|
||||
console.warn(
|
||||
`[smoke:browser-runtime] ${cmd} 第 ${attempt} 次请求失败,${INVOKE_RETRY_DELAY_MS}ms 后重试: ${detail}`,
|
||||
);
|
||||
await sleep(INVOKE_RETRY_DELAY_MS);
|
||||
}
|
||||
}
|
||||
|
||||
const payload = await response.json();
|
||||
if (payload?.error) {
|
||||
throw new Error(String(payload.error));
|
||||
}
|
||||
|
||||
return payload?.result;
|
||||
throw new Error(`[smoke:browser-runtime] ${cmd} 请求失败: unknown error`);
|
||||
}
|
||||
|
||||
async function waitForHealth(options) {
|
||||
@@ -180,7 +214,7 @@ async function main() {
|
||||
let sessionId = null;
|
||||
|
||||
try {
|
||||
const launchResponse = await invoke(options.invokeUrl, "launch_browser_session", {
|
||||
const launchResponse = await invoke(options, "launch_browser_session", {
|
||||
request: {
|
||||
profile_key: profileKey,
|
||||
url: options.launchUrl,
|
||||
@@ -201,7 +235,7 @@ async function main() {
|
||||
);
|
||||
|
||||
const sessionState = await invoke(
|
||||
options.invokeUrl,
|
||||
options,
|
||||
"get_browser_session_state",
|
||||
{
|
||||
request: {
|
||||
@@ -222,7 +256,7 @@ async function main() {
|
||||
"get_browser_session_state 未返回 target_id",
|
||||
);
|
||||
|
||||
const actionResult = await invoke(options.invokeUrl, "browser_execute_action", {
|
||||
const actionResult = await invoke(options, "browser_execute_action", {
|
||||
request: {
|
||||
profile_key: profileKey,
|
||||
action: "read_page",
|
||||
@@ -239,7 +273,7 @@ async function main() {
|
||||
"browser_execute_action 未返回对应的 target_id",
|
||||
);
|
||||
|
||||
const auditLogs = await invoke(options.invokeUrl, "get_browser_action_audit_logs", {
|
||||
const auditLogs = await invoke(options, "get_browser_action_audit_logs", {
|
||||
limit: 10,
|
||||
});
|
||||
const launchAudit = findLatestAudit(
|
||||
@@ -278,7 +312,7 @@ async function main() {
|
||||
} finally {
|
||||
if (sessionId) {
|
||||
try {
|
||||
await invoke(options.invokeUrl, "close_cdp_session", {
|
||||
await invoke(options, "close_cdp_session", {
|
||||
request: {
|
||||
session_id: sessionId,
|
||||
},
|
||||
|
||||
@@ -22,6 +22,7 @@ const ignoredDirectories = new Set([
|
||||
const frontendCommandPatterns = [
|
||||
/\bsafeInvoke(?:<[^>]+>)?\s*\(\s*["'`]([^"'`]+)["'`]/g,
|
||||
/\binvoke(?:<[^>]+>)?\s*\(\s*["'`]([^"'`]+)["'`]/g,
|
||||
/\binvokeAgentRuntimeBridge(?:<[^>]+>)?\s*\(\s*["'`]([^"'`]+)["'`]/g,
|
||||
];
|
||||
|
||||
const knownDeferredRegistrationReasons = new Map();
|
||||
@@ -84,11 +85,19 @@ function addUsage(map, command, relativePath) {
|
||||
map.get(command).add(relativePath);
|
||||
}
|
||||
|
||||
function isFrameworkPluginCommand(command) {
|
||||
return command.startsWith("plugin:");
|
||||
}
|
||||
|
||||
function extractCommandsFromSource(sourceCode) {
|
||||
const commands = new Set();
|
||||
for (const pattern of frontendCommandPatterns) {
|
||||
for (const match of sourceCode.matchAll(pattern)) {
|
||||
commands.add(match[1]);
|
||||
const command = match[1];
|
||||
if (isFrameworkPluginCommand(command)) {
|
||||
continue;
|
||||
}
|
||||
commands.add(command);
|
||||
}
|
||||
}
|
||||
return commands;
|
||||
@@ -109,6 +118,31 @@ function collectFrontendCommandUsage() {
|
||||
return commandUsage;
|
||||
}
|
||||
|
||||
function collectAgentRuntimeSchemaUsage() {
|
||||
const commandUsage = new Map();
|
||||
const schemaPath = path.join(
|
||||
repoRoot,
|
||||
"src/lib/governance/agentRuntimeCommandSchema.json",
|
||||
);
|
||||
if (!fs.existsSync(schemaPath)) {
|
||||
return commandUsage;
|
||||
}
|
||||
|
||||
const relativePath = normalizePath(path.relative(repoRoot, schemaPath));
|
||||
const parsed = JSON.parse(fs.readFileSync(schemaPath, "utf8"));
|
||||
const schemaCommands = Array.isArray(parsed?.commands) ? parsed.commands : [];
|
||||
|
||||
for (const entry of schemaCommands) {
|
||||
const command = String(entry?.command ?? "").trim();
|
||||
if (!command) {
|
||||
continue;
|
||||
}
|
||||
addUsage(commandUsage, command, relativePath);
|
||||
}
|
||||
|
||||
return commandUsage;
|
||||
}
|
||||
|
||||
function extractBalancedBlock(sourceCode, startIndex, openChar, closeChar) {
|
||||
let depth = 0;
|
||||
let inSingleQuote = false;
|
||||
@@ -309,6 +343,12 @@ function printCommandGroup(title, commands, usageMap) {
|
||||
|
||||
function main() {
|
||||
const frontendUsage = collectFrontendCommandUsage();
|
||||
const agentRuntimeSchemaUsage = collectAgentRuntimeSchemaUsage();
|
||||
for (const [command, files] of agentRuntimeSchemaUsage.entries()) {
|
||||
for (const file of files) {
|
||||
addUsage(frontendUsage, command, file);
|
||||
}
|
||||
}
|
||||
const frontendCommands = new Set(frontendUsage.keys());
|
||||
const registeredCommands = collectRegisteredCommands();
|
||||
const mockPriorityCommands = collectMockPriorityCommands();
|
||||
|
||||
@@ -0,0 +1,276 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const projectRoot = path.resolve(__dirname, "..");
|
||||
const schemaPath = path.join(
|
||||
projectRoot,
|
||||
"src/lib/governance/agentRuntimeCommandSchema.json",
|
||||
);
|
||||
const catalogPath = path.join(
|
||||
projectRoot,
|
||||
"src/lib/governance/agentCommandCatalog.json",
|
||||
);
|
||||
const outputPath = path.join(
|
||||
projectRoot,
|
||||
"src/lib/api/agentRuntime/commandManifest.generated.ts",
|
||||
);
|
||||
|
||||
const allowedDomains = new Set([
|
||||
"thread",
|
||||
"session",
|
||||
"export",
|
||||
"inventory",
|
||||
"subagent",
|
||||
]);
|
||||
const allowedLifecycles = new Set(["current", "compat", "deprecated"]);
|
||||
const allowedMockStrategies = new Set([
|
||||
"default-mock",
|
||||
"mock-priority",
|
||||
"bridge-only",
|
||||
]);
|
||||
|
||||
function toCommandKey(command) {
|
||||
return command
|
||||
.replace(/^agent_runtime_/, "")
|
||||
.split("_")
|
||||
.map((segment, index) =>
|
||||
index === 0
|
||||
? segment
|
||||
: segment.charAt(0).toUpperCase() + segment.slice(1),
|
||||
)
|
||||
.join("");
|
||||
}
|
||||
|
||||
async function readJson(jsonPath) {
|
||||
const raw = await fs.readFile(jsonPath, "utf8");
|
||||
return JSON.parse(raw);
|
||||
}
|
||||
|
||||
function validateSchema(schema) {
|
||||
const commands = Array.isArray(schema?.commands) ? schema.commands : [];
|
||||
if (commands.length === 0) {
|
||||
throw new Error("agentRuntimeCommandSchema.json 不能为空");
|
||||
}
|
||||
|
||||
const seenCommands = new Set();
|
||||
const seenKeys = new Set();
|
||||
|
||||
for (const entry of commands) {
|
||||
const command = String(entry?.command ?? "").trim();
|
||||
const domain = String(entry?.domain ?? "").trim();
|
||||
const requestType = String(entry?.requestType ?? "").trim();
|
||||
const responseType = String(entry?.responseType ?? "").trim();
|
||||
const lifecycle = String(entry?.lifecycle ?? "").trim();
|
||||
const mockStrategy = String(entry?.mockStrategy ?? "").trim();
|
||||
const docsSection = String(entry?.docsSection ?? "").trim();
|
||||
const key = toCommandKey(command);
|
||||
|
||||
if (!command.startsWith("agent_runtime_")) {
|
||||
throw new Error(`schema 命令必须以 agent_runtime_ 开头: ${command}`);
|
||||
}
|
||||
if (seenCommands.has(command)) {
|
||||
throw new Error(`schema 命令重复: ${command}`);
|
||||
}
|
||||
if (seenKeys.has(key)) {
|
||||
throw new Error(`schema 命令 key 重复: ${key}`);
|
||||
}
|
||||
if (!allowedDomains.has(domain)) {
|
||||
throw new Error(`schema domain 非法: ${domain}`);
|
||||
}
|
||||
if (!requestType) {
|
||||
throw new Error(`schema 缺少 requestType: ${command}`);
|
||||
}
|
||||
if (!responseType) {
|
||||
throw new Error(`schema 缺少 responseType: ${command}`);
|
||||
}
|
||||
if (!allowedLifecycles.has(lifecycle)) {
|
||||
throw new Error(`schema lifecycle 非法: ${command} -> ${lifecycle}`);
|
||||
}
|
||||
if (!allowedMockStrategies.has(mockStrategy)) {
|
||||
throw new Error(
|
||||
`schema mockStrategy 非法: ${command} -> ${mockStrategy}`,
|
||||
);
|
||||
}
|
||||
if (!docsSection) {
|
||||
throw new Error(`schema 缺少 docsSection: ${command}`);
|
||||
}
|
||||
|
||||
seenCommands.add(command);
|
||||
seenKeys.add(key);
|
||||
}
|
||||
|
||||
return commands.map((entry) => ({
|
||||
command: String(entry.command),
|
||||
key: toCommandKey(String(entry.command)),
|
||||
domain: String(entry.domain),
|
||||
requestType: String(entry.requestType),
|
||||
responseType: String(entry.responseType),
|
||||
lifecycle: String(entry.lifecycle),
|
||||
mockStrategy: String(entry.mockStrategy),
|
||||
docsSection: String(entry.docsSection),
|
||||
}));
|
||||
}
|
||||
|
||||
function assertCatalogMatchesSchema(schemaEntries, catalog) {
|
||||
const schemaCommands = schemaEntries.map((entry) => entry.command);
|
||||
const catalogCommands = Array.isArray(catalog?.runtimeGatewayCommands)
|
||||
? catalog.runtimeGatewayCommands.filter((command) =>
|
||||
String(command).startsWith("agent_runtime_"),
|
||||
)
|
||||
: [];
|
||||
|
||||
const schemaSet = new Set(schemaCommands);
|
||||
const catalogSet = new Set(catalogCommands);
|
||||
const missingFromCatalog = schemaCommands.filter(
|
||||
(command) => !catalogSet.has(command),
|
||||
);
|
||||
const extraInCatalog = catalogCommands.filter(
|
||||
(command) => !schemaSet.has(command),
|
||||
);
|
||||
|
||||
if (missingFromCatalog.length === 0 && extraInCatalog.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const lines = [];
|
||||
if (missingFromCatalog.length > 0) {
|
||||
lines.push("catalog 缺少以下 agent_runtime 命令:");
|
||||
for (const command of missingFromCatalog) {
|
||||
lines.push(`- ${command}`);
|
||||
}
|
||||
}
|
||||
if (extraInCatalog.length > 0) {
|
||||
lines.push("catalog 多出以下未进入 schema 的 agent_runtime 命令:");
|
||||
for (const command of extraInCatalog) {
|
||||
lines.push(`- ${command}`);
|
||||
}
|
||||
}
|
||||
throw new Error(lines.join("\n"));
|
||||
}
|
||||
|
||||
function buildGeneratedOutput(schemaEntries) {
|
||||
const domains = [...allowedDomains];
|
||||
const lifecycles = [...allowedLifecycles];
|
||||
const mockStrategies = [...allowedMockStrategies];
|
||||
|
||||
const commandObjectBody = schemaEntries
|
||||
.map((entry) => ` ${entry.key}: ${JSON.stringify(entry.command)},`)
|
||||
.join("\n");
|
||||
|
||||
const descriptorBody = schemaEntries
|
||||
.map(
|
||||
(entry) => ` {
|
||||
key: ${JSON.stringify(entry.key)},
|
||||
command: AGENT_RUNTIME_COMMANDS.${entry.key},
|
||||
domain: ${JSON.stringify(entry.domain)},
|
||||
requestType: ${JSON.stringify(entry.requestType)},
|
||||
responseType: ${JSON.stringify(entry.responseType)},
|
||||
lifecycle: ${JSON.stringify(entry.lifecycle)},
|
||||
mockStrategy: ${JSON.stringify(entry.mockStrategy)},
|
||||
docsSection: ${JSON.stringify(entry.docsSection)},
|
||||
},`,
|
||||
)
|
||||
.join("\n");
|
||||
|
||||
const namesBody = schemaEntries
|
||||
.map((entry) => ` AGENT_RUNTIME_COMMANDS.${entry.key},`)
|
||||
.join("\n");
|
||||
|
||||
const byDomainBody = domains
|
||||
.map((domain) => {
|
||||
const entries = schemaEntries.filter((entry) => entry.domain === domain);
|
||||
const lines = entries.map(
|
||||
(entry) => ` AGENT_RUNTIME_COMMANDS.${entry.key},`,
|
||||
);
|
||||
return ` ${JSON.stringify(domain)}: [\n${lines.join("\n")}\n ],`;
|
||||
})
|
||||
.join("\n");
|
||||
|
||||
return `/**
|
||||
* 由 scripts/generate-agent-runtime-clients.mjs 自动生成,请勿手改。
|
||||
*/
|
||||
|
||||
export const AGENT_RUNTIME_COMMANDS = {
|
||||
${commandObjectBody}
|
||||
} as const;
|
||||
|
||||
export type AgentRuntimeCommandKey = keyof typeof AGENT_RUNTIME_COMMANDS;
|
||||
export type AgentRuntimeCommandName =
|
||||
(typeof AGENT_RUNTIME_COMMANDS)[AgentRuntimeCommandKey];
|
||||
export type AgentRuntimeCommandDomain = ${domains
|
||||
.map((value) => JSON.stringify(value))
|
||||
.join(" | ")};
|
||||
export type AgentRuntimeCommandLifecycle = ${lifecycles
|
||||
.map((value) => JSON.stringify(value))
|
||||
.join(" | ")};
|
||||
export type AgentRuntimeCommandMockStrategy = ${mockStrategies
|
||||
.map((value) => JSON.stringify(value))
|
||||
.join(" | ")};
|
||||
|
||||
export interface AgentRuntimeCommandDescriptor {
|
||||
readonly key: AgentRuntimeCommandKey;
|
||||
readonly command: AgentRuntimeCommandName;
|
||||
readonly domain: AgentRuntimeCommandDomain;
|
||||
readonly requestType: string;
|
||||
readonly responseType: string;
|
||||
readonly lifecycle: AgentRuntimeCommandLifecycle;
|
||||
readonly mockStrategy: AgentRuntimeCommandMockStrategy;
|
||||
readonly docsSection: string;
|
||||
}
|
||||
|
||||
export const AGENT_RUNTIME_COMMAND_DESCRIPTORS = [
|
||||
${descriptorBody}
|
||||
] as const satisfies readonly AgentRuntimeCommandDescriptor[];
|
||||
|
||||
export const AGENT_RUNTIME_COMMAND_NAMES = [
|
||||
${namesBody}
|
||||
] as const satisfies readonly AgentRuntimeCommandName[];
|
||||
|
||||
export const AGENT_RUNTIME_COMMANDS_BY_DOMAIN = {
|
||||
${byDomainBody}
|
||||
} as const satisfies Record<
|
||||
AgentRuntimeCommandDomain,
|
||||
readonly AgentRuntimeCommandName[]
|
||||
>;
|
||||
`;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const schema = await readJson(schemaPath);
|
||||
const catalog = await readJson(catalogPath);
|
||||
const schemaEntries = validateSchema(schema);
|
||||
assertCatalogMatchesSchema(schemaEntries, catalog);
|
||||
|
||||
const output = buildGeneratedOutput(schemaEntries);
|
||||
const checkOnly = process.argv.includes("--check");
|
||||
|
||||
let current = "";
|
||||
try {
|
||||
current = await fs.readFile(outputPath, "utf8");
|
||||
} catch (error) {
|
||||
if (error?.code !== "ENOENT") {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
if (checkOnly) {
|
||||
if (current !== output) {
|
||||
throw new Error(
|
||||
"agent runtime generated manifest 已过期,请先运行 npm run generate:agent-runtime-clients",
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
await fs.mkdir(path.dirname(outputPath), { recursive: true });
|
||||
await fs.writeFile(outputPath, output, "utf8");
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(
|
||||
`[generate-agent-runtime-clients] ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
@@ -298,9 +298,13 @@ async function resolveStartupMode(options) {
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`[verify:gui-smoke] 检测到已有前端 dev server 正在占用 ${options.appUrl},但 DevBridge 尚未就绪(${options.healthUrl})。请先关闭现有 dev server,或启动完整 headless 环境后使用 --reuse-running。`,
|
||||
console.log(
|
||||
`[verify:gui-smoke] 检测到已有前端 dev server 正在占用 ${options.appUrl},但 DevBridge 尚未就绪;将继续拉起 headless Tauri,并复用现有前端壳。`,
|
||||
);
|
||||
return {
|
||||
shouldStart: true,
|
||||
reusedExisting: false,
|
||||
};
|
||||
}
|
||||
|
||||
async function main() {
|
||||
|
||||
Reference in New Issue
Block a user