feat(runtime): 即梦本机运行时 - 接入安全生成闭环与桌面本机渠道

Refs #225

Co-authored-by: zkhyww <127009345+zkhyww@users.noreply.github.com>
This commit is contained in:
ddCatandzkhyww committed 2026-08-15 12:45:29 +08:00
1 parent 8f2232feee
commit 57e1820f41
179 files changed
+49746 -1897

No files matched your search

+147
View File
@@ -0,0 +1,147 @@
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import type { LocalRuntimeConfig } from "../config.js";
import {
dreaminaGenerationInputSchema,
dreaminaMcpToolShape,
type DreaminaGenerationInput,
} from "../dreamina-cli-contract.js";
import {
parseDreaminaPublicRunError,
parseDreaminaPublicRuntimeResult,
} from "../dreamina-public-result.js";
type RuntimeConfig = Pick<LocalRuntimeConfig, "url" | "token">;
type DreaminaMcpRequestOptions = { signal?: AbortSignal; timeoutMs?: number };
const MCP_DEADLINE_MS = 180_000;
const MCP_RESPONSE_LIMIT_BYTES = 256 * 1024;
export type DreaminaMcpDependencies = {
postPublicTool?: typeof postDreaminaCliTool;
};
export function registerDreaminaMcp(
server: McpServer,
config: RuntimeConfig,
dependencies: DreaminaMcpDependencies = {},
) {
const postPublicTool = dependencies.postPublicTool ?? postDreaminaCliTool;
server.registerTool("dreamina_cli", {
description: "仅当用户明确要求使用 Dreamina 本机 OAuth CLI 时调用。生成会消耗 Dreamina credits;不得替代宿主自定义渠道或火山即梦 AK/SK API。For image generation with automatic resolution, omit resolutionType instead of inventing a tier or passing auto; image_upscale still requires an explicit tier.",
inputSchema: dreaminaMcpToolShape.shape,
}, async (input: unknown, extra) => {
const parsedInput = dreaminaGenerationInputSchema.parse(input);
if (extra.signal?.aborted) throw publicError("dreamina_cancelled");
let unsafeResult;
try {
unsafeResult = await postPublicTool(config, parsedInput, { signal: extra.signal });
} catch {
throw publicError("dreamina_submission_unknown");
}
let result;
try {
result = parseDreaminaPublicRuntimeResult(unsafeResult);
} catch {
throw publicError("dreamina_submission_unknown");
}
return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] };
});
}
export async function postDreaminaCliTool(
config: RuntimeConfig,
input: DreaminaGenerationInput,
options: DreaminaMcpRequestOptions = {},
) {
if (options.signal?.aborted) throw publicError("dreamina_cancelled");
const controller = new AbortController();
const timeoutMs = Math.max(1, Math.min(options.timeoutMs ?? MCP_DEADLINE_MS, MCP_DEADLINE_MS));
const cancel = () => controller.abort();
options.signal?.addEventListener("abort", cancel, { once: true });
const deadline = setTimeout(cancel, timeoutMs);
deadline.unref();
let dispatched = false;
let statusCode = 0;
let body: unknown;
try {
const responsePromise = fetch(`${config.url}/dreamina/run`, {
method: "POST",
headers: {
"content-type": "application/json",
"x-canvas-agent-token": config.token,
},
body: JSON.stringify(input),
signal: controller.signal,
redirect: "manual",
});
dispatched = true;
const response = await responsePromise;
statusCode = response.status;
body = await readBoundedJson(response);
} catch {
throw publicError(dispatched ? "dreamina_submission_unknown" : "dreamina_internal_error");
} finally {
clearTimeout(deadline);
options.signal?.removeEventListener("abort", cancel);
}
const envelope = parsePublicEnvelope(statusCode, body);
if (!envelope.ok) throw publicError(envelope.code);
return envelope.result;
}
async function readBoundedJson(response: Response): Promise<unknown> {
const declared = Number(response.headers.get("content-length"));
if (Number.isFinite(declared) && declared > MCP_RESPONSE_LIMIT_BYTES) throw new Error("response too large");
if (!response.body) throw new Error("missing response body");
const reader = response.body.getReader();
const chunks: Buffer[] = [];
let total = 0;
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
total += value.byteLength;
if (total > MCP_RESPONSE_LIMIT_BYTES) throw new Error("response too large");
chunks.push(Buffer.from(value));
}
} catch (error) {
await reader.cancel().catch(() => undefined);
throw error;
}
const parsed = JSON.parse(Buffer.concat(chunks).toString("utf8")) as unknown;
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("invalid response");
return parsed;
}
function parsePublicEnvelope(statusCode: number, value: unknown):
| { ok: true; result: ReturnType<typeof parseDreaminaPublicRuntimeResult> }
| { ok: false; code: string } {
if (!value || typeof value !== "object" || Array.isArray(value)) return { ok: false, code: "dreamina_submission_unknown" };
const source = value as Record<string, unknown>;
const successStatus = statusCode >= 200 && statusCode < 300;
if (source.ok === true && exactKeys(source, ["ok", "result"])) {
if (!successStatus) return { ok: false, code: "dreamina_submission_unknown" };
try {
return { ok: true, result: parseDreaminaPublicRuntimeResult(source.result) };
} catch {
return { ok: false, code: "dreamina_submission_unknown" };
}
}
if (source.ok === false && exactKeys(source, ["ok", "code", "message"])) {
if (successStatus) return { ok: false, code: "dreamina_submission_unknown" };
const error = parseDreaminaPublicRunError(source.code, source.message);
return error.code !== "dreamina_submission_unknown" && error.statusCode === statusCode
? { ok: false, code: error.code }
: { ok: false, code: "dreamina_submission_unknown" };
}
return { ok: false, code: "dreamina_submission_unknown" };
}
function exactKeys(source: Record<string, unknown>, expected: string[]) {
const keys = Object.keys(source);
return keys.length === expected.length && expected.every((key) => Object.hasOwn(source, key));
}
function publicError(code: string) {
return new Error(`Dreamina CLI request failed (${code})`);
}