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
suyx 92434aec17 feat(POSTV1-03): 增加本机测试直达入口
Dada P0-A isolated Windows CI / validate-and-package (push) Waiting to run
2026-08-05 14:53:20 +08:00
suyx f7cac5dabf fix(POSTV1-02): 修复便携网页静态资源类型
Dada P0-A isolated Windows CI / validate-and-package (push) Waiting to run
2026-08-05 14:07:42 +08:00
suyx a7f62adad4 fix(POSTV1-02): 确保AI探测输出后退出
Dada P0-A isolated Windows CI / validate-and-package (push) Waiting to run
2026-08-05 13:26:44 +08:00
suyx b1f143c238 feat(POSTV1-02): 增加AI真实生成探测
Dada P0-A isolated Windows CI / validate-and-package (push) Waiting to run
2026-08-05 13:14:40 +08:00
suyx 99fd3b1802 fix(POSTV1-02): 修复便携版网页入口门禁
Dada P0-A isolated Windows CI / validate-and-package (push) Waiting to run
2026-08-05 12:58:28 +08:00
suyx fd0003a804 fix(POSTV1-02): 补齐便携包Worker运行依赖
Dada P0-A isolated Windows CI / validate-and-package (push) Waiting to run
2026-08-05 12:46:59 +08:00
suyx bfdfe44f87 test(POSTV1-02): 修正便携包AI适配器检查
Dada P0-A isolated Windows CI / validate-and-package (push) Waiting to run
2026-08-05 12:34:46 +08:00
suyx cbb7f658a3 fix(POSTV1-02): 接通真实AI生成链路
Dada P0-A isolated Windows CI / validate-and-package (push) Waiting to run
2026-08-05 12:29:14 +08:00
suyx 43d946bb5c fix(POSTV1-02): 修复便携包首次启动链路
Dada P0-A isolated Windows CI / validate-and-package (push) Waiting to run
2026-08-05 12:00:23 +08:00
suyx 79b01ebc81 test(POSTV1-01): 增加最终便携包启动链路验收
Dada P0-A isolated Windows CI / validate-and-package (push) Waiting to run
2026-08-05 11:38:32 +08:00
suyx 90f812fae5 fix(POSTV1-01): 让Worker使用便携包SQLite原生绑定
Dada P0-A isolated Windows CI / validate-and-package (push) Waiting to run
2026-08-05 11:25:02 +08:00
suyx 1155a81c3b fix(POSTV1-01): 接入Worker生成任务消费链路
Dada P0-A isolated Windows CI / validate-and-package (push) Waiting to run
2026-08-05 11:24:14 +08:00
suyx 3dca4ad77c fix(POSTV1-01): 在最终包中提供产品网页与同源API
Dada P0-A isolated Windows CI / validate-and-package (push) Waiting to run
2026-08-05 11:22:58 +08:00
suyx 99fe07a761 fix(POSTV1-01): 允许非关键外部服务降级启动 2026-08-05 11:22:42 +08:00
tuyixuan 443e8b94f0 Merge pull request 'feat(P0-A): 整合第一版并冻结最终发布' (#1) from codex/wp7-07 into codex/wp0-09
Dada P0-A isolated Windows CI / validate-and-package (push) Waiting to run
Reviewed-on: #1
2026-08-05 10:29:22 +08:00
suyx 693fa117b7 chore(WP7-07): 冻结第一版发布记录
Dada P0-A isolated Windows CI / validate-and-package (push) Successful in 17m46s
Dada P0-A isolated Windows CI / validate-and-package (pull_request) Successful in 17m49s
2026-08-04 23:32:51 +08:00
suyx 08f3cccae4 fix(WP7-07): 排除发布扫描器路径误报
Dada P0-A isolated Windows CI / validate-and-package (push) Canceled after 0s
2026-08-04 23:20:40 +08:00
suyx a22b1f19e9 fix(WP7-07): 修正最终包浏览器门禁探测
Dada P0-A isolated Windows CI / validate-and-package (push) Canceled after 0s
2026-08-04 23:15:36 +08:00
suyx 194b59d4a5 fix(WP7-07): 统一构建全部工作区依赖
Dada P0-A isolated Windows CI / validate-and-package (push) Canceled after 0s
2026-08-04 23:07:01 +08:00
suyx 0f03b12f64 fix(WP7-07): 补齐便携包前置构建顺序
Dada P0-A isolated Windows CI / validate-and-package (push) Canceled after 0s
2026-08-04 23:05:38 +08:00
suyx ad86b4ddcc feat(WP7-07): 增加最终发布冻结与泄漏扫描
Dada P0-A isolated Windows CI / validate-and-package (push) Canceled after 0s
2026-08-04 23:03:11 +08:00
35 changed files with 1786 additions and 65 deletions
+29
View File
@@ -0,0 +1,29 @@
{
"appVersion": "0.0.0",
"browsers": [
{
"brand": "Google Chrome",
"fullVersion": "150.0.7871.187"
},
{
"brand": "Microsoft Edge",
"fullVersion": "151.0.4129.59"
}
],
"buildCommit": "08f3cccae4a1e75e2f2292eef14611313523916d",
"deferredExternalTasks": [
"TASK-WP7-03",
"TASK-WP7-04"
],
"finalRelease": true,
"fixedPort": 43121,
"frozenFromCommit": "08e9c39e49d68f8642d5acfe22b0fdb40a3a08fa",
"recordedAt": "2026-08-04T15:20:54.271Z",
"releaseStatus": "first_version_internal",
"schemaVersion": "1.0",
"windows": {
"arch": "x64",
"build": "26200.8875",
"displayVersion": "25H2"
}
}
+82 -4
View File
@@ -1,6 +1,6 @@
import { randomBytes, randomUUID } from "node:crypto"; import { randomBytes, randomUUID } from "node:crypto";
import { createReadStream, readFileSync } from "node:fs"; import { createReadStream, existsSync, readFileSync } from "node:fs";
import { resolve } from "node:path"; import { extname, resolve } from "node:path";
import { import {
AccountDeletionCompleteRequestSchema, AccountDeletionCompleteRequestSchema,
@@ -218,6 +218,24 @@ import type { ManagedStorage } from "./managed-storage.js";
import { PrivateContentError, PrivateContentService } from "./private-content.js"; import { PrivateContentError, PrivateContentService } from "./private-content.js";
import { assertSafeAdminDiagnostics, assertSafeAdminServicesStorage } from "./admin-state.js"; import { assertSafeAdminDiagnostics, assertSafeAdminServicesStorage } from "./admin-state.js";
const productAssetContentTypes: Readonly<Record<string, string>> = {
".css": "text/css; charset=utf-8",
".jpeg": "image/jpeg",
".jpg": "image/jpeg",
".js": "text/javascript; charset=utf-8",
".json": "application/json; charset=utf-8",
".mjs": "text/javascript; charset=utf-8",
".png": "image/png",
".svg": "image/svg+xml",
".webp": "image/webp",
".woff": "font/woff",
".woff2": "font/woff2",
};
function productAssetContentType(path: string) {
return productAssetContentTypes[extname(path).toLowerCase()] ?? "application/octet-stream";
}
const defaultBootstrap: BootstrapResponse = { const defaultBootstrap: BootstrapResponse = {
app_version: "0.0.0", app_version: "0.0.0",
dependencies: [], dependencies: [],
@@ -238,12 +256,14 @@ export interface CreateAppOptions {
assetReleases?: AssetReleaseReader; assetReleases?: AssetReleaseReader;
bootstrap?: () => BootstrapResponse | Promise<BootstrapResponse>; bootstrap?: () => BootstrapResponse | Promise<BootstrapResponse>;
browserGate?: boolean; browserGate?: boolean;
productIndexHtml?: string;
browserSupportRelease?: BrowserSupportRelease; browserSupportRelease?: BrowserSupportRelease;
browserSupportSecret?: Buffer; browserSupportSecret?: Buffer;
credits?: CreditService; credits?: CreditService;
eventHub?: EventHub; eventHub?: EventHub;
generations?: GenerationSubmissionService; generations?: GenerationSubmissionService;
latestExports?: LatestExportService; latestExports?: LatestExportService;
localTestAuth?: boolean;
models?: ModelConfigurationService; models?: ModelConfigurationService;
networkBoundary?: NetworkBoundaryOptions; networkBoundary?: NetworkBoundaryOptions;
publicAssets?: PublicAssetResolver; publicAssets?: PublicAssetResolver;
@@ -272,6 +292,10 @@ const supportGateDirectory = resolve(process.env.DADA_SUPPORT_GATE_ROOT ?? "apps
const supportGateHtml = readFileSync(resolve(supportGateDirectory, "index.html"), "utf8"); const supportGateHtml = readFileSync(resolve(supportGateDirectory, "index.html"), "utf8");
const supportGateCss = readFileSync(resolve(supportGateDirectory, "support-gate.css"), "utf8"); const supportGateCss = readFileSync(resolve(supportGateDirectory, "support-gate.css"), "utf8");
const supportGateJavaScript = readFileSync(resolve(supportGateDirectory, "support-gate.js"), "utf8"); const supportGateJavaScript = readFileSync(resolve(supportGateDirectory, "support-gate.js"), "utf8");
const productWebRoot = resolve(process.env.DADA_WEB_ROOT ?? "apps/web/dist");
const packagedProductIndexHtml = existsSync(resolve(productWebRoot, "index.html"))
? readFileSync(resolve(productWebRoot, "index.html"), "utf8")
: undefined;
const clientHints = "Sec-CH-UA, Sec-CH-UA-Full-Version-List, Sec-CH-UA-Platform"; const clientHints = "Sec-CH-UA, Sec-CH-UA-Full-Version-List, Sec-CH-UA-Platform";
const contentSecurityPolicy = [ const contentSecurityPolicy = [
"default-src 'self'", "default-src 'self'",
@@ -710,6 +734,7 @@ export async function createApp(options: CreateAppOptions = {}) {
) )
: undefined); : undefined);
const browserGate = options.browserGate ?? true; const browserGate = options.browserGate ?? true;
const productIndexHtml = options.productIndexHtml ?? packagedProductIndexHtml;
const browserSupportSecret = options.browserSupportSecret ?? randomBytes(32); const browserSupportSecret = options.browserSupportSecret ?? randomBytes(32);
const browserSupportRelease = options.browserSupportRelease; const browserSupportRelease = options.browserSupportRelease;
const app = Fastify({ const app = Fastify({
@@ -901,11 +926,25 @@ export async function createApp(options: CreateAppOptions = {}) {
}); });
for (const route of ["/", "/app", "/app/*", "/admin", "/admin/*"]) { for (const route of ["/", "/app", "/app/*", "/admin", "/admin/*"]) {
app.get(route, { schema: { hide: true } }, async (_request, reply) => { app.get(route, { schema: { hide: true } }, async (request, reply) => {
reply.type("text/html; charset=utf-8"); reply.type("text/html; charset=utf-8");
return supportGateHtml; if (!browserGate) return productIndexHtml ?? supportGateHtml;
const verified = verifyBrowserSupportCookie({
cookieHeader: headerValue(request.headers.cookie),
release: browserSupportRelease,
secChUa: headerValue(request.headers["sec-ch-ua"]),
secret: browserSupportSecret,
});
return verified.supported && productIndexHtml ? productIndexHtml : supportGateHtml;
}); });
} }
app.get("/assets/*", { schema: { hide: true } }, async (request, reply) => {
const relativePath = decodeURIComponent(request.url.split("?", 1)[0]!.slice("/assets/".length));
const assetPath = resolve(productWebRoot, "assets", relativePath);
if (!assetPath.startsWith(resolve(productWebRoot, "assets")) || !existsSync(assetPath)) return reply.code(404).send();
reply.type(productAssetContentType(assetPath));
return reply.send(readFileSync(assetPath));
});
app.get("/support-gate.css", { schema: { hide: true } }, async (_request, reply) => { app.get("/support-gate.css", { schema: { hide: true } }, async (_request, reply) => {
reply.type("text/css; charset=utf-8"); reply.type("text/css; charset=utf-8");
return supportGateCss; return supportGateCss;
@@ -2037,6 +2076,45 @@ export async function createApp(options: CreateAppOptions = {}) {
}, },
); );
if (options.localTestAuth && options.registration) {
app.get(
"/api/v1/auth/local-test",
{ schema: { hide: true } },
async () => ({ available: true }),
);
app.post(
"/api/v1/auth/local-test",
{ schema: { hide: true } },
async (request, reply) => {
try {
const result = options.registration!.createLocalTestSession();
reply.header(
"Set-Cookie",
`${userSessionCookieName}=${result.sessionToken}; Max-Age=${30 * 24 * 60 * 60}; Path=/; HttpOnly; SameSite=Strict`,
);
return {
audience: result.audience,
credits: {
available_balance: result.credits.availableBalance,
reserved_balance: result.credits.reservedBalance,
},
session_expires_at: new Date(result.sessionExpiresAt).toISOString(),
status: result.status,
user: {
creator_name: result.user.creatorName,
role: result.user.role,
social_id: result.user.socialId,
status: result.user.status,
user_id: result.user.userId,
},
};
} catch (error) {
return registrationFailure(reply, request.id, error);
}
},
);
}
app.post( app.post(
"/api/v1/auth/login/complete", "/api/v1/auth/login/complete",
{ {
+7 -3
View File
@@ -33,6 +33,12 @@ const fixedDirectories = [
"logs/supervisor", "logs/supervisor",
] as const; ] as const;
export function ensureLocalDataRuntimeDirectories(dataRoot: string) {
for (const directory of fixedDirectories) {
mkdirSync(join(resolve(dataRoot), directory), { recursive: true });
}
}
export const DATA_TRANSFER_POLICY = { export const DATA_TRANSFER_POLICY = {
allowed_downloads: ["original_generation", "jpg", "png"], allowed_downloads: ["original_generation", "jpg", "png"],
application_backup: false, application_backup: false,
@@ -219,9 +225,7 @@ export function initializeLocalDataRoot(input: {
const createdRoot = !existsSync(validation.normalized_path); const createdRoot = !existsSync(validation.normalized_path);
try { try {
for (const directory of fixedDirectories) { ensureLocalDataRuntimeDirectories(validation.normalized_path);
mkdirSync(join(validation.normalized_path, directory), { recursive: true });
}
openInstanceDatabase(join(validation.normalized_path, "db", "dada.sqlite3")); openInstanceDatabase(join(validation.normalized_path, "db", "dada.sqlite3"));
const configuration: InstanceConfiguration = { const configuration: InstanceConfiguration = {
data_root: validation.normalized_path, data_root: validation.normalized_path,
+22 -3
View File
@@ -5,7 +5,7 @@ import { registrationNotice } from "@dada/shared-contracts";
import { createApp } from "./app.js"; import { createApp } from "./app.js";
import { readBrowserSupportRelease } from "./browser-support.js"; import { readBrowserSupportRelease } from "./browser-support.js";
import { defaultInstanceConfigPath, readConfiguredLocalDataRoot } from "./local-data-root.js"; import { defaultInstanceConfigPath, ensureLocalDataRuntimeDirectories, readConfiguredLocalDataRoot } from "./local-data-root.js";
import { ManagedStorage } from "./managed-storage.js"; import { ManagedStorage } from "./managed-storage.js";
import { LatestExportService } from "./latest-exports.js"; import { LatestExportService } from "./latest-exports.js";
import { CreditService } from "./credits.js"; import { CreditService } from "./credits.js";
@@ -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 } 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,18 +33,22 @@ 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();
let localTestAuth = false;
const instanceConfigPath = process.env.DADA_INSTANCE_CONFIG_PATH ?? defaultInstanceConfigPath(); const instanceConfigPath = process.env.DADA_INSTANCE_CONFIG_PATH ?? defaultInstanceConfigPath();
if (credentialChannelEnabled) { if (credentialChannelEnabled) {
const clients = initializeApiCredentialClients(await receiveApiCredentials()); const clients = initializeApiCredentialClients(await receiveApiCredentials());
try { try {
amap = clients.amap; amap = clients.amap;
localTestAuth = !clients.resendConfigured;
const derivePepper = (purpose: string) => createHmac("sha256", clients.adminAllowlistPepper) const derivePepper = (purpose: string) => createHmac("sha256", clients.adminAllowlistPepper)
.update(`Dada/P0A/${purpose}/v1`, "utf8") .update(`Dada/P0A/${purpose}/v1`, "utf8")
.digest(); .digest();
const dataRoot = readConfiguredLocalDataRoot(instanceConfigPath); const dataRoot = readConfiguredLocalDataRoot(instanceConfigPath);
ensureLocalDataRuntimeDirectories(dataRoot);
const databasePath = join(dataRoot, "db", "dada.sqlite3"); const databasePath = join(dataRoot, "db", "dada.sqlite3");
registration = new RegistrationService({ registration = new RegistrationService({
adminAllowlistPepper: Buffer.from(clients.adminAllowlistPepper), adminAllowlistPepper: Buffer.from(clients.adminAllowlistPepper),
@@ -55,7 +64,12 @@ 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 });
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) {
@@ -65,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();
@@ -99,7 +115,9 @@ const app = await createApp({
amap, amap,
...(browserSupportRelease ? { browserSupportRelease } : {}), ...(browserSupportRelease ? { browserSupportRelease } : {}),
...(credits ? { credits } : {}), ...(credits ? { credits } : {}),
...(generations ? { generations } : {}),
...(latestExports ? { latestExports } : {}), ...(latestExports ? { latestExports } : {}),
...(registration && localTestAuth ? { localTestAuth: true } : {}),
...(models ? { models } : {}), ...(models ? { models } : {}),
...(projects ? { projects } : {}), ...(projects ? { projects } : {}),
...(registration ? { registration } : {}), ...(registration ? { registration } : {}),
@@ -121,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();
+90 -6
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:
@@ -111,7 +152,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 +179,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 +284,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 +583,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 +600,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 +612,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);
}); });
+55
View File
@@ -38,6 +38,7 @@ export interface RegistrationTransactionEvent {
| "registration_send" | "registration_send"
| "registration_complete" | "registration_complete"
| "registration_send_compensation" | "registration_send_compensation"
| "local_test_session"
| "login_send" | "login_send"
| "login_complete" | "login_complete"
| "admin_login_send" | "admin_login_send"
@@ -654,6 +655,60 @@ export class RegistrationService {
return outcome; return outcome;
} }
createLocalTestSession(): LoginCompleteResult {
const now = this.options.clock();
return this.runImmediate("local_test_session", () => {
const registrationId = "local-test-user-v1";
const existing = this.database.prepare(`
SELECT user_id, role, status FROM users WHERE registration_id = ?
`).get(registrationId) as {
role: "user" | "super_admin";
status: "active" | "suspended" | "deleted";
user_id: string;
} | undefined;
if (existing) {
if (existing.role !== "user" || existing.status !== "active") {
throw new RegistrationError("AUTH_ENTRY_REJECTED", "account_suspended");
}
const session = this.insertSession(existing.user_id, "user", now);
return {
outcome: "committed",
value: this.loginResult(this.readCompletedRegistration(existing.user_id, session.sessionId)),
};
}
const userId = randomUUID();
this.database.prepare(`
INSERT INTO users (
user_id, normalized_email, role, status, counts_toward_stage_limit,
registration_id, created_at
) VALUES (?, 'local-test-user@dada.invalid', 'user', 'active', 0, ?, ?)
`).run(userId, registrationId, now);
this.database.prepare(`
INSERT INTO user_profiles (
user_id, creator_name, social_id, private_content_notice_version,
private_content_notice_acknowledged_at
) VALUES (?, '本机测试用户', '@dada_local_test', NULL, NULL)
`).run(userId);
this.database.prepare(`
INSERT INTO credit_accounts (user_id, available_balance, reserved_balance, updated_at)
VALUES (?, 10, 0, ?)
`).run(userId, now);
this.database.prepare(`
INSERT INTO credit_ledger (
ledger_id, user_id, operation_key, entry_type, amount,
available_before, available_after, reserved_before, reserved_after, created_at
) VALUES (?, ?, 'local-test-registration:v1', 'registration_grant', 10, 0, 10, 0, 0, ?)
`).run(randomUUID(), userId, now);
const session = this.insertSession(userId, "user", now);
return {
outcome: "committed",
value: this.loginResult(this.readCompletedRegistration(userId, session.sessionId)),
};
});
}
applySecureConfig(candidate: SecureConfigCandidate) { applySecureConfig(candidate: SecureConfigCandidate) {
const now = this.options.clock(); const now = this.options.clock();
const fail = (reason: string): never => { const fail = (reason: string): never => {
+7 -6
View File
@@ -1,6 +1,6 @@
import { createConnection } from "node:net"; import { createConnection } from "node:net";
import { RealAmapAdapter } from "./amap-adapter.js"; import { MockAmapAdapter, RealAmapAdapter } from "./amap-adapter.js";
const API_CREDENTIALS = ["Dada/P0A/api/resend", "Dada/P0A/api/amap", "Dada/P0A/admin/pepper"] as const; const API_CREDENTIALS = ["Dada/P0A/api/resend", "Dada/P0A/api/amap", "Dada/P0A/admin/pepper"] as const;
@@ -15,7 +15,7 @@ export async function receiveApiCredentials(input: NodeJS.ReadableStream = proce
if (names.length !== expected.length || names.some((name, index) => name !== expected[index])) { if (names.length !== expected.length || names.some((name, index) => name !== expected[index])) {
throw new Error("API credential channel contains an unexpected credential scope."); throw new Error("API credential channel contains an unexpected credential scope.");
} }
if (expected.some((name) => typeof parsed[name] !== "string" || parsed[name] === "")) { if (expected.some((name) => typeof parsed[name] !== "string")) {
throw new Error("API credential channel contains an invalid credential value."); throw new Error("API credential channel contains an invalid credential value.");
} }
return parsed as Record<(typeof API_CREDENTIALS)[number], string>; return parsed as Record<(typeof API_CREDENTIALS)[number], string>;
@@ -27,12 +27,13 @@ export async function receiveApiCredentials(input: NodeJS.ReadableStream = proce
} }
export function initializeApiCredentialClients(credentials: Record<(typeof API_CREDENTIALS)[number], string>) { export function initializeApiCredentialClients(credentials: Record<(typeof API_CREDENTIALS)[number], string>) {
const configured = API_CREDENTIALS.every((name) => credentials[name].length > 0);
try { try {
if (!configured) throw new Error("API credential client initialization failed."); const adminPepper = credentials["Dada/P0A/admin/pepper"];
if (!adminPepper) throw new Error("admin_pepper_not_configured");
return { return {
adminAllowlistPepper: Buffer.from(credentials["Dada/P0A/admin/pepper"], "utf8"), adminAllowlistPepper: Buffer.from(adminPepper, "utf8"),
amap: new RealAmapAdapter(credentials["Dada/P0A/api/amap"]), amap: credentials["Dada/P0A/api/amap"] ? new RealAmapAdapter(credentials["Dada/P0A/api/amap"]) : new MockAmapAdapter(),
resendConfigured: Boolean(credentials["Dada/P0A/api/resend"]),
}; };
} finally { } finally {
for (const name of API_CREDENTIALS) credentials[name] = ""; for (const name of API_CREDENTIALS) credentials[name] = "";
+19
View File
@@ -124,6 +124,25 @@ button {
margin-inline: auto; margin-inline: auto;
} }
.auth-test-entry {
margin-bottom: 18px;
border-bottom: 1px solid #b4b4af;
padding-bottom: 18px;
}
.auth-test-entry .auth-primary {
margin-top: 0;
border-color: #111111;
background: #111111;
color: #f2f500;
}
.auth-test-entry .auth-primary:disabled {
border-color: #777773;
background: #deded9;
color: #777773;
}
.auth-tabs { .auth-tabs {
display: grid; display: grid;
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
+44
View File
@@ -77,6 +77,9 @@ export function UserAuthPage() {
const [sendState, setSendState] = useState<SendState>("idle"); const [sendState, setSendState] = useState<SendState>("idle");
const [countdown, setCountdown] = useState(0); const [countdown, setCountdown] = useState(0);
const [error, setError] = useState<string>(); const [error, setError] = useState<string>();
const [localTestAvailable, setLocalTestAvailable] = useState(false);
const [localTestError, setLocalTestError] = useState<string>();
const [localTestSubmitting, setLocalTestSubmitting] = useState(false);
const [submitting, setSubmitting] = useState(false); const [submitting, setSubmitting] = useState(false);
const emailValid = /^[^@\s]+@[^@\s]+$/.test(email); const emailValid = /^[^@\s]+@[^@\s]+$/.test(email);
const registrationReady = Boolean( const registrationReady = Boolean(
@@ -93,6 +96,18 @@ export function UserAuthPage() {
return () => window.clearInterval(timer); return () => window.clearInterval(timer);
}, [countdown]); }, [countdown]);
useEffect(() => {
const controller = new AbortController();
void fetch("/api/v1/auth/local-test", { credentials: "same-origin", signal: controller.signal })
.then(async (response) => {
if (!response.ok || !response.headers.get("content-type")?.includes("application/json")) return;
const body = await response.json() as { available?: boolean };
if (body.available === true) setLocalTestAvailable(true);
})
.catch(() => undefined);
return () => controller.abort();
}, []);
useEffect(() => { useEffect(() => {
if (!noticeOpen) return; if (!noticeOpen) return;
const previousOverflow = document.body.style.overflow; const previousOverflow = document.body.style.overflow;
@@ -255,6 +270,27 @@ export function UserAuthPage() {
} }
} }
async function enterLocalTest() {
if (localTestSubmitting) return;
setLocalTestSubmitting(true);
setLocalTestError(undefined);
try {
const response = await fetch("/api/v1/auth/local-test", {
credentials: "same-origin",
method: "POST",
});
if (!response.ok) {
setLocalTestError("本机测试会话未能建立,请重试。");
return;
}
window.location.assign("/app");
} catch {
setLocalTestError("本机测试会话未能建立,请重试。");
} finally {
setLocalTestSubmitting(false);
}
}
return ( return (
<> <>
<main className="auth-page"> <main className="auth-page">
@@ -271,6 +307,14 @@ export function UserAuthPage() {
<section className="auth-content"> <section className="auth-content">
<a className="auth-admin-link" href="/admin/login"></a> <a className="auth-admin-link" href="/admin/login"></a>
<div className="auth-panel"> <div className="auth-panel">
{localTestAvailable ? (
<div className="auth-test-entry">
<button className="auth-primary" disabled={localTestSubmitting} onClick={enterLocalTest} type="button">
{localTestSubmitting ? "正在进入" : "直接进入本机测试"}
</button>
{localTestError ? <p className="auth-error" role="alert">{localTestError}</p> : null}
</div>
) : null}
<div className="auth-tabs" role="tablist" aria-label="认证方式"> <div className="auth-tabs" role="tablist" aria-label="认证方式">
<button <button
aria-selected={mode === "login"} aria-selected={mode === "login"}
+1
View File
@@ -69,6 +69,7 @@ async function checkSupport() {
browserValue.textContent = `${result.browser.brand} ${result.browser.major}`; browserValue.textContent = `${result.browser.brand} ${result.browser.major}`;
supportedValue.textContent = supportedLabel(result.supported_browsers); supportedValue.textContent = supportedLabel(result.supported_browsers);
window.dispatchEvent(new CustomEvent("dada:support-ready")); window.dispatchEvent(new CustomEvent("dada:support-ready"));
window.location.replace(window.location.pathname.startsWith("/admin") ? "/admin" : "/app");
return; return;
} }
showBlocked( showBlocked(
+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>;
} }
+47
View File
@@ -0,0 +1,47 @@
import type { GenerationAdapter } from "./ai-adapter-contract.js";
export type AiRuntimeProbeResult =
| {
code: "ai_probe_passed";
mime_type: "image/jpeg" | "image/png" | "image/webp";
pixel_height: number;
pixel_width: number;
real_calls: 1;
success: true;
}
| {
code: "ai_probe_failed";
error_category: string;
real_calls: 1;
success: false;
};
export async function runAiRuntimeProbe(adapter: GenerationAdapter): Promise<AiRuntimeProbeResult> {
const result = await adapter.start({
configSnapshot: { probe: true },
generationId: "00000000-0000-4000-8000-000000000002",
modelId: "gemini-3.1-flash-image-preview",
prompt: "生成一张简洁的红蓝几何色块测试图,不含文字。",
ratio: "1:1",
referenceAssetIds: [],
});
if (result.status === "failed") {
return { code: "ai_probe_failed", error_category: result.category, real_calls: 1, success: false };
}
if (result.status !== "completed" || result.outputs.length !== 1) {
return { code: "ai_probe_failed", error_category: "gateway_contract_invalid", real_calls: 1, success: false };
}
const output = result.outputs[0]!;
try {
return {
code: "ai_probe_passed",
mime_type: output.mimeType,
pixel_height: output.pixelHeight,
pixel_width: output.pixelWidth,
real_calls: 1,
success: true,
};
} finally {
output.bytes.fill(0);
}
}
+38 -4
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";
@@ -91,7 +91,8 @@ export class GenerationProcessor {
this.clock = input.clock ?? Date.now; this.clock = input.clock ?? Date.now;
this.dataRoot = resolve(input.dataRoot); this.dataRoot = resolve(input.dataRoot);
this.workerId = input.workerId; this.workerId = input.workerId;
this.database = new Database(input.databasePath); const nativeBinding = process.env.DADA_SQLITE_NATIVE_BINDING;
this.database = new Database(input.databasePath, nativeBinding ? { nativeBinding } : undefined);
configureWorkerDatabase(this.database); configureWorkerDatabase(this.database);
this.migrate(); this.migrate();
this.gatewayBalance = new GatewayBalanceRuntime({ clock: this.clock, database: this.database }); this.gatewayBalance = new GatewayBalanceRuntime({ clock: this.clock, database: this.database });
@@ -119,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();
} }
@@ -143,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");
@@ -155,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);
@@ -174,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);
}
}
+7 -3
View File
@@ -13,7 +13,7 @@ export async function receiveWorkerCredentials(input: NodeJS.ReadableStream = pr
if (names.length !== expected.length || names.some((name, index) => name !== expected[index])) { if (names.length !== expected.length || names.some((name, index) => name !== expected[index])) {
throw new Error("Worker credential channel contains an unexpected credential scope."); throw new Error("Worker credential channel contains an unexpected credential scope.");
} }
if (expected.some((name) => typeof parsed[name] !== "string" || parsed[name] === "")) { if (expected.some((name) => typeof parsed[name] !== "string")) {
throw new Error("Worker credential channel contains an invalid credential value."); throw new Error("Worker credential channel contains an invalid credential value.");
} }
return parsed as Record<(typeof WORKER_CREDENTIALS)[number], string>; return parsed as Record<(typeof WORKER_CREDENTIALS)[number], string>;
@@ -25,9 +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 configured = WORKER_CREDENTIALS.every((name) => credentials[name].length > 0); 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] = "";
if (!configured) throw new Error("Worker credential client initialization failed."); }
} }
export function attachWorkerSupervisorControl(pipeName: string, shutdown: () => Promise<void> | void) { export function attachWorkerSupervisorControl(pipeName: string, shutdown: () => Promise<void> | void) {
+34 -2
View File
@@ -2,6 +2,9 @@ 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 { runAiRuntimeProbe } from "./ai-runtime-probe.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";
@@ -21,8 +24,25 @@ if (workerPort) {
}); });
} }
if (!workerPort && process.argv.includes("--dada-credential-stdin")) { if (!workerPort && process.argv.includes("--dada-ai-probe")) {
initializeWorkerCredentialClient(await receiveWorkerCredentials()); const credentialClient = initializeWorkerCredentialClient(await receiveWorkerCredentials());
let adapter: OneApiGenerationAdapter | undefined;
let probeResult: Awaited<ReturnType<typeof runAiRuntimeProbe>> | { code: "ai_probe_failed"; error_category: "upstream_failed"; real_calls: 0; success: false };
try {
adapter = new OneApiGenerationAdapter({ credential: credentialClient.aiGatewayCredential });
probeResult = await runAiRuntimeProbe(adapter);
} catch {
probeResult = { code: "ai_probe_failed", error_category: "upstream_failed", real_calls: 0, success: false };
} finally {
credentialClient.aiGatewayCredential.fill(0);
adapter?.dispose();
}
await new Promise<void>((resolveWrite, rejectWrite) => {
process.stdout.write(JSON.stringify(probeResult), (error) => error ? rejectWrite(error) : resolveWrite());
});
process.exit(probeResult.success ? 0 : 2);
} else if (!workerPort && process.argv.includes("--dada-credential-stdin")) {
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.");
@@ -31,11 +51,15 @@ if (!workerPort && process.argv.includes("--dada-credential-stdin")) {
let retention: RetentionCleanup | undefined; let retention: RetentionCleanup | undefined;
let projectCleanup: ProjectPurgeCleanup | undefined; let projectCleanup: ProjectPurgeCleanup | undefined;
let retentionTimer: ReturnType<typeof setInterval> | undefined; let retentionTimer: ReturnType<typeof setInterval> | undefined;
let processor: GenerationProcessor | undefined;
let generationTimer: ReturnType<typeof setInterval> | undefined;
const control = attachWorkerSupervisorControl(controlPipe, () => { const control = attachWorkerSupervisorControl(controlPipe, () => {
clearInterval(keepAlive); clearInterval(keepAlive);
if (retentionTimer) clearInterval(retentionTimer); if (retentionTimer) clearInterval(retentionTimer);
retention?.close(); retention?.close();
projectCleanup?.close(); projectCleanup?.close();
if (generationTimer) clearInterval(generationTimer);
processor?.close();
storage?.close(); storage?.close();
}); });
let storageStatus: "active" | "unavailable" = "active"; let storageStatus: "active" | "unavailable" = "active";
@@ -45,6 +69,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 });
let adapter: OneApiGenerationAdapter;
try {
adapter = new OneApiGenerationAdapter({ credential: credentialClient.aiGatewayCredential });
} finally {
credentialClient.aiGatewayCredential.fill(0);
}
processor = new GenerationProcessor({ adapter, dataRoot, databasePath, workerId: `portable-oneapi-worker-${process.pid}` });
const runRetentionCleanup = () => { const runRetentionCleanup = () => {
try { try {
retention?.purgeExpired(); retention?.purgeExpired();
@@ -71,6 +102,7 @@ if (!workerPort && process.argv.includes("--dada-credential-stdin")) {
}); });
logger.write({ error_category: "none", status_category: "ready" }); logger.write({ error_category: "none", status_category: "ready" });
new WorkerAiCallGate({ getStorageStatus: () => storageStatus === "unavailable" ? storageStatus : (storage?.getStatus() ?? "unavailable"), logger }); new WorkerAiCallGate({ getStorageStatus: () => storageStatus === "unavailable" ? storageStatus : (storage?.getStatus() ?? "unavailable"), logger });
generationTimer = setInterval(() => { void processor?.processNext().catch(() => undefined); }, 250);
} catch { } catch {
storageStatus = "unavailable"; storageStatus = "unavailable";
control.reportStatus("storage_unavailable"); control.reportStatus("storage_unavailable");
+3 -1
View File
@@ -116,7 +116,9 @@
"test:wp7-05": "node scripts/run-wp7-05-validation.mjs", "test:wp7-05": "node scripts/run-wp7-05-validation.mjs",
"test:wp7-05:unit": "node --test tests/package/wp7-05-ui-gate.test.mjs tests/package/wp7-05-coverage.test.mjs", "test:wp7-05:unit": "node --test tests/package/wp7-05-ui-gate.test.mjs tests/package/wp7-05-coverage.test.mjs",
"test:wp7-06": "node scripts/run-wp7-06-validation.mjs", "test:wp7-06": "node scripts/run-wp7-06-validation.mjs",
"test:wp7-06:unit": "node --test tests/package/wp7-06-prefreeze.test.mjs" "test:wp7-06:unit": "node --test tests/package/wp7-06-prefreeze.test.mjs",
"test:wp7-07": "node scripts/run-wp7-07-validation.mjs",
"test:wp7-07:unit": "node --test tests/package/wp7-07-final-release.test.mjs"
}, },
"devDependencies": { "devDependencies": {
"@playwright/test": "1.62.0", "@playwright/test": "1.62.0",
+11 -1
View File
@@ -1,10 +1,20 @@
import { execFileSync } from "node:child_process";
import { readFileSync } from "node:fs";
import { resolve } from "node:path"; import { resolve } from "node:path";
import { buildAndValidatePortablePackage } from "./lib/portable-package.mjs"; import { buildAndValidatePortablePackage } from "./lib/portable-package.mjs";
import { validateFinalReleaseRecord } from "./lib/wp7-07-final-release.mjs";
const outputIndex = process.argv.indexOf("--output"); const outputIndex = process.argv.indexOf("--output");
const outputRoot = outputIndex >= 0 ? resolve(process.argv[outputIndex + 1]) : resolve(".build", "portable-release"); const outputRoot = outputIndex >= 0 ? resolve(process.argv[outputIndex + 1]) : resolve(".build", "portable-release");
const result = await buildAndValidatePortablePackage({ outputRoot }); const previousRelease = JSON.parse(readFileSync(resolve("RELEASE.json"), "utf8"));
const releaseRecord = validateFinalReleaseRecord({
...previousRelease,
buildCommit: execFileSync("git", ["rev-parse", "HEAD"], { encoding: "utf8" }).trim(),
maintenanceFromCommit: previousRelease.buildCommit,
recordedAt: new Date().toISOString(),
});
const result = await buildAndValidatePortablePackage({ outputRoot, releaseRecord });
console.log(JSON.stringify({ console.log(JSON.stringify({
package: result.packageManifest.package_name, package: result.packageManifest.package_name,
sha256: result.packageManifest.zip_sha256, sha256: result.packageManifest.zip_sha256,
+47 -15
View File
@@ -165,7 +165,7 @@ function copyApplication(source, destination, runtimeDependencies) {
function buildArtifacts(stagingRoot) { function buildArtifacts(stagingRoot) {
debug("build workspace artifacts"); debug("build workspace artifacts");
run("pnpm", ["--filter", "@dada/shared-contracts", "build"]); run("pnpm", ["build:workspace-packages"]);
run("pnpm", ["--filter", "@dada/web", "build"]); run("pnpm", ["--filter", "@dada/web", "build"]);
run("pnpm", ["--filter", "@dada/api", "build"]); run("pnpm", ["--filter", "@dada/api", "build"]);
run("pnpm", ["--filter", "@dada/worker", "build"]); run("pnpm", ["--filter", "@dada/worker", "build"]);
@@ -208,7 +208,7 @@ async function waitForHealth(child) {
throw new Error("Packaged API did not become healthy on fixed port 43121.", { cause: lastError }); throw new Error("Packaged API did not become healthy on fixed port 43121.", { cause: lastError });
} }
async function verifyExtractedPackage(zipPath, packageName) { export async function verifyExtractedPackage(zipPath, packageName, expectedSupport) {
const extractRoot = mkdtempSync(join(tmpdir(), "dada-wp0-09-")); const extractRoot = mkdtempSync(join(tmpdir(), "dada-wp0-09-"));
try { try {
const escapedZip = zipPath.replaceAll("'", "''"); const escapedZip = zipPath.replaceAll("'", "''");
@@ -235,21 +235,37 @@ async function verifyExtractedPackage(zipPath, packageName) {
}); });
try { try {
const health = await waitForHealth(api); const health = await waitForHealth(api);
const brands = [
{ brand: "Not_A Brand", version: "99" },
{ brand: "Chromium", version: String(expectedSupport.major) },
{ brand: expectedSupport.brand, version: String(expectedSupport.major) },
];
const fullVersionList = [
{ brand: "Not_A Brand", version: "99.0.0.0" },
{ brand: "Chromium", version: expectedSupport.fullVersion },
{ brand: expectedSupport.brand, version: expectedSupport.fullVersion },
];
const serializeBrands = (values) => values.map(({ brand, version }) => `"${brand}";v="${version}"`).join(", ");
const releaseGate = await fetch(`http://127.0.0.1:${fixedPort}/api/v1/support/check`, { const releaseGate = await fetch(`http://127.0.0.1:${fixedPort}/api/v1/support/check`, {
body: JSON.stringify({ body: JSON.stringify({
brands: [{ brand: "Google Chrome", version: "150" }], brands,
full_version_list: [{ brand: "Google Chrome", version: "150.0.0.0" }], full_version_list: fullVersionList,
platform: "Windows", platform: "Windows",
}), }),
headers: { headers: {
"content-type": "application/json", "content-type": "application/json",
"sec-ch-ua": '"Google Chrome";v="150"', host: `127.0.0.1:${fixedPort}`,
"sec-ch-ua-full-version-list": '"Google Chrome";v="150.0.0.0"', origin: `http://127.0.0.1:${fixedPort}`,
"sec-ch-ua": serializeBrands(brands),
"sec-ch-ua-full-version-list": serializeBrands(fullVersionList),
"sec-ch-ua-platform": '"Windows"', "sec-ch-ua-platform": '"Windows"',
}, },
method: "POST", method: "POST",
}); });
if (releaseGate.status !== 426) throw new Error(`Candidate RELEASE.json unexpectedly passed with ${releaseGate.status}.`); if (releaseGate.status !== expectedSupport.statusCode) {
const responseBody = await releaseGate.text();
throw new Error(`Packaged RELEASE.json support gate returned ${releaseGate.status}; expected ${expectedSupport.statusCode}: ${responseBody}`);
}
return { return {
api: { executable: "runtime/node.exe", health, pid: api.pid, release_gate: { status_code: releaseGate.status }, status: "passed" }, api: { executable: "runtime/node.exe", health, pid: api.pid, release_gate: { status_code: releaseGate.status }, status: "passed" },
native, native,
@@ -291,7 +307,7 @@ function scanPackage(packageDirectory) {
return { disallowed_matches: disallowedMatches, reparse_points: reparsePoints, scanned_files: files.length, status: disallowedMatches.length === 0 && reparsePoints.length === 0 ? "passed" : "failed" }; return { disallowed_matches: disallowedMatches, reparse_points: reparsePoints, scanned_files: files.length, status: disallowedMatches.length === 0 && reparsePoints.length === 0 ? "passed" : "failed" };
} }
export async function buildAndValidatePortablePackage({ evidenceDirectory, outputRoot }) { export async function buildAndValidatePortablePackage({ evidenceDirectory, outputRoot, releaseRecord }) {
if (process.platform !== frozenRuntime.os || process.arch !== frozenRuntime.arch || process.version.slice(1) !== frozenRuntime.node) { if (process.platform !== frozenRuntime.os || process.arch !== frozenRuntime.arch || process.version.slice(1) !== frozenRuntime.node) {
throw new Error("Portable package build requires frozen Node 24.13.0 on win-x64."); throw new Error("Portable package build requires frozen Node 24.13.0 on win-x64.");
} }
@@ -320,7 +336,7 @@ export async function buildAndValidatePortablePackage({ evidenceDirectory, outpu
debug("copy API application"); debug("copy API application");
const apiDependencies = copyApplication(join(repositoryRoot, "apps", "api"), join(serverRoot, "api"), ["@fastify/multipart", "@fastify/swagger", "@sinclair/typebox", "better-sqlite3", "fastify", "sharp"]); const apiDependencies = copyApplication(join(repositoryRoot, "apps", "api"), join(serverRoot, "api"), ["@fastify/multipart", "@fastify/swagger", "@sinclair/typebox", "better-sqlite3", "fastify", "sharp"]);
debug("copy Worker application"); debug("copy Worker application");
const workerDependencies = copyApplication(join(repositoryRoot, "apps", "worker"), join(serverRoot, "worker"), ["better-sqlite3"]); const workerDependencies = copyApplication(join(repositoryRoot, "apps", "worker"), join(serverRoot, "worker"), ["better-sqlite3", "sharp"]);
const sharedDestination = join(serverRoot, "api", "node_modules", "@dada", "shared-contracts"); const sharedDestination = join(serverRoot, "api", "node_modules", "@dada", "shared-contracts");
mkdirSync(sharedDestination, { recursive: true }); mkdirSync(sharedDestination, { recursive: true });
copyTree(join(repositoryRoot, "packages", "shared-contracts", "dist"), join(sharedDestination, "dist")); copyTree(join(repositoryRoot, "packages", "shared-contracts", "dist"), join(sharedDestination, "dist"));
@@ -351,7 +367,8 @@ export async function buildAndValidatePortablePackage({ evidenceDirectory, outpu
writeJson(join(packageDirectory, "LICENSES", "third-party.json"), { api: apiDependencies, runtime: { node: frozenRuntime.node }, schema_version: "1.0", worker: workerDependencies }); writeJson(join(packageDirectory, "LICENSES", "third-party.json"), { api: apiDependencies, runtime: { node: frozenRuntime.node }, schema_version: "1.0", worker: workerDependencies });
const commit = run("git", ["rev-parse", "HEAD"]); const commit = run("git", ["rev-parse", "HEAD"]);
writeJson(join(packageDirectory, "RELEASE.json"), { const finalRelease = releaseRecord !== undefined;
writeJson(join(packageDirectory, "RELEASE.json"), releaseRecord ?? {
app_version: appVersion, app_version: appVersion,
browsers: [], browsers: [],
build_commit: commit, build_commit: commit,
@@ -360,16 +377,20 @@ export async function buildAndValidatePortablePackage({ evidenceDirectory, outpu
windows_build: null, windows_build: null,
}); });
writeFileSync(join(packageDirectory, "START-HERE.txt"), [ writeFileSync(join(packageDirectory, "START-HERE.txt"), [
"Dada P0-A candidate package", finalRelease ? "Dada P0-A first-version portable package" : "Dada P0-A candidate package",
"", "",
"This candidate is unsigned and is not a final P0-A release.", finalRelease
? "This unsigned first-version package passed the local P0-A release gates recorded in RELEASE.json."
: "This candidate is unsigned and is not a final P0-A release.",
"Verify the adjacent SHA-256 file before first launch.", "Verify the adjacent SHA-256 file before first launch.",
"Windows SmartScreen may warn on first launch because the executable is unsigned.", "Windows SmartScreen may warn on first launch because the executable is unsigned.",
"For an antivirus alert, compare the package hash with the Gitea build record.", "For an antivirus alert, compare the package hash with the Gitea build record.",
"Do not disable antivirus protection, add broad exclusions, or skip hash verification.", "Do not disable antivirus protection, add broad exclusions, or skip hash verification.",
"To update, exit Dada from the tray and replace the complete program directory.", "To update, exit Dada from the tray and replace the complete program directory.",
"Dada uses 127.0.0.1:43121 and does not support LAN or remote access.", "Dada uses 127.0.0.1:43121 and does not support LAN or remote access.",
"A final RELEASE.json is created only after WP-7 acceptance.", finalRelease
? "Resend and Amap real-provider validation remain explicitly deferred and are not recorded as passed."
: "A final RELEASE.json is created only after WP-7 acceptance.",
"", "",
].join("\r\n")); ].join("\r\n"));
@@ -381,7 +402,18 @@ export async function buildAndValidatePortablePackage({ evidenceDirectory, outpu
const zipSha256 = fileSha256(zipPath); const zipSha256 = fileSha256(zipPath);
const shaPath = `${zipPath}.sha256`; const shaPath = `${zipPath}.sha256`;
writeFileSync(shaPath, `${zipSha256} ${basename(zipPath)}\n`); writeFileSync(shaPath, `${zipSha256} ${basename(zipPath)}\n`);
const processTree = await verifyExtractedPackage(zipPath, packageName); const supportBrowser = finalRelease ? releaseRecord.browsers[0] : undefined;
const processTree = await verifyExtractedPackage(zipPath, packageName, finalRelease ? {
brand: supportBrowser.brand,
fullVersion: supportBrowser.fullVersion,
major: Number.parseInt(supportBrowser.fullVersion.split(".")[0], 10),
statusCode: 200,
} : {
brand: "Google Chrome",
fullVersion: "150.0.0.0",
major: 150,
statusCode: 426,
});
const fileEntries = listFiles(packageDirectory).files.map((path) => ({ const fileEntries = listFiles(packageDirectory).files.map((path) => ({
path: relative(packageDirectory, path).replaceAll("\\", "/"), path: relative(packageDirectory, path).replaceAll("\\", "/"),
sha256: fileSha256(path), sha256: fileSha256(path),
@@ -392,7 +424,7 @@ export async function buildAndValidatePortablePackage({ evidenceDirectory, outpu
files: fileEntries, files: fileEntries,
fixed_port: fixedPort, fixed_port: fixedPort,
package_name: packageName, package_name: packageName,
release_status: "candidate_unvalidated", release_status: finalRelease ? releaseRecord.releaseStatus : "candidate_unvalidated",
schema_version: "1.0", schema_version: "1.0",
zip_sha256: zipSha256, zip_sha256: zipSha256,
}; };
+96
View File
@@ -0,0 +1,96 @@
import { createHash } from "node:crypto";
import { readFileSync, readdirSync, statSync } from "node:fs";
import { extname, join, relative } from "node:path";
const SHA40 = /^[a-f0-9]{40}$/i;
const SHA64 = /^[a-f0-9]{64}$/i;
const VERSION = /^[1-9][0-9]*\.[0-9]+\.[0-9]+\.[0-9]+$/;
const ABSOLUTE_PATH = /(?:[A-Za-z]:[\\/](?:Users|Documents)[\\/][^\\/"'\s]+[\\/]|\/Users\/[^/"'\s]+\/|\/home\/[^/"'\s]+\/)/;
const CREDENTIAL = /\b(?:sk|key)-[A-Za-z0-9_-]{16,}\b|-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/i;
const TEXT_EXTENSIONS = new Set([".cjs", ".cs", ".css", ".html", ".js", ".json", ".mjs", ".ts", ".tsx", ".txt", ".xml", ".yaml", ".yml"]);
export const DEFERRED_EXTERNAL_TASKS = Object.freeze(["TASK-WP7-03", "TASK-WP7-04"]);
export function buildFinalReleaseRecord({ appVersion, browsers, buildCommit, frozenFromCommit, recordedAt, windows }) {
const record = {
appVersion,
browsers: browsers.map(({ brand, fullVersion }) => ({ brand, fullVersion })),
buildCommit: buildCommit.toLowerCase(),
deferredExternalTasks: [...DEFERRED_EXTERNAL_TASKS],
finalRelease: true,
fixedPort: 43121,
frozenFromCommit: frozenFromCommit.toLowerCase(),
recordedAt,
releaseStatus: "first_version_internal",
schemaVersion: "1.0",
windows: { arch: windows.arch, build: windows.build, displayVersion: windows.displayVersion },
};
return validateFinalReleaseRecord(record);
}
export function validateFinalReleaseRecord(record) {
const errors = [];
if (record?.schemaVersion !== "1.0") errors.push("schemaVersion");
if (record?.releaseStatus !== "first_version_internal") errors.push("releaseStatus");
if (record?.finalRelease !== true) errors.push("finalRelease");
if (record?.fixedPort !== 43121) errors.push("fixedPort");
if (!SHA40.test(record?.buildCommit ?? "")) errors.push("buildCommit");
if (!SHA40.test(record?.frozenFromCommit ?? "")) errors.push("frozenFromCommit");
if (!Number.isFinite(Date.parse(record?.recordedAt ?? ""))) errors.push("recordedAt");
if (!Array.isArray(record?.deferredExternalTasks) || record.deferredExternalTasks.join("|") !== DEFERRED_EXTERNAL_TASKS.join("|")) errors.push("deferredExternalTasks");
if (record?.windows?.arch !== "x64" || !/^\d+\.\d+$/.test(record?.windows?.build ?? "")) errors.push("windows");
if (!Array.isArray(record?.browsers) || record.browsers.length !== 2) {
errors.push("browsers");
} else {
const brands = record.browsers.map(({ brand }) => brand).sort();
if (brands.join("|") !== "Google Chrome|Microsoft Edge") errors.push("browserBrands");
for (const browser of record.browsers) {
if (!VERSION.test(browser.fullVersion ?? "")) errors.push(`${browser.brand}.fullVersion`);
if ("path" in browser || "executablePath" in browser || "executableSha256" in browser) errors.push(`${browser.brand}.privateMetadata`);
}
}
const serialized = JSON.stringify(record);
if (ABSOLUTE_PATH.test(serialized) || CREDENTIAL.test(serialized)) errors.push("sensitiveValue");
if (errors.length > 0) throw new Error(`WP7_07_RELEASE_INVALID:${[...new Set(errors)].join(",")}`);
return record;
}
export function sha256File(path) {
return createHash("sha256").update(readFileSync(path)).digest("hex").toUpperCase();
}
export function scanReleaseFiles({ roots, allowedFixturePaths = [] }) {
const allowed = new Set(allowedFixturePaths.map((value) => value.replaceAll("\\", "/")));
const findings = [];
let scannedFiles = 0;
function visit(root, current = root) {
for (const entry of readdirSync(current, { withFileTypes: true })) {
if ([".git", ".pnpm-store", "node_modules", "bin", "obj"].includes(entry.name)) continue;
const path = join(current, entry.name);
if (entry.isDirectory()) {
visit(root, path);
continue;
}
if (!entry.isFile()) continue;
scannedFiles += 1;
if (!TEXT_EXTENSIONS.has(extname(entry.name).toLowerCase())) continue;
const logicalPath = relative(root, path).replaceAll("\\", "/");
const content = readFileSync(path, "utf8");
if (!allowed.has(logicalPath) && ABSOLUTE_PATH.test(content)) findings.push({ path: logicalPath, rule: "absolute_user_path" });
if (!allowed.has(logicalPath) && CREDENTIAL.test(content)) findings.push({ path: logicalPath, rule: "credential_shape" });
}
}
for (const root of roots) {
if (!statSync(root).isDirectory()) throw new Error(`WP7_07_SCAN_ROOT_INVALID:${root}`);
visit(root);
}
return { findings, scanned_files: scannedFiles, status: findings.length === 0 ? "passed" : "failed" };
}
export function validateFinalEvidence({ packageManifest, release, releaseSha256, scan }) {
validateFinalReleaseRecord(release);
if (!SHA64.test(releaseSha256 ?? "")) throw new Error("WP7_07_RELEASE_HASH_INVALID");
if (packageManifest?.release_status !== release.releaseStatus || !SHA64.test(packageManifest?.zip_sha256 ?? "")) throw new Error("WP7_07_PACKAGE_MANIFEST_INVALID");
if (scan?.status !== "passed" || scan.findings?.length !== 0) throw new Error("WP7_07_LEAK_SCAN_FAILED");
return { release_sha256: releaseSha256.toUpperCase(), status: "passed", zip_sha256: packageManifest.zip_sha256.toUpperCase() };
}
+157
View File
@@ -0,0 +1,157 @@
import { spawnSync } from "node:child_process";
import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { basename, join, resolve } from "node:path";
import { buildAndValidatePortablePackage } from "./lib/portable-package.mjs";
import { readCandidateEnvironment } from "./lib/release-candidate.mjs";
import {
buildFinalReleaseRecord,
scanReleaseFiles,
sha256File,
validateFinalEvidence,
} from "./lib/wp7-07-final-release.mjs";
const runId = process.env.DADA_TDD_RUN_ID ?? `wp7-07-final-${new Date().toISOString().replace(/[^0-9]/g, "")}`;
const runDirectory = resolve("artifacts", "tdd", runId);
const releaseCase = resolve(runDirectory, "cases", "TDD-WP7-REL-001-final-release-record");
const securityCase = resolve(runDirectory, "cases", "TDD-WP7-SEC-001-artifact-leak-scan");
const outputRoot = resolve(".build", "wp7-07-final-release");
if (existsSync(runDirectory)) throw new Error(`Evidence run already exists: ${runId}`);
mkdirSync(releaseCase, { recursive: true });
mkdirSync(securityCase, { recursive: true });
function writeJson(path, value) {
writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`);
}
function git(args) {
const result = spawnSync("git", args, { encoding: "utf8", maxBuffer: 64 * 1024 * 1024, timeout: 120_000 });
if ((result.status ?? 1) !== 0) throw new Error(`WP7_07_GIT_FAILED:${args.join(" ")}`);
return result.stdout.trim();
}
function gitGrep(args) {
const result = spawnSync("git", ["grep", ...args], { encoding: "utf8", maxBuffer: 64 * 1024 * 1024, timeout: 120_000 });
if (![0, 1].includes(result.status ?? 2)) throw new Error("WP7_07_GIT_GREP_FAILED");
return result.status === 0 ? result.stdout.trim() : "";
}
function run(name, command) {
const startedAt = new Date().toISOString();
const result = spawnSync(process.env.ComSpec ?? "cmd.exe", ["/d", "/s", "/c", command], {
encoding: "utf8",
env: process.env,
maxBuffer: 64 * 1024 * 1024,
});
if (result.stdout) process.stdout.write(result.stdout);
if (result.stderr) process.stderr.write(result.stderr);
return { command, exit_code: result.status ?? 1, finished_at: new Date().toISOString(), name, started_at: startedAt };
}
const currentCommit = git(["rev-parse", "HEAD"]);
const prefreezeCommit = git(["ls-remote", "origin", "refs/heads/codex/wp7-06"]).split(/\s+/)[0];
if (!prefreezeCommit || spawnSync("git", ["merge-base", "--is-ancestor", prefreezeCommit, "HEAD"]).status !== 0) {
throw new Error("WP7_07_PREFREEZE_LINEAGE_INVALID");
}
const commands = [
run("unit", "node --test tests/package/wp7-07-final-release.test.mjs"),
run("security", "pnpm test:security"),
run("trace", "pnpm validate:tdd-trace"),
];
if (commands.some(({ exit_code }) => exit_code !== 0)) {
writeJson(join(releaseCase, "commands.json"), { commands, run_id: runId, schema_version: "1.0" });
process.exit(1);
}
const environment = readCandidateEnvironment();
const packageJson = JSON.parse(readFileSync("package.json", "utf8"));
const release = buildFinalReleaseRecord({
appVersion: packageJson.version,
browsers: environment.browsers.map(({ brand, full_version }) => ({ brand, fullVersion: full_version })),
buildCommit: currentCommit,
frozenFromCommit: prefreezeCommit,
recordedAt: new Date().toISOString(),
windows: {
arch: environment.windows.arch,
build: environment.windows.build,
displayVersion: environment.windows.display_version,
},
});
writeJson(resolve("RELEASE.json"), release);
const packageResult = await buildAndValidatePortablePackage({ evidenceDirectory: releaseCase, outputRoot, releaseRecord: release });
const packageDirectory = join(outputRoot, packageResult.packageManifest.package_name);
const zipPath = join(outputRoot, `${packageResult.packageManifest.package_name}.zip`);
const releaseSha256 = sha256File(resolve("RELEASE.json"));
const packageReleaseSha256 = sha256File(join(packageDirectory, "RELEASE.json"));
if (releaseSha256 !== packageReleaseSha256) throw new Error("WP7_07_PACKAGE_RELEASE_DRIFT");
const finalScan = scanReleaseFiles({ roots: [packageDirectory, releaseCase] });
const trackedSensitive = gitGrep(["-I", "-n", "-E", "C:\\\\Users\\\\[^\\\\]+|sk-[A-Za-z0-9_-]{24,}", "HEAD", "--", ":!tests", ":!scripts/lib/wp7-07-final-release.mjs"]);
const scan = {
...finalScan,
git_current_findings: trackedSensitive ? trackedSensitive.split(/\r?\n/).filter(Boolean) : [],
status: finalScan.status === "passed" && !trackedSensitive ? "passed" : "failed",
};
writeJson(join(securityCase, "scan-report.json"), scan);
writeJson(join(securityCase, "allowlist.json"), {
entries: ["tests/**:synthetic security traps", "scripts/lib/wp7-07-final-release.mjs:scanner patterns"],
real_values_allowed: false,
schema_version: "1.0",
});
if (scan.status !== "passed") throw new Error("WP7_07_LEAK_SCAN_FAILED");
const finalEvidence = validateFinalEvidence({ packageManifest: packageResult.packageManifest, release, releaseSha256, scan });
copyFileSync(resolve("RELEASE.json"), join(releaseCase, "RELEASE.json"));
copyFileSync(join(packageDirectory, "START-HERE.txt"), join(releaseCase, "START-HERE.txt"));
writeJson(join(releaseCase, "environment.json"), {
browsers: release.browsers,
fixed_port: release.fixedPort,
windows: release.windows,
schema_version: "1.0",
});
writeJson(join(releaseCase, "final-package.json"), {
file_name: basename(zipPath),
release_sha256: finalEvidence.release_sha256,
release_status: release.releaseStatus,
zip_sha256: finalEvidence.zip_sha256,
schema_version: "1.0",
});
writeJson(join(releaseCase, "commands.json"), { commands, run_id: runId, schema_version: "1.0" });
writeJson(join(releaseCase, "result.json"), {
acceptance_criteria: ["AC-24", "AC-41", "AC-48", "AC-56"],
deferred_external_tasks: release.deferredExternalTasks,
evidence_refs: ["RELEASE.json", "START-HERE.txt", "environment.json", "package-manifest.json", "final-package.json"],
release_gate: ["release:P0-A"],
requirements: ["NFR-01", "NFR-09", "PRIV-01", "PRIV-02"],
status: "passed",
task_id: "TASK-WP7-07",
test_id: "TDD-WP7-REL-001-final-release-record",
});
writeJson(join(securityCase, "result.json"), {
evidence_refs: ["scan-report.json", "allowlist.json"],
status: "passed",
task_id: "TASK-WP7-07",
test_id: "TDD-WP7-SEC-001-artifact-leak-scan",
});
writeJson(join(runDirectory, "evidence.json"), {
cases: [
{ missing_evidence: [], status: "passed", test_id: "TDD-WP7-REL-001-final-release-record" },
{ missing_evidence: [], status: "passed", test_id: "TDD-WP7-SEC-001-artifact-leak-scan" },
],
deferred_external_tasks: release.deferredExternalTasks,
release_sha256: finalEvidence.release_sha256,
run_id: runId,
status: "passed",
zip_sha256: finalEvidence.zip_sha256,
schema_version: "1.0",
});
console.log(JSON.stringify({
deferred_external_tasks: release.deferredExternalTasks,
release_sha256: finalEvidence.release_sha256,
run_id: runId,
status: "passed",
zip_sha256: finalEvidence.zip_sha256,
}, null, 2));
@@ -40,6 +40,7 @@ internal static class Program
var supervisor = await TestSupervisorLifecycleAsync(); var supervisor = await TestSupervisorLifecycleAsync();
await TestAmapProbeSecurityAsync(); await TestAmapProbeSecurityAsync();
TestSecureConfigurationPersistence(); TestSecureConfigurationPersistence();
TestRuntimeDirectoryBootstrap();
TestStructuredLogging(); TestStructuredLogging();
WriteEvidence(Environment.GetEnvironmentVariable("DADA_EVIDENCE_DIR_SEC"), security); WriteEvidence(Environment.GetEnvironmentVariable("DADA_EVIDENCE_DIR_SEC"), security);
WriteEvidence(Environment.GetEnvironmentVariable("DADA_EVIDENCE_DIR_SUP"), supervisor); WriteEvidence(Environment.GetEnvironmentVariable("DADA_EVIDENCE_DIR_SUP"), supervisor);
@@ -125,6 +126,29 @@ internal static class Program
} }
} }
private static void TestRuntimeDirectoryBootstrap()
{
var root = Path.Combine(Path.GetTempPath(), $"dada-runtime-root-{Guid.NewGuid():N}");
try
{
Directory.CreateDirectory(root);
SupervisorRuntime.EnsureRuntimeDirectories(root);
foreach (var relativePath in new[]
{
"db", "content/references", "content/generated", "content/exports",
"managed-assets", "derived-assets", "staging",
"logs/api", "logs/worker", "logs/supervisor",
})
{
True(Directory.Exists(Path.Combine(root, relativePath)), $"runtime directory missing: {relativePath}");
}
}
finally
{
if (Directory.Exists(root)) Directory.Delete(root, recursive: true);
}
}
private static async Task<object> TestCredentialBoundaryAsync() private static async Task<object> TestCredentialBoundaryAsync()
{ {
var store = new TestCredentialStore(); var store = new TestCredentialStore();
@@ -146,6 +170,8 @@ internal static class Program
True(leakProbe.SensitiveOutputDetected, "credential echo must be detected"); True(leakProbe.SensitiveOutputDetected, "credential echo must be detected");
Equal(string.Empty, leakProbe.StandardOutput, "credential echo output discarded"); Equal(string.Empty, leakProbe.StandardOutput, "credential echo output discarded");
Equal(string.Empty, leakProbe.StandardError, "credential echo error discarded"); Equal(string.Empty, leakProbe.StandardError, "credential echo error discarded");
True(AiGatewayProbe.TryValidateOutput("{\"code\":\"ai_probe_passed\",\"mime_type\":\"image/png\",\"pixel_height\":1080,\"pixel_width\":1080,\"real_calls\":1,\"success\":true}", out _), "AI probe success output accepted");
False(AiGatewayProbe.TryValidateOutput("{\"code\":\"ai_probe_passed\",\"raw_body\":\"private\",\"real_calls\":1,\"success\":true}", out _), "AI probe private output rejected");
var externalArguments = new[] var externalArguments = new[]
{ {
@@ -0,0 +1,74 @@
using System.Diagnostics;
using System.Text.Json;
namespace Dada.Supervisor;
internal static class AiGatewayProbe
{
internal static async Task<int> RunAsync(ICredentialStore credentials, CancellationToken cancellationToken = default)
{
var node = Path.Combine(AppContext.BaseDirectory, "runtime", "node.exe");
var worker = Path.Combine(AppContext.BaseDirectory, "server", "worker.mjs");
if (!File.Exists(node) || !File.Exists(worker)) return WriteFailure("ai_probe_runtime_missing", 0);
var startInfo = new ProcessStartInfo(node) { WorkingDirectory = AppContext.BaseDirectory };
startInfo.Environment["DADA_SQLITE_NATIVE_BINDING"] = Path.Combine(AppContext.BaseDirectory, "server", "native", "better_sqlite3.node");
startInfo.ArgumentList.Add(worker);
startInfo.ArgumentList.Add("--dada-ai-probe");
startInfo.ArgumentList.Add("--dada-credential-stdin");
var result = await CredentialProcessLauncher.RunToCompletionAsync(startInfo, ChildRole.Worker, credentials, cancellationToken);
if (result.SensitiveOutputDetected || result.StandardError.Length > 0 || !TryValidateOutput(result.StandardOutput, out var sanitized))
{
return WriteFailure("ai_probe_runtime_failed", 0);
}
Console.WriteLine(sanitized);
return result.ExitCode;
}
internal static bool TryValidateOutput(string output, out string sanitized)
{
sanitized = string.Empty;
try
{
using var document = JsonDocument.Parse(output);
var root = document.RootElement;
if (root.ValueKind != JsonValueKind.Object) return false;
var allowed = new HashSet<string>(StringComparer.Ordinal)
{
"code", "error_category", "mime_type", "pixel_height", "pixel_width", "real_calls", "success",
};
if (root.EnumerateObject().Any(property => !allowed.Contains(property.Name))) return false;
if (!root.TryGetProperty("success", out var success) || success.ValueKind is not (JsonValueKind.True or JsonValueKind.False)) return false;
if (!root.TryGetProperty("real_calls", out var realCalls) || realCalls.ValueKind != JsonValueKind.Number || !realCalls.TryGetInt32(out var count) || count is < 0 or > 1) return false;
var passed = success.GetBoolean();
var code = root.GetProperty("code").GetString();
if (passed)
{
if (code != "ai_probe_passed" || count != 1) return false;
var mime = root.GetProperty("mime_type").GetString();
if (mime is not ("image/jpeg" or "image/png" or "image/webp")) return false;
if (!PositiveDimension(root, "pixel_width") || !PositiveDimension(root, "pixel_height")) return false;
}
else
{
if (code != "ai_probe_failed" || !root.TryGetProperty("error_category", out var category)
|| category.ValueKind != JsonValueKind.String || (category.GetString()?.Length ?? 0) is < 1 or > 64) return false;
}
sanitized = JsonSerializer.Serialize(root);
return true;
}
catch (Exception exception) when (exception is JsonException or InvalidOperationException or KeyNotFoundException)
{
return false;
}
}
private static bool PositiveDimension(JsonElement root, string name) =>
root.TryGetProperty(name, out var value) && value.ValueKind == JsonValueKind.Number
&& value.TryGetInt32(out var dimension) && dimension is > 0 and <= 4096;
private static int WriteFailure(string code, int realCalls)
{
Console.WriteLine(JsonSerializer.Serialize(new { code, real_calls = realCalls, success = false }));
return 2;
}
}
+6 -2
View File
@@ -50,7 +50,9 @@ internal static class CredentialProcessLauncher
var credentials = new Dictionary<string, string>(StringComparer.Ordinal); var credentials = new Dictionary<string, string>(StringComparer.Ordinal);
foreach (var target in CredentialCatalog.RequiredFor(role)) foreach (var target in CredentialCatalog.RequiredFor(role))
{ {
credentials[target] = store.Read(target) ?? throw new MissingCredentialException(target); var value = store.Read(target);
if (role == ChildRole.Worker && string.IsNullOrWhiteSpace(value)) throw new MissingCredentialException(target);
credentials[target] = value ?? string.Empty;
} }
startInfo.UseShellExecute = false; startInfo.UseShellExecute = false;
@@ -98,7 +100,9 @@ internal static class CredentialProcessLauncher
var credentials = new Dictionary<string, string>(StringComparer.Ordinal); var credentials = new Dictionary<string, string>(StringComparer.Ordinal);
foreach (var target in CredentialCatalog.RequiredFor(role)) foreach (var target in CredentialCatalog.RequiredFor(role))
{ {
credentials[target] = store.Read(target) ?? throw new MissingCredentialException(target); var value = store.Read(target);
if (role == ChildRole.Worker && string.IsNullOrWhiteSpace(value)) throw new MissingCredentialException(target);
credentials[target] = value ?? string.Empty;
} }
startInfo.UseShellExecute = false; startInfo.UseShellExecute = false;
@@ -26,7 +26,7 @@ internal static class OfflineCommandRouter
return args[0] switch return args[0] switch
{ {
"configure" => RunConfigure(args.Skip(1).ToArray()), "configure" => RunConfigure(args.Skip(1).ToArray()),
"secrets" => RunSecrets(args.Skip(1).ToArray(), credentials), "secrets" => await RunSecretsAsync(args.Skip(1).ToArray(), credentials),
"admin-allowlist" => RunAdminAllowlist(args.Skip(1).ToArray(), credentials), "admin-allowlist" => RunAdminAllowlist(args.Skip(1).ToArray(), credentials),
"doctor" when args.Length == 1 => RunDoctor(credentials), "doctor" when args.Length == 1 => RunDoctor(credentials),
"validate-external" => await ControlledExternalValidationLauncher.RunAsync(args.Skip(1).ToArray(), credentials), "validate-external" => await ControlledExternalValidationLauncher.RunAsync(args.Skip(1).ToArray(), credentials),
@@ -66,7 +66,7 @@ internal static class OfflineCommandRouter
return 0; return 0;
} }
private static int RunSecrets(string[] args, ICredentialStore store) private static async Task<int> RunSecretsAsync(string[] args, ICredentialStore store)
{ {
if (args.Length != 2 || !TryResolveCredential(args[1], out var target)) return Usage(); if (args.Length != 2 || !TryResolveCredential(args[1], out var target)) return Usage();
switch (args[0]) switch (args[0])
@@ -86,6 +86,8 @@ internal static class OfflineCommandRouter
return 0; return 0;
case "probe" when target == CredentialCatalog.ApiAmap: case "probe" when target == CredentialCatalog.ApiAmap:
return AmapProbe.Run(store.Read(target)); return AmapProbe.Run(store.Read(target));
case "probe" when target == CredentialCatalog.WorkerAiGateway:
return await AiGatewayProbe.RunAsync(store);
default: default:
return Usage(); return Usage();
} }
@@ -1,9 +1,23 @@
using System.Diagnostics; using System.Diagnostics;
using System.Security.Cryptography;
namespace Dada.Supervisor; namespace Dada.Supervisor;
internal sealed class SupervisorRuntime : IAsyncDisposable internal sealed class SupervisorRuntime : IAsyncDisposable
{ {
private static readonly string[] RequiredDataDirectories =
[
"db",
Path.Combine("content", "references"),
Path.Combine("content", "generated"),
Path.Combine("content", "exports"),
"managed-assets",
"derived-assets",
"staging",
Path.Combine("logs", "api"),
Path.Combine("logs", "worker"),
Path.Combine("logs", "supervisor"),
];
private readonly ICredentialStore credentials; private readonly ICredentialStore credentials;
private ManagedComponentSupervisor? api; private ManagedComponentSupervisor? api;
private ManagedComponentSupervisor? worker; private ManagedComponentSupervisor? worker;
@@ -25,6 +39,7 @@ internal sealed class SupervisorRuntime : IAsyncDisposable
{ {
return SupervisorState.StartupFailed; return SupervisorState.StartupFailed;
} }
EnsureRuntimeDirectories(configuration.LocalDataRoot);
try try
{ {
logger = new StructuredJsonlLogger(Path.Combine(configuration.LocalDataRoot, "logs", "supervisor"), "supervisor"); logger = new StructuredJsonlLogger(Path.Combine(configuration.LocalDataRoot, "logs", "supervisor"), "supervisor");
@@ -34,16 +49,15 @@ internal sealed class SupervisorRuntime : IAsyncDisposable
{ {
return SupervisorState.StorageUnavailable; return SupervisorState.StorageUnavailable;
} }
if (CredentialCatalog.RequiredFor(ChildRole.Api).Concat(CredentialCatalog.RequiredFor(ChildRole.Worker)).Any(target => !credentials.IsConfigured(target))) EnsureAdminPepper();
{
return SupervisorState.StartupFailed;
}
var node = Path.Combine(AppContext.BaseDirectory, "runtime", "node.exe"); var node = Path.Combine(AppContext.BaseDirectory, "runtime", "node.exe");
var apiEntry = Path.Combine(AppContext.BaseDirectory, "server", "api.mjs"); var apiEntry = Path.Combine(AppContext.BaseDirectory, "server", "api.mjs");
var workerEntry = Path.Combine(AppContext.BaseDirectory, "server", "worker.mjs"); var workerEntry = Path.Combine(AppContext.BaseDirectory, "server", "worker.mjs");
if (!File.Exists(node) || !File.Exists(apiEntry) || !File.Exists(workerEntry)) return SupervisorState.StartupFailed; if (!File.Exists(node) || !File.Exists(apiEntry) || !File.Exists(workerEntry)) return SupervisorState.StartupFailed;
try
{
api = CreateComponent(node, apiEntry, ChildRole.Api, SupervisorState.ApiDegraded); api = CreateComponent(node, apiEntry, ChildRole.Api, SupervisorState.ApiDegraded);
await api.StartAsync(cancellationToken); await api.StartAsync(cancellationToken);
@@ -51,6 +65,30 @@ internal sealed class SupervisorRuntime : IAsyncDisposable
await worker.StartAsync(cancellationToken); await worker.StartAsync(cancellationToken);
return TryLog(new StructuredLogEvent("ready", ErrorCategory: "none")) ? SupervisorState.Ready : SupervisorState.StorageUnavailable; return TryLog(new StructuredLogEvent("ready", ErrorCategory: "none")) ? SupervisorState.Ready : SupervisorState.StorageUnavailable;
} }
catch
{
await StopComponentsAsync();
return TryLog(new StructuredLogEvent("failed", ErrorCategory: "service_unavailable"))
? SupervisorState.StartupFailed
: SupervisorState.StorageUnavailable;
}
}
internal static void EnsureRuntimeDirectories(string dataRoot)
{
foreach (var directory in RequiredDataDirectories)
{
Directory.CreateDirectory(Path.Combine(dataRoot, directory));
}
}
private void EnsureAdminPepper()
{
if (credentials.IsConfigured(CredentialCatalog.AdminPepper)) return;
var pepper = Convert.ToBase64String(RandomNumberGenerator.GetBytes(32));
credentials.Write(CredentialCatalog.AdminPepper, pepper);
Array.Clear(System.Text.Encoding.UTF8.GetBytes(pepper));
}
private ManagedComponentSupervisor CreateComponent(string node, string entry, ChildRole role, SupervisorState degradedState) private ManagedComponentSupervisor CreateComponent(string node, string entry, ChildRole role, SupervisorState degradedState)
{ {
@@ -60,6 +98,7 @@ internal sealed class SupervisorRuntime : IAsyncDisposable
startInfo.WorkingDirectory = AppContext.BaseDirectory; startInfo.WorkingDirectory = AppContext.BaseDirectory;
startInfo.Environment["DADA_SQLITE_NATIVE_BINDING"] = Path.Combine(AppContext.BaseDirectory, "server", "native", "better_sqlite3.node"); startInfo.Environment["DADA_SQLITE_NATIVE_BINDING"] = Path.Combine(AppContext.BaseDirectory, "server", "native", "better_sqlite3.node");
startInfo.Environment["DADA_SUPPORT_GATE_ROOT"] = Path.Combine(AppContext.BaseDirectory, "web", "support-gate"); startInfo.Environment["DADA_SUPPORT_GATE_ROOT"] = Path.Combine(AppContext.BaseDirectory, "web", "support-gate");
startInfo.Environment["DADA_WEB_ROOT"] = Path.Combine(AppContext.BaseDirectory, "web");
startInfo.Environment["DADA_INSTANCE_CONFIG_PATH"] = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Dada", "P0A", "config", "instance.json"); startInfo.Environment["DADA_INSTANCE_CONFIG_PATH"] = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Dada", "P0A", "config", "instance.json");
startInfo.ArgumentList.Add(entry); startInfo.ArgumentList.Add(entry);
var child = await ManagedChildProcess.StartAsync(startInfo, role, credentials, cancellationToken); var child = await ManagedChildProcess.StartAsync(startInfo, role, credentials, cancellationToken);
@@ -95,10 +134,26 @@ internal sealed class SupervisorRuntime : IAsyncDisposable
} }
public async ValueTask DisposeAsync() public async ValueTask DisposeAsync()
{
await StopComponentsAsync();
}
private async Task StopComponentsAsync()
{ {
var stops = new List<Task>(); var stops = new List<Task>();
if (worker is not null) stops.Add(worker.DisposeAsync().AsTask()); if (worker is not null) stops.Add(worker.DisposeAsync().AsTask());
if (api is not null) stops.Add(api.DisposeAsync().AsTask()); if (api is not null) stops.Add(api.DisposeAsync().AsTask());
try
{
await Task.WhenAll(stops); await Task.WhenAll(stops);
} }
catch
{
}
finally
{
worker = null;
api = null;
}
}
} }
@@ -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");
});
});
@@ -0,0 +1,95 @@
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { createApp } from "../../apps/api/src/app.js";
import { RegistrationService } from "../../apps/api/src/registration.js";
import { MockResendAdapter } from "../../apps/api/src/resend-adapter.js";
const fixedNow = Date.parse("2026-08-05T06:00:00.000Z");
const writeHeaders = { host: "127.0.0.1:43121", origin: "http://127.0.0.1:43121" };
const roots: string[] = [];
const services: RegistrationService[] = [];
function createRegistrationService() {
const root = mkdtempSync(join(tmpdir(), "dada-local-test-session-"));
roots.push(root);
const registration = new RegistrationService({
challengePepper: Buffer.alloc(32, 0x51),
clock: () => fixedNow,
currentPrivacyNoticeVersion: "p0a-notice-v1",
databasePath: join(root, "dada.sqlite3"),
invitePepper: Buffer.alloc(32, 0x52),
resend: new MockResendAdapter(),
sessionPepper: Buffer.alloc(32, 0x53),
});
services.push(registration);
return registration;
}
afterEach(() => {
for (const service of services.splice(0)) {
try { service.close(); } catch { /* already closed by the test */ }
}
for (const root of roots.splice(0)) rmSync(root, { force: true, recursive: true });
});
describe("POSTV1-03 local test session", () => {
it("does not expose the local test route unless explicitly enabled", async () => {
const registration = createRegistrationService();
const app = await createApp({ browserGate: false, networkBoundary: { allowTestPort: true }, registration });
const status = await app.inject({ headers: writeHeaders, method: "GET", url: "/api/v1/auth/local-test" });
const created = await app.inject({ headers: writeHeaders, method: "POST", url: "/api/v1/auth/local-test" });
expect(status.statusCode).toBe(404);
expect(created.statusCode).toBe(404);
await app.close();
});
it("creates one isolated fixture account and restores it without duplicate credits", async () => {
const registration = createRegistrationService();
const app = await createApp({
browserGate: false,
localTestAuth: true,
networkBoundary: { allowTestPort: true },
registration,
});
const status = await app.inject({ headers: writeHeaders, method: "GET", url: "/api/v1/auth/local-test" });
expect(status.statusCode).toBe(200);
expect(status.json()).toEqual({ available: true });
const first = await app.inject({ headers: writeHeaders, method: "POST", url: "/api/v1/auth/local-test" });
expect(first.statusCode).toBe(200);
expect(first.json()).toMatchObject({
audience: "user",
credits: { available_balance: 10, reserved_balance: 0 },
status: "authenticated",
user: { creator_name: "本机测试用户", role: "user", social_id: "@dada_local_test", status: "active" },
});
expect(first.headers["set-cookie"]).toContain("dada_session=");
const session = await app.inject({
headers: { cookie: first.headers["set-cookie"], host: "127.0.0.1:43121" },
method: "GET",
url: "/api/v1/auth/session",
});
expect(session.statusCode).toBe(200);
expect(session.json()).toMatchObject({ authenticated: true, credits: { available_balance: 10 } });
const second = await app.inject({ headers: writeHeaders, method: "POST", url: "/api/v1/auth/local-test" });
expect(second.statusCode).toBe(200);
expect(second.json().user.user_id).toBe(first.json().user.user_id);
expect(registration.database.prepare("SELECT COUNT(*) AS count FROM users").get()).toEqual({ count: 1 });
expect(registration.database.prepare("SELECT COUNT(*) AS count FROM credit_ledger").get()).toEqual({ count: 1 });
expect(registration.database.prepare("SELECT counts_toward_stage_limit FROM users").get()).toEqual({ counts_toward_stage_limit: 0 });
const openapi = JSON.stringify(app.swagger());
expect(openapi).not.toContain("/api/v1/auth/local-test");
expect(openapi).not.toContain("local-test-user");
await app.close();
});
});
+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();
}
});
});
+14 -1
View File
@@ -125,7 +125,12 @@ afterAll(async () => {
describe("TDD-WP0-BRW-001 supported browser contract", () => { describe("TDD-WP0-BRW-001 supported browser contract", () => {
it("cross-checks UA-CH and issues only a short-lived signed support cookie", async () => { it("cross-checks UA-CH and issues only a short-lived signed support cookie", async () => {
const app = await createApp({ browserSupportRelease: testBrowserSupportRelease } as never); const app = await createApp({
browserSupportRelease: testBrowserSupportRelease,
productIndexHtml: "<!doctype html><title>Dada product test</title><div id=\"root\"></div>",
} as never);
const gate = await app.inject({ headers: { host: "127.0.0.1:43121" }, method: "GET", url: "/" });
expect(gate.body).toContain("当前浏览器无法使用 Dada");
const checked = await app.inject({ const checked = await app.inject({
headers: supportedEdge.headers, headers: supportedEdge.headers,
method: "POST", method: "POST",
@@ -150,6 +155,14 @@ describe("TDD-WP0-BRW-001 supported browser contract", () => {
const cookie = supportCookie(checked); const cookie = supportCookie(checked);
expect(cookie).toBeDefined(); expect(cookie).toBeDefined();
const productHtml = await app.inject({
headers: { cookie, host: "127.0.0.1:43121", "sec-ch-ua": supportedEdge.headers["sec-ch-ua"] },
method: "GET",
url: "/app",
});
expect(productHtml.statusCode).toBe(200);
expect(productHtml.body).toContain("Dada product test");
expect(productHtml.body).not.toContain("当前浏览器无法使用 Dada");
const product = await app.inject({ const product = await app.inject({
headers: { headers: {
cookie, cookie,
@@ -40,8 +40,23 @@ describe("TDD-WP7-EXT-003 production Amap adapter", () => {
const clients = initializeApiCredentialClients(credentials); const clients = initializeApiCredentialClients(credentials);
expect(clients.amap).toBeInstanceOf(RealAmapAdapter); expect(clients.amap).toBeInstanceOf(RealAmapAdapter);
expect(clients.resendConfigured).toBe(true);
expect(Object.values(credentials)).toEqual(["", "", ""]); expect(Object.values(credentials)).toEqual(["", "", ""]);
clients.amap.dispose(); clients.amap.dispose?.();
clients.adminAllowlistPepper.fill(0);
});
it("reports an empty Resend credential without retaining its value", () => {
const credentials = {
"Dada/P0A/admin/pepper": "fixture-admin-value",
"Dada/P0A/api/amap": "",
"Dada/P0A/api/resend": "",
};
const clients = initializeApiCredentialClients(credentials);
expect(clients.resendConfigured).toBe(false);
expect(Object.values(credentials)).toEqual(["", "", ""]);
clients.amap.dispose?.();
clients.adminAllowlistPepper.fill(0); clients.adminAllowlistPepper.fill(0);
}); });
}); });
+23
View File
@@ -23,6 +23,29 @@ test.beforeAll(async () => {
test.afterAll(async () => vite.close()); test.afterAll(async () => vite.close());
test("POSTV1-03 enters the workspace through the local test session", async ({ page }) => {
await page.route("**/api/v1/auth/local-test", (route) => {
if (route.request().method() === "GET") {
return route.fulfill({ contentType: "application/json", status: 200, body: JSON.stringify({ available: true }) });
}
expect(route.request().postData()).toBeNull();
return route.fulfill({ contentType: "application/json", status: 200, body: JSON.stringify({ status: "authenticated" }) });
});
await page.goto(webUrl);
const button = page.getByRole("button", { name: "直接进入本机测试" });
await expect(button).toBeVisible();
await button.click();
await expect(page).toHaveURL(`${webUrl}/app`);
});
test("POSTV1-03 hides the local test entry when the API does not enable it", async ({ page }) => {
await page.route("**/api/v1/auth/local-test", (route) => route.fulfill({ status: 404, body: "" }));
await page.goto(webUrl);
await expect(page.getByRole("button", { name: "直接进入本机测试" })).toHaveCount(0);
});
test("TDD-WP1-NOTICE-001 expands DVPM8 only after successful code delivery", async ({ page }) => { test("TDD-WP1-NOTICE-001 expands DVPM8 only after successful code delivery", async ({ page }) => {
await page.route("**/api/v1/auth/register/send", (route) => route.fulfill({ await page.route("**/api/v1/auth/register/send", (route) => route.fulfill({
contentType: "application/json", contentType: "application/json",
+167
View File
@@ -0,0 +1,167 @@
import test from "node:test";
import assert from "node:assert/strict";
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { existsSync } from "node:fs";
import { join, resolve } from "node:path";
import { tmpdir } from "node:os";
import { spawn } from "node:child_process";
import { createServer } from "node:net";
const packageRoot = resolve(process.env.DADA_POSTV1_PACKAGE_ROOT ?? ".build/portable-release/Dada-P0A-0.0.0-win-x64");
const port = 43121;
async function waitForHealth(child) {
const deadline = Date.now() + 15_000;
while (Date.now() < deadline) {
if (child.exitCode !== null) throw new Error(`packaged api exited: ${child.exitCode}: ${child.errorOutput ?? ""}`);
try {
const response = await fetch(`http://127.0.0.1:${port}/healthz`, { headers: { host: `127.0.0.1:${port}` } });
if (response.ok) return;
} catch {}
await new Promise((resolveDelay) => setTimeout(resolveDelay, 100));
}
throw new Error("packaged api health timeout");
}
function startApi(configPath, dataRoot) {
const child = spawn(join(packageRoot, "runtime", "node.exe"), [join(packageRoot, "server", "api.mjs"), "--dada-credential-stdin"], {
cwd: packageRoot,
env: { ...process.env, DADA_INSTANCE_CONFIG_PATH: configPath, DADA_SUPPORT_GATE_ROOT: join(packageRoot, "web", "support-gate"), DADA_WEB_ROOT: join(packageRoot, "web") },
stdio: ["pipe", "ignore", "pipe"],
windowsHide: true,
});
child.errorOutput = "";
child.stderr.setEncoding("utf8");
child.stderr.on("data", (chunk) => { child.errorOutput += chunk; });
child.stdin.end(JSON.stringify({ "Dada/P0A/api/amap": "", "Dada/P0A/api/resend": "", "Dada/P0A/admin/pepper": "portable-test-pepper-00000000000000000000000000000000" }));
return child;
}
async function stop(child) {
if (child.exitCode === null) {
child.kill();
await new Promise((resolveExit) => child.once("exit", resolveExit));
}
}
async function verifyWorkerStartup(configPath) {
const pipeName = `Dada.P0A.PostV1.${process.pid}.${Date.now()}`;
const pipePath = `\\\\.\\pipe\\${pipeName}`;
const server = createServer();
await new Promise((resolveListen, rejectListen) => {
server.once("error", rejectListen);
server.listen(pipePath, resolveListen);
});
const child = spawn(join(packageRoot, "runtime", "node.exe"), [
join(packageRoot, "server", "worker.mjs"),
"--dada-control-pipe", pipeName,
"--dada-credential-stdin",
], {
cwd: packageRoot,
env: {
...process.env,
DADA_INSTANCE_CONFIG_PATH: configPath,
DADA_SQLITE_NATIVE_BINDING: join(packageRoot, "server", "native", "better_sqlite3.node"),
},
stdio: ["pipe", "ignore", "ignore"],
windowsHide: true,
});
child.stdin.end(JSON.stringify({ "Dada/P0A/worker/ai-gateway": "synthetic-runtime-token" }));
try {
await new Promise((resolveReady, rejectReady) => {
let settled = false;
const finish = (error) => {
if (settled) return;
settled = true;
clearTimeout(deadline);
child.off("exit", onExit);
if (error) rejectReady(error); else resolveReady();
};
const deadline = setTimeout(() => finish(new Error("packaged worker ready timeout")), 15_000);
const onExit = (code) => finish(new Error(`packaged worker exited before ready: ${code}`));
child.once("exit", onExit);
server.once("connection", (connection) => {
connection.setEncoding("utf8");
let pending = "";
connection.on("data", (chunk) => {
pending += chunk;
while (pending.includes("\n")) {
const newline = pending.indexOf("\n");
const status = pending.slice(0, newline).trim();
pending = pending.slice(newline + 1);
if (status === "storage_unavailable") finish(new Error("packaged worker reported storage_unavailable"));
if (status === "ready") {
setTimeout(() => {
if (settled) return;
connection.write("shutdown\n");
finish();
}, 500);
}
}
});
});
});
const exitCode = await new Promise((resolveExit) => child.once("exit", resolveExit));
assert.equal(exitCode, 0);
} finally {
if (child.exitCode === null) child.kill();
await new Promise((resolveClose) => server.close(resolveClose));
}
}
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, "web", "index.html")));
const packagedWorker = await readFile(join(packageRoot, "server", "worker", "dist", "worker.js"), "utf8");
const packagedOneApiAdapter = await readFile(join(packageRoot, "server", "worker", "dist", "oneapi-generation-adapter.js"), "utf8");
assert.match(packagedWorker, /GenerationProcessor/);
assert.match(packagedWorker, /OneApiGenerationAdapter/);
assert.match(packagedOneApiAdapter, /oneapi\.intelligrow\.cn/);
assert.doesNotMatch(packagedWorker, /portable-mock-worker/);
const root = await mkdtemp(join(tmpdir(), "dada-postv1-"));
const dataRoot = join(root, "data");
const configPath = join(root, "instance.json");
await writeFile(configPath, JSON.stringify({ data_root: dataRoot, initialized: true, instance_id: "portable-test", schema_version: 1, secure_config_revision: 1, admin_allowlist_hashes: [], admin_recovery_hashes: [] }));
let api = startApi(configPath, dataRoot);
try {
await waitForHealth(api);
const initialPage = await fetch(`http://127.0.0.1:${port}/`, { headers: { host: `127.0.0.1:${port}` } });
assert.equal(initialPage.status, 200);
assert.match(await initialPage.text(), /当前浏览器无法使用 Dada/);
const support = await fetch(`http://127.0.0.1:${port}/api/v1/support/check`, {
method: "POST",
headers: { host: `127.0.0.1:${port}`, origin: `http://127.0.0.1:${port}`, "content-type": "application/json", "sec-ch-ua": '"Google Chrome";v="150"', "sec-ch-ua-full-version-list": '"Google Chrome";v="150.0.0.0"', "sec-ch-ua-platform": '"Windows"' },
body: JSON.stringify({ brands: [{ brand: "Google Chrome", version: "150" }], full_version_list: [{ brand: "Google Chrome", version: "150.0.0.0" }], platform: "Windows" }),
});
assert.equal(support.status, 200);
const cookie = support.headers.get("set-cookie")?.split(";", 1)[0];
assert.ok(cookie);
const page = await fetch(`http://127.0.0.1:${port}/app`, { headers: { host: `127.0.0.1:${port}`, cookie, "sec-ch-ua": '"Google Chrome";v="150"' } });
assert.equal(page.status, 200);
const pageHtml = await page.text();
assert.match(pageHtml, /<div id="root"><\/div>/);
const scriptPath = pageHtml.match(/<script[^>]+src="([^"]+)"/)?.[1];
const stylesheetPath = pageHtml.match(/<link[^>]+href="([^"]+)"/)?.[1];
assert.ok(scriptPath);
assert.ok(stylesheetPath);
const assetHeaders = { host: `127.0.0.1:${port}`, cookie, "sec-ch-ua": '"Google Chrome";v="150"' };
const script = await fetch(`http://127.0.0.1:${port}${scriptPath}`, { headers: assetHeaders });
const stylesheet = await fetch(`http://127.0.0.1:${port}${stylesheetPath}`, { headers: assetHeaders });
assert.equal(script.status, 200);
assert.match(script.headers.get("content-type") ?? "", /^(?:application|text)\/javascript\b/);
assert.equal(stylesheet.status, 200);
assert.match(stylesheet.headers.get("content-type") ?? "", /^text\/css\b/);
assert.ok(existsSync(join(dataRoot, "db", "dada.sqlite3")));
await verifyWorkerStartup(configPath);
} finally {
await stop(api);
}
api = startApi(configPath, dataRoot);
try {
await waitForHealth(api);
assert.ok(existsSync(join(dataRoot, "db", "dada.sqlite3")));
} finally {
await stop(api);
await rm(root, { recursive: true, force: true });
}
});
@@ -0,0 +1,47 @@
import assert from "node:assert/strict";
import { mkdtempSync, mkdirSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { test } from "node:test";
import { buildFinalReleaseRecord, scanReleaseFiles, validateFinalEvidence } from "../../scripts/lib/wp7-07-final-release.mjs";
function release() {
return buildFinalReleaseRecord({
appVersion: "0.0.0",
browsers: [
{ brand: "Google Chrome", fullVersion: "150.0.7871.187" },
{ brand: "Microsoft Edge", fullVersion: "151.0.4129.59" },
],
buildCommit: "a".repeat(40),
frozenFromCommit: "b".repeat(40),
recordedAt: "2026-08-04T06:00:00.000Z",
windows: { arch: "x64", build: "26200.8875", displayVersion: "25H2" },
});
}
test("TDD-WP7-REL-001 creates a browser-gate compatible first-version record", () => {
const record = release();
assert.equal(record.finalRelease, true);
assert.equal(record.fixedPort, 43121);
assert.deepEqual(record.deferredExternalTasks, ["TASK-WP7-03", "TASK-WP7-04"]);
assert.deepEqual(record.browsers.map(({ brand }) => brand).sort(), ["Google Chrome", "Microsoft Edge"]);
});
test("TDD-WP7-SEC-001 rejects credential shapes and absolute user paths", () => {
const root = mkdtempSync(join(tmpdir(), "dada-wp7-07-scan-"));
mkdirSync(join(root, "logs"));
writeFileSync(join(root, "logs", "diagnostic.txt"), "credential=key-abcdefghijklmnop C:\\Users\\person\\private.txt\n");
const scan = scanReleaseFiles({ roots: [root] });
assert.equal(scan.status, "failed");
assert.deepEqual(new Set(scan.findings.map(({ rule }) => rule)), new Set(["absolute_user_path", "credential_shape"]));
});
test("TDD-WP7-REL-001 binds release and package hashes only after a zero-finding scan", () => {
assert.deepEqual(validateFinalEvidence({
packageManifest: { release_status: "first_version_internal", zip_sha256: "C".repeat(64) },
release: release(),
releaseSha256: "D".repeat(64),
scan: { findings: [], status: "passed" },
}), { release_sha256: "D".repeat(64), status: "passed", zip_sha256: "C".repeat(64) });
});
@@ -0,0 +1,85 @@
import { describe, expect, it, vi } from "vitest";
import type { GenerationAdapterRequest } from "../../apps/worker/src/ai-adapter-contract.js";
import { runAiRuntimeProbe } from "../../apps/worker/src/ai-runtime-probe.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",
});
});
it("returns only a bounded probe summary and wipes generated bytes", async () => {
const bytes = Buffer.from("probe-output");
const result = await runAiRuntimeProbe({
async start() {
return { outputs: [{ bytes, mimeType: "image/png", pixelHeight: 1080, pixelWidth: 1080 }], status: "completed" };
},
});
expect(result).toEqual({
code: "ai_probe_passed",
mime_type: "image/png",
pixel_height: 1080,
pixel_width: 1080,
real_calls: 1,
success: true,
});
expect(bytes.every((value) => value === 0)).toBe(true);
});
});