Files
AIGameCreator App be89296492 拆分 AI 游戏创作客户端大型模块
拆分 App 认证、壳层、运行配置与项目摘要模块
拆分 Tauri 项目能力与 Rust 测试领域模块
拆分界面测试与 Agent Runtime 真实 E2E 套件
补充源码扫描和客户端模块化文档约定
2026-07-21 22:53:29 +08:00

155 lines
4.7 KiB
JavaScript

import { assert } from '../assertions/core.mjs';
import { createHash, fs, path } from '../dependencies.mjs';
import { mainAgentId, state } from '../runtime-state.mjs';
import { decodeUtf8Fatal, splitJsonlBufferLines } from './reporting.mjs';
export function collectApiKeys(value, keys = []) {
if (!value || typeof value !== 'object') return keys;
if (Array.isArray(value)) {
for (const item of value) collectApiKeys(item, keys);
return [...new Set(keys)];
}
for (const [key, child] of Object.entries(value)) {
if (
/^api_?key$/i.test(key) &&
typeof child === 'string' &&
child.length > 0
) {
keys.push(child);
} else {
collectApiKeys(child, keys);
}
}
return [...new Set(keys)];
}
export function parseAssignedJson(output, names) {
const matches = [];
for (const line of output.split(/\r?\n/u)) {
for (const name of names) {
if (line.startsWith(`${name}=`)) {
matches.push({ name, payload: line.slice(name.length + 1) });
}
}
}
assert(matches.length === 1, 'cli-assigned-json-output-invalid');
return JSON.parse(matches[0].payload);
}
export async function listFiles(root) {
const files = [];
let metadata;
try {
metadata = await fs.lstat(root);
} catch (error) {
if (error?.code === 'ENOENT') return files;
throw error;
}
if (metadata.isSymbolicLink()) return files;
if (metadata.isFile()) return [root];
const entries = await fs.readdir(root, { withFileTypes: true });
for (const entry of entries) {
const file = path.join(root, entry.name);
if (entry.isSymbolicLink()) continue;
if (entry.isDirectory()) files.push(...(await listFiles(file)));
else if (entry.isFile()) files.push(file);
}
return files;
}
export async function readJson(file) {
return JSON.parse(await fs.readFile(file, 'utf8'));
}
export function validateCommandOutputSidecar(sidecar, audit, file) {
const relative = relativeProjectPath(file);
assert(
sidecar?.schemaVersion === 'game-creator-command-output.v1' &&
sidecar.outputRef === relative &&
sidecar.identity?.agentId === mainAgentId &&
sidecar.identity?.taskId === audit.taskId &&
sidecar.identity?.sessionId === audit.sessionId &&
sidecar.identity?.runId === state.initialRunId &&
sidecar.identity?.actionId === audit.actionId &&
sidecar.identity?.actionFingerprint === audit.actionFingerprint &&
sidecar.outputSha256 === audit.outputSha256 &&
sidecar.totalLines === audit.totalLines &&
sidecar.captureTruncated === audit.captureTruncated &&
sidecar.exitCode === audit.exitCode &&
sidecar.timedOut === audit.timedOut &&
sidecar.sourceChanged === audit.sourceChanged &&
typeof sidecar.output === 'string' &&
createHash('sha256').update(sidecar.output).digest('hex') ===
sidecar.outputSha256 &&
(sidecar.output.length === 0
? sidecar.totalLines === 0
: sidecar.output.split('\n').length === sidecar.totalLines),
'command-output-sidecar-identity-invalid',
);
}
export async function readJsonl(file) {
const content = await fs.readFile(file);
const records = [];
for (const lineRecord of splitJsonlBufferLines(content)) {
const line = decodeUtf8Fatal(lineRecord.bytes, 'jsonl-invalid-utf8');
if (line.trim().length > 0) records.push(JSON.parse(line));
}
return records;
}
export async function readOptionalJsonl(file) {
try {
return await readJsonl(file);
} catch (error) {
if (error?.code === 'ENOENT') return [];
throw error;
}
}
export function resolveProjectRelative(value) {
const candidate = path.isAbsolute(value)
? path.resolve(value)
: path.resolve(state.projectRoot, value);
assert(
isPathInside(state.projectRoot, candidate),
'evidence-path-outside-project',
);
return candidate;
}
export function relativeProjectPath(value) {
const relative = path.relative(state.projectRoot, path.resolve(value));
assert(
relative && !relative.startsWith('..') && !path.isAbsolute(relative),
'relative-evidence-path-invalid',
);
return relative.split(path.sep).join('/');
}
export function isPathInside(parent, child) {
const relative = path.relative(path.resolve(parent), path.resolve(child));
return (
relative !== '' && !relative.startsWith('..') && !path.isAbsolute(relative)
);
}
export function isTerminalRuntime(runtime) {
return ['completed', 'failed', 'cancelled', 'budget-exhausted'].includes(
runtime.phase,
);
}
export function isLiveTask(task) {
return (
['pending', 'running', 'waiting-for-confirmation'].includes(task.status) ||
[
'queued',
'running',
'executing',
'finalizing',
'waiting-for-confirmation',
].includes(task.phase)
);
}