mirror of
https://github.com/aiclientproxy/proxycast.git
synced 2026-09-24 23:10:56 +08:00
release: v0.92.0
This commit is contained in:
@@ -0,0 +1,427 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import process from "node:process";
|
||||
|
||||
const repoRoot = process.cwd();
|
||||
|
||||
const sourceRoots = ["src"];
|
||||
const sourceExtensions = new Set([".ts", ".tsx", ".js", ".jsx"]);
|
||||
const ignoredDirectories = new Set([
|
||||
".git",
|
||||
".idea",
|
||||
".vscode",
|
||||
"coverage",
|
||||
"dist",
|
||||
"docs",
|
||||
"node_modules",
|
||||
"target",
|
||||
]);
|
||||
|
||||
const frontendCommandPatterns = [
|
||||
/\bsafeInvoke(?:<[^>]+>)?\s*\(\s*["'`]([^"'`]+)["'`]/g,
|
||||
/\binvoke(?:<[^>]+>)?\s*\(\s*["'`]([^"'`]+)["'`]/g,
|
||||
];
|
||||
|
||||
const knownDeferredRegistrationReasons = new Map();
|
||||
|
||||
function normalizePath(filePath) {
|
||||
return filePath.split(path.sep).join("/");
|
||||
}
|
||||
|
||||
function isRuntimeSource(relativePath) {
|
||||
const normalizedPath = normalizePath(relativePath);
|
||||
const extension = path.extname(normalizedPath);
|
||||
if (!sourceExtensions.has(extension)) {
|
||||
return false;
|
||||
}
|
||||
if (normalizedPath.endsWith(".d.ts")) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
normalizedPath.includes("/__tests__/") ||
|
||||
normalizedPath.includes("/__mocks__/") ||
|
||||
/\.test\.[^.]+$/.test(normalizedPath) ||
|
||||
/\.spec\.[^.]+$/.test(normalizedPath)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function walkDirectory(rootDirectory) {
|
||||
const results = [];
|
||||
if (!fs.existsSync(rootDirectory)) {
|
||||
return results;
|
||||
}
|
||||
|
||||
const entries = fs.readdirSync(rootDirectory, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
if (ignoredDirectories.has(entry.name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const absolutePath = path.join(rootDirectory, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
results.push(...walkDirectory(absolutePath));
|
||||
continue;
|
||||
}
|
||||
|
||||
const relativePath = normalizePath(path.relative(repoRoot, absolutePath));
|
||||
if (isRuntimeSource(relativePath)) {
|
||||
results.push(relativePath);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
function addUsage(map, command, relativePath) {
|
||||
if (!map.has(command)) {
|
||||
map.set(command, new Set());
|
||||
}
|
||||
map.get(command).add(relativePath);
|
||||
}
|
||||
|
||||
function extractCommandsFromSource(sourceCode) {
|
||||
const commands = new Set();
|
||||
for (const pattern of frontendCommandPatterns) {
|
||||
for (const match of sourceCode.matchAll(pattern)) {
|
||||
commands.add(match[1]);
|
||||
}
|
||||
}
|
||||
return commands;
|
||||
}
|
||||
|
||||
function collectFrontendCommandUsage() {
|
||||
const commandUsage = new Map();
|
||||
for (const root of sourceRoots) {
|
||||
const absoluteRoot = path.join(repoRoot, root);
|
||||
for (const relativePath of walkDirectory(absoluteRoot)) {
|
||||
const absolutePath = path.join(repoRoot, relativePath);
|
||||
const sourceCode = fs.readFileSync(absolutePath, "utf8");
|
||||
for (const command of extractCommandsFromSource(sourceCode)) {
|
||||
addUsage(commandUsage, command, relativePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
return commandUsage;
|
||||
}
|
||||
|
||||
function extractBalancedBlock(sourceCode, startIndex, openChar, closeChar) {
|
||||
let depth = 0;
|
||||
let inSingleQuote = false;
|
||||
let inDoubleQuote = false;
|
||||
let inTemplateString = false;
|
||||
let inLineComment = false;
|
||||
let inBlockComment = false;
|
||||
let escaped = false;
|
||||
|
||||
for (let index = startIndex; index < sourceCode.length; index += 1) {
|
||||
const currentChar = sourceCode[index];
|
||||
const nextChar = sourceCode[index + 1];
|
||||
|
||||
if (inLineComment) {
|
||||
if (currentChar === "\n") {
|
||||
inLineComment = false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (inBlockComment) {
|
||||
if (currentChar === "*" && nextChar === "/") {
|
||||
inBlockComment = false;
|
||||
index += 1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (inSingleQuote) {
|
||||
if (!escaped && currentChar === "'") {
|
||||
inSingleQuote = false;
|
||||
}
|
||||
escaped = !escaped && currentChar === "\\";
|
||||
continue;
|
||||
}
|
||||
|
||||
if (inDoubleQuote) {
|
||||
if (!escaped && currentChar === '"') {
|
||||
inDoubleQuote = false;
|
||||
}
|
||||
escaped = !escaped && currentChar === "\\";
|
||||
continue;
|
||||
}
|
||||
|
||||
if (inTemplateString) {
|
||||
if (!escaped && currentChar === "`") {
|
||||
inTemplateString = false;
|
||||
}
|
||||
escaped = !escaped && currentChar === "\\";
|
||||
continue;
|
||||
}
|
||||
|
||||
if (currentChar === "/" && nextChar === "/") {
|
||||
inLineComment = true;
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (currentChar === "/" && nextChar === "*") {
|
||||
inBlockComment = true;
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (currentChar === "'") {
|
||||
inSingleQuote = true;
|
||||
escaped = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (currentChar === '"') {
|
||||
inDoubleQuote = true;
|
||||
escaped = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (currentChar === "`") {
|
||||
inTemplateString = true;
|
||||
escaped = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (currentChar === openChar) {
|
||||
depth += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (currentChar === closeChar) {
|
||||
depth -= 1;
|
||||
if (depth === 0) {
|
||||
return sourceCode.slice(startIndex + 1, index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`无法提取 ${openChar}${closeChar} 平衡块`);
|
||||
}
|
||||
|
||||
function collectRegisteredCommands() {
|
||||
const runnerPath = path.join(repoRoot, "src-tauri/src/app/runner.rs");
|
||||
const sourceCode = fs.readFileSync(runnerPath, "utf8");
|
||||
const marker = "tauri::generate_handler![";
|
||||
const markerIndex = sourceCode.indexOf(marker);
|
||||
if (markerIndex < 0) {
|
||||
throw new Error("未找到 tauri::generate_handler! 注册块");
|
||||
}
|
||||
|
||||
const bracketStart = markerIndex + marker.length - 1;
|
||||
const handlerBody = extractBalancedBlock(sourceCode, bracketStart, "[", "]");
|
||||
const registeredCommands = new Set();
|
||||
const withoutBlockComments = handlerBody.replace(/\/\*[\s\S]*?\*\//g, "");
|
||||
|
||||
for (const line of withoutBlockComments.split("\n")) {
|
||||
const trimmedLine = line.replace(/\/\/.*$/, "").trim();
|
||||
if (!trimmedLine) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const match = trimmedLine.match(/^([A-Za-z0-9_:]+)\s*,?$/);
|
||||
if (!match) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const fullPath = match[1];
|
||||
const command = fullPath.split("::").pop();
|
||||
if (command) {
|
||||
registeredCommands.add(command);
|
||||
}
|
||||
}
|
||||
|
||||
return registeredCommands;
|
||||
}
|
||||
|
||||
function collectMockPriorityCommands() {
|
||||
const filePath = path.join(
|
||||
repoRoot,
|
||||
"src/lib/dev-bridge/mockPriorityCommands.ts",
|
||||
);
|
||||
const sourceCode = fs.readFileSync(filePath, "utf8");
|
||||
const match = sourceCode.match(
|
||||
/const mockPriorityCommands = new Set<string>\(\[([\s\S]*?)\]\);/,
|
||||
);
|
||||
if (!match) {
|
||||
throw new Error("未找到 mockPriorityCommands 定义");
|
||||
}
|
||||
|
||||
const commands = new Set();
|
||||
for (const stringMatch of match[1].matchAll(/["'`]([^"'`]+)["'`]/g)) {
|
||||
commands.add(stringMatch[1]);
|
||||
}
|
||||
return commands;
|
||||
}
|
||||
|
||||
function collectDefaultMockCommands() {
|
||||
const filePath = path.join(repoRoot, "src/lib/tauri-mock/core.ts");
|
||||
const sourceCode = fs.readFileSync(filePath, "utf8");
|
||||
const marker = "const defaultMocks: Record<string, any> = {";
|
||||
const markerIndex = sourceCode.indexOf(marker);
|
||||
if (markerIndex < 0) {
|
||||
throw new Error("未找到 tauri-mock defaultMocks 定义");
|
||||
}
|
||||
|
||||
const braceStart = markerIndex + marker.length - 1;
|
||||
const objectBody = extractBalancedBlock(sourceCode, braceStart, "{", "}");
|
||||
const mockCommands = new Set();
|
||||
|
||||
for (const match of objectBody.matchAll(/^ ([A-Za-z0-9_]+)\s*:/gm)) {
|
||||
mockCommands.add(match[1]);
|
||||
}
|
||||
|
||||
return mockCommands;
|
||||
}
|
||||
|
||||
function readAgentCommandCatalog() {
|
||||
const catalogPath = path.join(
|
||||
repoRoot,
|
||||
"src/lib/governance/agentCommandCatalog.json",
|
||||
);
|
||||
return JSON.parse(fs.readFileSync(catalogPath, "utf8"));
|
||||
}
|
||||
|
||||
function sortCommands(commands) {
|
||||
return [...commands].sort((left, right) => left.localeCompare(right));
|
||||
}
|
||||
|
||||
function printCommandGroup(title, commands, usageMap) {
|
||||
console.error(`\n## ${title}`);
|
||||
for (const command of sortCommands(commands)) {
|
||||
console.error(`- ${command}`);
|
||||
if (usageMap?.has(command)) {
|
||||
const files = sortCommands(usageMap.get(command));
|
||||
for (const file of files) {
|
||||
console.error(` - ${file}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function main() {
|
||||
const frontendUsage = collectFrontendCommandUsage();
|
||||
const frontendCommands = new Set(frontendUsage.keys());
|
||||
const registeredCommands = collectRegisteredCommands();
|
||||
const mockPriorityCommands = collectMockPriorityCommands();
|
||||
const defaultMockCommands = collectDefaultMockCommands();
|
||||
const agentCommandCatalog = readAgentCommandCatalog();
|
||||
|
||||
const deprecatedCommands = new Set(
|
||||
Object.keys(agentCommandCatalog.deprecatedCommandReplacements ?? {}),
|
||||
);
|
||||
const runtimeGatewayCommands = new Set(
|
||||
agentCommandCatalog.runtimeGatewayCommands ?? [],
|
||||
);
|
||||
|
||||
const deferredCommands = new Set(knownDeferredRegistrationReasons.keys());
|
||||
|
||||
const missingRegistrations = new Set(
|
||||
[...frontendCommands].filter(
|
||||
(command) =>
|
||||
!registeredCommands.has(command) && !deferredCommands.has(command),
|
||||
),
|
||||
);
|
||||
const deprecatedCommandsStillUsed = new Set(
|
||||
[...frontendCommands].filter((command) => deprecatedCommands.has(command)),
|
||||
);
|
||||
const mockPriorityMissingMocks = new Set(
|
||||
[...mockPriorityCommands].filter(
|
||||
(command) => !defaultMockCommands.has(command),
|
||||
),
|
||||
);
|
||||
const mockPriorityMissingRegistrations = new Set(
|
||||
[...mockPriorityCommands].filter(
|
||||
(command) =>
|
||||
!registeredCommands.has(command) && !deferredCommands.has(command),
|
||||
),
|
||||
);
|
||||
const runtimeGatewayMissingRegistrations = new Set(
|
||||
[...runtimeGatewayCommands].filter(
|
||||
(command) =>
|
||||
!registeredCommands.has(command) && !deferredCommands.has(command),
|
||||
),
|
||||
);
|
||||
|
||||
console.log("[command-contracts] frontend commands:", frontendCommands.size);
|
||||
console.log(
|
||||
"[command-contracts] rust registered commands:",
|
||||
registeredCommands.size,
|
||||
);
|
||||
console.log(
|
||||
"[command-contracts] mock priority commands:",
|
||||
mockPriorityCommands.size,
|
||||
);
|
||||
console.log(
|
||||
"[command-contracts] default mock commands:",
|
||||
defaultMockCommands.size,
|
||||
);
|
||||
|
||||
if (knownDeferredRegistrationReasons.size > 0) {
|
||||
console.log("\n[command-contracts] 已登记的延期命令:");
|
||||
for (const command of sortCommands(
|
||||
knownDeferredRegistrationReasons.keys(),
|
||||
)) {
|
||||
console.log(`- ${command}`);
|
||||
console.log(` ${knownDeferredRegistrationReasons.get(command)}`);
|
||||
}
|
||||
}
|
||||
|
||||
let hasError = false;
|
||||
|
||||
if (missingRegistrations.size > 0) {
|
||||
hasError = true;
|
||||
printCommandGroup(
|
||||
"前端调用但未注册的命令",
|
||||
missingRegistrations,
|
||||
frontendUsage,
|
||||
);
|
||||
}
|
||||
|
||||
if (deprecatedCommandsStillUsed.size > 0) {
|
||||
hasError = true;
|
||||
printCommandGroup(
|
||||
"前端仍在调用的废弃命令",
|
||||
deprecatedCommandsStillUsed,
|
||||
frontendUsage,
|
||||
);
|
||||
}
|
||||
|
||||
if (mockPriorityMissingMocks.size > 0) {
|
||||
hasError = true;
|
||||
printCommandGroup("mock 优先命令缺少 mock 实现", mockPriorityMissingMocks);
|
||||
}
|
||||
|
||||
if (mockPriorityMissingRegistrations.size > 0) {
|
||||
hasError = true;
|
||||
printCommandGroup(
|
||||
"mock 优先命令缺少 Rust 注册",
|
||||
mockPriorityMissingRegistrations,
|
||||
);
|
||||
}
|
||||
|
||||
if (runtimeGatewayMissingRegistrations.size > 0) {
|
||||
hasError = true;
|
||||
printCommandGroup(
|
||||
"runtime gateway 命令缺少 Rust 注册",
|
||||
runtimeGatewayMissingRegistrations,
|
||||
);
|
||||
}
|
||||
|
||||
if (hasError) {
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("\n[command-contracts] 所有命令契约检查通过。");
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -0,0 +1,310 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { execFileSync, spawnSync } from "node:child_process";
|
||||
import process from "node:process";
|
||||
|
||||
const options = parseArgs(process.argv.slice(2));
|
||||
const rootDir = process.cwd();
|
||||
|
||||
const npmCommand = process.platform === "win32" ? "npm.cmd" : "npm";
|
||||
const cargoCommand = process.platform === "win32" ? "cargo.exe" : "cargo";
|
||||
const gitCommand = process.platform === "win32" ? "git.exe" : "git";
|
||||
|
||||
const FRONTEND_ROOT_FILES = new Set([
|
||||
"package.json",
|
||||
"package-lock.json",
|
||||
"vite.config.ts",
|
||||
"tsconfig.json",
|
||||
"tsconfig.node.json",
|
||||
"eslint.config.js",
|
||||
"tailwind.config.js",
|
||||
"postcss.config.js",
|
||||
"index.html",
|
||||
]);
|
||||
|
||||
const BRIDGE_FILES = new Set([
|
||||
"vite.config.ts",
|
||||
"scripts/check-dev-bridge-health.mjs",
|
||||
"scripts/social-workbench-e2e-smoke.mjs",
|
||||
"scripts/chrome-bridge-e2e.mjs",
|
||||
"docs/aiprompts/playwright-e2e.md",
|
||||
]);
|
||||
|
||||
function parseArgs(argv) {
|
||||
const result = {
|
||||
full: false,
|
||||
staged: false,
|
||||
base: "",
|
||||
help: false,
|
||||
};
|
||||
|
||||
for (let index = 0; index < argv.length; index += 1) {
|
||||
const arg = argv[index];
|
||||
if (arg === "--full") {
|
||||
result.full = true;
|
||||
continue;
|
||||
}
|
||||
if (arg === "--staged") {
|
||||
result.staged = true;
|
||||
continue;
|
||||
}
|
||||
if (arg === "--base" && argv[index + 1]) {
|
||||
result.base = String(argv[index + 1]).trim();
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
if (arg === "--help" || arg === "-h") {
|
||||
result.help = true;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function printHelp() {
|
||||
console.log(`
|
||||
Lime 本地校验入口
|
||||
|
||||
用法:
|
||||
npm run verify:local
|
||||
npm run verify:local -- --staged
|
||||
npm run verify:local -- --base origin/main
|
||||
npm run verify:local:full
|
||||
|
||||
选项:
|
||||
--full 忽略改动检测,执行全量本地校验
|
||||
--staged 仅基于已暂存文件判断要跑的检查
|
||||
--base REF 基于指定基线计算改动文件
|
||||
-h, --help 显示帮助
|
||||
`);
|
||||
}
|
||||
|
||||
function runCommand(command, args) {
|
||||
console.log(`\n[local-ci] > ${command} ${args.join(" ")}`);
|
||||
const result = spawnSync(command, args, {
|
||||
cwd: rootDir,
|
||||
stdio: "inherit",
|
||||
env: process.env,
|
||||
});
|
||||
|
||||
if (typeof result.status === "number" && result.status !== 0) {
|
||||
process.exit(result.status);
|
||||
}
|
||||
|
||||
if (result.error) {
|
||||
throw result.error;
|
||||
}
|
||||
}
|
||||
|
||||
function gitOutput(args) {
|
||||
try {
|
||||
return execFileSync(gitCommand, args, {
|
||||
cwd: rootDir,
|
||||
encoding: "utf8",
|
||||
stdio: ["ignore", "pipe", "ignore"],
|
||||
}).trim();
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
function splitLines(value) {
|
||||
if (!value) {
|
||||
return [];
|
||||
}
|
||||
return value
|
||||
.split("\n")
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function resolveDiffBase() {
|
||||
if (options.base) {
|
||||
return options.base;
|
||||
}
|
||||
|
||||
const upstream = gitOutput(["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{upstream}"]);
|
||||
if (upstream) {
|
||||
return upstream;
|
||||
}
|
||||
|
||||
for (const candidate of ["origin/main", "origin/master", "main", "master"]) {
|
||||
const exists = gitOutput(["rev-parse", "--verify", candidate]);
|
||||
if (exists) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
function collectChangedFiles() {
|
||||
if (options.full) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (options.staged) {
|
||||
return uniquePaths(
|
||||
splitLines(gitOutput(["diff", "--cached", "--name-only", "--diff-filter=ACMR"])),
|
||||
);
|
||||
}
|
||||
|
||||
const base = resolveDiffBase();
|
||||
const candidates = [];
|
||||
|
||||
if (base) {
|
||||
candidates.push(
|
||||
...splitLines(gitOutput(["diff", "--name-only", "--diff-filter=ACMR", `${base}...HEAD`])),
|
||||
);
|
||||
}
|
||||
|
||||
candidates.push(
|
||||
...splitLines(gitOutput(["diff", "--name-only", "--diff-filter=ACMR", "HEAD"])),
|
||||
);
|
||||
candidates.push(
|
||||
...splitLines(gitOutput(["ls-files", "--others", "--exclude-standard"])),
|
||||
);
|
||||
|
||||
return uniquePaths(candidates);
|
||||
}
|
||||
|
||||
function uniquePaths(paths) {
|
||||
return Array.from(new Set(paths));
|
||||
}
|
||||
|
||||
function isFrontendChange(file) {
|
||||
return (
|
||||
file.startsWith("src/") ||
|
||||
FRONTEND_ROOT_FILES.has(file)
|
||||
);
|
||||
}
|
||||
|
||||
function isRustChange(file) {
|
||||
return file.startsWith("src-tauri/");
|
||||
}
|
||||
|
||||
function isBridgeChange(file) {
|
||||
return (
|
||||
file.startsWith("src/lib/dev-bridge/") ||
|
||||
file.startsWith("src/lib/tauri-mock/") ||
|
||||
BRIDGE_FILES.has(file)
|
||||
);
|
||||
}
|
||||
|
||||
function isDocsOnlyChange(files) {
|
||||
return files.length > 0 && files.every((file) => file.startsWith("docs/"));
|
||||
}
|
||||
|
||||
function detectTasks(changedFiles) {
|
||||
if (options.full) {
|
||||
return {
|
||||
frontend: true,
|
||||
rust: true,
|
||||
bridge: true,
|
||||
};
|
||||
}
|
||||
|
||||
if (changedFiles.length === 0) {
|
||||
return {
|
||||
frontend: true,
|
||||
rust: true,
|
||||
bridge: true,
|
||||
fallback: true,
|
||||
};
|
||||
}
|
||||
|
||||
if (isDocsOnlyChange(changedFiles)) {
|
||||
return {
|
||||
frontend: false,
|
||||
rust: false,
|
||||
bridge: false,
|
||||
docsOnly: true,
|
||||
};
|
||||
}
|
||||
|
||||
const frontend = changedFiles.some(isFrontendChange);
|
||||
const rust = changedFiles.some(isRustChange);
|
||||
const bridge = changedFiles.some(isBridgeChange);
|
||||
|
||||
return {
|
||||
frontend,
|
||||
rust,
|
||||
bridge,
|
||||
};
|
||||
}
|
||||
|
||||
function printSummary(changedFiles, tasks) {
|
||||
console.log("[local-ci] 模式:", options.full ? "full" : "smart");
|
||||
if (!options.full) {
|
||||
console.log("[local-ci] 检测到改动文件数:", changedFiles.length);
|
||||
if (changedFiles.length > 0) {
|
||||
const preview = changedFiles.slice(0, 12);
|
||||
for (const file of preview) {
|
||||
console.log(`[local-ci] - ${file}`);
|
||||
}
|
||||
if (changedFiles.length > preview.length) {
|
||||
console.log(`[local-ci] ... 其余 ${changedFiles.length - preview.length} 个文件省略`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (tasks.docsOnly) {
|
||||
console.log("[local-ci] 当前仅检测到文档改动,跳过本地代码校验。");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("[local-ci] 计划执行:");
|
||||
if (tasks.frontend) {
|
||||
console.log("[local-ci] - 前端校验");
|
||||
}
|
||||
if (tasks.bridge) {
|
||||
console.log("[local-ci] - bridge 校验");
|
||||
}
|
||||
if (tasks.rust) {
|
||||
console.log("[local-ci] - Rust 校验");
|
||||
}
|
||||
if (tasks.fallback) {
|
||||
console.log("[local-ci] - 未检测到改动,执行全量兜底校验");
|
||||
}
|
||||
}
|
||||
|
||||
function runSelectedTasks(tasks) {
|
||||
if (tasks.docsOnly) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (tasks.frontend) {
|
||||
runCommand(npmCommand, ["run", "lint"]);
|
||||
runCommand(npmCommand, ["run", "typecheck"]);
|
||||
runCommand(npmCommand, ["test"]);
|
||||
}
|
||||
|
||||
if (tasks.bridge) {
|
||||
if (!tasks.frontend) {
|
||||
runCommand(npmCommand, ["run", "test:bridge"]);
|
||||
}
|
||||
runCommand(npmCommand, ["run", "test:contracts"]);
|
||||
}
|
||||
|
||||
if (tasks.rust) {
|
||||
runCommand(cargoCommand, ["test", "--manifest-path", "src-tauri/Cargo.toml"]);
|
||||
if (options.full) {
|
||||
runCommand(cargoCommand, ["clippy", "--manifest-path", "src-tauri/Cargo.toml"]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function main() {
|
||||
if (options.help) {
|
||||
printHelp();
|
||||
return;
|
||||
}
|
||||
|
||||
const changedFiles = collectChangedFiles();
|
||||
const tasks = detectTasks(changedFiles);
|
||||
printSummary(changedFiles, tasks);
|
||||
runSelectedTasks(tasks);
|
||||
console.log("\n[local-ci] 本地校验完成。");
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -172,6 +172,27 @@ const importSurfaceMonitors = [
|
||||
targets: ["src/lib/api/contextMemory.ts"],
|
||||
allowedPaths: [],
|
||||
},
|
||||
{
|
||||
id: "team-subagent-scheduler-hook",
|
||||
classification: "compat",
|
||||
description: "旧 SubAgent scheduler Hook 只允许停留在 compat 展示层",
|
||||
targets: ["src/hooks/useSubAgentScheduler.ts"],
|
||||
allowedPaths: [
|
||||
"src/components/agent/chat/hooks/useCompatSubagentRuntime.ts",
|
||||
"src/components/subagent/SubAgentProgress.tsx",
|
||||
"src/components/subagent/index.ts",
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "team-subagent-scheduler-api",
|
||||
classification: "compat",
|
||||
description: "旧 SubAgent scheduler API 只允许被 compat Hook 与降级展示层引用",
|
||||
targets: ["src/lib/api/subAgentScheduler.ts"],
|
||||
allowedPaths: [
|
||||
"src/hooks/useSubAgentScheduler.ts",
|
||||
"src/components/agent/chat/utils/compatSubagentRuntime.ts",
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const commandSurfaceMonitors = [
|
||||
@@ -293,10 +314,24 @@ const commandSurfaceMonitors = [
|
||||
],
|
||||
allowedPaths: [],
|
||||
},
|
||||
{
|
||||
id: "team-subagent-scheduler-commands",
|
||||
classification: "compat",
|
||||
description: "旧 execute_subagent_tasks/cancel_subagent_tasks 只允许通过 compat API 网关暴露",
|
||||
commands: ["execute_subagent_tasks", "cancel_subagent_tasks"],
|
||||
allowedPaths: ["src/lib/api/subAgentScheduler.ts"],
|
||||
},
|
||||
];
|
||||
|
||||
const frontendTextSurfaceMonitors = [
|
||||
...agentLegacyHelperSurfaceMonitors,
|
||||
{
|
||||
id: "frontend-subagent-scheduler-event-bus",
|
||||
classification: "compat",
|
||||
description: "旧 subagent scheduler 事件名只允许 compat Hook 持有",
|
||||
patterns: ["subagent-scheduler-event"],
|
||||
allowedPaths: ["src/hooks/useSubAgentScheduler.ts"],
|
||||
},
|
||||
{
|
||||
id: "frontend-assistant-settings-surfaces",
|
||||
classification: "dead-candidate",
|
||||
@@ -323,6 +358,13 @@ const frontendTextSurfaceMonitors = [
|
||||
];
|
||||
|
||||
const rustTextSurfaceMonitors = [
|
||||
{
|
||||
id: "rust-subagent-scheduler-event-bus",
|
||||
classification: "compat",
|
||||
description: "旧 subagent scheduler 事件名只允许 compat Rust emitter 持有",
|
||||
patterns: ["subagent-scheduler-event"],
|
||||
allowedPaths: ["src-tauri/src/agent/subagent_scheduler.rs"],
|
||||
},
|
||||
{
|
||||
id: "rust-general-chat-dao",
|
||||
classification: "dead-candidate",
|
||||
@@ -378,7 +420,8 @@ const rustTextSurfaceMonitors = [
|
||||
{
|
||||
id: "rust-legacy-general-module-imports",
|
||||
classification: "dead-candidate",
|
||||
description: "已零引用的 Rust 外部模块 direct pending/legacy general 子模块",
|
||||
description:
|
||||
"已零引用的 Rust 外部模块 direct pending/legacy general 子模块",
|
||||
patterns: [
|
||||
"crate::database::legacy_general_chat::",
|
||||
"lime_core::database::legacy_general_chat::",
|
||||
@@ -441,7 +484,8 @@ const rustTextSurfaceMonitors = [
|
||||
{
|
||||
id: "rust-memory-profile-prompt-helper-leak",
|
||||
classification: "deprecated",
|
||||
description: "低层 build_memory_profile_prompt helper 泄漏到统一装配边界之外",
|
||||
description:
|
||||
"低层 build_memory_profile_prompt helper 泄漏到统一装配边界之外",
|
||||
patterns: ["build_memory_profile_prompt("],
|
||||
includePathPrefixes: ["src-tauri/src"],
|
||||
allowedPaths: ["src-tauri/src/services/memory_profile_prompt_service.rs"],
|
||||
@@ -449,7 +493,8 @@ const rustTextSurfaceMonitors = [
|
||||
{
|
||||
id: "rust-memory-sources-prompt-helper-leak",
|
||||
classification: "deprecated",
|
||||
description: "低层 build_memory_sources_prompt helper 泄漏到统一装配边界之外",
|
||||
description:
|
||||
"低层 build_memory_sources_prompt helper 泄漏到统一装配边界之外",
|
||||
patterns: ["build_memory_sources_prompt("],
|
||||
includePathPrefixes: ["src-tauri/src"],
|
||||
allowedPaths: [
|
||||
@@ -525,7 +570,8 @@ const rustTextSurfaceMonitors = [
|
||||
{
|
||||
id: "rust-skill-runtime-command-bootstrap-leak",
|
||||
classification: "dead-candidate",
|
||||
description: "已零引用的 skill runtime 准备与 provider fallback 回流到 skill_exec_cmd 命令层",
|
||||
description:
|
||||
"已零引用的 skill runtime 准备与 provider fallback 回流到 skill_exec_cmd 命令层",
|
||||
patterns: [
|
||||
"ensure_browser_mcp_tools_registered(",
|
||||
"ensure_social_image_tool_registered(",
|
||||
@@ -540,7 +586,8 @@ const rustTextSurfaceMonitors = [
|
||||
{
|
||||
id: "rust-skill-catalog-command-leak",
|
||||
classification: "dead-candidate",
|
||||
description: "已零引用的 skill catalog 枚举与详情装配回流到 skill_exec_cmd 命令层",
|
||||
description:
|
||||
"已零引用的 skill catalog 枚举与详情装配回流到 skill_exec_cmd 命令层",
|
||||
patterns: [
|
||||
"get_skill_roots(",
|
||||
"load_skills_from_directory(",
|
||||
@@ -557,9 +604,10 @@ const rustTextSurfaceMonitors = [
|
||||
{
|
||||
id: "rust-skill-mode-branch-command-leak",
|
||||
classification: "dead-candidate",
|
||||
description: "已零引用的 skill execution_mode 分支回流到 skill_exec_cmd 命令层",
|
||||
description:
|
||||
"已零引用的 skill execution_mode 分支回流到 skill_exec_cmd 命令层",
|
||||
patterns: [
|
||||
"skill.execution_mode == \"workflow\"",
|
||||
'skill.execution_mode == "workflow"',
|
||||
"!skill.workflow_steps.is_empty()",
|
||||
"execute_skill_workflow(",
|
||||
"execute_skill_prompt(",
|
||||
@@ -581,7 +629,8 @@ const rustTextSurfaceMonitors = [
|
||||
{
|
||||
id: "rust-service-agent-table-query-leak",
|
||||
classification: "dead-candidate",
|
||||
description: "已零引用的 Tauri service 层 direct agent_sessions/agent_messages 查询回流",
|
||||
description:
|
||||
"已零引用的 Tauri service 层 direct agent_sessions/agent_messages 查询回流",
|
||||
patterns: [
|
||||
"FROM agent_sessions s",
|
||||
"FROM agent_messages m",
|
||||
@@ -593,8 +642,12 @@ const rustTextSurfaceMonitors = [
|
||||
{
|
||||
id: "rust-service-model-usage-table-query-leak",
|
||||
classification: "dead-candidate",
|
||||
description: "已零引用的 Tauri service 层 direct model_usage_stats 查询回流",
|
||||
patterns: ["FROM model_usage_stats", "SELECT COUNT(*) FROM model_usage_stats"],
|
||||
description:
|
||||
"已零引用的 Tauri service 层 direct model_usage_stats 查询回流",
|
||||
patterns: [
|
||||
"FROM model_usage_stats",
|
||||
"SELECT COUNT(*) FROM model_usage_stats",
|
||||
],
|
||||
includePathPrefixes: ["src-tauri/src/services"],
|
||||
allowedPaths: [],
|
||||
},
|
||||
@@ -617,12 +670,15 @@ const rustTextSurfaceMonitors = [
|
||||
description: "legacy runtime queue 表名从数据库迁移边界向外扩散",
|
||||
patterns: ["agent_runtime_queued_turns"],
|
||||
includePathPrefixes: ["src-tauri/src", "src-tauri/crates"],
|
||||
allowedPaths: ["src-tauri/crates/core/src/database/agent_runtime_queue_repository.rs"],
|
||||
allowedPaths: [
|
||||
"src-tauri/crates/core/src/database/agent_runtime_queue_repository.rs",
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "rust-agent-runtime-legacy-queue-migration-leak",
|
||||
classification: "dead-candidate",
|
||||
description: "已零引用的 legacy runtime queue 启动迁移 helper 回流到其他模块",
|
||||
description:
|
||||
"已零引用的 legacy runtime queue 启动迁移 helper 回流到其他模块",
|
||||
patterns: ["migrate_legacy_runtime_queue_to_aster_store("],
|
||||
includePathPrefixes: ["src-tauri/src", "src-tauri/crates"],
|
||||
allowedPaths: [],
|
||||
@@ -638,15 +694,20 @@ const rustTextSurfaceMonitors = [
|
||||
{
|
||||
id: "rust-agent-session-structured-todo-helper-bypass",
|
||||
classification: "dead-candidate",
|
||||
description: "已零引用的 Lime 业务层绕过 unified todo helper 直接读取 TodoListState",
|
||||
patterns: ["TodoListState::from_extension_data(", "TodoListState::from_markdown("],
|
||||
description:
|
||||
"已零引用的 Lime 业务层绕过 unified todo helper 直接读取 TodoListState",
|
||||
patterns: [
|
||||
"TodoListState::from_extension_data(",
|
||||
"TodoListState::from_markdown(",
|
||||
],
|
||||
includePathPrefixes: ["src-tauri/src", "src-tauri/crates"],
|
||||
allowedPaths: [],
|
||||
},
|
||||
{
|
||||
id: "rust-services-default-workspace-query-leak",
|
||||
classification: "dead-candidate",
|
||||
description: "已零引用的 services crate direct 默认 workspace root 查询回流",
|
||||
description:
|
||||
"已零引用的 services crate direct 默认 workspace root 查询回流",
|
||||
patterns: ["SELECT root_path FROM workspaces WHERE is_default = 1 LIMIT 1"],
|
||||
includePathPrefixes: ["src-tauri/crates/services/src"],
|
||||
allowedPaths: [],
|
||||
@@ -670,7 +731,9 @@ const rustTextSurfaceMonitors = [
|
||||
"AgentDao::update_working_dir(",
|
||||
"AgentDao::update_execution_strategy(",
|
||||
],
|
||||
allowedPaths: ["src-tauri/crates/core/src/database/agent_session_repository.rs"],
|
||||
allowedPaths: [
|
||||
"src-tauri/crates/core/src/database/agent_session_repository.rs",
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "rust-agent-session-direct-delete",
|
||||
@@ -684,7 +747,9 @@ const rustTextSurfaceMonitors = [
|
||||
classification: "deprecated",
|
||||
description: "Rust 业务层 direct AgentDao::create_session 回流",
|
||||
patterns: ["AgentDao::create_session("],
|
||||
allowedPaths: ["src-tauri/crates/core/src/database/agent_session_repository.rs"],
|
||||
allowedPaths: [
|
||||
"src-tauri/crates/core/src/database/agent_session_repository.rs",
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "rust-agent-dao-row-type-leak",
|
||||
@@ -746,6 +811,92 @@ const rustTextSurfaceMonitors = [
|
||||
includePathPrefixes: ["src-tauri/crates/agent/src/lib.rs"],
|
||||
allowedPaths: [],
|
||||
},
|
||||
{
|
||||
id: "rust-agent-tool-permission-public-module-leak",
|
||||
classification: "dead-candidate",
|
||||
description: "lime-agent 重新对 crate 外暴露旧 tool_permissions 模块",
|
||||
patterns: ["pub mod tool_permissions;"],
|
||||
includePathPrefixes: ["src-tauri/crates/agent/src/lib.rs"],
|
||||
allowedPaths: [],
|
||||
},
|
||||
{
|
||||
id: "rust-agent-shell-security-public-module-leak",
|
||||
classification: "dead-candidate",
|
||||
description: "lime-agent 重新对 crate 外暴露旧 shell_security 模块",
|
||||
patterns: ["pub mod shell_security;"],
|
||||
includePathPrefixes: ["src-tauri/crates/agent/src/lib.rs"],
|
||||
allowedPaths: [],
|
||||
},
|
||||
{
|
||||
id: "rust-agent-tool-permission-module-compiled-leak",
|
||||
classification: "dead-candidate",
|
||||
description: "旧 tool_permissions 模块重新回到 lime-agent lib.rs 编译图",
|
||||
patterns: ["mod tool_permissions;"],
|
||||
includePathPrefixes: ["src-tauri/crates/agent/src/lib.rs"],
|
||||
allowedPaths: [],
|
||||
},
|
||||
{
|
||||
id: "rust-agent-shell-security-module-compiled-leak",
|
||||
classification: "dead-candidate",
|
||||
description: "旧 shell_security 模块重新回到 lime-agent lib.rs 编译图",
|
||||
patterns: ["mod shell_security;"],
|
||||
includePathPrefixes: ["src-tauri/crates/agent/src/lib.rs"],
|
||||
allowedPaths: [],
|
||||
},
|
||||
{
|
||||
id: "rust-agent-tool-permission-root-export-leak",
|
||||
classification: "dead-candidate",
|
||||
description: "lime-agent crate 根重新暴露旧 tool_permissions 类型出口",
|
||||
patterns: ["pub use tool_permissions::"],
|
||||
includePathPrefixes: ["src-tauri/crates/agent/src/lib.rs"],
|
||||
allowedPaths: [],
|
||||
},
|
||||
{
|
||||
id: "rust-agent-shell-security-root-export-leak",
|
||||
classification: "dead-candidate",
|
||||
description: "lime-agent crate 根重新暴露旧 shell_security 类型出口",
|
||||
patterns: ["pub use shell_security::"],
|
||||
includePathPrefixes: ["src-tauri/crates/agent/src/lib.rs"],
|
||||
allowedPaths: [],
|
||||
},
|
||||
{
|
||||
id: "rust-agent-tool-permission-direct-module-usage",
|
||||
classification: "dead-candidate",
|
||||
description:
|
||||
"上层模块重新 direct 依赖 lime_agent::tool_permissions 模块路径",
|
||||
patterns: ["lime_agent::tool_permissions::"],
|
||||
includePathPrefixes: ["src-tauri/src", "src-tauri/crates"],
|
||||
allowedPaths: [],
|
||||
},
|
||||
{
|
||||
id: "rust-agent-shell-security-direct-module-usage",
|
||||
classification: "dead-candidate",
|
||||
description: "上层模块重新 direct 依赖 lime_agent::shell_security 模块路径",
|
||||
patterns: ["lime_agent::shell_security::"],
|
||||
includePathPrefixes: ["src-tauri/src", "src-tauri/crates"],
|
||||
allowedPaths: [],
|
||||
},
|
||||
{
|
||||
id: "rust-agent-tool-permission-root-type-usage",
|
||||
classification: "dead-candidate",
|
||||
description: "上层模块重新 direct 依赖 lime_agent 根导出的旧权限类型",
|
||||
patterns: [
|
||||
"lime_agent::DynamicPermissionCheck",
|
||||
"lime_agent::PermissionBehavior",
|
||||
"lime_agent::ShellSecurityChecker",
|
||||
],
|
||||
includePathPrefixes: ["src-tauri/src", "src-tauri/crates"],
|
||||
allowedPaths: [],
|
||||
},
|
||||
{
|
||||
id: "rust-agent-tool-permission-internal-module-usage",
|
||||
classification: "dead-candidate",
|
||||
description:
|
||||
"lime-agent 内部除兼容壳外重新扩散 crate::tool_permissions 模块依赖",
|
||||
patterns: ["crate::tool_permissions::"],
|
||||
includePathPrefixes: ["src-tauri/crates/agent/src"],
|
||||
allowedPaths: ["src-tauri/crates/agent/src/shell_security.rs"],
|
||||
},
|
||||
{
|
||||
id: "rust-agent-integration-public-module-leak",
|
||||
classification: "dead-candidate",
|
||||
@@ -773,7 +924,8 @@ const rustTextSurfaceMonitors = [
|
||||
{
|
||||
id: "rust-agent-subagent-direct-module-usage",
|
||||
classification: "dead-candidate",
|
||||
description: "应用层重新 direct 依赖 crate::agent::subagent_scheduler 模块路径",
|
||||
description:
|
||||
"应用层重新 direct 依赖 crate::agent::subagent_scheduler 模块路径",
|
||||
patterns: ["crate::agent::subagent_scheduler::"],
|
||||
includePathPrefixes: ["src-tauri/src"],
|
||||
allowedPaths: [],
|
||||
@@ -781,7 +933,8 @@ const rustTextSurfaceMonitors = [
|
||||
{
|
||||
id: "rust-aster-runtime-snapshot-helper-leak",
|
||||
classification: "dead-candidate",
|
||||
description: "已零引用的 Lime 业务层 direct Aster runtime snapshot helper 回流",
|
||||
description:
|
||||
"已零引用的 Lime 业务层 direct Aster runtime snapshot helper 回流",
|
||||
patterns: ["load_session_runtime_snapshot("],
|
||||
includePathPrefixes: ["src-tauri/src", "src-tauri/crates"],
|
||||
allowedPaths: [],
|
||||
@@ -789,7 +942,8 @@ const rustTextSurfaceMonitors = [
|
||||
{
|
||||
id: "rust-aster-runtime-store-leak",
|
||||
classification: "dead-candidate",
|
||||
description: "已零引用的 Lime 业务层 direct Aster shared runtime store 回流",
|
||||
description:
|
||||
"已零引用的 Lime 业务层 direct Aster shared runtime store 回流",
|
||||
patterns: [
|
||||
"shared_thread_runtime_store(",
|
||||
"initialize_shared_thread_runtime_store(",
|
||||
@@ -801,33 +955,43 @@ const rustTextSurfaceMonitors = [
|
||||
{
|
||||
id: "rust-aster-runtime-store-public-require-api-leak",
|
||||
classification: "dead-candidate",
|
||||
description: "Aster runtime support 重新对 crate 外暴露 require_aster_thread_runtime_store",
|
||||
description:
|
||||
"Aster runtime support 重新对 crate 外暴露 require_aster_thread_runtime_store",
|
||||
patterns: ["pub fn require_aster_thread_runtime_store("],
|
||||
includePathPrefixes: ["src-tauri/crates/agent/src/aster_runtime_support.rs"],
|
||||
includePathPrefixes: [
|
||||
"src-tauri/crates/agent/src/aster_runtime_support.rs",
|
||||
],
|
||||
allowedPaths: [],
|
||||
},
|
||||
{
|
||||
id: "rust-aster-runtime-init-return-store-api-leak",
|
||||
classification: "dead-candidate",
|
||||
description: "Aster runtime 启动初始化 API 重新向 crate 外返回 runtime store",
|
||||
description:
|
||||
"Aster runtime 启动初始化 API 重新向 crate 外返回 runtime store",
|
||||
patterns: [
|
||||
"pub fn initialize_aster_thread_runtime_store() -> Result<Arc<dyn ThreadRuntimeStore>, String>",
|
||||
],
|
||||
includePathPrefixes: ["src-tauri/crates/agent/src/aster_runtime_support.rs"],
|
||||
includePathPrefixes: [
|
||||
"src-tauri/crates/agent/src/aster_runtime_support.rs",
|
||||
],
|
||||
allowedPaths: [],
|
||||
},
|
||||
{
|
||||
id: "rust-aster-runtime-public-legacy-init-helper-leak",
|
||||
classification: "dead-candidate",
|
||||
description: "Aster runtime support 重新对 crate 外暴露旧 initialize_aster_thread_runtime_store helper",
|
||||
description:
|
||||
"Aster runtime support 重新对 crate 外暴露旧 initialize_aster_thread_runtime_store helper",
|
||||
patterns: ["pub fn initialize_aster_thread_runtime_store("],
|
||||
includePathPrefixes: ["src-tauri/crates/agent/src/aster_runtime_support.rs"],
|
||||
includePathPrefixes: [
|
||||
"src-tauri/crates/agent/src/aster_runtime_support.rs",
|
||||
],
|
||||
allowedPaths: [],
|
||||
},
|
||||
{
|
||||
id: "rust-aster-runtime-snapshot-root-export-leak",
|
||||
classification: "dead-candidate",
|
||||
description: "lime-agent crate 根重新暴露 load_aster_runtime_snapshot helper",
|
||||
description:
|
||||
"lime-agent crate 根重新暴露 load_aster_runtime_snapshot helper",
|
||||
patterns: ["load_aster_runtime_snapshot"],
|
||||
includePathPrefixes: ["src-tauri/crates/agent/src/lib.rs"],
|
||||
allowedPaths: [],
|
||||
@@ -835,16 +999,20 @@ const rustTextSurfaceMonitors = [
|
||||
{
|
||||
id: "rust-aster-runtime-queue-service-leak",
|
||||
classification: "dead-candidate",
|
||||
description: "已零引用的 Lime 业务层 direct Aster shared runtime queue service 回流",
|
||||
description:
|
||||
"已零引用的 Lime 业务层 direct Aster shared runtime queue service 回流",
|
||||
patterns: [],
|
||||
regexPatterns: [String.raw`(?<!require_)shared_session_runtime_queue_service\(`],
|
||||
regexPatterns: [
|
||||
String.raw`(?<!require_)shared_session_runtime_queue_service\(`,
|
||||
],
|
||||
includePathPrefixes: ["src-tauri/src", "src-tauri/crates"],
|
||||
allowedPaths: [],
|
||||
},
|
||||
{
|
||||
id: "rust-aster-path-root-env-leak",
|
||||
classification: "dead-candidate",
|
||||
description: "已零引用的 Lime 业务层 direct ASTER_PATH_ROOT 环境变量处理回流",
|
||||
description:
|
||||
"已零引用的 Lime 业务层 direct ASTER_PATH_ROOT 环境变量处理回流",
|
||||
patterns: ['"ASTER_PATH_ROOT"'],
|
||||
includePathPrefixes: ["src-tauri/src", "src-tauri/crates"],
|
||||
allowedPaths: [],
|
||||
@@ -852,7 +1020,8 @@ const rustTextSurfaceMonitors = [
|
||||
{
|
||||
id: "rust-aster-global-session-store-leak",
|
||||
classification: "dead-candidate",
|
||||
description: "已零引用的 Aster 全局 session store 注册回流到统一 runtime support 边界之外",
|
||||
description:
|
||||
"已零引用的 Aster 全局 session store 注册回流到统一 runtime support 边界之外",
|
||||
patterns: ["set_global_session_store("],
|
||||
includePathPrefixes: ["src-tauri/src", "src-tauri/crates"],
|
||||
allowedPaths: [],
|
||||
@@ -1038,7 +1207,8 @@ const rustTextSurfaceMonitors = [
|
||||
{
|
||||
id: "rust-hardcoded-memory-path-leak",
|
||||
classification: "dead-candidate",
|
||||
description: "已零引用的 memory 相关模块硬编码 legacy memory 或 AGENTS 路径",
|
||||
description:
|
||||
"已零引用的 memory 相关模块硬编码 legacy memory 或 AGENTS 路径",
|
||||
patterns: [
|
||||
"~/.lime/AGENTS.md",
|
||||
'join(".lime").join("AGENTS.md")',
|
||||
@@ -1699,10 +1869,7 @@ const classificationDriftCandidates = [
|
||||
),
|
||||
...frontendTextResults
|
||||
.filter((result) =>
|
||||
isStatusClassificationDrift(
|
||||
getTextStatus(result),
|
||||
result.classification,
|
||||
),
|
||||
isStatusClassificationDrift(getTextStatus(result), result.classification),
|
||||
)
|
||||
.map(
|
||||
(result) =>
|
||||
@@ -1710,10 +1877,7 @@ const classificationDriftCandidates = [
|
||||
),
|
||||
...rustTextResults
|
||||
.filter((result) =>
|
||||
isStatusClassificationDrift(
|
||||
getTextStatus(result),
|
||||
result.classification,
|
||||
),
|
||||
isStatusClassificationDrift(getTextStatus(result), result.classification),
|
||||
)
|
||||
.map(
|
||||
(result) =>
|
||||
|
||||
@@ -0,0 +1,340 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import path from "node:path";
|
||||
import process from "node:process";
|
||||
|
||||
const DEFAULTS = {
|
||||
healthUrl: "http://127.0.0.1:3030/health",
|
||||
invokeUrl: "http://127.0.0.1:3030/invoke",
|
||||
timeoutMs: 60_000,
|
||||
intervalMs: 1_000,
|
||||
sampleProjectName: "Lime Smoke Workspace",
|
||||
};
|
||||
|
||||
function printHelp() {
|
||||
console.log(`
|
||||
Lime Workspace Ready Smoke
|
||||
|
||||
用途:
|
||||
验证 DevBridge 已就绪,并检查默认 workspace 可获取、可修复、可按路径回查。
|
||||
|
||||
用法:
|
||||
node scripts/workspace-ready-smoke.mjs [选项]
|
||||
|
||||
选项:
|
||||
--health-url <url> 健康检查地址,默认 http://127.0.0.1:3030/health
|
||||
--invoke-url <url> DevBridge invoke 地址,默认 http://127.0.0.1:3030/invoke
|
||||
--timeout-ms <ms> 等待健康检查超时,默认 60000
|
||||
--interval-ms <ms> 健康检查轮询间隔,默认 1000
|
||||
--sample-project-name <s> 用于校验目录解析的示例项目名
|
||||
-h, --help 显示帮助
|
||||
|
||||
示例:
|
||||
npm run smoke:workspace-ready
|
||||
npm run smoke:workspace-ready -- --timeout-ms 120000
|
||||
`);
|
||||
}
|
||||
|
||||
function parseArgs(argv) {
|
||||
const options = { ...DEFAULTS };
|
||||
|
||||
for (let index = 0; index < argv.length; index += 1) {
|
||||
const arg = argv[index];
|
||||
if (arg === "--health-url" && argv[index + 1]) {
|
||||
options.healthUrl = String(argv[index + 1]).trim();
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
if (arg === "--invoke-url" && argv[index + 1]) {
|
||||
options.invokeUrl = String(argv[index + 1]).trim();
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
if (arg === "--timeout-ms" && argv[index + 1]) {
|
||||
options.timeoutMs = Number(argv[index + 1]);
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
if (arg === "--interval-ms" && argv[index + 1]) {
|
||||
options.intervalMs = Number(argv[index + 1]);
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
if (arg === "--sample-project-name" && argv[index + 1]) {
|
||||
options.sampleProjectName = String(argv[index + 1]).trim();
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
if (arg === "--help" || arg === "-h") {
|
||||
printHelp();
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
if (!Number.isFinite(options.timeoutMs) || options.timeoutMs < 1000) {
|
||||
throw new Error("--timeout-ms 必须是 >= 1000 的数字");
|
||||
}
|
||||
if (!Number.isFinite(options.intervalMs) || options.intervalMs < 100) {
|
||||
throw new Error("--interval-ms 必须是 >= 100 的数字");
|
||||
}
|
||||
if (!options.sampleProjectName) {
|
||||
throw new Error("--sample-project-name 不能为空");
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
function assert(condition, message) {
|
||||
if (!condition) {
|
||||
throw new Error(message);
|
||||
}
|
||||
}
|
||||
|
||||
function normalizePath(value) {
|
||||
return String(value || "")
|
||||
.trim()
|
||||
.replace(/\\/g, "/")
|
||||
.replace(/\/+$/, "");
|
||||
}
|
||||
|
||||
function pickStringField(target, ...keys) {
|
||||
if (!target || typeof target !== "object") {
|
||||
return "";
|
||||
}
|
||||
|
||||
for (const key of keys) {
|
||||
const value = target[key];
|
||||
if (typeof value === "string" && value.trim()) {
|
||||
return value.trim();
|
||||
}
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
async function checkHealth(url) {
|
||||
const response = await fetch(url, { method: "GET" });
|
||||
const text = await response.text();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
return text ? JSON.parse(text) : null;
|
||||
}
|
||||
|
||||
async function waitForHealth(options) {
|
||||
const startedAt = Date.now();
|
||||
let lastError = null;
|
||||
|
||||
while (Date.now() - startedAt < options.timeoutMs) {
|
||||
try {
|
||||
const payload = await checkHealth(options.healthUrl);
|
||||
console.log(
|
||||
`[smoke:workspace-ready] DevBridge 已就绪 (${Date.now() - startedAt}ms)${
|
||||
payload?.status ? ` status=${payload.status}` : ""
|
||||
}`,
|
||||
);
|
||||
return payload;
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
await sleep(options.intervalMs);
|
||||
}
|
||||
}
|
||||
|
||||
const detail =
|
||||
lastError instanceof Error
|
||||
? lastError.message
|
||||
: String(lastError || "unknown error");
|
||||
throw new Error(
|
||||
`[smoke:workspace-ready] DevBridge 未就绪,请先启动 npm run tauri:dev:headless。最后错误: ${detail}`,
|
||||
);
|
||||
}
|
||||
|
||||
async function invoke(invokeUrl, cmd, args) {
|
||||
const response = await fetch(invokeUrl, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ cmd, args }),
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
function assertWorkspaceShape(project, label) {
|
||||
assert(project && typeof project === "object", `${label} 返回为空`);
|
||||
assert(
|
||||
typeof project.id === "string" && project.id.trim(),
|
||||
`${label} 缺少 id`,
|
||||
);
|
||||
assert(
|
||||
pickStringField(project, "rootPath", "root_path"),
|
||||
`${label} 缺少 rootPath`,
|
||||
);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
if (typeof fetch !== "function") {
|
||||
throw new Error("当前 Node 运行时不支持 fetch,请使用 Node 18+");
|
||||
}
|
||||
|
||||
const options = parseArgs(process.argv.slice(2));
|
||||
await waitForHealth(options);
|
||||
|
||||
const projectsRoot = await invoke(
|
||||
options.invokeUrl,
|
||||
"workspace_get_projects_root",
|
||||
);
|
||||
assert(
|
||||
typeof projectsRoot === "string" && projectsRoot.trim(),
|
||||
"workspace_get_projects_root 返回为空",
|
||||
);
|
||||
|
||||
const resolvedSamplePath = await invoke(
|
||||
options.invokeUrl,
|
||||
"workspace_resolve_project_path",
|
||||
{
|
||||
name: options.sampleProjectName,
|
||||
},
|
||||
);
|
||||
assert(
|
||||
typeof resolvedSamplePath === "string" && resolvedSamplePath.trim(),
|
||||
"workspace_resolve_project_path 返回为空",
|
||||
);
|
||||
|
||||
const normalizedProjectsRoot = normalizePath(path.resolve(projectsRoot));
|
||||
const normalizedResolvedSamplePath = normalizePath(
|
||||
path.resolve(resolvedSamplePath),
|
||||
);
|
||||
assert(
|
||||
normalizedResolvedSamplePath.startsWith(`${normalizedProjectsRoot}/`) ||
|
||||
normalizedResolvedSamplePath === normalizedProjectsRoot,
|
||||
`解析后的项目目录未落在 workspace 根目录下: ${resolvedSamplePath}`,
|
||||
);
|
||||
|
||||
const defaultProject = await invoke(
|
||||
options.invokeUrl,
|
||||
"get_or_create_default_project",
|
||||
);
|
||||
assertWorkspaceShape(defaultProject, "get_or_create_default_project");
|
||||
|
||||
const defaultProjectDetail = await invoke(
|
||||
options.invokeUrl,
|
||||
"workspace_get",
|
||||
{
|
||||
id: defaultProject.id,
|
||||
},
|
||||
);
|
||||
assertWorkspaceShape(defaultProjectDetail, "workspace_get");
|
||||
|
||||
const defaultProjectFromDefault = await invoke(
|
||||
options.invokeUrl,
|
||||
"workspace_get_default",
|
||||
);
|
||||
assertWorkspaceShape(defaultProjectFromDefault, "workspace_get_default");
|
||||
assert(
|
||||
defaultProjectFromDefault.id === defaultProject.id,
|
||||
"workspace_get_default 与 get_or_create_default_project 返回的默认 workspace 不一致",
|
||||
);
|
||||
|
||||
const ensureDefault = await invoke(
|
||||
options.invokeUrl,
|
||||
"workspace_ensure_default_ready",
|
||||
);
|
||||
assert(
|
||||
ensureDefault && typeof ensureDefault === "object",
|
||||
"默认 workspace 健康检查返回为空",
|
||||
);
|
||||
assert(
|
||||
pickStringField(ensureDefault, "workspaceId", "workspace_id") ===
|
||||
defaultProject.id,
|
||||
"workspace_ensure_default_ready 返回的 workspace_id 不匹配",
|
||||
);
|
||||
assert(
|
||||
pickStringField(ensureDefault, "rootPath", "root_path"),
|
||||
"workspace_ensure_default_ready 缺少 rootPath",
|
||||
);
|
||||
|
||||
const ensureExplicit = await invoke(
|
||||
options.invokeUrl,
|
||||
"workspace_ensure_ready",
|
||||
{
|
||||
id: defaultProject.id,
|
||||
},
|
||||
);
|
||||
assert(
|
||||
ensureExplicit && typeof ensureExplicit === "object",
|
||||
"workspace_ensure_ready 返回为空",
|
||||
);
|
||||
assert(
|
||||
pickStringField(ensureExplicit, "workspaceId", "workspace_id") ===
|
||||
defaultProject.id,
|
||||
"workspace_ensure_ready 返回的 workspace_id 不匹配",
|
||||
);
|
||||
|
||||
const ensuredRootPath = pickStringField(
|
||||
ensureExplicit,
|
||||
"rootPath",
|
||||
"root_path",
|
||||
);
|
||||
assert(ensuredRootPath, "workspace_ensure_ready 缺少 rootPath");
|
||||
|
||||
const workspaceByPath = await invoke(
|
||||
options.invokeUrl,
|
||||
"workspace_get_by_path",
|
||||
{
|
||||
rootPath: ensuredRootPath,
|
||||
},
|
||||
);
|
||||
assertWorkspaceShape(workspaceByPath, "workspace_get_by_path");
|
||||
assert(
|
||||
workspaceByPath.id === defaultProject.id,
|
||||
"workspace_get_by_path 未返回默认 workspace",
|
||||
);
|
||||
|
||||
const workspaces = await invoke(options.invokeUrl, "workspace_list");
|
||||
assert(Array.isArray(workspaces), "workspace_list 返回非数组");
|
||||
assert(
|
||||
workspaces.some((item) => item?.id === defaultProject.id),
|
||||
"workspace_list 中未找到默认 workspace",
|
||||
);
|
||||
|
||||
console.log("\n[smoke:workspace-ready] 通过");
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
{
|
||||
projectsRoot,
|
||||
sampleProjectPath: resolvedSamplePath,
|
||||
defaultWorkspaceId: defaultProject.id,
|
||||
defaultWorkspaceRoot: ensuredRootPath,
|
||||
workspaceCount: workspaces.length,
|
||||
repaired: Boolean(ensureExplicit.repaired),
|
||||
relocated: Boolean(ensureExplicit.relocated),
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(
|
||||
error instanceof Error ? error.message : String(error || "unknown error"),
|
||||
);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user