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,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})`);
|
||||
}
|
||||
Reference in new issue
Block a user