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 f549767cda
commit 914717cae6
179 files changed
+49746 -1897

No files matched your search

+298
View File
@@ -0,0 +1,298 @@
import type { LocalRuntimeTransport } from "@/services/local-runtime";
import { LocalRuntimeClientError } from "@/services/local-runtime-session";
export type DreaminaCliState = "missing" | "installed" | "login_pending" | "authenticated" | "error";
export type DreaminaLifecycleStatusCode = "dreamina_missing" | "dreamina_version_failed" | "dreamina_login_required" | "dreamina_login_pending";
export type DreaminaCliStatus = {
provider: "dreamina-cli";
state: DreaminaCliState;
installed: boolean;
authenticated: boolean;
version?: string;
totalCredit?: number;
accountBinding?: string;
sessionEpoch?: number;
creditObservedAt?: string;
code?: DreaminaLifecycleStatusCode;
message: string;
verificationUri?: string;
userCode?: string;
expiresAt?: string;
};
export type DreaminaRequestOptions = {
signal?: AbortSignal;
timeoutMs?: number;
now?: () => number;
};
export class DreaminaAgentError extends Error {
constructor(
readonly code: string,
readonly status: number,
message: string,
) {
super(message);
this.name = "DreaminaAgentError";
}
}
const MAX_RESPONSE_BYTES = 64 * 1024;
export const DREAMINA_REQUEST_TIMEOUTS = { status: 30_000, login: 35_000, logout: 20_000 } as const;
export type DreaminaLifecycleAction = keyof typeof DREAMINA_REQUEST_TIMEOUTS;
export function dreaminaRequestTimeout(action: DreaminaLifecycleAction, override?: number) {
return override ?? DREAMINA_REQUEST_TIMEOUTS[action];
}
const LIFECYCLE_CODES = new Set<DreaminaLifecycleStatusCode>(["dreamina_missing", "dreamina_version_failed", "dreamina_login_required", "dreamina_login_pending"]);
const STABLE_ERROR_MESSAGES: Record<string, string> = {
dreamina_cancelled: "Dreamina 操作已取消",
dreamina_cleanup_failed: "Dreamina 进程清理失败,请检查本机进程状态",
dreamina_command_invalid: "Dreamina CLI 命令无效",
dreamina_command_timeout: "Dreamina 操作超时,相关进程已清理",
dreamina_environment_invalid: "Dreamina 环境变量无效",
dreamina_idempotency_conflict: "Dreamina 操作标识冲突",
dreamina_internal_error: "Dreamina CLI 请求失败",
dreamina_login_failed: "无法启动 Dreamina OAuth 登录",
dreamina_login_pending: "请先完成 Dreamina 官方页面授权",
dreamina_login_required: "Dreamina CLI 需要登录",
dreamina_login_response_invalid: "Dreamina 登录响应无法识别,请升级 CLI",
dreamina_logout_failed: "Dreamina 退出失败,请重试",
dreamina_missing: "未检测到 Dreamina CLI",
dreamina_output_too_large: "Dreamina 返回内容超过安全上限",
dreamina_query_failed: "Dreamina 查询失败",
dreamina_query_response_invalid: "Dreamina 查询响应无效",
dreamina_reference_budget_exceeded: "Dreamina 参考文件超过安全上限",
dreamina_reference_cleanup_failed: "Dreamina 参考文件清理失败",
dreamina_reference_invalid: "Dreamina 参考文件无效",
dreamina_request_invalid: "Dreamina 请求参数无效",
dreamina_spawn_failed: "无法启动 Dreamina CLI",
dreamina_state_busy: "Dreamina 正在处理另一项操作",
dreamina_state_fenced: "Dreamina 操作已失效",
dreamina_state_invalid: "Dreamina 本机状态无效",
dreamina_submission_unknown: "Dreamina 提交结果未知,请先检查状态",
dreamina_version_failed: "Dreamina CLI 版本检测失败",
};
export function getDreaminaStatus(client: LocalRuntimeTransport, options: DreaminaRequestOptions = {}) {
return requestDreamina(client, "GET", "/dreamina/status", {
...options,
timeoutMs: dreaminaRequestTimeout("status", options.timeoutMs),
});
}
export function loginDreamina(client: LocalRuntimeTransport, options: DreaminaRequestOptions = {}) {
return requestDreamina(client, "POST", "/dreamina/login", {
...options,
timeoutMs: dreaminaRequestTimeout("login", options.timeoutMs),
});
}
export function logoutDreamina(client: LocalRuntimeTransport, options: DreaminaRequestOptions = {}) {
return requestDreamina(client, "POST", "/dreamina/logout", {
...options,
timeoutMs: dreaminaRequestTimeout("logout", options.timeoutMs),
});
}
async function requestDreamina(client: LocalRuntimeTransport, method: "GET" | "POST", route: "/dreamina/status" | "/dreamina/login" | "/dreamina/logout", options: DreaminaRequestOptions) {
if (options.signal?.aborted) throw publicError("dreamina_cancelled", 499);
const controller = new AbortController();
let timedOut = false;
const abort = () => controller.abort();
options.signal?.addEventListener("abort", abort, { once: true });
const timer = setTimeout(
() => {
timedOut = true;
controller.abort();
},
Math.max(1, options.timeoutMs ?? DREAMINA_REQUEST_TIMEOUTS.status),
);
try {
const response = await client.request(route, {
method,
...(method === "POST"
? {
headers: { "content-type": "application/json" },
body: "{}",
}
: {}),
signal: controller.signal,
});
if (response.redirected || response.type === "opaqueredirect" || (response.status >= 300 && response.status < 400)) {
throw publicError("dreamina_response_invalid", 502);
}
const body = await readBoundedJson(response);
if (!response.ok || !isRecord(body) || body.ok !== true) {
const code = isRecord(body) && typeof body.code === "string" && body.code in STABLE_ERROR_MESSAGES ? body.code : "dreamina_internal_error";
throw publicError(code, response.status || 500);
}
return parseStatus(body.status, options.now?.() ?? Date.now());
} catch (error) {
if (error instanceof DreaminaAgentError) throw error;
if (controller.signal.aborted) {
throw publicError(timedOut ? "dreamina_timeout" : "dreamina_cancelled", timedOut ? 504 : 499);
}
if (error instanceof LocalRuntimeClientError && error.code === "session_required") {
throw publicError("dreamina_runtime_required", 401);
}
throw publicError("dreamina_runtime_unreachable", 503);
} finally {
clearTimeout(timer);
options.signal?.removeEventListener("abort", abort);
}
}
async function readBoundedJson(response: Response): Promise<unknown> {
const declaredLength = Number(response.headers.get("content-length"));
if (Number.isFinite(declaredLength) && declaredLength > MAX_RESPONSE_BYTES) {
throw publicError("dreamina_response_invalid", 502);
}
if (!response.body) throw publicError("dreamina_response_invalid", 502);
const reader = response.body.getReader();
const chunks: Uint8Array[] = [];
let total = 0;
try {
while (true) {
const item = await reader.read();
if (item.done) break;
total += item.value.byteLength;
if (total > MAX_RESPONSE_BYTES) {
await reader.cancel();
throw publicError("dreamina_response_invalid", 502);
}
chunks.push(item.value);
}
} finally {
reader.releaseLock();
}
const bytes = new Uint8Array(total);
let offset = 0;
for (const chunk of chunks) {
bytes.set(chunk, offset);
offset += chunk.byteLength;
}
try {
return JSON.parse(new TextDecoder().decode(bytes)) as unknown;
} catch {
throw publicError("dreamina_response_invalid", 502);
}
}
function parseStatus(value: unknown, now: number): DreaminaCliStatus {
if (!isRecord(value)) throw publicError("dreamina_response_invalid", 502);
const allowed = new Set(["provider", "state", "installed", "authenticated", "version", "code", "message"]);
if (value.state === "authenticated") {
allowed.add("totalCredit");
allowed.add("accountBinding");
allowed.add("sessionEpoch");
allowed.add("creditObservedAt");
}
if (value.state === "login_pending") {
allowed.add("verificationUri");
allowed.add("userCode");
allowed.add("expiresAt");
}
if (
Object.keys(value).some((key) => !allowed.has(key)) ||
value.provider !== "dreamina-cli" ||
!isDreaminaState(value.state) ||
typeof value.installed !== "boolean" ||
typeof value.authenticated !== "boolean" ||
typeof value.message !== "string" ||
value.message.length < 1 ||
value.message.length > 500 ||
(value.version !== undefined && (typeof value.version !== "string" || !/^[A-Za-z0-9._+:-]{1,120}$/.test(value.version))) ||
(value.totalCredit !== undefined && (value.state !== "authenticated" || !Number.isInteger(value.totalCredit) || (value.totalCredit as number) < 0 || (value.totalCredit as number) > 1_000_000_000)) ||
(value.accountBinding === undefined) !== (value.sessionEpoch === undefined) ||
(value.accountBinding !== undefined && (value.state !== "authenticated" || typeof value.accountBinding !== "string" || !/^[A-Za-z0-9._:-]{8,160}$/.test(value.accountBinding))) ||
(value.sessionEpoch !== undefined && (value.state !== "authenticated" || !Number.isSafeInteger(value.sessionEpoch) || (value.sessionEpoch as number) < 0)) ||
(value.creditObservedAt !== undefined && (value.totalCredit === undefined || typeof value.creditObservedAt !== "string" || !Number.isFinite(Date.parse(value.creditObservedAt)))) ||
(value.code !== undefined && (typeof value.code !== "string" || !LIFECYCLE_CODES.has(value.code as DreaminaLifecycleStatusCode)))
) {
throw publicError("dreamina_response_invalid", 502);
}
const code = value.code as DreaminaLifecycleStatusCode | undefined;
if (!consistentStatus(value.state, value.installed, value.authenticated, code)) {
throw publicError("dreamina_response_invalid", 502);
}
let verification: Pick<DreaminaCliStatus, "verificationUri" | "userCode" | "expiresAt"> = {};
if (value.state === "login_pending") {
if (!isOfficialVerificationUri(value.verificationUri) || typeof value.userCode !== "string" || !/^[A-Za-z0-9-]{4,32}$/.test(value.userCode) || typeof value.expiresAt !== "string") {
throw publicError("dreamina_response_invalid", 502);
}
const expiresAt = Date.parse(value.expiresAt);
if (!Number.isFinite(expiresAt) || expiresAt <= now || expiresAt > now + 31 * 60_000) {
throw publicError("dreamina_response_invalid", 502);
}
verification = {
verificationUri: value.verificationUri,
userCode: value.userCode,
expiresAt: value.expiresAt,
};
}
return {
provider: "dreamina-cli",
state: value.state,
installed: value.installed,
authenticated: value.authenticated,
...(typeof value.version === "string" ? { version: value.version } : {}),
...(typeof value.totalCredit === "number" ? { totalCredit: value.totalCredit } : {}),
...(typeof value.accountBinding === "string" ? { accountBinding: value.accountBinding } : {}),
...(typeof value.sessionEpoch === "number" ? { sessionEpoch: value.sessionEpoch } : {}),
...(typeof value.creditObservedAt === "string" ? { creditObservedAt: value.creditObservedAt } : {}),
...(code ? { code } : {}),
message: publicStatusMessage(value.state),
...verification,
};
}
function consistentStatus(state: DreaminaCliState, installed: boolean, authenticated: boolean, code: DreaminaLifecycleStatusCode | undefined) {
if (state === "missing") return !installed && !authenticated && code === "dreamina_missing";
if (state === "installed") return installed && !authenticated && code === "dreamina_login_required";
if (state === "login_pending") return installed && !authenticated && code === "dreamina_login_pending";
if (state === "authenticated") return installed && authenticated && code === undefined;
return installed && !authenticated && code === "dreamina_version_failed";
}
function isDreaminaState(value: unknown): value is DreaminaCliState {
return value === "missing" || value === "installed" || value === "login_pending" || value === "authenticated" || value === "error";
}
function isOfficialVerificationUri(value: unknown): value is string {
if (typeof value !== "string" || value.length > 2_048) return false;
try {
const url = new URL(value);
return url.protocol === "https:" && url.hostname === "jimeng.jianying.com" && url.pathname === "/ai-tool/cli-auth" && !url.username && !url.password && !url.hash;
} catch {
return false;
}
}
function publicStatusMessage(state: DreaminaCliState) {
if (state === "missing") return "未检测到 Dreamina CLI,请先完成本机安装";
if (state === "installed") return "Dreamina CLI 已安装,需要登录";
if (state === "login_pending") return "请在官方页面确认 Dreamina 登录";
if (state === "authenticated") return "Dreamina CLI 已登录";
return "已找到 Dreamina CLI,但版本检测失败";
}
function publicError(code: string, status: number) {
const messages: Record<string, string> = {
...STABLE_ERROR_MESSAGES,
dreamina_cancelled: "Dreamina 操作已取消",
dreamina_response_invalid: "Dreamina CLI 响应无效",
dreamina_runtime_required: "请先连接本机运行时",
dreamina_runtime_unreachable: "本机运行时暂时不可用",
dreamina_timeout: "Dreamina CLI 请求超时",
};
return new DreaminaAgentError(code, status, messages[code] ?? "Dreamina CLI 请求失败");
}
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value && typeof value === "object" && !Array.isArray(value));
}