release: v1.7.0

This commit is contained in:
coso
2026-04-11 01:17:15 +08:00
parent 182c0d464a
commit 26751bc117
249 changed files with 23042 additions and 10392 deletions
+50 -16
View File
@@ -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,
},
+41 -1
View File
@@ -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();
+276
View File
@@ -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;
});
+6 -2
View File
@@ -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() {