fix(POSTV1-02): 接通真实AI生成链路
Dada P0-A isolated Windows CI / validate-and-package (push) Waiting to run

This commit is contained in:
suyx
2026-08-05 12:29:14 +08:00
parent 43d946bb5c
commit cbb7f658a3
10 changed files with 440 additions and 24 deletions
+2 -2
View File
@@ -16,7 +16,7 @@ import { MockResendAdapter } from "./resend-adapter.js";
import { readSecureConfigCandidate } from "./secure-config.js"; import { readSecureConfigCandidate } from "./secure-config.js";
import { StructuredJsonlLogger } from "./structured-log.js"; import { StructuredJsonlLogger } from "./structured-log.js";
import { attachApiSupervisorControl, initializeApiCredentialClients, receiveApiCredentials } from "./supervisor-channel.js"; import { attachApiSupervisorControl, initializeApiCredentialClients, receiveApiCredentials } from "./supervisor-channel.js";
import { ModelConfigurationService } from "./model-configuration.js"; import { ModelConfigurationService, portableRuntimeModelCandidates } from "./model-configuration.js";
import { MockAmapAdapter, type AmapAdapter } from "./amap-adapter.js"; import { MockAmapAdapter, type AmapAdapter } from "./amap-adapter.js";
import { StickerReleaseService } from "./sticker-releases.js"; import { StickerReleaseService } from "./sticker-releases.js";
import { createAdminDiagnosticsProvider, createAdminServicesStorageProvider } from "./admin-state.js"; import { createAdminDiagnosticsProvider, createAdminServicesStorageProvider } from "./admin-state.js";
@@ -56,7 +56,7 @@ if (credentialChannelEnabled) {
storage = new ManagedStorage({ dataRoot, databasePath }); storage = new ManagedStorage({ dataRoot, databasePath });
stickers = new StickerReleaseService({ databasePath, storage }); stickers = new StickerReleaseService({ databasePath, storage });
latestExports = new LatestExportService({ databasePath, storage }); latestExports = new LatestExportService({ databasePath, storage });
models = new ModelConfigurationService({ database: registration.database }); models = new ModelConfigurationService({ database: registration.database, seedCandidates: portableRuntimeModelCandidates });
recentAssets = new RecentAssetService({ database: registration.database }); recentAssets = new RecentAssetService({ database: registration.database });
registration.applySecureConfig(readSecureConfigCandidate(instanceConfigPath)); registration.applySecureConfig(readSecureConfigCandidate(instanceConfigPath));
} catch (error) { } catch (error) {
+49 -6
View File
@@ -111,7 +111,7 @@ const defaultErrorMapping: Record<string, string> = {
upstream_timeout: "upstream_timeout", upstream_timeout: "upstream_timeout",
}; };
const seedCandidates: ModelConfigCandidate[] = [ const defaultSeedCandidates: ModelConfigCandidate[] = [
{ {
model_id: modelIds[0], display_name: "Gemini 3.1 Flash Image Preview", enabled: true, is_default: true, model_id: modelIds[0], display_name: "Gemini 3.1 Flash Image Preview", enabled: true, is_default: true,
recommendation_priority: 1, route_profile: { endpoint: "https://mock.invalid/v1/images", mode: "sync" }, recommendation_priority: 1, route_profile: { endpoint: "https://mock.invalid/v1/images", mode: "sync" },
@@ -138,6 +138,42 @@ const seedCandidates: ModelConfigCandidate[] = [
}, },
]; ];
export const portableRuntimeModelCandidates: ModelConfigCandidate[] = [
{
...defaultSeedCandidates[0]!,
display_name: "Gemini 3.1 Flash Image",
route_profile: {
endpoint: "https://oneapi.intelligrow.cn/v1/chat/completions",
mode: "sync",
protocol_version: "gemini-openai-chat-v1",
provider_model_id: "gemini-3.1-flash-image",
},
gateway_account_ref: "oneapi-intelligrow-test",
contract_validation_status: "verified",
contract_evidence_ref: "contract:wp7-02:gemini-3.1-flash-image:v7",
},
{
...defaultSeedCandidates[1]!,
enabled: false,
route_profile: { endpoint: "https://oneapi.intelligrow.cn/unsupported", mode: "disabled" },
gateway_account_ref: "oneapi-intelligrow-test",
contract_validation_status: "unverified",
contract_evidence_ref: null,
},
{
...defaultSeedCandidates[2]!,
route_profile: {
endpoint: "https://oneapi.intelligrow.cn/v1/images/generations",
mode: "sync",
protocol_version: "openai-images-v1",
reference_endpoint: "https://oneapi.intelligrow.cn/v1/images/edits",
},
gateway_account_ref: "oneapi-intelligrow-test",
contract_validation_status: "verified",
contract_evidence_ref: "contract:wp7-02:gpt-image-2:v2",
},
];
function stableJson(value: unknown): string { function stableJson(value: unknown): string {
if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`; if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
if (value && typeof value === "object") { if (value && typeof value === "object") {
@@ -207,17 +243,20 @@ export interface ModelConfigurationServiceOptions {
clock?: () => number; clock?: () => number;
database: BetterSqlite3.Database; database: BetterSqlite3.Database;
onChanged?: (configSetVersion: number) => void; onChanged?: (configSetVersion: number) => void;
seedCandidates?: ModelConfigCandidate[];
} }
export class ModelConfigurationService { export class ModelConfigurationService {
readonly database: BetterSqlite3.Database; readonly database: BetterSqlite3.Database;
readonly #clock: () => number; readonly #clock: () => number;
readonly #onChanged: ((configSetVersion: number) => void) | undefined; readonly #onChanged: ((configSetVersion: number) => void) | undefined;
readonly #seedCandidates: ModelConfigCandidate[];
constructor(options: ModelConfigurationServiceOptions) { constructor(options: ModelConfigurationServiceOptions) {
this.database = options.database; this.database = options.database;
this.#clock = options.clock ?? Date.now; this.#clock = options.clock ?? Date.now;
this.#onChanged = options.onChanged; this.#onChanged = options.onChanged;
this.#seedCandidates = structuredClone(options.seedCandidates ?? defaultSeedCandidates);
this.ensureSchema(); this.ensureSchema();
} }
@@ -503,7 +542,7 @@ export class ModelConfigurationService {
const current = this.database.prepare("SELECT config_set_id FROM model_config_current WHERE singleton = 1").get() as { config_set_id: string } | undefined; const current = this.database.prepare("SELECT config_set_id FROM model_config_current WHERE singleton = 1").get() as { config_set_id: string } | undefined;
if (current) return; if (current) return;
const seed = this.database.transaction(() => { const seed = this.database.transaction(() => {
validateModelConfigurationCandidateSet(seedCandidates); validateModelConfigurationCandidateSet(this.#seedCandidates);
const now = this.#clock(); const now = this.#clock();
const setId = randomUUID(); const setId = randomUUID();
this.database.prepare("INSERT INTO model_config_sets (config_set_id, config_set_version, created_at, created_by) VALUES (?, 1, ?, 'system_seed')") this.database.prepare("INSERT INTO model_config_sets (config_set_id, config_set_version, created_at, created_by) VALUES (?, 1, ?, 'system_seed')")
@@ -520,7 +559,9 @@ export class ModelConfigurationService {
INSERT INTO model_config_set_members (config_set_id, model_id, config_version, enabled, is_default, recommendation_priority) INSERT INTO model_config_set_members (config_set_id, model_id, config_version, enabled, is_default, recommendation_priority)
VALUES (?, ?, 1, ?, ?, ?) VALUES (?, ?, 1, ?, ?, ?)
`); `);
for (const candidate of seedCandidates) { for (const candidate of this.#seedCandidates) {
const contractStatus = candidate.contract_validation_status ?? "unverified";
const contractEvidenceRef = contractStatus === "unverified" ? null : candidate.contract_evidence_ref ?? null;
const routeProfileId = profileRef("route", candidate.route_profile); const routeProfileId = profileRef("route", candidate.route_profile);
const errorMappingProfileId = profileRef("error", candidate.error_mapping_profile); const errorMappingProfileId = profileRef("error", candidate.error_mapping_profile);
this.database.prepare("INSERT OR IGNORE INTO gateway_route_profiles (route_profile_id, profile_json, created_at) VALUES (?, ?, ?)") this.database.prepare("INSERT OR IGNORE INTO gateway_route_profiles (route_profile_id, profile_json, created_at) VALUES (?, ?, ?)")
@@ -530,12 +571,14 @@ export class ModelConfigurationService {
insertVersion.run(candidate.model_id, candidate.display_name, candidate.enabled ? 1 : 0, candidate.is_default ? 1 : 0, insertVersion.run(candidate.model_id, candidate.display_name, candidate.enabled ? 1 : 0, candidate.is_default ? 1 : 0,
candidate.recommendation_priority, routeProfileId, stableJson(candidate.route_profile), candidate.gateway_account_ref, candidate.recommendation_priority, routeProfileId, stableJson(candidate.route_profile), candidate.gateway_account_ref,
errorMappingProfileId, stableJson(candidate.error_mapping_profile), candidate.credit_cost, stableJson(candidate.supported_ratios), stableJson(candidate.reference_limits), errorMappingProfileId, stableJson(candidate.error_mapping_profile), candidate.credit_cost, stableJson(candidate.supported_ratios), stableJson(candidate.reference_limits),
candidate.prompt_max_length, candidate.safety_source, "unverified", null, fingerprint(candidate), now); candidate.prompt_max_length, candidate.safety_source, contractStatus, contractEvidenceRef, fingerprint(candidate), now);
insertMember.run(setId, candidate.model_id, candidate.enabled ? 1 : 0, candidate.is_default ? 1 : 0, candidate.recommendation_priority); insertMember.run(setId, candidate.model_id, candidate.enabled ? 1 : 0, candidate.is_default ? 1 : 0, candidate.recommendation_priority);
const available = candidate.enabled && contractStatus === "verified";
const runtimeReason = !candidate.enabled ? "configured_disabled" : available ? "available" : "contract_unverified";
this.database.prepare(` this.database.prepare(`
INSERT INTO model_runtime_availability (model_id, available_for_new_jobs, reason, checked_at, runtime_availability_version) INSERT INTO model_runtime_availability (model_id, available_for_new_jobs, reason, checked_at, runtime_availability_version)
VALUES (?, 0, 'contract_unverified', ?, 0) VALUES (?, ?, ?, ?, 0)
`).run(candidate.model_id, now); `).run(candidate.model_id, available ? 1 : 0, runtimeReason, now);
} }
this.database.prepare("INSERT INTO model_config_current (singleton, config_set_id) VALUES (1, ?)").run(setId); this.database.prepare("INSERT INTO model_config_current (singleton, config_set_id) VALUES (1, ?)").run(setId);
}); });
+6
View File
@@ -7,6 +7,11 @@ export interface GenerationAdapterRequest {
prompt: string; prompt: string;
ratio: "3:4" | "1:1" | "4:3" | "9:16"; ratio: "3:4" | "1:1" | "4:3" | "9:16";
referenceAssetIds: readonly string[]; referenceAssetIds: readonly string[];
referenceImages?: readonly {
assetId: string;
bytes: Buffer;
mimeType: "image/jpeg" | "image/png" | "image/webp";
}[];
} }
export interface NormalizedGenerationOutput { export interface NormalizedGenerationOutput {
@@ -27,6 +32,7 @@ export type GenerationAdapterResult =
}; };
export interface GenerationAdapter { export interface GenerationAdapter {
dispose?(): void;
start(request: GenerationAdapterRequest): Promise<GenerationAdapterResult>; start(request: GenerationAdapterRequest): Promise<GenerationAdapterResult>;
poll?(upstreamJobReference: string): Promise<GenerationAdapterResult>; poll?(upstreamJobReference: string): Promise<GenerationAdapterResult>;
} }
+36 -3
View File
@@ -1,11 +1,11 @@
import { createHash, randomUUID } from "node:crypto"; import { createHash, randomUUID } from "node:crypto";
import { existsSync, mkdirSync, renameSync, rmSync, writeFileSync } from "node:fs"; import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
import { dirname, join, resolve } from "node:path"; import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
import Database from "better-sqlite3"; import Database from "better-sqlite3";
import type BetterSqlite3 from "better-sqlite3"; import type BetterSqlite3 from "better-sqlite3";
import type { GenerationAdapter, GenerationAdapterResult, NormalizedGenerationOutput } from "./ai-adapter-contract.js"; import type { GenerationAdapter, GenerationAdapterRequest, GenerationAdapterResult, NormalizedGenerationOutput } from "./ai-adapter-contract.js";
import { GatewayBalanceRuntime } from "./gateway-balance-runtime.js"; import { GatewayBalanceRuntime } from "./gateway-balance-runtime.js";
import { generationErrorRegistry, type GenerationErrorCategory } from "./generation-error-registry.js"; import { generationErrorRegistry, type GenerationErrorCategory } from "./generation-error-registry.js";
import { configureWorkerDatabase } from "./sqlite-connection.js"; import { configureWorkerDatabase } from "./sqlite-connection.js";
@@ -120,6 +120,7 @@ export class GenerationProcessor {
.run("worker_stopped", now, this.workerId); .run("worker_stopped", now, this.workerId);
}); });
this.gatewayBalance.close(); this.gatewayBalance.close();
this.adapter.dispose?.();
this.database.close(); this.database.close();
} }
@@ -144,6 +145,12 @@ export class GenerationProcessor {
SELECT managed_file_id FROM generation_reference_snapshots WHERE generation_id = ? ORDER BY position SELECT managed_file_id FROM generation_reference_snapshots WHERE generation_id = ? ORDER BY position
`).all(generationId) as Array<{ managed_file_id: string }>; `).all(generationId) as Array<{ managed_file_id: string }>;
let adapterResult: GenerationAdapterResult; let adapterResult: GenerationAdapterResult;
let referenceImages: NonNullable<GenerationAdapterRequest["referenceImages"]>;
try {
referenceImages = this.loadReferenceImages(references.map((row) => row.managed_file_id));
} catch {
return this.completeFailure(job, "reference_invalid", "reference_load_failed");
}
try { try {
if (job.upstream_job_reference) { if (job.upstream_job_reference) {
if (!this.adapter.poll) return this.completeFailure(job, "unknown_retryable", "poll_unsupported", undefined, false, "pending_manual_review"); if (!this.adapter.poll) return this.completeFailure(job, "unknown_retryable", "poll_unsupported", undefined, false, "pending_manual_review");
@@ -156,10 +163,13 @@ export class GenerationProcessor {
prompt: job.prompt, prompt: job.prompt,
ratio: job.ratio, ratio: job.ratio,
referenceAssetIds: references.map((row) => row.managed_file_id), referenceAssetIds: references.map((row) => row.managed_file_id),
referenceImages,
})); }));
} }
} catch { } catch {
return this.completeFailure(job, "unknown_retryable", "adapter_exception", undefined, false, "pending_manual_review"); return this.completeFailure(job, "unknown_retryable", "adapter_exception", undefined, false, "pending_manual_review");
} finally {
for (const reference of referenceImages) reference.bytes.fill(0);
} }
if (adapterResult.status === "failed") return this.completeFailure(job, adapterResult.category, adapterResult.sourceCategory, adapterResult.balanceSignal); if (adapterResult.status === "failed") return this.completeFailure(job, adapterResult.category, adapterResult.sourceCategory, adapterResult.balanceSignal);
@@ -175,6 +185,29 @@ export class GenerationProcessor {
} }
} }
private loadReferenceImages(referenceAssetIds: string[]): NonNullable<GenerationAdapterRequest["referenceImages"]> {
return referenceAssetIds.map((assetId) => {
const row = this.database.prepare(`
SELECT relative_path, mime_type FROM managed_files
WHERE file_id = ? AND file_kind = 'reference' AND status = 'committed'
`).get(assetId) as { mime_type: string; relative_path: string } | undefined;
if (!row || !["image/jpeg", "image/png", "image/webp"].includes(row.mime_type) || isAbsolute(row.relative_path)) {
throw new Error("reference_invalid");
}
const path = resolve(this.dataRoot, row.relative_path);
const child = relative(this.dataRoot, path);
if (!child || child === ".." || child.startsWith(`..${sep}`) || isAbsolute(child)
|| !existsSync(path) || !statSync(path).isFile()) {
throw new Error("reference_invalid");
}
return {
assetId,
bytes: readFileSync(path),
mimeType: row.mime_type as "image/jpeg" | "image/png" | "image/webp",
};
});
}
private claim(generationId: string) { private claim(generationId: string) {
return this.immediate(() => { return this.immediate(() => {
const row = this.readJob(generationId); const row = this.readJob(generationId);
@@ -0,0 +1,217 @@
import sharp from "sharp";
import type {
GenerationAdapter,
GenerationAdapterRequest,
GenerationAdapterResult,
NormalizedGenerationOutput,
} from "./ai-adapter-contract.js";
import { gptImageRequestSizeForRatio, normalizeImageOutputToRatio } from "./image-output-normalizer.mjs";
const geminiProductModelId = "gemini-3.1-flash-image-preview";
const geminiProviderModelId = "gemini-3.1-flash-image";
const gptImageModelId = "gpt-image-2";
const geminiEndpoint = "https://oneapi.intelligrow.cn/v1/chat/completions";
const gptImageEndpoint = "https://oneapi.intelligrow.cn/v1/images/generations";
const gptImageReferenceEndpoint = "https://oneapi.intelligrow.cn/v1/images/edits";
const maximumResponseBytes = 32 * 1024 * 1024;
const requestTimeoutMilliseconds = 180_000;
type FetchLike = typeof fetch;
class OneApiRuntimeError extends Error {
constructor(
readonly category: "gateway_balance_insufficient" | "gateway_contract_invalid" | "model_disabled" | "reference_invalid" | "upstream_failed" | "upstream_timeout" | "unknown_non_retryable",
readonly sourceCategory: string,
) {
super(sourceCategory);
}
}
function failure(error: unknown): GenerationAdapterResult {
if (error instanceof OneApiRuntimeError) {
return { category: error.category, sourceCategory: error.sourceCategory, status: "failed" };
}
if (error instanceof Error && error.name === "AbortError") {
return { category: "upstream_timeout", sourceCategory: "upstream_timeout", status: "failed" };
}
return { category: "upstream_failed", sourceCategory: "upstream_failed", status: "failed" };
}
function mapHttpFailure(status: number) {
if (status === 408 || status === 504) return new OneApiRuntimeError("upstream_timeout", `upstream_http_${status}`);
if (status === 429) return new OneApiRuntimeError("gateway_balance_insufficient", "upstream_http_429");
if (status >= 500) return new OneApiRuntimeError("upstream_failed", `upstream_http_${status}`);
if (status === 400 || status === 404 || status === 422) return new OneApiRuntimeError("gateway_contract_invalid", `upstream_http_${status}`);
return new OneApiRuntimeError("unknown_non_retryable", `upstream_http_${status}`);
}
async function readBoundedJson(response: Response) {
const declaredLength = Number(response.headers.get("content-length") ?? 0);
if (Number.isFinite(declaredLength) && declaredLength > maximumResponseBytes) {
throw new OneApiRuntimeError("gateway_contract_invalid", "upstream_response_too_large");
}
if (!response.body) throw new OneApiRuntimeError("gateway_contract_invalid", "upstream_response_empty");
const reader = response.body.getReader();
const chunks: Buffer[] = [];
let total = 0;
try {
while (true) {
const next = await reader.read();
if (next.done) break;
const chunk = Buffer.from(next.value);
total += chunk.length;
if (total > maximumResponseBytes) {
await reader.cancel();
throw new OneApiRuntimeError("gateway_contract_invalid", "upstream_response_too_large");
}
chunks.push(chunk);
}
try {
return JSON.parse(Buffer.concat(chunks).toString("utf8")) as unknown;
} catch {
throw new OneApiRuntimeError("gateway_contract_invalid", "upstream_response_invalid");
}
} finally {
for (const chunk of chunks) chunk.fill(0);
}
}
function extractGeminiImage(response: unknown) {
if (!response || typeof response !== "object" || !("choices" in response) || !Array.isArray(response.choices)) {
throw new OneApiRuntimeError("gateway_contract_invalid", "response_shape_invalid");
}
const choice = response.choices[0];
const content = choice && typeof choice === "object" && "message" in choice && choice.message && typeof choice.message === "object"
&& "content" in choice.message && typeof choice.message.content === "string" ? choice.message.content : "";
const matches = [...content.matchAll(/!\[[^\]]*\]\(\s*data:(image\/(?:jpeg|png|webp));base64,([A-Za-z0-9+/=\r\n]+)\s*\)/gi)];
if (matches.length !== 1) throw new OneApiRuntimeError("gateway_contract_invalid", "response_single_image_required");
return { bytes: Buffer.from(matches[0]![2]!, "base64"), declaredMimeType: matches[0]![1]!.toLowerCase() };
}
function extractGptImage(response: unknown) {
if (!response || typeof response !== "object" || !("data" in response) || !Array.isArray(response.data)
|| response.data.length !== 1 || !response.data[0] || typeof response.data[0] !== "object"
|| !("b64_json" in response.data[0]) || typeof response.data[0].b64_json !== "string") {
throw new OneApiRuntimeError("gateway_contract_invalid", "response_single_image_required");
}
return { bytes: Buffer.from(response.data[0].b64_json, "base64"), declaredMimeType: undefined };
}
async function normalizeOutput(bytes: Buffer, declaredMimeType: string | undefined, ratio: GenerationAdapterRequest["ratio"]): Promise<NormalizedGenerationOutput> {
try {
const metadata = await sharp(bytes, { failOn: "error", limitInputPixels: 40_000_000 }).metadata();
const mimeType = metadata.format === "png" ? "image/png" : metadata.format === "jpeg" ? "image/jpeg" : metadata.format === "webp" ? "image/webp" : undefined;
if (!mimeType || !metadata.width || !metadata.height || (declaredMimeType && declaredMimeType !== mimeType)) {
throw new OneApiRuntimeError("gateway_contract_invalid", "response_media_invalid");
}
const normalized = await normalizeImageOutputToRatio({ bytes, mimeType, pixelHeight: metadata.height, pixelWidth: metadata.width, ratio });
return { bytes: normalized.bytes, mimeType: normalized.mimeType, pixelHeight: normalized.pixelHeight, pixelWidth: normalized.pixelWidth };
} catch (error) {
if (error instanceof OneApiRuntimeError) throw error;
throw new OneApiRuntimeError("gateway_contract_invalid", "response_media_invalid");
}
}
function validateRequest(request: GenerationAdapterRequest) {
if (!request.prompt.trim() || request.prompt.length > 1_000) throw new OneApiRuntimeError("gateway_contract_invalid", "prompt_invalid");
const references = request.referenceImages ?? [];
if (references.length !== request.referenceAssetIds.length || references.length > 2) {
throw new OneApiRuntimeError("reference_invalid", "reference_count_invalid");
}
const totalBytes = references.reduce((total, reference) => total + reference.bytes.length, 0);
if (totalBytes > 20 * 1024 * 1024 || references.some((reference) => reference.bytes.length === 0 || reference.bytes.length > 10 * 1024 * 1024)) {
throw new OneApiRuntimeError("reference_invalid", "reference_size_invalid");
}
return references;
}
function buildRequest(request: GenerationAdapterRequest) {
const references = validateRequest(request);
if (request.modelId === geminiProductModelId) {
const content = references.length === 0
? request.prompt
: [
{ text: request.prompt, type: "text" },
...references.map((reference) => ({
image_url: { url: `data:${reference.mimeType};base64,${reference.bytes.toString("base64")}` },
type: "image_url",
})),
];
return {
body: JSON.stringify({
extra_body: { google: { image_config: { aspect_ratio: request.ratio, image_size: "1K" } } },
messages: [{ content, role: "user" }],
model: geminiProviderModelId,
stream: false,
}),
contentType: "application/json",
endpoint: geminiEndpoint,
parser: extractGeminiImage,
};
}
if (request.modelId !== gptImageModelId) throw new OneApiRuntimeError("model_disabled", "model_not_supported");
if (references.length > 0) {
const form = new FormData();
form.append("model", gptImageModelId);
form.append("prompt", request.prompt);
form.append("response_format", "b64_json");
form.append("size", gptImageRequestSizeForRatio(request.ratio));
references.forEach((reference, index) => form.append("image[]", new Blob([reference.bytes], { type: reference.mimeType }), `reference-${index + 1}.png`));
return { body: form, contentType: undefined, endpoint: gptImageReferenceEndpoint, parser: extractGptImage };
}
return {
body: JSON.stringify({ model: gptImageModelId, prompt: request.prompt, response_format: "b64_json", size: gptImageRequestSizeForRatio(request.ratio) }),
contentType: "application/json",
endpoint: gptImageEndpoint,
parser: extractGptImage,
};
}
export class OneApiGenerationAdapter implements GenerationAdapter {
private readonly credential: Buffer;
private readonly fetchImpl: FetchLike;
private disposed = false;
constructor(input: { credential: Buffer; fetch?: FetchLike }) {
if (input.credential.length < 8) throw new Error("ai_gateway_credential_invalid");
this.credential = Buffer.from(input.credential);
this.fetchImpl = input.fetch ?? fetch;
}
async start(request: GenerationAdapterRequest): Promise<GenerationAdapterResult> {
if (this.disposed) return { category: "upstream_failed", sourceCategory: "adapter_disposed", status: "failed" };
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), requestTimeoutMilliseconds);
let sourceBytes: Buffer | undefined;
try {
const providerRequest = buildRequest(request);
const headers = new Headers({ authorization: `Bearer ${this.credential.toString("utf8")}` });
if (providerRequest.contentType) headers.set("content-type", providerRequest.contentType);
const response = await this.fetchImpl(providerRequest.endpoint, {
body: providerRequest.body,
headers,
method: "POST",
redirect: "error",
signal: controller.signal,
});
if (!response.ok) throw mapHttpFailure(response.status);
const parsed = await readBoundedJson(response);
const extracted = providerRequest.parser(parsed);
sourceBytes = extracted.bytes;
const output = await normalizeOutput(sourceBytes, extracted.declaredMimeType, request.ratio);
return { outputs: [output], status: "completed" };
} catch (error) {
return failure(error);
} finally {
clearTimeout(timeout);
sourceBytes?.fill(0);
}
}
dispose() {
if (this.disposed) return;
this.disposed = true;
this.credential.fill(0);
}
}
+6
View File
@@ -25,7 +25,13 @@ export async function receiveWorkerCredentials(input: NodeJS.ReadableStream = pr
} }
export function initializeWorkerCredentialClient(credentials: Record<(typeof WORKER_CREDENTIALS)[number], string>) { export function initializeWorkerCredentialClient(credentials: Record<(typeof WORKER_CREDENTIALS)[number], string>) {
const value = credentials["Dada/P0A/worker/ai-gateway"];
try {
if (!value) throw new Error("worker_ai_gateway_not_configured");
return { aiGatewayCredential: Buffer.from(value, "utf8") };
} finally {
for (const name of WORKER_CREDENTIALS) credentials[name] = ""; for (const name of WORKER_CREDENTIALS) credentials[name] = "";
}
} }
export function attachWorkerSupervisorControl(pipeName: string, shutdown: () => Promise<void> | void) { export function attachWorkerSupervisorControl(pipeName: string, shutdown: () => Promise<void> | void) {
+9 -11
View File
@@ -2,8 +2,8 @@ import { parentPort } from "node:worker_threads";
import { join } from "node:path"; import { join } from "node:path";
import { WorkerAiCallGate } from "./ai-call-gate.js"; import { WorkerAiCallGate } from "./ai-call-gate.js";
import { MockGenerationAdapter } from "./ai-adapter-contract.js";
import { GenerationProcessor } from "./generation-processor.js"; import { GenerationProcessor } from "./generation-processor.js";
import { OneApiGenerationAdapter } from "./oneapi-generation-adapter.js";
import { readConfiguredLocalDataRoot } from "./runtime-config.js"; import { readConfiguredLocalDataRoot } from "./runtime-config.js";
import { RetentionCleanup } from "./retention-cleanup.js"; import { RetentionCleanup } from "./retention-cleanup.js";
import { ProjectPurgeCleanup } from "./project-purge-cleanup.js"; import { ProjectPurgeCleanup } from "./project-purge-cleanup.js";
@@ -24,7 +24,7 @@ if (workerPort) {
} }
if (!workerPort && process.argv.includes("--dada-credential-stdin")) { if (!workerPort && process.argv.includes("--dada-credential-stdin")) {
initializeWorkerCredentialClient(await receiveWorkerCredentials()); const credentialClient = initializeWorkerCredentialClient(await receiveWorkerCredentials());
const controlPipeIndex = process.argv.indexOf("--dada-control-pipe"); const controlPipeIndex = process.argv.indexOf("--dada-control-pipe");
const controlPipe = process.argv[controlPipeIndex + 1]; const controlPipe = process.argv[controlPipeIndex + 1];
if (controlPipeIndex < 0 || !controlPipe) throw new Error("Supervisor control pipe name is required."); if (controlPipeIndex < 0 || !controlPipe) throw new Error("Supervisor control pipe name is required.");
@@ -51,15 +51,13 @@ if (!workerPort && process.argv.includes("--dada-credential-stdin")) {
storage = new WorkerStorageStatus(databasePath); storage = new WorkerStorageStatus(databasePath);
retention = new RetentionCleanup({ databasePath }); retention = new RetentionCleanup({ databasePath });
projectCleanup = new ProjectPurgeCleanup({ dataRoot, databasePath }); projectCleanup = new ProjectPurgeCleanup({ dataRoot, databasePath });
processor = new GenerationProcessor({ let adapter: OneApiGenerationAdapter;
adapter: new MockGenerationAdapter({ try {
status: "completed", adapter = new OneApiGenerationAdapter({ credential: credentialClient.aiGatewayCredential });
outputs: [{ bytes: Buffer.from("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=", "base64"), mimeType: "image/png", pixelWidth: 1080, pixelHeight: 1440 }], } finally {
}), credentialClient.aiGatewayCredential.fill(0);
dataRoot, }
databasePath, processor = new GenerationProcessor({ adapter, dataRoot, databasePath, workerId: `portable-oneapi-worker-${process.pid}` });
workerId: `portable-mock-worker-${process.pid}`,
});
const runRetentionCleanup = () => { const runRetentionCleanup = () => {
try { try {
retention?.purgeExpired(); retention?.purgeExpired();
+44
View File
@@ -0,0 +1,44 @@
import { createRequire } from "node:module";
import { describe, expect, it } from "vitest";
import {
ModelConfigurationService,
portableRuntimeModelCandidates,
} from "../../apps/api/src/model-configuration.js";
const requireFromApi = createRequire(new URL("../../apps/api/package.json", import.meta.url));
const Database = requireFromApi("better-sqlite3") as new (path: string) => {
close(): void;
};
describe("POSTV1-02 portable runtime model seed", () => {
it("enables only models backed by the real OneAPI contract", () => {
const database = new Database(":memory:");
try {
const models = new ModelConfigurationService({ database, seedCandidates: portableRuntimeModelCandidates }).read();
const flash = models.models.find((model) => model.model_id === "gemini-3.1-flash-image-preview");
const pro = models.models.find((model) => model.model_id === "gemini-3-pro-image-preview");
const gpt = models.models.find((model) => model.model_id === "gpt-image-2");
expect(models.configured_default_model_id).toBe("gemini-3.1-flash-image-preview");
expect(flash).toMatchObject({
contract_validation_status: "verified",
enabled: true,
runtime_availability: { available_for_new_jobs: true, reason: "available" },
});
expect(flash?.route_profile).toMatchObject({ endpoint: "https://oneapi.intelligrow.cn/v1/chat/completions" });
expect(pro).toMatchObject({
contract_validation_status: "unverified",
enabled: false,
runtime_availability: { available_for_new_jobs: false, reason: "configured_disabled" },
});
expect(gpt).toMatchObject({
contract_validation_status: "verified",
enabled: true,
runtime_availability: { available_for_new_jobs: true, reason: "available" },
});
} finally {
database.close();
}
});
});
+4 -1
View File
@@ -46,7 +46,10 @@ async function stop(child) {
test("portable package serves the product and keeps SQLite data across API restart", async () => { test("portable package serves the product and keeps SQLite data across API restart", async () => {
assert.ok(existsSync(join(packageRoot, "Dada.exe"))); assert.ok(existsSync(join(packageRoot, "Dada.exe")));
assert.ok(existsSync(join(packageRoot, "web", "index.html"))); assert.ok(existsSync(join(packageRoot, "web", "index.html")));
assert.match(await readFile(join(packageRoot, "server", "worker", "dist", "worker.js"), "utf8"), /GenerationProcessor/); const packagedWorker = await readFile(join(packageRoot, "server", "worker", "dist", "worker.js"), "utf8");
assert.match(packagedWorker, /GenerationProcessor/);
assert.match(packagedWorker, /oneapi\.intelligrow\.cn/);
assert.doesNotMatch(packagedWorker, /portable-mock-worker/);
const root = await mkdtemp(join(tmpdir(), "dada-postv1-")); const root = await mkdtemp(join(tmpdir(), "dada-postv1-"));
const dataRoot = join(root, "data"); const dataRoot = join(root, "data");
const configPath = join(root, "instance.json"); const configPath = join(root, "instance.json");
@@ -0,0 +1,66 @@
import { describe, expect, it, vi } from "vitest";
import type { GenerationAdapterRequest } from "../../apps/worker/src/ai-adapter-contract.js";
import { OneApiGenerationAdapter } from "../../apps/worker/src/oneapi-generation-adapter.js";
function request(overrides: Partial<GenerationAdapterRequest> = {}): GenerationAdapterRequest {
return {
configSnapshot: {},
generationId: "00000000-0000-4000-8000-000000000001",
modelId: "gemini-3.1-flash-image-preview",
prompt: "一张用于本机验收的抽象色彩图",
ratio: "1:1",
referenceAssetIds: [],
...overrides,
};
}
describe("POSTV1-02 OneAPI runtime adapter", () => {
it("uses the fixed Gemini gateway and normalizes one real-shaped response", async () => {
const source = Buffer.from(
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=",
"base64",
);
const gateway = vi.fn(async (_url: string | URL | Request, init?: RequestInit) => {
const headers = new Headers(init?.headers);
expect(headers.get("authorization")).toBe("Bearer synthetic-runtime-token");
expect(init?.redirect).toBe("error");
return new Response(JSON.stringify({
choices: [{ message: { content: `![result](data:image/png;base64,${source.toString("base64")})` } }],
}), { headers: { "content-type": "application/json" }, status: 200 });
});
const credential = Buffer.from("synthetic-runtime-token");
const adapter = new OneApiGenerationAdapter({ credential, fetch: gateway as typeof fetch });
const result = await adapter.start(request());
expect(gateway).toHaveBeenCalledOnce();
expect(gateway.mock.calls[0]?.[0]).toBe("https://oneapi.intelligrow.cn/v1/chat/completions");
expect(result.status === "failed" ? result.sourceCategory : "completed").toBe("completed");
if (result.status === "completed") {
expect(result.outputs).toHaveLength(1);
expect(result.outputs[0]).toMatchObject({ mimeType: "image/png", pixelHeight: 1080, pixelWidth: 1080 });
expect(result.outputs[0]?.bytes.length).toBeGreaterThan(0);
}
expect(JSON.stringify(result)).not.toContain("synthetic-runtime-token");
adapter.dispose();
credential.fill(0);
});
it("fails closed instead of returning a mock image", async () => {
const adapter = new OneApiGenerationAdapter({
credential: Buffer.from("synthetic-runtime-token"),
fetch: vi.fn(async () => new Response(null, { status: 503 })) as typeof fetch,
});
await expect(adapter.start(request())).resolves.toEqual({
category: "upstream_failed",
sourceCategory: "upstream_http_503",
status: "failed",
});
adapter.dispose();
await expect(adapter.start(request())).resolves.toEqual({
category: "upstream_failed",
sourceCategory: "adapter_disposed",
status: "failed",
});
});
});