feat(runtime): 即梦本机运行时 - 接入安全生成闭环与桌面本机渠道
Refs #225 Co-authored-by: zkhyww <127009345+zkhyww@users.noreply.github.com>
This commit is contained in:
1 parent
8f2232feee
commit
57e1820f41
179 files changed
+49746
-1897
No files matched your search
@@ -0,0 +1,154 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { EventEmitter } from "node:events";
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
import { waitForLocalDreaminaGenerationTask } from "../../../web/src/services/local-dreamina-generation.ts";
|
||||
import { DreaminaCliRuntime } from "../../src/dreamina-cli-runtime.js";
|
||||
import { createDreaminaHttpModule } from "../../src/modules/dreamina-http.js";
|
||||
|
||||
const configDir = await fs.mkdtemp(path.join(os.tmpdir(), "dreamina-node-web-cross-runtime-"));
|
||||
const stateFile = path.join(configDir, "dreamina-runtime-state.json");
|
||||
const id = "dreamina-node-web-cross-runtime-0001";
|
||||
const ownerId = "owner-node-web-cross-runtime-0001";
|
||||
const videoBytes = Buffer.from("00000018667479706d703432", "hex");
|
||||
await fs.writeFile(stateFile, JSON.stringify({
|
||||
version: 1,
|
||||
records: [{
|
||||
ownerId,
|
||||
idempotencyKey: id,
|
||||
requestHash: "9".repeat(64),
|
||||
state: "accepted",
|
||||
submitId: "receipt-node-web-cross-runtime",
|
||||
updatedAt: "2026-08-13T00:00:00.000Z",
|
||||
taskVersion: 1,
|
||||
operation: "text2video",
|
||||
mode: "video",
|
||||
model: "seedance2.0mini",
|
||||
createdAt: "2026-08-13T00:00:00.000Z",
|
||||
nextPollAt: "2000-01-01T00:00:00.000Z",
|
||||
}],
|
||||
}));
|
||||
let releaseWinner!: () => void;
|
||||
let markWinnerEntered!: () => void;
|
||||
const winnerGate = new Promise<void>((resolve) => { releaseWinner = resolve; });
|
||||
const winnerEntered = new Promise<void>((resolve) => { markWinnerEntered = resolve; });
|
||||
let winnerQueries = 0;
|
||||
let waiterQueries = 0;
|
||||
const common = {
|
||||
ownerId,
|
||||
stateFile,
|
||||
ensureReady: async () => undefined,
|
||||
discover: async () => ({ installed: true as const, executable: "dreamina-fixture" }),
|
||||
};
|
||||
const winner = new DreaminaCliRuntime({
|
||||
...common,
|
||||
runProcess: async (input) => {
|
||||
winnerQueries += 1;
|
||||
if (winnerQueries === 1) {
|
||||
markWinnerEntered();
|
||||
await winnerGate;
|
||||
}
|
||||
const output = input.args.find((arg) => arg.startsWith("--download_dir="))?.slice("--download_dir=".length);
|
||||
if (output) await fs.writeFile(path.join(output, "result.mp4"), videoBytes);
|
||||
return { exitCode: 0, stdout: '{"status":"completed"}', stderr: "" };
|
||||
},
|
||||
});
|
||||
const waiter = new DreaminaCliRuntime({
|
||||
...common,
|
||||
runProcess: async () => {
|
||||
waiterQueries += 1;
|
||||
return { exitCode: 0, stdout: '{"status":"failed"}', stderr: "" };
|
||||
},
|
||||
});
|
||||
const module = createDreaminaHttpModule({ ownerId, configDir, dreaminaRuntime: waiter });
|
||||
try {
|
||||
await winner.start();
|
||||
await winnerEntered;
|
||||
const parsed = waitForLocalDreaminaGenerationTask(id, "video", {
|
||||
client: {
|
||||
async connect() {
|
||||
return {
|
||||
state: "connected" as const,
|
||||
runtimeVersion: 2,
|
||||
session: {
|
||||
sessionId: "session-test-000000000001",
|
||||
keyId: "browser-key-test",
|
||||
scopes: ["dreamina:generate"],
|
||||
expiresAt: "2099-01-01T00:00:00.000Z",
|
||||
},
|
||||
};
|
||||
},
|
||||
async request(route, init) {
|
||||
const response = await invoke(module, route, Buffer.from(String(init?.body)));
|
||||
return new Response(JSON.stringify(response.body), {
|
||||
status: response.status,
|
||||
headers: { "content-type": "application/json" },
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
await Promise.resolve();
|
||||
releaseWinner();
|
||||
const task = await parsed;
|
||||
assert.equal(task.status, "succeeded");
|
||||
assert.deepEqual(task.result, {
|
||||
mode: "video",
|
||||
video: {
|
||||
dataUrl: `data:video/mp4;base64,${videoBytes.toString("base64")}`,
|
||||
mimeType: "video/mp4",
|
||||
bytes: videoBytes.byteLength,
|
||||
},
|
||||
});
|
||||
assert.equal(waiterQueries, 0);
|
||||
assert.equal(winnerQueries, 2);
|
||||
|
||||
const journalText = await fs.readFile(stateFile, "utf8");
|
||||
const productText = await fs.readFile(path.join(configDir, "dreamina-generation-task-store.json"), "utf8");
|
||||
for (const durableText of [journalText, productText]) {
|
||||
assert.equal(durableText.includes("data:video/"), false);
|
||||
assert.equal(durableText.includes(videoBytes.toString("base64")), false);
|
||||
assert.equal(/stdout|stderr|token|cookie/i.test(durableText), false);
|
||||
}
|
||||
process.stdout.write("dreamina-cross-runtime-web-ok\n");
|
||||
} finally {
|
||||
releaseWinner();
|
||||
await Promise.allSettled([winner.dispose(), module.dispose?.()]);
|
||||
await fs.rm(configDir, { recursive: true, force: true, maxRetries: 10, retryDelay: 10 });
|
||||
}
|
||||
|
||||
async function invoke(
|
||||
runtimeModule: ReturnType<typeof createDreaminaHttpModule>,
|
||||
routePath: string,
|
||||
body: Buffer,
|
||||
): Promise<{ status: number; body: unknown }> {
|
||||
const route = runtimeModule.routes.find((candidate) => candidate.path === routePath);
|
||||
assert(route);
|
||||
const req = new EventEmitter() as EventEmitter & { body?: Buffer };
|
||||
req.body = body;
|
||||
const res = new EventEmitter() as EventEmitter & {
|
||||
destroyed: boolean;
|
||||
writableEnded: boolean;
|
||||
statusCode: number;
|
||||
json(value: unknown): void;
|
||||
status(code: number): typeof res;
|
||||
};
|
||||
res.destroyed = false;
|
||||
res.writableEnded = false;
|
||||
res.statusCode = 200;
|
||||
let resolve!: (value: { status: number; body: unknown }) => void;
|
||||
const result = new Promise<{ status: number; body: unknown }>((done) => { resolve = done; });
|
||||
res.json = (value) => {
|
||||
res.writableEnded = true;
|
||||
resolve({ status: res.statusCode, body: value });
|
||||
};
|
||||
res.status = (code) => {
|
||||
res.statusCode = code;
|
||||
return res;
|
||||
};
|
||||
route.handler(req as never, res as never, (error?: unknown) => {
|
||||
if (error) resolve({ status: 500, body: { ok: false, code: "test_route_error" } });
|
||||
});
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import type { RequestHandler } from "express";
|
||||
|
||||
import type { LocalRuntimeConfig } from "../../src/config.js";
|
||||
import { startLocalRuntime } from "../../src/local-runtime-host.js";
|
||||
import type { LocalRuntimeModule } from "../../src/local-runtime.js";
|
||||
|
||||
const FORBIDDEN_PORTS = new Set([3000, 8080, 17371]);
|
||||
const WORKTREE_ROOT = path.resolve(fileURLToPath(new URL("../../..", import.meta.url)));
|
||||
const DEFAULT_TRUSTED_ORIGIN = "http://127.0.0.1:31300";
|
||||
|
||||
export type BrowserRuntimeScenario =
|
||||
| "missing"
|
||||
| "installed"
|
||||
| "login_pending"
|
||||
| "authenticated"
|
||||
| "error"
|
||||
| "origin_denied";
|
||||
|
||||
export type BrowserRuntimeFixtureOptions = {
|
||||
scenario: BrowserRuntimeScenario;
|
||||
port: number;
|
||||
configDir: string;
|
||||
trustedOrigin: string;
|
||||
log?: (line: string) => void;
|
||||
};
|
||||
|
||||
export async function startBrowserRuntimeFixture(options: BrowserRuntimeFixtureOptions) {
|
||||
validatePort(options.port);
|
||||
await requireContainedConfigDirectory(options.configDir);
|
||||
const trustedOrigin = exactWebOrigin(options.trustedOrigin);
|
||||
const calls = { count: 0 };
|
||||
const dreamina = fakeDreaminaModule(options.scenario, calls);
|
||||
const config: LocalRuntimeConfig = {
|
||||
url: `http://127.0.0.1:${options.port}`,
|
||||
token: "browser-harness-master-canary-must-not-leak",
|
||||
ownerId: "browser-harness-owner-01",
|
||||
origins: [],
|
||||
trustedWebOrigins: options.scenario === "origin_denied" ? [] : [trustedOrigin],
|
||||
browserRegistrations: [],
|
||||
};
|
||||
const runtime = startLocalRuntime({
|
||||
config,
|
||||
modules: [fakeCanvasModule(), dreamina],
|
||||
port: options.port,
|
||||
log: options.log ?? (() => undefined),
|
||||
persistConfig: () => undefined,
|
||||
});
|
||||
await runtime.ready;
|
||||
return {
|
||||
pid: process.pid,
|
||||
endpoint: `http://127.0.0.1:${options.port}`,
|
||||
cliCalls: () => calls.count,
|
||||
close: runtime.close,
|
||||
};
|
||||
}
|
||||
|
||||
function fakeCanvasModule(): LocalRuntimeModule {
|
||||
return {
|
||||
descriptor: {
|
||||
id: "canvas-agent",
|
||||
displayName: "Canvas Agent",
|
||||
apiVersion: 1,
|
||||
scopes: ["canvas:connect"],
|
||||
},
|
||||
routes: [],
|
||||
};
|
||||
}
|
||||
|
||||
function fakeDreaminaModule(
|
||||
initialScenario: BrowserRuntimeScenario,
|
||||
calls: { count: number },
|
||||
): LocalRuntimeModule {
|
||||
let scenario = initialScenario === "origin_denied" ? "installed" : initialScenario;
|
||||
const status: RequestHandler = (_req, res) => {
|
||||
calls.count += 1;
|
||||
if (scenario === "error") {
|
||||
res.status(500).json({
|
||||
ok: false,
|
||||
code: "dreamina_internal_error",
|
||||
message: "Dreamina CLI 请求失败",
|
||||
});
|
||||
return;
|
||||
}
|
||||
res.json({ ok: true, status: dreaminaStatus(scenario) });
|
||||
};
|
||||
const login: RequestHandler = (_req, res) => {
|
||||
calls.count += 1;
|
||||
if (scenario === "missing") {
|
||||
res.status(404).json({
|
||||
ok: false,
|
||||
code: "dreamina_missing",
|
||||
message: "未检测到 Dreamina CLI",
|
||||
});
|
||||
return;
|
||||
}
|
||||
scenario = "login_pending";
|
||||
res.json({ ok: true, status: dreaminaStatus(scenario) });
|
||||
};
|
||||
const logout: RequestHandler = (_req, res) => {
|
||||
calls.count += 1;
|
||||
scenario = "installed";
|
||||
res.json({ ok: true, status: dreaminaStatus(scenario) });
|
||||
};
|
||||
return {
|
||||
descriptor: {
|
||||
id: "dreamina",
|
||||
displayName: "Dreamina CLI",
|
||||
apiVersion: 1,
|
||||
scopes: ["dreamina:status", "dreamina:login", "dreamina:logout", "dreamina:run"],
|
||||
},
|
||||
routes: [
|
||||
{ method: "GET", path: "/dreamina/status", scope: "dreamina:status", handler: status },
|
||||
{ method: "POST", path: "/dreamina/login", scope: "dreamina:login", handler: login },
|
||||
{ method: "POST", path: "/dreamina/logout", scope: "dreamina:logout", handler: logout },
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
function dreaminaStatus(scenario: Exclude<BrowserRuntimeScenario, "origin_denied" | "error">) {
|
||||
if (scenario === "missing") {
|
||||
return {
|
||||
provider: "dreamina-cli",
|
||||
state: "missing",
|
||||
installed: false,
|
||||
authenticated: false,
|
||||
code: "dreamina_missing",
|
||||
message: "未检测到 Dreamina CLI",
|
||||
};
|
||||
}
|
||||
if (scenario === "authenticated") {
|
||||
return {
|
||||
provider: "dreamina-cli",
|
||||
state: "authenticated",
|
||||
installed: true,
|
||||
authenticated: true,
|
||||
version: "fixture-1.0.0",
|
||||
message: "Dreamina CLI 已登录",
|
||||
};
|
||||
}
|
||||
if (scenario === "login_pending") {
|
||||
return {
|
||||
provider: "dreamina-cli",
|
||||
state: "login_pending",
|
||||
installed: true,
|
||||
authenticated: false,
|
||||
version: "fixture-1.0.0",
|
||||
code: "dreamina_login_pending",
|
||||
message: "请在官方页面确认 Dreamina 登录",
|
||||
verificationUri: "https://jimeng.jianying.com/ai-tool/cli-auth",
|
||||
userCode: "DREAMINA-FIXTURE",
|
||||
expiresAt: new Date(Date.now() + 10 * 60_000).toISOString(),
|
||||
};
|
||||
}
|
||||
return {
|
||||
provider: "dreamina-cli",
|
||||
state: "installed",
|
||||
installed: true,
|
||||
authenticated: false,
|
||||
version: "fixture-1.0.0",
|
||||
code: "dreamina_login_required",
|
||||
message: "Dreamina CLI 已安装,需要登录",
|
||||
};
|
||||
}
|
||||
|
||||
function validatePort(port: number) {
|
||||
if (!Number.isInteger(port) || port < 1 || port > 65_535 || FORBIDDEN_PORTS.has(port)) {
|
||||
throw new Error("Browser Runtime fixture requires an isolated non-production port");
|
||||
}
|
||||
}
|
||||
|
||||
async function requireContainedConfigDirectory(configDir: string) {
|
||||
if (!path.isAbsolute(configDir)) {
|
||||
throw new Error("Browser Runtime fixture config directory must be absolute");
|
||||
}
|
||||
const [worktreeRoot, resolvedConfigDir] = await Promise.all([
|
||||
fs.realpath(WORKTREE_ROOT),
|
||||
fs.realpath(configDir),
|
||||
]);
|
||||
if (resolvedConfigDir === worktreeRoot
|
||||
|| !resolvedConfigDir.startsWith(`${worktreeRoot}${path.sep}`)) {
|
||||
throw new Error("Browser Runtime fixture config directory must stay inside the worktree");
|
||||
}
|
||||
}
|
||||
|
||||
function exactWebOrigin(value: string) {
|
||||
const url = new URL(value);
|
||||
if (!['http:', 'https:'].includes(url.protocol)
|
||||
|| url.username
|
||||
|| url.password
|
||||
|| url.pathname !== "/"
|
||||
|| url.search
|
||||
|| url.hash
|
||||
|| url.origin !== value) {
|
||||
throw new Error("Browser Runtime fixture trusted origin is invalid");
|
||||
}
|
||||
return url.origin;
|
||||
}
|
||||
|
||||
function parseCliArguments(argv: readonly string[]): BrowserRuntimeFixtureOptions {
|
||||
const values = new Map<string, string>();
|
||||
for (let index = 0; index < argv.length; index += 2) {
|
||||
const key = argv[index];
|
||||
const value = argv[index + 1];
|
||||
if (!key?.startsWith("--") || value === undefined || values.has(key)) {
|
||||
throw new Error("Browser Runtime fixture arguments are invalid");
|
||||
}
|
||||
values.set(key, value);
|
||||
}
|
||||
const allowed = new Set(["--scenario", "--port", "--config-dir", "--trusted-origin"]);
|
||||
if ([...values.keys()].some((key) => !allowed.has(key))) {
|
||||
throw new Error("Browser Runtime fixture arguments are invalid");
|
||||
}
|
||||
const scenario = values.get("--scenario");
|
||||
if (!isScenario(scenario)) throw new Error("Browser Runtime fixture scenario is invalid");
|
||||
return {
|
||||
scenario,
|
||||
port: Number(values.get("--port")),
|
||||
configDir: values.get("--config-dir") ?? "",
|
||||
trustedOrigin: values.get("--trusted-origin") ?? DEFAULT_TRUSTED_ORIGIN,
|
||||
};
|
||||
}
|
||||
|
||||
function isScenario(value: unknown): value is BrowserRuntimeScenario {
|
||||
return value === "missing"
|
||||
|| value === "installed"
|
||||
|| value === "login_pending"
|
||||
|| value === "authenticated"
|
||||
|| value === "error"
|
||||
|| value === "origin_denied";
|
||||
}
|
||||
|
||||
async function runCli() {
|
||||
const fixture = await startBrowserRuntimeFixture(parseCliArguments(process.argv.slice(2)));
|
||||
console.log(`Browser Runtime fixture listening at ${fixture.endpoint}`);
|
||||
console.log(`Browser Runtime fixture PID: ${fixture.pid}`);
|
||||
await new Promise<void>((resolve) => {
|
||||
process.once("SIGINT", resolve);
|
||||
process.once("SIGTERM", resolve);
|
||||
});
|
||||
await fixture.close();
|
||||
}
|
||||
|
||||
if (path.resolve(process.argv[1] ?? "") === path.resolve(fileURLToPath(import.meta.url))) {
|
||||
runCli().catch(() => {
|
||||
console.error("Browser Runtime fixture failed to start");
|
||||
process.exitCode = 1;
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user