mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-12 15:23:27 +08:00
重构主控编排与运行时预热链路,统一工作区提示词/专家调度协议并补齐 wiki 记忆注入与写回闭环。
同时收敛启动与运维脚本默认行为(含 wiki worker)、更新 Admin 可观测性与相关测试,降低首轮时延并提高运行稳定性。 Made-with: Cursor
This commit is contained in:
parent
4a23b715a2
commit
dbbe3add6a
14438 changed files with 2693620 additions and 2546 deletions
|
|
@ -0,0 +1,314 @@
|
|||
import { spawn } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import { createRequire } from "node:module";
|
||||
import path, { resolve } from "node:path";
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const repoRoot = resolve(import.meta.dirname, "..");
|
||||
const tscBin = require.resolve("typescript/bin/tsc");
|
||||
const TYPE_INPUT_EXTENSIONS = new Set([".ts", ".tsx", ".d.ts", ".js", ".mjs", ".json"]);
|
||||
const VALID_MODES = new Set(["all", "package-boundary"]);
|
||||
|
||||
const ROOT_DTS_INPUTS = [
|
||||
"tsconfig.json",
|
||||
"tsconfig.plugin-sdk.dts.json",
|
||||
"src/channels/plugins",
|
||||
"src/plugin-sdk",
|
||||
"src/video-generation/dashscope-compatible.ts",
|
||||
"src/video-generation/types.ts",
|
||||
"src/types",
|
||||
];
|
||||
const ROOT_DTS_OUTPUTS = [
|
||||
"dist/plugin-sdk/.tsbuildinfo",
|
||||
"dist/plugin-sdk/src/plugin-sdk/error-runtime.d.ts",
|
||||
"dist/plugin-sdk/src/plugin-sdk/plugin-entry.d.ts",
|
||||
"dist/plugin-sdk/src/plugin-sdk/provider-auth.d.ts",
|
||||
"dist/plugin-sdk/src/plugin-sdk/video-generation.d.ts",
|
||||
];
|
||||
const PACKAGE_DTS_INPUTS = [
|
||||
"tsconfig.json",
|
||||
"packages/plugin-sdk/tsconfig.json",
|
||||
"src/channels/plugins",
|
||||
"src/plugin-sdk",
|
||||
"src/video-generation/dashscope-compatible.ts",
|
||||
"src/video-generation/types.ts",
|
||||
"src/types",
|
||||
];
|
||||
const PACKAGE_DTS_OUTPUTS = [
|
||||
"packages/plugin-sdk/dist/.tsbuildinfo",
|
||||
"packages/plugin-sdk/dist/src/plugin-sdk/error-runtime.d.ts",
|
||||
"packages/plugin-sdk/dist/src/plugin-sdk/plugin-entry.d.ts",
|
||||
"packages/plugin-sdk/dist/src/plugin-sdk/provider-auth.d.ts",
|
||||
"packages/plugin-sdk/dist/src/plugin-sdk/video-generation.d.ts",
|
||||
];
|
||||
const ENTRY_SHIMS_INPUTS = [
|
||||
"scripts/write-plugin-sdk-entry-dts.ts",
|
||||
"scripts/lib/plugin-sdk-entrypoints.json",
|
||||
"scripts/lib/plugin-sdk-entries.mjs",
|
||||
];
|
||||
|
||||
function isRelevantTypeInput(filePath) {
|
||||
const basename = path.basename(filePath);
|
||||
if (basename.endsWith(".test.ts")) {
|
||||
return false;
|
||||
}
|
||||
return TYPE_INPUT_EXTENSIONS.has(path.extname(filePath));
|
||||
}
|
||||
|
||||
export function parseMode(argv = process.argv.slice(2)) {
|
||||
const modeArg = argv.find((arg) => arg.startsWith("--mode="));
|
||||
const mode = modeArg?.slice("--mode=".length) ?? "all";
|
||||
if (!VALID_MODES.has(mode)) {
|
||||
throw new Error(`Unknown mode: ${mode}`);
|
||||
}
|
||||
return mode;
|
||||
}
|
||||
|
||||
function collectNewestMtime(paths, params = {}) {
|
||||
const rootDir = params.rootDir ?? repoRoot;
|
||||
const includeFile = params.includeFile ?? (() => true);
|
||||
let newestMtimeMs = 0;
|
||||
|
||||
function visit(entryPath) {
|
||||
if (!fs.existsSync(entryPath)) {
|
||||
return;
|
||||
}
|
||||
const stats = fs.statSync(entryPath);
|
||||
if (stats.isDirectory()) {
|
||||
for (const child of fs.readdirSync(entryPath)) {
|
||||
visit(path.join(entryPath, child));
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!includeFile(entryPath)) {
|
||||
return;
|
||||
}
|
||||
newestMtimeMs = Math.max(newestMtimeMs, stats.mtimeMs);
|
||||
}
|
||||
|
||||
for (const relativePath of paths) {
|
||||
visit(resolve(rootDir, relativePath));
|
||||
}
|
||||
|
||||
return newestMtimeMs;
|
||||
}
|
||||
|
||||
function collectOldestMtime(paths, params = {}) {
|
||||
const rootDir = params.rootDir ?? repoRoot;
|
||||
let oldestMtimeMs = Number.POSITIVE_INFINITY;
|
||||
|
||||
for (const relativePath of paths) {
|
||||
const absolutePath = resolve(rootDir, relativePath);
|
||||
if (!fs.existsSync(absolutePath)) {
|
||||
return null;
|
||||
}
|
||||
oldestMtimeMs = Math.min(oldestMtimeMs, fs.statSync(absolutePath).mtimeMs);
|
||||
}
|
||||
|
||||
return Number.isFinite(oldestMtimeMs) ? oldestMtimeMs : null;
|
||||
}
|
||||
|
||||
export function isArtifactSetFresh(params) {
|
||||
const newestInputMtimeMs = collectNewestMtime(params.inputPaths, {
|
||||
rootDir: params.rootDir,
|
||||
includeFile: params.includeFile,
|
||||
});
|
||||
const oldestOutputMtimeMs = collectOldestMtime(params.outputPaths, { rootDir: params.rootDir });
|
||||
return oldestOutputMtimeMs !== null && oldestOutputMtimeMs >= newestInputMtimeMs;
|
||||
}
|
||||
|
||||
function hasMissingOutput(paths) {
|
||||
return paths.some((relativePath) => !fs.existsSync(resolve(repoRoot, relativePath)));
|
||||
}
|
||||
|
||||
function removeIncrementalStateForMissingOutput(params) {
|
||||
if (!hasMissingOutput(params.outputPaths)) {
|
||||
return;
|
||||
}
|
||||
fs.rmSync(resolve(repoRoot, params.tsBuildInfoPath), { force: true });
|
||||
}
|
||||
|
||||
export function createPrefixedOutputWriter(label, target) {
|
||||
let buffered = "";
|
||||
const prefix = `[${label}] `;
|
||||
|
||||
return {
|
||||
write(chunk) {
|
||||
buffered += chunk;
|
||||
while (true) {
|
||||
const newlineIndex = buffered.indexOf("\n");
|
||||
if (newlineIndex === -1) {
|
||||
return;
|
||||
}
|
||||
const line = buffered.slice(0, newlineIndex + 1);
|
||||
buffered = buffered.slice(newlineIndex + 1);
|
||||
target.write(`${prefix}${line}`);
|
||||
}
|
||||
},
|
||||
flush() {
|
||||
if (!buffered) {
|
||||
return;
|
||||
}
|
||||
target.write(`${prefix}${buffered}`);
|
||||
buffered = "";
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function abortSiblingSteps(abortController) {
|
||||
if (abortController && !abortController.signal.aborted) {
|
||||
abortController.abort();
|
||||
}
|
||||
}
|
||||
|
||||
export function runNodeStep(label, args, timeoutMs, params = {}) {
|
||||
const abortController = params.abortController;
|
||||
return new Promise((resolvePromise, rejectPromise) => {
|
||||
const child = spawn(process.execPath, args, {
|
||||
cwd: repoRoot,
|
||||
env: process.env,
|
||||
signal: abortController?.signal,
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
|
||||
let settled = false;
|
||||
const stdoutWriter = createPrefixedOutputWriter(label, process.stdout);
|
||||
const stderrWriter = createPrefixedOutputWriter(label, process.stderr);
|
||||
const timer = setTimeout(() => {
|
||||
if (settled) {
|
||||
return;
|
||||
}
|
||||
child.kill("SIGTERM");
|
||||
settled = true;
|
||||
stdoutWriter.flush();
|
||||
stderrWriter.flush();
|
||||
abortSiblingSteps(abortController);
|
||||
rejectPromise(new Error(`${label} timed out after ${timeoutMs}ms`));
|
||||
}, timeoutMs);
|
||||
|
||||
child.stdout.setEncoding("utf8");
|
||||
child.stderr.setEncoding("utf8");
|
||||
child.stdout.on("data", (chunk) => {
|
||||
stdoutWriter.write(chunk);
|
||||
});
|
||||
child.stderr.on("data", (chunk) => {
|
||||
stderrWriter.write(chunk);
|
||||
});
|
||||
child.on("error", (error) => {
|
||||
if (settled) {
|
||||
return;
|
||||
}
|
||||
clearTimeout(timer);
|
||||
settled = true;
|
||||
stdoutWriter.flush();
|
||||
stderrWriter.flush();
|
||||
if (error.name === "AbortError" && abortController?.signal.aborted) {
|
||||
rejectPromise(new Error(`${label} canceled after sibling failure`));
|
||||
return;
|
||||
}
|
||||
abortSiblingSteps(abortController);
|
||||
rejectPromise(new Error(`${label} failed to start: ${error.message}`));
|
||||
});
|
||||
child.on("close", (code) => {
|
||||
if (settled) {
|
||||
return;
|
||||
}
|
||||
clearTimeout(timer);
|
||||
settled = true;
|
||||
stdoutWriter.flush();
|
||||
stderrWriter.flush();
|
||||
if (code === 0) {
|
||||
resolvePromise();
|
||||
return;
|
||||
}
|
||||
abortSiblingSteps(abortController);
|
||||
rejectPromise(new Error(`${label} failed with exit code ${code ?? 1}`));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function runNodeStepsInParallel(steps) {
|
||||
const abortController = new AbortController();
|
||||
const results = await Promise.allSettled(
|
||||
steps.map((step) => runNodeStep(step.label, step.args, step.timeoutMs, { abortController })),
|
||||
);
|
||||
const firstFailure = results.find((result) => result.status === "rejected");
|
||||
if (firstFailure) {
|
||||
throw firstFailure.reason;
|
||||
}
|
||||
}
|
||||
|
||||
export async function main(argv = process.argv.slice(2)) {
|
||||
try {
|
||||
const mode = parseMode(argv);
|
||||
const rootDtsFresh = isArtifactSetFresh({
|
||||
inputPaths: ROOT_DTS_INPUTS,
|
||||
outputPaths: ROOT_DTS_OUTPUTS,
|
||||
includeFile: isRelevantTypeInput,
|
||||
});
|
||||
const packageDtsFresh = isArtifactSetFresh({
|
||||
inputPaths: PACKAGE_DTS_INPUTS,
|
||||
outputPaths: PACKAGE_DTS_OUTPUTS,
|
||||
includeFile: isRelevantTypeInput,
|
||||
});
|
||||
const entryShimsFresh = isArtifactSetFresh({
|
||||
inputPaths: [
|
||||
...ENTRY_SHIMS_INPUTS,
|
||||
"dist/plugin-sdk/.tsbuildinfo",
|
||||
"packages/plugin-sdk/dist/.tsbuildinfo",
|
||||
],
|
||||
outputPaths: ["dist/plugin-sdk/.boundary-entry-shims.stamp"],
|
||||
});
|
||||
|
||||
const pendingSteps = [];
|
||||
if (mode === "all") {
|
||||
if (!rootDtsFresh) {
|
||||
removeIncrementalStateForMissingOutput({
|
||||
outputPaths: ROOT_DTS_OUTPUTS,
|
||||
tsBuildInfoPath: "dist/plugin-sdk/.tsbuildinfo",
|
||||
});
|
||||
pendingSteps.push({
|
||||
label: "plugin-sdk boundary dts",
|
||||
args: [tscBin, "-p", "tsconfig.plugin-sdk.dts.json"],
|
||||
timeoutMs: 300_000,
|
||||
});
|
||||
} else {
|
||||
process.stdout.write("[plugin-sdk boundary dts] fresh; skipping\n");
|
||||
}
|
||||
}
|
||||
if (!packageDtsFresh) {
|
||||
removeIncrementalStateForMissingOutput({
|
||||
outputPaths: PACKAGE_DTS_OUTPUTS,
|
||||
tsBuildInfoPath: "packages/plugin-sdk/dist/.tsbuildinfo",
|
||||
});
|
||||
pendingSteps.push({
|
||||
label: "plugin-sdk package boundary dts",
|
||||
args: [tscBin, "-p", "packages/plugin-sdk/tsconfig.json"],
|
||||
timeoutMs: 300_000,
|
||||
});
|
||||
} else {
|
||||
process.stdout.write("[plugin-sdk package boundary dts] fresh; skipping\n");
|
||||
}
|
||||
|
||||
if (pendingSteps.length > 0) {
|
||||
await runNodeStepsInParallel(pendingSteps);
|
||||
}
|
||||
|
||||
if (mode === "all" && (!entryShimsFresh || pendingSteps.length > 0)) {
|
||||
await runNodeStep(
|
||||
"plugin-sdk boundary root shims",
|
||||
["--import", "tsx", resolve(repoRoot, "scripts/write-plugin-sdk-entry-dts.ts")],
|
||||
120_000,
|
||||
);
|
||||
} else if (mode === "all") {
|
||||
process.stdout.write("[plugin-sdk boundary root shims] fresh; skipping\n");
|
||||
}
|
||||
} catch (error) {
|
||||
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (import.meta.main) {
|
||||
await main();
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue