refactor(core): migrate to Bun and converge acquisition policies and Agent boundaries

This commit is contained in:
root committed 2026-10-13 14:37:03 +08:00
1 parent 3f1c6d02f6
commit 27c0f27434
268 files changed
+7468 -7852

No files matched your search

@@ -12,16 +12,16 @@ import { type AlistAuth, AlistClient } from "./client.ts";
// UMT_ALIST_REAL_ROOT2 可选:另一个挂载(驱动)下的可写目录,测跨驱动复制 / 移动(异步任务)
// 跑法:docs/runbook/storage-alist.md「真机验收」。
const URL_ = Deno.env.get("UMT_ALIST_REAL_URL");
const ROOT = Deno.env.get("UMT_ALIST_REAL_ROOT");
const ROOT2 = Deno.env.get("UMT_ALIST_REAL_ROOT2");
const auth: AlistAuth | null = Deno.env.get("UMT_ALIST_REAL_TOKEN")
? { kind: "token", token: Deno.env.get("UMT_ALIST_REAL_TOKEN")! }
: Deno.env.get("UMT_ALIST_REAL_USER")
const URL_ = process.env.UMT_ALIST_REAL_URL;
const ROOT = process.env.UMT_ALIST_REAL_ROOT;
const ROOT2 = process.env.UMT_ALIST_REAL_ROOT2;
const auth: AlistAuth | null = process.env.UMT_ALIST_REAL_TOKEN
? { kind: "token", token: process.env.UMT_ALIST_REAL_TOKEN! }
: process.env.UMT_ALIST_REAL_USER
? {
kind: "password",
username: Deno.env.get("UMT_ALIST_REAL_USER")!,
password: Deno.env.get("UMT_ALIST_REAL_PASSWORD") ?? "",
username: process.env.UMT_ALIST_REAL_USER!,
password: process.env.UMT_ALIST_REAL_PASSWORD ?? "",
}
: null;
const enabled = !!URL_ && !!ROOT && !!auth;
+15 -6
View File
@@ -388,14 +388,17 @@ describe("上传", () => {
it("空闲超时:服务端不读数据时中止", async () => {
const mock = createMockAlist({ token: "t" });
const stuck: typeof fetch = (async (input: Request | URL | string, init?: RequestInit) => {
const stuck: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response> = (async (
input: Request | URL | string,
init?: RequestInit,
) => {
const req = input instanceof Request ? input : new Request(String(input), init);
if (new URL(req.url).pathname === "/api/fs/put")
return new Promise<Response>((_, reject) =>
init?.signal?.addEventListener("abort", () => reject(init.signal!.reason)),
);
return mock.fetch(input, init);
}) as typeof fetch;
}) as (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
const client = new AlistClient({
baseUrl: mock.base,
auth: { kind: "token", token: "t" },
@@ -453,14 +456,17 @@ describe("下载", () => {
mock.put("/s1/a.mkv", bytes(1000));
const realFetch = mock.fetch;
let expired = false;
const f: typeof fetch = (async (input: Request | URL | string, init?: RequestInit) => {
const f: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response> = (async (
input: Request | URL | string,
init?: RequestInit,
) => {
const u = String(input instanceof Request ? input.url : input);
if (u.includes("/d/") && !expired) {
expired = true;
mock.expireSigns();
}
return realFetch(input, init);
}) as typeof fetch;
}) as (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
const c2 = new AlistClient({ baseUrl: mock.base, auth: { kind: "token", token: "perm-token" }, fetch: f, qps: 0 });
await c2.download("/s1/a.mkv", join(dir, "a.mkv"));
expect((await stat(join(dir, "a.mkv"))).size).toBe(1000);
@@ -478,7 +484,10 @@ describe("下载", () => {
const mock = createMockAlist({ token: "t" });
mock.put("/s1/a.mkv", "x");
const calls: { url: string; auth: string | null }[] = [];
const f: typeof fetch = (async (input: Request | URL | string, init?: RequestInit) => {
const f: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response> = (async (
input: Request | URL | string,
init?: RequestInit,
) => {
const req = input instanceof Request ? input : new Request(String(input), init);
calls.push({ url: req.url, auth: req.headers.get("authorization") });
if (req.url.startsWith("https://cdn.example")) return new Response("x");
@@ -489,7 +498,7 @@ describe("下载", () => {
return Response.json(j);
}
return res;
}) as typeof fetch;
}) as (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
const c = new AlistClient({ baseUrl: mock.base, auth: { kind: "token", token: "t" }, fetch: f, qps: 0 });
await c.download("/s1/a.mkv", join(dir, "a.mkv"));
expect(calls.find((x) => x.url.startsWith("https://cdn.example"))!.auth).toBeNull();
+2 -2
View File
@@ -24,7 +24,7 @@ export interface AlistClientOptions {
auth: AlistAuth;
/** 路径密码(Alist「元信息」里给目录设的密码),list / get 时带上 */
pathPassword?: string | null;
fetch?: typeof fetch;
fetch?: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
/** 普通请求超时(ms),缺省 30 s */
timeoutMs?: number;
/** 数据流无进度空闲超时(ms),缺省 120 s */
@@ -203,7 +203,7 @@ function idleWatch(ms: number, outer?: AbortSignal) {
export class AlistClient {
readonly baseUrl: string;
private readonly fetchFn: typeof fetch;
private readonly fetchFn: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
private readonly limiter: AlistLimiter;
private readonly o: Required<
Pick<
+5 -4
View File
@@ -3,6 +3,7 @@
// 令牌无效 code 401;copy 一律异步任务;同挂载 move 立即完成、跨挂载 move 是任务;put 覆盖同名;/d 支持 Range 与 sign。
// 顶层目录视为「挂载」(驱动),用于区分同驱动 / 跨驱动。
import { createHash, randomUUID } from "node:crypto";
import { type NetAddr, serve } from "@umt/runtime";
export interface MockFile {
data: Uint8Array;
@@ -43,7 +44,7 @@ export interface MockAlist {
readonly base: string;
setOrigin(origin: string): void;
handler: (req: Request) => Promise<Response>;
fetch: typeof fetch;
fetch: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
files: Map<string, MockFile>;
dirs: Map<string, number>;
tasks: Map<string, Task>;
@@ -432,7 +433,7 @@ export function createMockAlist(opts: MockAlistOptions = {}): MockAlist {
// 老版本(noTaskIds):没有任务 id 的复制 / 移动,在之后的任意 get 上推进
if (opts.noTaskIds) for (const t of tasks.values()) advanceTask(t);
return handler(req);
}) as typeof fetch,
}) as (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>,
files,
dirs,
tasks,
@@ -471,13 +472,13 @@ export function createMockAlist(opts: MockAlistOptions = {}): MockAlist {
/** 起一个真 HTTP 监听(ffprobe / 下载代理等需要真实 URL 的测试用);返回 base 与关闭函数 */
export function serveMockAlist(mock: MockAlist): { base: string; close: () => Promise<void> } {
const server = Deno.serve({ port: 0, hostname: "127.0.0.1", onListen: () => {} }, (req) => {
const server = serve({ port: 0, hostname: "127.0.0.1", onListen: () => {} }, (req) => {
const u = new URL(req.url);
return mock
.handler(new Request(`${mock.base}${u.pathname}${u.search}`, req))
.catch(() => new Response("connection reset", { status: 502 }));
});
const addr = server.addr as Deno.NetAddr;
const addr = server.addr as NetAddr;
const prev = mock.base;
const base = `http://127.0.0.1:${addr.port}`;
mock.setOrigin(base);