feat: 完善两阶段 Agent 自进化 Pipeline、Skills 与 Benchmark (#129)

This commit is contained in:
Jingzhe Xu
2026-07-30 15:59:58 +08:00
committed by GitHub
parent 3bd0a7208a
commit 081fe2d172
34 changed files with 2093 additions and 763 deletions
@@ -7,7 +7,7 @@
import fs from "node:fs/promises";
import { constants as fsc } from "node:fs";
import path from "node:path";
import type { WorkspaceFilesResponse } from "../api/types.js";
import type { WorkspaceFileEntry, WorkspaceFilesResponse } from "../api/types.js";
import { HttpError } from "../http/errors.js";
import { badRequest } from "../http/validate.js";
@@ -48,6 +48,13 @@ export interface WorkspaceFileContent {
contentType: string;
/** Types whose same-origin inline rendering would execute scripts (html/svg): inline preview must fall back to plain text. */
scriptable: boolean;
/** True when a bounded preview returned only the beginning of the file. */
truncated?: boolean;
}
export interface WorkspaceFileReadOptions {
/** Return at most this many bytes. Used for bounded text previews. */
maxBytes?: number;
}
export class WorkspaceFilesService {
@@ -181,9 +188,14 @@ export class WorkspaceFilesService {
return unique.filter((_, i) => exists[i]);
}
/** List a directory: dirs come first, each group sorted by name; kind follows the symlink target (consistent with read behavior). */
/**
* List a directory: dirs come first, each group sorted by name. Entries whose
* canonical target leaves the Workspace are omitted, so listing cannot expose
* metadata for an out-of-bounds symlink.
*/
async list(workspace: string, rel: string): Promise<WorkspaceFilesResponse> {
const dir = await this.resolveRead(workspace, rel);
const realBase = await this.realBase(workspace);
let dirents;
try {
dirents = await fs.readdir(dir, { withFileTypes: true });
@@ -196,28 +208,24 @@ export class WorkspaceFilesService {
}
throw err;
}
const entries = await Promise.all(
dirents.map(async (d) => {
let sizeBytes = 0;
let mtime = "";
// Dirent doesn't report the target type for a symlink, so stat (following the link) is used to determine dir/file.
let isDir = d.isDirectory();
const listed = await Promise.all(
dirents.map(async (d): Promise<WorkspaceFileEntry | null> => {
try {
const stat = await fs.stat(path.join(dir, d.name));
sizeBytes = stat.size;
mtime = stat.mtime.toISOString();
isDir = stat.isDirectory();
const canonical = await fs.realpath(path.join(dir, d.name));
this.assertInside(canonical, realBase);
const stat = await fs.stat(canonical);
return {
name: d.name,
kind: stat.isDirectory() ? "dir" : "file",
sizeBytes: stat.size,
mtime: stat.mtime.toISOString(),
};
} catch {
// A dangling symlink or similar: keep the entry, with size/time left at defaults.
return null;
}
return {
name: d.name,
kind: isDir ? ("dir" as const) : ("file" as const),
sizeBytes,
mtime,
};
}),
);
const entries = listed.filter((entry): entry is WorkspaceFileEntry => entry !== null);
entries.sort((a, b) =>
a.kind === b.kind ? a.name.localeCompare(b.name) : a.kind === "dir" ? -1 : 1,
);
@@ -225,7 +233,11 @@ export class WorkspaceFilesService {
}
/** Read a file (preview/download): IO on the canonical path (resolveRead has already eliminated symlink escapes). */
async read(workspace: string, rel: string): Promise<WorkspaceFileContent> {
async read(
workspace: string,
rel: string,
options?: WorkspaceFileReadOptions,
): Promise<WorkspaceFileContent> {
const file = await this.resolveRead(workspace, rel);
let stat;
try {
@@ -234,16 +246,38 @@ export class WorkspaceFilesService {
throw new HttpError(404, "path_not_found", "File does not exist.");
}
if (stat.isDirectory()) throw badRequest("path is a directory.");
if (stat.size > MAX_READ_BYTES) {
const maxBytes = options?.maxBytes;
if (
maxBytes !== undefined &&
(!Number.isSafeInteger(maxBytes) || maxBytes < 1 || maxBytes > MAX_READ_BYTES)
) {
throw badRequest("maxBytes must be a positive integer within the read limit.");
}
if (maxBytes === undefined && stat.size > MAX_READ_BYTES) {
throw new HttpError(413, "file_too_large", "File exceeds the 50MB read limit.");
}
const data = await fs.readFile(file);
let data: Buffer;
let truncated = false;
if (maxBytes !== undefined && stat.size > maxBytes) {
const handle = await fs.open(file, "r");
try {
const buffer = Buffer.alloc(maxBytes);
const { bytesRead } = await handle.read(buffer, 0, maxBytes, 0);
data = buffer.subarray(0, bytesRead);
truncated = true;
} finally {
await handle.close();
}
} else {
data = await fs.readFile(file);
}
const ext = path.extname(file).toLowerCase();
return {
data,
fileName: path.basename(file),
contentType: CONTENT_TYPES[ext] ?? "application/octet-stream",
scriptable: ext === ".html" || ext === ".htm" || ext === ".svg",
...(truncated ? { truncated: true } : {}),
};
}