fix(POSTV1-06): 恢复贴纸与字体运行时资源链路
Dada P0-A isolated Windows CI / validate-and-package (push) Waiting to run

This commit is contained in:
suyx
2026-08-05 16:58:45 +08:00
parent 7de633d4c9
commit d9e39702e0
16 changed files with 12179 additions and 10 deletions
+1 -1
View File
@@ -16,7 +16,7 @@ const forbiddenDiagnosticPatterns = [
/https?:\/\//i,
];
const safePauseReasons = new Set([
"asset_root_state_missing", "balance_insufficient", "configured_disabled", "contract_blocked",
"asset_manifest_invalid", "asset_root_missing", "asset_root_state_missing", "balance_insufficient", "configured_disabled", "contract_blocked",
"contract_unverified", "gateway_balance_insufficient", "gateway_paused", "health_check_failed",
"model_disabled", "provider_unavailable", "quota_exhausted", "service_state_missing", "unknown",
"worker_degraded", "worker_state_missing", "worker_stopped",
+15 -6
View File
@@ -19,7 +19,7 @@ import { dirname, isAbsolute, join, parse, relative, resolve, sep } from "node:p
const require = createRequire(import.meta.url);
const Database = require("better-sqlite3") as typeof import("better-sqlite3");
const assetIdPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
const assetIdPattern = /^[a-z0-9][a-z0-9_-]{2,119}$/i;
const fixedDirectories = [
"db",
"content/references",
@@ -82,13 +82,21 @@ export function readConfiguredLocalDataRoot(configFile = defaultInstanceConfigPa
return resolve(candidate);
}
export function readConfiguredAssetRoot(configFile = defaultInstanceConfigPath()) {
const configuration = JSON.parse(readFileSync(configFile, "utf8")) as Record<string, unknown>;
if (typeof configuration.asset_root !== "string" || !isAbsolute(configuration.asset_root)) {
throw new Error("asset_root_configuration_invalid");
}
return resolve(configuration.asset_root);
}
export interface ValidatedReadOnlyAssetRoot {
absolute_root: string;
ok: true;
root_ref: string;
}
interface PublicAssetEntry {
export interface PublicAssetEntry {
assetId: string;
mimeType: string;
relativePath: string;
@@ -317,18 +325,19 @@ export function createPublicAssetResolver(input: {
const roots = new Map(input.roots.map((root) => [root.root_ref, root.absolute_root]));
const entries = new Map<string, PublicAssetEntry>();
for (const entry of input.entries) {
if (!assetIdPattern.test(entry.assetId) || entries.has(entry.assetId)) throw new Error("asset_id_invalid");
const key = `${entry.resourceVersion}\u0000${entry.assetId}`;
if (!assetIdPattern.test(entry.assetId) || entries.has(key)) throw new Error("asset_id_invalid");
if (!roots.has(entry.rootRef)) throw new Error("asset_root_unvalidated");
if (!/^[a-z0-9][a-z0-9._-]{0,79}$/i.test(entry.resourceVersion)) throw new Error("resource_version_invalid");
if (!/^[a-z0-9][a-z0-9.+-]*\/[a-z0-9][a-z0-9.+-]*$/i.test(entry.mimeType)) throw new Error("mime_type_invalid");
entries.set(entry.assetId, { ...entry });
entries.set(key, { ...entry });
}
return {
read(resourceVersion, assetId) {
if (!assetIdPattern.test(assetId)) return undefined;
const entry = entries.get(assetId);
if (!entry || entry.resourceVersion !== resourceVersion) return undefined;
const entry = entries.get(`${resourceVersion}\u0000${assetId}`);
if (!entry) return undefined;
const root = roots.get(entry.rootRef);
if (!root) return undefined;
let path: string;
+13 -1
View File
@@ -5,7 +5,7 @@ import { registrationNotice } from "@dada/shared-contracts";
import { createApp } from "./app.js";
import { readBrowserSupportRelease } from "./browser-support.js";
import { defaultInstanceConfigPath, ensureLocalDataRuntimeDirectories, readConfiguredLocalDataRoot } from "./local-data-root.js";
import { defaultInstanceConfigPath, ensureLocalDataRuntimeDirectories, readConfiguredLocalDataRoot, type PublicAssetResolver } from "./local-data-root.js";
import { ManagedStorage } from "./managed-storage.js";
import { LatestExportService } from "./latest-exports.js";
import { CreditService } from "./credits.js";
@@ -25,6 +25,7 @@ import {
import { MockAmapAdapter, type AmapAdapter } from "./amap-adapter.js";
import { StickerReleaseService } from "./sticker-releases.js";
import { createAdminDiagnosticsProvider, createAdminServicesStorageProvider } from "./admin-state.js";
import { loadConfiguredRuntimeAssets, type RuntimeAssetState } from "./runtime-assets.js";
const credentialChannelEnabled = process.argv.includes("--dada-credential-stdin");
let registration: RegistrationService | undefined;
@@ -36,6 +37,8 @@ let models: ModelConfigurationService | undefined;
let generations: GenerationSubmissionService | undefined;
let recentAssets: RecentAssetService | undefined;
let stickers: StickerReleaseService | undefined;
let publicAssets: PublicAssetResolver | undefined;
let assetRootState: RuntimeAssetState | undefined;
let amap: AmapAdapter = new MockAmapAdapter();
let localTestAuth = false;
const instanceConfigPath = process.env.DADA_INSTANCE_CONFIG_PATH ?? defaultInstanceConfigPath();
@@ -49,6 +52,13 @@ if (credentialChannelEnabled) {
.digest();
const dataRoot = readConfiguredLocalDataRoot(instanceConfigPath);
ensureLocalDataRuntimeDirectories(dataRoot);
const runtimeAssets = loadConfiguredRuntimeAssets({
configFile: instanceConfigPath,
dataRoot,
trustedManifestPath: resolve("asset-metadata", "manifest.json"),
});
publicAssets = runtimeAssets.publicAssets;
assetRootState = runtimeAssets.state;
const databasePath = join(dataRoot, "db", "dada.sqlite3");
registration = new RegistrationService({
adminAllowlistPepper: Buffer.from(clients.adminAllowlistPepper),
@@ -101,6 +111,7 @@ const adminServicesStorage = registration
database: registration.database,
...(models ? { models } : {}),
...(storage ? { storage } : {}),
...(assetRootState ? { assetRoot: assetRootState } : {}),
})
: undefined;
const adminDiagnostics = adminServicesStorage
@@ -120,6 +131,7 @@ const app = await createApp({
...(registration && localTestAuth ? { localTestAuth: true } : {}),
...(models ? { models } : {}),
...(projects ? { projects } : {}),
...(publicAssets ? { publicAssets } : {}),
...(registration ? { registration } : {}),
...(recentAssets ? { recentAssets } : {}),
...(stickers ? { stickers } : {}),
+93
View File
@@ -0,0 +1,93 @@
import { createHash } from "node:crypto";
import { existsSync, readFileSync } from "node:fs";
import {
createPublicAssetResolver,
readConfiguredAssetRoot,
validateReadOnlyAssetRoot,
type PublicAssetEntry,
type PublicAssetResolver,
} from "./local-data-root.js";
const rootRef = "p0a_runtime_assets";
const schemaVersion = "DadaRuntimeAssets/v1";
const assetIdPattern = /^[a-z0-9][a-z0-9_-]{2,119}$/i;
const releasePattern = /^[a-z0-9][a-z0-9._-]{0,79}$/i;
const shaPattern = /^[a-f0-9]{64}$/i;
export interface RuntimeAssetState {
checked_at: string;
configured: boolean;
pause_reason: "asset_manifest_invalid" | "asset_root_missing" | "asset_root_state_missing" | null;
status: "active" | "unavailable";
}
export interface LoadedRuntimeAssets {
publicAssets?: PublicAssetResolver;
state: RuntimeAssetState;
}
function parseRuntimeManifest(bytes: Buffer): PublicAssetEntry[] {
const value = JSON.parse(bytes.toString("utf8")) as Record<string, unknown>;
if (value.schema_version !== schemaVersion || value.source !== "external_read_only" || value.root_ref !== rootRef) {
throw new Error("runtime_asset_manifest_invalid");
}
if (!Array.isArray(value.entries) || value.entries.length === 0) throw new Error("runtime_asset_manifest_invalid");
return value.entries.map((candidate) => {
if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) throw new Error("runtime_asset_manifest_invalid");
const entry = candidate as Record<string, unknown>;
if (
typeof entry.assetId !== "string" || !assetIdPattern.test(entry.assetId)
|| typeof entry.mimeType !== "string" || !/^[a-z0-9][a-z0-9.+-]*\/[a-z0-9][a-z0-9.+-]*$/i.test(entry.mimeType)
|| typeof entry.relativePath !== "string" || entry.relativePath.includes("\\") || entry.relativePath.split("/").includes("..")
|| typeof entry.resourceVersion !== "string" || !releasePattern.test(entry.resourceVersion)
|| entry.rootRef !== rootRef
|| typeof entry.sha256 !== "string" || !shaPattern.test(entry.sha256)
) throw new Error("runtime_asset_manifest_invalid");
return entry as unknown as PublicAssetEntry;
});
}
function unavailable(
configured: boolean,
pauseReason: Exclude<RuntimeAssetState["pause_reason"], null>,
checkedAt: string,
): LoadedRuntimeAssets {
return { state: { checked_at: checkedAt, configured, pause_reason: pauseReason, status: "unavailable" } };
}
export function loadConfiguredRuntimeAssets(input: {
configFile: string;
dataRoot: string;
trustedManifestPath: string;
clock?: () => number;
}): LoadedRuntimeAssets {
const checkedAt = new Date((input.clock ?? Date.now)()).toISOString();
let assetRoot: string;
try {
assetRoot = readConfiguredAssetRoot(input.configFile);
} catch {
return unavailable(false, "asset_root_state_missing", checkedAt);
}
if (!existsSync(input.trustedManifestPath)) return unavailable(true, "asset_manifest_invalid", checkedAt);
try {
const trustedBytes = readFileSync(input.trustedManifestPath);
const entries = parseRuntimeManifest(trustedBytes);
const validatedRoot = validateReadOnlyAssetRoot({
dataRoot: input.dataRoot,
expectedSha256: createHash("sha256").update(trustedBytes).digest("hex"),
manifestRelativePath: "manifest.json",
root: assetRoot,
rootRef,
});
if (!validatedRoot.ok) {
return unavailable(true, validatedRoot.reason === "asset_root_missing" ? "asset_root_missing" : "asset_manifest_invalid", checkedAt);
}
return {
publicAssets: createPublicAssetResolver({ entries, roots: [validatedRoot] }),
state: { checked_at: checkedAt, configured: true, pause_reason: null, status: "active" },
};
} catch {
return unavailable(true, "asset_manifest_invalid", checkedAt);
}
}