Compare commits

...
Author SHA1 Message Date
suyx b995662388 fix(POSTV1-04): 修复生成服务运行时装配
Dada P0-A isolated Windows CI / validate-and-package (push) Waiting to run
2026-08-05 15:36:47 +08:00
3 changed files with 157 additions and 1 deletions
+16 -1
View File
@@ -16,7 +16,12 @@ 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, portableRuntimeModelCandidates } from "./model-configuration.js"; import { GenerationSubmissionService } from "./generation-submission.js";
import {
GenerationModelConfigurationCatalog,
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";
@@ -28,6 +33,7 @@ let credits: CreditService | undefined;
let storage: ManagedStorage | undefined; let storage: ManagedStorage | undefined;
let latestExports: LatestExportService | undefined; let latestExports: LatestExportService | undefined;
let models: ModelConfigurationService | undefined; let models: ModelConfigurationService | undefined;
let generations: GenerationSubmissionService | undefined;
let recentAssets: RecentAssetService | undefined; let recentAssets: RecentAssetService | undefined;
let stickers: StickerReleaseService | undefined; let stickers: StickerReleaseService | undefined;
let amap: AmapAdapter = new MockAmapAdapter(); let amap: AmapAdapter = new MockAmapAdapter();
@@ -59,6 +65,11 @@ if (credentialChannelEnabled) {
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, seedCandidates: portableRuntimeModelCandidates }); models = new ModelConfigurationService({ database: registration.database, seedCandidates: portableRuntimeModelCandidates });
generations = new GenerationSubmissionService({
credits,
models: new GenerationModelConfigurationCatalog(models),
storage,
});
recentAssets = new RecentAssetService({ database: registration.database }); recentAssets = new RecentAssetService({ database: registration.database });
registration.applySecureConfig(readSecureConfigCandidate(instanceConfigPath)); registration.applySecureConfig(readSecureConfigCandidate(instanceConfigPath));
} catch (error) { } catch (error) {
@@ -68,6 +79,8 @@ if (credentialChannelEnabled) {
stickers = undefined; stickers = undefined;
latestExports?.close(); latestExports?.close();
latestExports = undefined; latestExports = undefined;
generations?.close();
generations = undefined;
storage?.close(); storage?.close();
storage = undefined; storage = undefined;
credits?.close(); credits?.close();
@@ -102,6 +115,7 @@ const app = await createApp({
amap, amap,
...(browserSupportRelease ? { browserSupportRelease } : {}), ...(browserSupportRelease ? { browserSupportRelease } : {}),
...(credits ? { credits } : {}), ...(credits ? { credits } : {}),
...(generations ? { generations } : {}),
...(latestExports ? { latestExports } : {}), ...(latestExports ? { latestExports } : {}),
...(registration && localTestAuth ? { localTestAuth: true } : {}), ...(registration && localTestAuth ? { localTestAuth: true } : {}),
...(models ? { models } : {}), ...(models ? { models } : {}),
@@ -125,6 +139,7 @@ if (controlPipeIndex >= 0) {
await app.close(); await app.close();
amap.dispose?.(); amap.dispose?.();
latestExports?.close(); latestExports?.close();
generations?.close();
credits?.close(); credits?.close();
projects?.close(); projects?.close();
registration?.close(); registration?.close();
+41
View File
@@ -2,6 +2,8 @@ import { randomUUID, createHash } from "node:crypto";
import type BetterSqlite3 from "better-sqlite3"; import type BetterSqlite3 from "better-sqlite3";
import { serializeAuditSummary, auditRetentionMilliseconds } from "./audit-policy.js"; import { serializeAuditSummary, auditRetentionMilliseconds } from "./audit-policy.js";
import type { GenerationModelCatalog, GenerationModelSnapshot } from "./generation-submission.js";
import { projectRatios } from "./projects.js";
export const modelIds = [ export const modelIds = [
"gemini-3.1-flash-image-preview", "gemini-3.1-flash-image-preview",
@@ -59,6 +61,45 @@ export interface ModelConfigurationView {
models: ModelConfigView[]; models: ModelConfigView[];
} }
type ReadableModelConfiguration = Pick<ModelConfigurationService, "read">;
function generationRuntimeReason(reason: ModelRuntimeReason): GenerationModelSnapshot["runtimeAvailability"]["reason"] {
if (reason === "gateway_balance_insufficient") return reason;
if (reason === "contract_unverified" || reason === "contract_blocked") return "gateway_contract_invalid";
if (reason === "available") return null;
return "model_disabled";
}
export class GenerationModelConfigurationCatalog implements GenerationModelCatalog {
constructor(private readonly models: ReadableModelConfiguration) {}
readModel(modelId: string): GenerationModelSnapshot | undefined {
const configuration = this.models.read();
const model = configuration.models.find((entry) => entry.model_id === modelId);
if (!model) return undefined;
const supportedRatios = projectRatios.filter((ratio) => model.supported_ratios.includes(ratio));
return {
configSetVersion: configuration.config_set_version,
configVersion: model.config_version,
contractValidationStatus: model.contract_validation_status === "verified" ? "verified" : "unverified",
creditCost: model.credit_cost,
enabled: model.enabled,
modelId: model.model_id,
promptMaxLength: model.prompt_max_length,
referenceLimits: {
maxFileBytes: model.reference_limits.max_file_bytes,
maxFiles: model.reference_limits.max_files,
maxTotalBytes: model.reference_limits.max_total_bytes,
},
runtimeAvailability: {
availableForNewJobs: model.runtime_availability.available_for_new_jobs,
reason: generationRuntimeReason(model.runtime_availability.reason),
},
supportedRatios,
};
}
}
export class ModelConfigurationError extends Error { export class ModelConfigurationError extends Error {
constructor( constructor(
readonly code: readonly code:
@@ -0,0 +1,100 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
import { GenerationModelConfigurationCatalog, type ModelConfigurationView } from "../../apps/api/src/model-configuration.js";
describe("POSTV1-04 generation runtime wiring", () => {
it("constructs, injects and closes the production generation submission service", () => {
const main = readFileSync("apps/api/src/main.ts", "utf8");
expect(main).toContain('import { GenerationSubmissionService } from "./generation-submission.js";');
expect(main).toContain("let generations: GenerationSubmissionService | undefined;");
expect(main).toContain("generations = new GenerationSubmissionService({");
expect(main).toContain("models: new GenerationModelConfigurationCatalog(models),");
expect(main).toContain("...(generations ? { generations } : {}),");
expect(main).toContain("generations?.close();");
});
it("maps the current model configuration into the generation submission contract", () => {
const configuration: ModelConfigurationView = {
config_set_version: 7,
configured_default_model_id: "gemini-3.1-flash-image-preview",
recommended_model_id: "gemini-3.1-flash-image-preview",
models: [{
config_version: 3,
contract_evidence_ref: "fixture-contract",
contract_validation_status: "verified",
credit_cost: 2,
display_name: "Fixture model",
enabled: true,
error_mapping_profile: {},
gateway_account_ref: "fixture-gateway",
is_default: true,
model_id: "gemini-3.1-flash-image-preview",
prompt_max_length: 1_000,
recommendation_priority: 1,
reference_limits: { max_file_bytes: 10, max_files: 2, max_total_bytes: 20 },
route_profile: {},
runtime_availability: { available_for_new_jobs: true, checked_at: "2026-08-05T00:00:00.000Z", reason: "available" },
safety_source: "provider",
supported_ratios: ["3:4", "invalid"],
}],
};
const catalog = new GenerationModelConfigurationCatalog({ read: () => configuration });
expect(catalog.readModel("gemini-3.1-flash-image-preview")).toEqual({
configSetVersion: 7,
configVersion: 3,
contractValidationStatus: "verified",
creditCost: 2,
enabled: true,
modelId: "gemini-3.1-flash-image-preview",
promptMaxLength: 1_000,
referenceLimits: { maxFileBytes: 10, maxFiles: 2, maxTotalBytes: 20 },
runtimeAvailability: { availableForNewJobs: true, reason: null },
supportedRatios: ["3:4"],
});
expect(catalog.readModel("missing")).toBeUndefined();
});
it("maps model and contract blocks into generation error categories", () => {
const configuration = {
config_set_version: 1,
configured_default_model_id: "gemini-3.1-flash-image-preview",
recommended_model_id: null,
models: [],
} satisfies ModelConfigurationView;
const catalog = new GenerationModelConfigurationCatalog({ read: () => configuration });
const base = {
config_version: 1,
contract_evidence_ref: null,
contract_validation_status: "verified" as const,
credit_cost: 1,
display_name: "Fixture model",
enabled: true,
error_mapping_profile: {},
gateway_account_ref: "fixture-gateway",
is_default: true,
model_id: "gemini-3.1-flash-image-preview" as const,
prompt_max_length: 1_000,
recommendation_priority: 1,
reference_limits: { max_file_bytes: 10, max_files: 2, max_total_bytes: 20 },
route_profile: {},
safety_source: "provider",
supported_ratios: ["3:4"],
};
configuration.models = [{
...base,
runtime_availability: { available_for_new_jobs: false, checked_at: "2026-08-05T00:00:00.000Z", reason: "contract_blocked" },
}];
expect(catalog.readModel(base.model_id)?.runtimeAvailability.reason).toBe("gateway_contract_invalid");
configuration.models = [{
...base,
runtime_availability: { available_for_new_jobs: false, checked_at: "2026-08-05T00:00:00.000Z", reason: "worker_degraded" },
}];
expect(catalog.readModel(base.model_id)?.runtimeAvailability.reason).toBe("model_disabled");
});
});