flnxi-Wu/lib/license/premium-module-service.ts
2026-06-03 18:41:52 +08:00

2498 lines
78 KiB
TypeScript

import { prisma } from '@/lib/prisma';
import { decryptLocal, encryptLocal, hashData, verifySignature } from './crypto-service';
import { generateFingerprint } from './fingerprint-service';
import { buildSignedLicenseRequest } from './request-signature';
import { buildLicenseServerLicenseApiUrl, getLicenseProjectCode, getLicenseServerUrl } from './server-config';
import { getLocalLicense, isLicenseConfigExpired } from './license-service';
import type { LicenseType } from './types';
import type {
FormattedUploadTemplateField,
UploadTemplateSaveValidationResult,
} from '../upload-template-save-validation';
export interface PremiumModuleManifest {
version: number;
licenseType: LicenseType;
instanceId: string;
modules: Array<{
id: string;
minLicenseType: LicenseType;
artifact: {
type: 'registry-image';
image: string;
digest?: string | null;
version: string;
};
sizeBytes: number;
sha256: string;
entitlementVersion?: number;
limits?: {
maxUsers: number;
maxMaterials: number;
};
}>;
issuedAt: string;
expiresAt: string;
}
interface SignedPremiumModuleManifest {
success: boolean;
manifest?: PremiumModuleManifest;
signature?: string;
error?: string;
}
interface CachedPremiumModuleManifest {
manifest: PremiumModuleManifest;
signature: string;
syncedAt: string;
}
interface PremiumModuleEntitlementResponse {
success: boolean;
entitlement?: {
moduleId: string;
entitlementBase64: string;
sha256: string;
sizeBytes: number;
expiresAt: string;
artifact: PremiumModuleManifest['modules'][number]['artifact'];
};
error?: string;
}
export interface PremiumModulePullCredential {
registryHost: string;
repository: string;
tag: string;
digest: string | null;
username: string;
pullToken: string;
expiresAt: string;
}
interface PremiumModulePullCredentialResponse {
success: boolean;
pullCredential?: PremiumModulePullCredential;
error?: string;
}
export type PremiumModuleRuntimeStatusValue =
| 'missing'
| 'pulling'
| 'running'
| 'unhealthy'
| 'expired'
| 'revoked';
export interface PremiumModuleRuntimeStatus {
moduleId: string;
image: string;
digest: string | null;
status: PremiumModuleRuntimeStatusValue;
lastSyncAt: string;
lastError?: string;
}
interface CachedPremiumModuleEntitlements {
entitlements: Record<
string,
{
entitlementBase64: string;
sha256: string;
sizeBytes: number;
expiresAt: string;
artifact: PremiumModuleManifest['modules'][number]['artifact'];
pullCredential?: PremiumModulePullCredential;
syncedAt: string;
}
>;
}
const PREMIUM_MODULE_CONFIG_KEY = 'premium_module_manifest';
const PREMIUM_MODULE_ENTITLEMENTS_CONFIG_KEY = 'premium_module_entitlements';
const PREMIUM_MODULE_RUNTIME_STATUS_CONFIG_KEY = 'premium_module_runtime_status';
const PREMIUM_MODULE_PULL_USERNAME = 'flnxi-module';
const PREMIUM_MODULE_HEALTH_TIMEOUT_MS = 3000;
export const PREMIUM_MODULE_IDS = {
PAID_CORE: 'paid-core',
} as const;
export interface PremiumModuleRuntimeGateResult {
available: boolean;
moduleId: string;
status?: PremiumModuleRuntimeStatusValue;
message?: string;
}
export interface PremiumModuleAuthorizeResult extends PremiumModuleRuntimeGateResult {
action: string;
}
type PremiumModuleActionContext = Record<string, unknown>;
export type TemplateStructureChange =
| {
type: 'materialTypeChanged';
before: string;
after: string;
}
| {
type: 'materialKindChanged';
before: string;
after: string;
}
| {
type: 'fieldDeleted';
fieldId: string;
fieldKey: string;
fieldLabel: string;
}
| {
type: 'fieldKeyChanged';
fieldId: string;
before: string;
after: string;
fieldLabel: string;
}
| {
type: 'fieldTypeChanged';
fieldId: string;
fieldKey: string;
fieldLabel: string;
before: string;
after: string;
}
| {
type: 'fieldCategoryDisabled';
fieldId: string;
fieldKey: string;
fieldLabel: string;
}
| {
type: 'fieldSearchDisabled';
fieldId: string;
fieldKey: string;
fieldLabel: string;
}
| {
type: 'fieldMadeReadOnly';
fieldId: string;
fieldKey: string;
fieldLabel: string;
}
| {
type: 'fieldMadeRequired';
fieldId: string;
fieldKey: string;
fieldLabel: string;
}
| {
type: 'fieldValidationChanged';
fieldId: string;
fieldKey: string;
fieldLabel: string;
}
| {
type: 'fieldOptionsRemoved';
fieldId: string;
fieldKey: string;
fieldLabel: string;
removedOptions: string[];
}
| {
type: 'fieldConditionsChanged';
fieldId: string;
fieldKey: string;
fieldLabel: string;
};
export type TemplateStructureChangeDetectionResult =
| { valid: true; changes: TemplateStructureChange[] }
| {
valid: false;
error: string;
field: string;
details?: Record<string, unknown>;
};
export type UploadTemplateSaveMode = 'create' | 'update';
export type NormalizedUploadTemplateSaveResult =
| {
valid: true;
mode: UploadTemplateSaveMode;
fieldsProvided: boolean;
template: {
name?: string;
description?: unknown;
status?: string;
materialType?: string;
materialKind?: string;
fieldGroups?: unknown;
displayConfig?: unknown;
fields?: FormattedUploadTemplateField[];
};
}
| {
valid: false;
error: string;
field: string;
details?: Record<string, unknown>;
};
export type UploadTemplateDeletePlanResult =
| {
valid: true;
allowed: true;
templateName: string;
materialCount: number;
}
| {
valid: true;
allowed: false;
error: string;
errorCode: string;
details?: Record<string, unknown>;
}
| {
valid: false;
allowed: false;
error: string;
errorCode: string;
details?: Record<string, unknown>;
};
export type UploadTemplateUpdatePlanResult =
| {
valid: true;
updateData: Record<string, unknown>;
fieldsProvided: boolean;
fields: FormattedUploadTemplateField[] | null;
nextVersion: number;
}
| {
valid: false;
error: string;
errorCode: string;
field?: string;
statusCode?: number;
details?: Record<string, unknown>;
};
type TemplateFieldForSubmittedValidation = {
key: string;
label?: string | null;
type?: string | null;
required?: boolean | null;
conditions?: string | null;
validations?: string | null;
};
export type SubmittedTemplateFieldsValidationResult =
| {
valid: true;
templateName?: string;
templateFields: Record<string, unknown>;
visibleFieldKeys: string[];
}
| {
valid: false;
error: string;
errorCode: string;
details?: Record<string, unknown>;
};
export type MaterialMetadataPrepareResult =
| {
valid: true;
standardData: Record<string, string>;
metadata: string | null;
metadataSize: {
ok: true;
size: number;
maxSize: number;
};
}
| {
valid: false;
error: string;
errorCode: string;
details?: Record<string, unknown>;
};
export type TemplateMigrationMetadataPrepareResult =
| {
valid: true;
standardData: Record<string, string | null>;
metadata: string | null;
metadataSize: {
ok: true;
size: number;
maxSize: number;
};
templateFields: Record<string, unknown>;
droppedTemplateFieldCount: number;
droppedStandardFieldCount: number;
droppedTemplateFields: Record<string, unknown>;
droppedStandardFields: Record<string, unknown>;
}
| {
valid: false;
error: string;
errorCode: string;
details?: Record<string, unknown>;
};
export type TemplateMigrationPreviewBuildResult =
| {
valid: true;
migrationBackup: {
materialsWithDroppedFields: number;
droppedTemplateFieldCount: number;
droppedStandardFieldCount: number;
};
migrationPreview: {
totalMaterialCount: number;
totalMaterials: number;
previewLimited: boolean;
previewLimit?: number;
targetCurrentFields: Array<{
key: string;
label: string;
type?: string;
required: boolean;
}>;
backupFields: {
templateFields: Array<{
key: string;
label: string;
type?: string;
source: 'template';
affectedCount: number;
}>;
standardFields: Array<{
key: string;
label: string;
type?: string;
source: 'standard';
affectedCount: number;
}>;
};
samples: Array<{
materialId: string;
productModel: string | null;
droppedTemplateFields: Array<{
key: string;
label: string;
type?: string;
source: 'template';
}>;
droppedStandardFields: Array<{
key: string;
label: string;
type?: string;
source: 'standard';
}>;
}>;
};
}
| {
valid: false;
error: string;
errorCode: string;
details?: Record<string, unknown>;
};
export type TemplateMigrationBatchPlanResult =
| {
valid: true;
allowed: true;
sourceTemplateId: string;
targetTemplateId: string;
sourceTemplateName: string;
targetTemplateName: string;
totalMaterialCount: number;
previewOnly: boolean;
forceLargeMigration: boolean;
updateMaterialType: boolean;
migrationLimit: number;
safeLimit: number;
maxBatchLimit: number;
batchLimited: boolean;
materialTake: number | null;
migratedAt: string;
}
| {
valid: true;
allowed: false;
statusCode: number;
error?: string;
errorCode?: string;
response?: Record<string, unknown>;
details?: Record<string, unknown>;
}
| {
valid: false;
allowed: false;
error: string;
errorCode: string;
statusCode?: number;
details?: Record<string, unknown>;
};
export type PublicTemplateOptionsProjectionResult =
| {
valid: true;
templates: Array<Record<string, unknown>>;
uploadTemplates: Array<Record<string, unknown>>;
materialTypes: string[];
}
| {
valid: false;
error: string;
errorCode: string;
details?: Record<string, unknown>;
};
export type TemplateFieldStatsCalculationResult =
| {
valid: true;
fieldStats: null | {
totalMaterials: number;
fields: Array<{
fieldKey: string;
fieldLabel: string;
fieldType: string;
filledCount: number;
totalCount: number;
fillRate: number;
isLowFillRate: boolean;
}>;
lowFillRateFields: Array<{
fieldKey: string;
fieldLabel: string;
fieldType: string;
filledCount: number;
totalCount: number;
fillRate: number;
isLowFillRate: boolean;
}>;
};
}
| {
valid: false;
error: string;
errorCode: string;
details?: Record<string, unknown>;
};
export type MaterialCustomSearchPlanResult =
| {
valid: true;
search: string;
customFieldKeys: string[];
}
| {
valid: false;
error: string;
errorCode: string;
details?: Record<string, unknown>;
};
export type MaterialListQueryPlanResult =
| {
valid: true;
search: string;
standardSearchFields: string[];
customSearchFieldKeys: string[];
standardHomeFilters: Array<{ key: string; value: string }>;
customHomeFilters: Array<{ key: string; value: string }>;
moduleAvailable: boolean;
}
| {
valid: false;
error: string;
errorCode: string;
details?: Record<string, unknown>;
};
export type HomeFilterCustomFieldCountsPlanResult =
| {
valid: true;
customFieldKeys: string[];
}
| {
valid: false;
error: string;
errorCode: string;
details?: Record<string, unknown>;
};
export type HomeFilterFieldCountsPlanResult =
| {
valid: true;
fieldPlans: Array<{
key: string;
source: 'column' | 'metadata';
}>;
moduleAvailable: boolean;
}
| {
valid: false;
error: string;
errorCode: string;
details?: Record<string, unknown>;
};
export type DepartmentScopePlanResult =
| {
valid: true;
scopedDepartments: string[];
withinScope: boolean;
}
| {
valid: false;
error: string;
errorCode: string;
details?: Record<string, unknown>;
};
export type DepartmentStructureChangePlanResult =
| {
valid: true;
allowed: true;
action: 'create' | 'rename' | 'delete';
departmentOps: Array<Record<string, unknown>>;
userOps: Array<Record<string, unknown>>;
summary?: Record<string, unknown>;
}
| {
valid: true;
allowed: false;
statusCode: number;
error?: string;
errorCode?: string;
response?: Record<string, unknown>;
details?: Record<string, unknown>;
}
| {
valid: false;
allowed: false;
error: string;
errorCode: string;
statusCode?: number;
details?: Record<string, unknown>;
};
function isManifestExpired(manifest: PremiumModuleManifest): boolean {
const expiresAt = new Date(manifest.expiresAt).getTime();
return Number.isNaN(expiresAt) || expiresAt <= Date.now();
}
function verifyPremiumModuleManifest(manifest: PremiumModuleManifest, signature: string): boolean {
return verifySignature(JSON.stringify(manifest), signature);
}
async function savePremiumModuleManifest(
manifest: PremiumModuleManifest,
signature: string
): Promise<void> {
const cached: CachedPremiumModuleManifest = {
manifest,
signature,
syncedAt: new Date().toISOString(),
};
await prisma.systemConfig.upsert({
where: { key: PREMIUM_MODULE_CONFIG_KEY },
update: { value: encryptLocal(JSON.stringify(cached)) },
create: { key: PREMIUM_MODULE_CONFIG_KEY, value: encryptLocal(JSON.stringify(cached)) },
});
}
async function getCachedPremiumModuleEntitlements(): Promise<CachedPremiumModuleEntitlements> {
try {
const config = await prisma.systemConfig.findUnique({
where: { key: PREMIUM_MODULE_ENTITLEMENTS_CONFIG_KEY },
});
if (!config) return { entitlements: {} };
const cached = JSON.parse(decryptLocal(config.value)) as CachedPremiumModuleEntitlements;
if (!cached.entitlements || typeof cached.entitlements !== 'object') return { entitlements: {} };
return cached;
} catch (error) {
console.warn('[License] 读取授权模块凭证缓存失败:', error);
return { entitlements: {} };
}
}
async function savePremiumModuleEntitlements(cached: CachedPremiumModuleEntitlements): Promise<void> {
await prisma.systemConfig.upsert({
where: { key: PREMIUM_MODULE_ENTITLEMENTS_CONFIG_KEY },
update: { value: encryptLocal(JSON.stringify(cached)) },
create: { key: PREMIUM_MODULE_ENTITLEMENTS_CONFIG_KEY, value: encryptLocal(JSON.stringify(cached)) },
});
}
export async function getCachedPremiumModuleManifest(): Promise<CachedPremiumModuleManifest | null> {
try {
const config = await prisma.systemConfig.findUnique({
where: { key: PREMIUM_MODULE_CONFIG_KEY },
});
if (!config) return null;
const cached = JSON.parse(decryptLocal(config.value)) as CachedPremiumModuleManifest;
if (!cached.manifest || !cached.signature) return null;
if (isManifestExpired(cached.manifest)) return null;
if (!verifyPremiumModuleManifest(cached.manifest, cached.signature)) return null;
const localLicense = await getLocalLicense();
if (!localLicense || isLicenseConfigExpired(localLicense.cachedConfig)) return null;
if (cached.manifest.licenseType !== localLicense.cachedConfig.type) return null;
if (!cached.manifest.instanceId || cached.manifest.instanceId !== localLicense.instanceId) return null;
return cached;
} catch (error) {
console.warn('[License] 读取授权模块缓存失败:', error);
return null;
}
}
function buildPremiumModuleDownloadUrl(moduleId: string): string {
const baseUrl = getLicenseServerUrl();
const projectCode = encodeURIComponent(getLicenseProjectCode());
return `${baseUrl}/api/license/${projectCode}/premium-modules/${encodeURIComponent(moduleId)}`;
}
function buildPremiumModulePullTokenUrl(moduleId: string): string {
return `${buildPremiumModuleDownloadUrl(moduleId)}/pull-token`;
}
function parseModuleImage(image: string): { registryHost: string; repository: string; tag: string } | null {
const [registryHost, ...pathParts] = image.split('/');
if (!registryHost || pathParts.length === 0) return null;
const imagePath = pathParts.join('/');
const lastSlashIndex = imagePath.lastIndexOf('/');
const tagSeparatorIndex = imagePath.lastIndexOf(':');
if (tagSeparatorIndex <= lastSlashIndex) return null;
return {
registryHost,
repository: imagePath.slice(0, tagSeparatorIndex),
tag: imagePath.slice(tagSeparatorIndex + 1),
};
}
function isSameArtifact(
left: PremiumModuleManifest['modules'][number]['artifact'] | null | undefined,
right: PremiumModuleManifest['modules'][number]['artifact'] | null | undefined
): boolean {
if (!left || !right) return false;
return (
left.type === right.type &&
left.image === right.image &&
(left.digest || null) === (right.digest || null) &&
left.version === right.version
);
}
function isCachedEntitlementValid(
cached: CachedPremiumModuleEntitlements,
module: PremiumModuleManifest['modules'][number]
): boolean {
const entitlement = cached.entitlements[module.id];
if (!entitlement) return false;
if (!isSameArtifact(entitlement.artifact, module.artifact)) return false;
if (entitlement.sha256 !== module.sha256) return false;
if (entitlement.sizeBytes !== module.sizeBytes) return false;
if (hashData(entitlement.entitlementBase64) !== module.sha256) return false;
const entitlementExpiresAt = new Date(entitlement.expiresAt).getTime();
return !Number.isNaN(entitlementExpiresAt) && entitlementExpiresAt > Date.now();
}
function isPullCredentialValid(
credential: PremiumModulePullCredential | null | undefined,
module: PremiumModuleManifest['modules'][number]
): credential is PremiumModulePullCredential {
if (!credential) return false;
if (credential.username !== PREMIUM_MODULE_PULL_USERNAME) return false;
const imageParts = parseModuleImage(module.artifact.image);
if (!imageParts) return false;
if (credential.registryHost !== imageParts.registryHost) return false;
if (credential.repository !== imageParts.repository) return false;
if (credential.tag !== imageParts.tag) return false;
if ((credential.digest || null) !== (module.artifact.digest || null)) return false;
if (!credential.pullToken || typeof credential.pullToken !== 'string') return false;
const expiresAt = new Date(credential.expiresAt).getTime();
return !Number.isNaN(expiresAt) && expiresAt > Date.now();
}
function getPremiumModuleServiceBaseUrl(moduleId: string): string {
const envByModuleId: Record<string, string | undefined> = {
[PREMIUM_MODULE_IDS.PAID_CORE]: process.env.PAID_CORE_MODULE_URL,
};
return (envByModuleId[moduleId] || '').trim().replace(/\/+$/, '');
}
function isRuntimeStatusForModule(
status: PremiumModuleRuntimeStatus | null | undefined,
module: PremiumModuleManifest['modules'][number]
): boolean {
if (!status) return false;
if (status.moduleId !== module.id) return false;
if (status.status !== 'running') return false;
if (status.image !== module.artifact.image) return false;
if ((status.digest || null) !== (module.artifact.digest || null)) return false;
return true;
}
async function fetchModuleJson(
baseUrl: string,
path: string,
init: RequestInit = {}
): Promise<{ ok: boolean; status: number; data: any }> {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), PREMIUM_MODULE_HEALTH_TIMEOUT_MS);
try {
const response = await fetch(`${baseUrl}${path}`, {
...init,
signal: controller.signal,
});
const data = await response.json().catch(() => null);
return { ok: response.ok, status: response.status, data };
} finally {
clearTimeout(timer);
}
}
async function checkPremiumModuleHealth(moduleId: string, baseUrl: string): Promise<boolean> {
try {
const result = await fetchModuleJson(baseUrl, '/health', { method: 'GET' });
return result.ok && result.data?.moduleId === moduleId && result.data?.status === 'healthy';
} catch (error) {
console.warn(`[License] 授权模块 ${moduleId} 健康检查失败:`, error);
return false;
}
}
async function getPremiumModuleInfo(
moduleId: string
): Promise<PremiumModuleManifest['modules'][number] | null> {
const cached = await getCachedPremiumModuleManifest();
if (!cached) return null;
return cached.manifest.modules.find((item) => item.id === moduleId) || null;
}
async function hasValidPremiumModuleEntitlement(
module: PremiumModuleManifest['modules'][number]
): Promise<boolean> {
const cachedEntitlements = await getCachedPremiumModuleEntitlements();
return isCachedEntitlementValid(cachedEntitlements, module);
}
async function downloadPremiumModuleEntitlement(
module: PremiumModuleManifest['modules'][number],
fingerprint: Awaited<ReturnType<typeof generateFingerprint>>,
manifestExpiresAt: string
): Promise<PremiumModuleEntitlementResponse['entitlement']> {
const signedRequest = buildSignedLicenseRequest('premium-module-download', {
moduleId: module.id,
instanceId: fingerprint.instanceId,
fingerprint,
manifestExpiresAt,
});
const response = await fetch(buildPremiumModuleDownloadUrl(module.id), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(signedRequest),
});
const data = (await response.json()) as PremiumModuleEntitlementResponse;
if (!response.ok || !data.success || !data.entitlement) {
throw new Error(data.error || `授权模块 ${module.id} 授权凭证获取失败`);
}
if (data.entitlement.moduleId !== module.id) {
throw new Error(`授权模块 ${module.id} 返回内容不匹配`);
}
if (!isSameArtifact(data.entitlement.artifact, module.artifact)) {
throw new Error(`授权模块 ${module.id} 镜像地址校验失败`);
}
if (data.entitlement.sha256 !== module.sha256 || hashData(data.entitlement.entitlementBase64) !== module.sha256) {
throw new Error(`授权模块 ${module.id} 授权凭证哈希校验失败`);
}
if (data.entitlement.sizeBytes !== module.sizeBytes) {
throw new Error(`授权模块 ${module.id} 大小校验失败`);
}
return data.entitlement;
}
async function downloadPremiumModulePullCredential(
module: PremiumModuleManifest['modules'][number],
fingerprint: Awaited<ReturnType<typeof generateFingerprint>>
): Promise<PremiumModulePullCredential> {
const signedRequest = buildSignedLicenseRequest('premium-module-pull-token', {
moduleId: module.id,
instanceId: fingerprint.instanceId,
fingerprint,
});
const response = await fetch(buildPremiumModulePullTokenUrl(module.id), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(signedRequest),
});
const data = (await response.json()) as PremiumModulePullCredentialResponse;
if (!response.ok || !data.success || !data.pullCredential) {
throw new Error(data.error || `授权模块 ${module.id} 镜像拉取凭证获取失败`);
}
if (!isPullCredentialValid(data.pullCredential, module)) {
throw new Error(`授权模块 ${module.id} 镜像拉取凭证校验失败`);
}
return data.pullCredential;
}
export async function getCachedPremiumModuleRuntimeStatus(): Promise<Record<string, PremiumModuleRuntimeStatus>> {
try {
const config = await prisma.systemConfig.findUnique({
where: { key: PREMIUM_MODULE_RUNTIME_STATUS_CONFIG_KEY },
});
if (!config) return {};
const cached = JSON.parse(decryptLocal(config.value)) as Record<string, PremiumModuleRuntimeStatus>;
if (!cached || typeof cached !== 'object') return {};
return cached;
} catch (error) {
console.warn('[License] 读取授权模块运行状态失败:', error);
return {};
}
}
async function savePremiumModuleRuntimeStatus(statuses: Record<string, PremiumModuleRuntimeStatus>): Promise<void> {
await prisma.systemConfig.upsert({
where: { key: PREMIUM_MODULE_RUNTIME_STATUS_CONFIG_KEY },
update: { value: encryptLocal(JSON.stringify(statuses)) },
create: { key: PREMIUM_MODULE_RUNTIME_STATUS_CONFIG_KEY, value: encryptLocal(JSON.stringify(statuses)) },
});
}
export async function syncPremiumModules(): Promise<{ synced: boolean; skipped?: boolean; error?: string }> {
try {
const localLicense = await getLocalLicense();
if (!localLicense || isLicenseConfigExpired(localLicense.cachedConfig)) {
return { synced: false, skipped: true, error: '本地许可证不可用' };
}
const fingerprint = await generateFingerprint();
const signedRequest = buildSignedLicenseRequest('premium-modules', {
instanceId: fingerprint.instanceId,
fingerprint,
});
const response = await fetch(buildLicenseServerLicenseApiUrl('premium-modules'), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(signedRequest),
});
const data = (await response.json()) as SignedPremiumModuleManifest;
if (!response.ok || !data.success || !data.manifest || !data.signature) {
return {
synced: false,
error: data.error || '授权模块清单同步失败',
};
}
if (data.manifest.licenseType !== localLicense.cachedConfig.type) {
return { synced: false, error: '授权模块清单许可证类型不匹配' };
}
if (!verifyPremiumModuleManifest(data.manifest, data.signature)) {
return { synced: false, error: '授权模块清单签名验证失败' };
}
const cachedEntitlements = await getCachedPremiumModuleEntitlements();
const runtimeStatuses = await getCachedPremiumModuleRuntimeStatus();
const activeModuleIds = new Set(data.manifest.modules.map((moduleInfo) => moduleInfo.id));
for (const moduleInfo of data.manifest.modules) {
const existing = cachedEntitlements.entitlements[moduleInfo.id];
const hasValidEntitlement = isCachedEntitlementValid(cachedEntitlements, moduleInfo);
let nextEntitlement = existing;
if (!hasValidEntitlement) {
const entitlement = await downloadPremiumModuleEntitlement(
moduleInfo,
fingerprint,
data.manifest.expiresAt
);
if (!entitlement) {
throw new Error(`授权模块 ${moduleInfo.id} 授权凭证为空`);
}
nextEntitlement = {
entitlementBase64: entitlement.entitlementBase64,
sha256: entitlement.sha256,
sizeBytes: entitlement.sizeBytes,
expiresAt: entitlement.expiresAt,
artifact: entitlement.artifact,
pullCredential: existing?.pullCredential,
syncedAt: new Date().toISOString(),
};
}
if (!nextEntitlement) {
throw new Error(`授权模块 ${moduleInfo.id} 本地凭证为空`);
}
if (!isPullCredentialValid(nextEntitlement.pullCredential, moduleInfo)) {
nextEntitlement.pullCredential = await downloadPremiumModulePullCredential(moduleInfo, fingerprint);
nextEntitlement.syncedAt = new Date().toISOString();
}
cachedEntitlements.entitlements[moduleInfo.id] = nextEntitlement;
const existingStatus = runtimeStatuses[moduleInfo.id];
const canCarryRuntimeStatus = Boolean(
existingStatus &&
existingStatus.image === moduleInfo.artifact.image &&
(existingStatus.digest || null) === (moduleInfo.artifact.digest || null) &&
existingStatus.status !== 'expired' &&
existingStatus.status !== 'revoked'
);
runtimeStatuses[moduleInfo.id] = {
moduleId: moduleInfo.id,
image: moduleInfo.artifact.image,
digest: moduleInfo.artifact.digest || null,
status: canCarryRuntimeStatus && existingStatus ? existingStatus.status : 'missing',
lastSyncAt: new Date().toISOString(),
lastError: canCarryRuntimeStatus && existingStatus ? existingStatus.lastError : undefined,
};
}
for (const [moduleId, status] of Object.entries(runtimeStatuses)) {
if (!activeModuleIds.has(moduleId)) {
runtimeStatuses[moduleId] = {
...status,
status: 'revoked',
lastSyncAt: new Date().toISOString(),
lastError: '许可证未包含该授权模块',
};
}
}
await savePremiumModuleManifest(data.manifest, data.signature);
await savePremiumModuleEntitlements(cachedEntitlements);
await savePremiumModuleRuntimeStatus(runtimeStatuses);
return { synced: true };
} catch (error) {
const message = error instanceof Error ? error.message : '授权模块清单同步异常';
return { synced: false, error: message };
}
}
export async function getCachedModulePullCredential(moduleId: string): Promise<PremiumModulePullCredential | null> {
const cached = await getCachedPremiumModuleManifest();
if (!cached) return null;
const moduleInfo = cached.manifest.modules.find((item) => item.id === moduleId);
if (!moduleInfo) return null;
const cachedEntitlements = await getCachedPremiumModuleEntitlements();
if (!isCachedEntitlementValid(cachedEntitlements, moduleInfo)) return null;
const credential = cachedEntitlements.entitlements[moduleId]?.pullCredential;
return isPullCredentialValid(credential, moduleInfo) ? credential : null;
}
export async function hasPremiumModule(moduleId: string): Promise<boolean> {
const moduleInfo = await getPremiumModuleInfo(moduleId);
if (!moduleInfo) return false;
return hasValidPremiumModuleEntitlement(moduleInfo);
}
export async function isPremiumModuleRunning(moduleId: string): Promise<PremiumModuleRuntimeGateResult> {
const moduleInfo = await getPremiumModuleInfo(moduleId);
if (!moduleInfo) {
return {
available: false,
moduleId,
message: '当前许可证未包含该授权模块',
};
}
if (!(await hasValidPremiumModuleEntitlement(moduleInfo))) {
return {
available: false,
moduleId,
message: '授权模块凭证不可用,请重新验证许可证',
};
}
const runtimeStatuses = await getCachedPremiumModuleRuntimeStatus();
const runtimeStatus = runtimeStatuses[moduleId];
if (!isRuntimeStatusForModule(runtimeStatus, moduleInfo)) {
return {
available: false,
moduleId,
status: runtimeStatus?.status || 'missing',
message:
runtimeStatus?.lastError ||
'授权模块尚未运行,请先通过 Agent 拉取并启动对应模块',
};
}
const baseUrl = getPremiumModuleServiceBaseUrl(moduleId);
if (!baseUrl) {
return {
available: false,
moduleId,
status: runtimeStatus.status,
message: '授权模块服务地址未配置,无法确认模块健康状态',
};
}
if (!(await checkPremiumModuleHealth(moduleId, baseUrl))) {
return {
available: false,
moduleId,
status: 'unhealthy',
message: '授权模块健康检查未通过',
};
}
return {
available: true,
moduleId,
status: runtimeStatus.status,
};
}
export async function hasUsablePremiumModule(moduleId: string): Promise<boolean> {
return (await isPremiumModuleRunning(moduleId)).available;
}
export async function authorizePremiumModuleAction(
moduleId: string,
action: string,
context: PremiumModuleActionContext = {}
): Promise<PremiumModuleAuthorizeResult> {
const gate = await isPremiumModuleRunning(moduleId);
if (!gate.available) {
return {
...gate,
action,
};
}
const baseUrl = getPremiumModuleServiceBaseUrl(moduleId);
try {
const result = await fetchModuleJson(baseUrl, '/authorize', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action, context }),
});
const allowed = result.ok && result.data?.allowed === true && result.data?.moduleId === moduleId;
return {
available: allowed,
moduleId,
action,
status: allowed ? 'running' : 'unhealthy',
message: allowed ? undefined : result.data?.error || '授权模块拒绝该操作',
};
} catch (error) {
const message = error instanceof Error ? error.message : '授权模块操作授权失败';
return {
available: false,
moduleId,
action,
status: 'unhealthy',
message,
};
}
}
export async function validateUploadTemplateFieldsWithPaidCore(
fields: unknown
): Promise<UploadTemplateSaveValidationResult> {
const moduleId = PREMIUM_MODULE_IDS.PAID_CORE;
const gate = await isPremiumModuleRunning(moduleId);
if (!gate.available) {
return {
valid: false,
error: gate.message || '付费增强包不可用,无法校验自定义模板字段',
field: 'fields',
details: {
moduleId,
status: gate.status || 'missing',
},
};
}
const baseUrl = getPremiumModuleServiceBaseUrl(moduleId);
try {
const result = await fetchModuleJson(baseUrl, '/templates/validate-fields', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ fields }),
});
const data = result.data || {};
if (result.ok && data.valid === true && Array.isArray(data.fields)) {
return {
valid: true,
fields: data.fields,
};
}
return {
valid: false,
error: typeof data.error === 'string' ? data.error : '付费增强包拒绝该模板字段配置',
field: typeof data.field === 'string' ? data.field : 'fields',
details: data.details && typeof data.details === 'object' ? data.details : undefined,
};
} catch (error) {
return {
valid: false,
error: error instanceof Error ? error.message : '付费增强包模板字段校验失败',
field: 'fields',
details: { moduleId },
};
}
}
export async function normalizeUploadTemplateSaveWithPaidCore(
payload: unknown,
mode: UploadTemplateSaveMode
): Promise<NormalizedUploadTemplateSaveResult> {
const moduleId = PREMIUM_MODULE_IDS.PAID_CORE;
const gate = await isPremiumModuleRunning(moduleId);
if (!gate.available) {
return {
valid: false,
error: gate.message || '付费增强包不可用,无法校验自定义模板保存数据',
field: 'template',
details: {
moduleId,
status: gate.status || 'missing',
},
};
}
const baseUrl = getPremiumModuleServiceBaseUrl(moduleId);
try {
const result = await fetchModuleJson(baseUrl, '/templates/normalize-save', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ mode, payload }),
});
const data = result.data || {};
if (result.ok && data.valid === true && data.template && typeof data.template === 'object') {
return {
valid: true,
mode: data.mode === 'update' ? 'update' : 'create',
fieldsProvided: data.fieldsProvided === true,
template: data.template,
};
}
return {
valid: false,
error: typeof data.error === 'string' ? data.error : '付费增强包拒绝该模板保存数据',
field: typeof data.field === 'string' ? data.field : 'template',
details: data.details && typeof data.details === 'object' ? data.details : undefined,
};
} catch (error) {
return {
valid: false,
error: error instanceof Error ? error.message : '付费增强包模板保存数据校验失败',
field: 'template',
details: { moduleId },
};
}
}
export async function planUploadTemplateDeleteWithPaidCore(params: {
templateName: string;
materialCount: number;
}): Promise<UploadTemplateDeletePlanResult> {
const moduleId = PREMIUM_MODULE_IDS.PAID_CORE;
const gate = await isPremiumModuleRunning(moduleId);
if (!gate.available) {
return {
valid: false,
allowed: false,
error: gate.message || '付费增强包不可用,无法规划自定义模板删除',
errorCode: 'PAID_CORE_TEMPLATE_DELETE_PLAN_UNAVAILABLE',
details: {
moduleId,
status: gate.status || 'missing',
},
};
}
const baseUrl = getPremiumModuleServiceBaseUrl(moduleId);
try {
const result = await fetchModuleJson(baseUrl, '/templates/plan-delete', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(params),
});
const data = result.data || {};
if (result.ok && data.valid === true && data.allowed === true) {
return {
valid: true,
allowed: true,
templateName: typeof data.templateName === 'string' ? data.templateName : params.templateName,
materialCount: Number.isFinite(Number(data.materialCount))
? Number(data.materialCount)
: params.materialCount,
};
}
if (data.valid === true && data.allowed === false) {
return {
valid: true,
allowed: false,
error: typeof data.error === 'string' ? data.error : '付费增强包拒绝删除该模板',
errorCode: typeof data.errorCode === 'string' ? data.errorCode : 'TEMPLATE_DELETE_DENIED',
details: data.details && typeof data.details === 'object' ? data.details : undefined,
};
}
return {
valid: false,
allowed: false,
error: typeof data.error === 'string' ? data.error : '付费增强包无法规划自定义模板删除',
errorCode: typeof data.errorCode === 'string' ? data.errorCode : 'PAID_CORE_TEMPLATE_DELETE_PLAN_FAILED',
details: data.details && typeof data.details === 'object' ? data.details : undefined,
};
} catch (error) {
return {
valid: false,
allowed: false,
error: error instanceof Error ? error.message : '付费增强包模板删除规划失败',
errorCode: 'PAID_CORE_TEMPLATE_DELETE_PLAN_FAILED',
details: { moduleId },
};
}
}
export async function planUploadTemplateUpdateWithPaidCore(params: {
payload: unknown;
existingTemplate: {
id: string;
version: number;
name: string;
updatedAt: unknown;
materialType: string;
materialKind: string;
materialCount: number;
fields: Array<{
id: string;
key: string;
type: string;
label: string;
required: boolean;
addToCategory: boolean;
isSearchable: boolean;
isReadOnly: boolean;
validations: string | null;
optionsStatic: string | null;
conditions: string | null;
}>;
};
}): Promise<UploadTemplateUpdatePlanResult> {
const moduleId = PREMIUM_MODULE_IDS.PAID_CORE;
const gate = await isPremiumModuleRunning(moduleId);
if (!gate.available) {
return {
valid: false,
error: gate.message || '付费增强包不可用,无法规划自定义模板更新',
errorCode: 'PAID_CORE_TEMPLATE_UPDATE_PLAN_UNAVAILABLE',
field: 'template',
statusCode: 403,
details: {
moduleId,
status: gate.status || 'missing',
},
};
}
const baseUrl = getPremiumModuleServiceBaseUrl(moduleId);
try {
const result = await fetchModuleJson(baseUrl, '/templates/plan-update', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(params),
});
const data = result.data || {};
if (result.ok && data.valid === true && data.updateData && typeof data.updateData === 'object') {
return {
valid: true,
updateData: data.updateData,
fieldsProvided: data.fieldsProvided === true,
fields: Array.isArray(data.fields) ? data.fields : null,
nextVersion: Number.isFinite(Number(data.nextVersion))
? Number(data.nextVersion)
: Number(params.existingTemplate.version || 1) + 1,
};
}
return {
valid: false,
error: typeof data.error === 'string' ? data.error : '付费增强包无法规划自定义模板更新',
errorCode: typeof data.errorCode === 'string' ? data.errorCode : 'PAID_CORE_TEMPLATE_UPDATE_PLAN_FAILED',
field: typeof data.field === 'string' ? data.field : 'template',
statusCode: Number.isFinite(Number(data.statusCode)) ? Number(data.statusCode) : result.status || 400,
details: data.details && typeof data.details === 'object' ? data.details : undefined,
};
} catch (error) {
return {
valid: false,
error: error instanceof Error ? error.message : '付费增强包模板更新规划失败',
errorCode: 'PAID_CORE_TEMPLATE_UPDATE_PLAN_FAILED',
field: 'template',
statusCode: 500,
details: { moduleId },
};
}
}
export async function validateSubmittedTemplateFieldsWithPaidCore(params: {
templateId: string;
templateName?: string;
fields: TemplateFieldForSubmittedValidation[];
templateFields?: Record<string, unknown>;
allowUnknownKeys?: boolean;
}): Promise<SubmittedTemplateFieldsValidationResult> {
const moduleId = PREMIUM_MODULE_IDS.PAID_CORE;
const gate = await isPremiumModuleRunning(moduleId);
if (!gate.available) {
return {
valid: false,
error: gate.message || '付费增强包不可用,无法校验自定义模板字段内容',
errorCode: 'PAID_CORE_TEMPLATE_FIELD_VALIDATION_UNAVAILABLE',
details: {
moduleId,
status: gate.status || 'missing',
templateId: params.templateId,
},
};
}
const baseUrl = getPremiumModuleServiceBaseUrl(moduleId);
try {
const result = await fetchModuleJson(baseUrl, '/materials/validate-template-fields', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(params),
});
const data = result.data || {};
if (
result.ok &&
data.valid === true &&
data.templateFields &&
typeof data.templateFields === 'object' &&
Array.isArray(data.visibleFieldKeys)
) {
return {
valid: true,
templateName: typeof data.templateName === 'string' ? data.templateName : undefined,
templateFields: data.templateFields,
visibleFieldKeys: data.visibleFieldKeys,
};
}
return {
valid: false,
error: typeof data.error === 'string' ? data.error : '付费增强包拒绝该自定义模板字段内容',
errorCode: typeof data.errorCode === 'string' ? data.errorCode : 'TEMPLATE_FIELD_VALIDATION_FAILED',
details: data.details && typeof data.details === 'object' ? data.details : undefined,
};
} catch (error) {
return {
valid: false,
error: error instanceof Error ? error.message : '付费增强包自定义模板字段内容校验失败',
errorCode: 'PAID_CORE_TEMPLATE_FIELD_VALIDATION_FAILED',
details: { moduleId, templateId: params.templateId },
};
}
}
export async function prepareMaterialMetadataWithPaidCore(params: {
templateId: string | null;
templateFields?: Record<string, unknown>;
previewUrls?: string[];
existingMetadata?: string | null;
maxSizeBytes?: number;
}): Promise<MaterialMetadataPrepareResult> {
const moduleId = PREMIUM_MODULE_IDS.PAID_CORE;
const gate = await isPremiumModuleRunning(moduleId);
if (!gate.available) {
return {
valid: false,
error: gate.message || '付费增强包不可用,无法准备自定义模板素材数据',
errorCode: 'PAID_CORE_METADATA_PREPARE_UNAVAILABLE',
details: {
moduleId,
status: gate.status || 'missing',
templateId: params.templateId,
},
};
}
const baseUrl = getPremiumModuleServiceBaseUrl(moduleId);
try {
const result = await fetchModuleJson(baseUrl, '/materials/prepare-metadata', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(params),
});
const data = result.data || {};
if (
result.ok &&
data.valid === true &&
data.metadataSize &&
typeof data.metadataSize === 'object'
) {
return {
valid: true,
standardData: data.standardData && typeof data.standardData === 'object' ? data.standardData : {},
metadata: typeof data.metadata === 'string' ? data.metadata : null,
metadataSize: {
ok: true,
size: Number(data.metadataSize.size || 0),
maxSize: Number(data.metadataSize.maxSize || 0),
},
};
}
return {
valid: false,
error: typeof data.error === 'string' ? data.error : '付费增强包拒绝该自定义模板素材数据',
errorCode: typeof data.errorCode === 'string' ? data.errorCode : 'METADATA_PREPARE_FAILED',
details: data.details && typeof data.details === 'object' ? data.details : undefined,
};
} catch (error) {
return {
valid: false,
error: error instanceof Error ? error.message : '付费增强包自定义模板素材数据准备失败',
errorCode: 'PAID_CORE_METADATA_PREPARE_FAILED',
details: { moduleId, templateId: params.templateId },
};
}
}
export async function planTemplateMigrationBatchWithPaidCore(params: {
sourceTemplateId: string;
targetTemplateId?: string | null;
sourceTemplateName?: string | null;
targetTemplateName?: string | null;
totalMaterialCount: number;
request: {
updateMaterialType?: unknown;
previewOnly?: unknown;
forceLargeMigration?: unknown;
migrationLimit?: unknown;
};
}): Promise<TemplateMigrationBatchPlanResult> {
const moduleId = PREMIUM_MODULE_IDS.PAID_CORE;
const gate = await isPremiumModuleRunning(moduleId);
if (!gate.available) {
return {
valid: false,
allowed: false,
error: gate.message || '付费增强包不可用,无法规划模板迁移批次',
errorCode: 'PAID_CORE_TEMPLATE_MIGRATION_BATCH_PLAN_UNAVAILABLE',
statusCode: 403,
details: {
moduleId,
status: gate.status || 'missing',
sourceTemplateId: params.sourceTemplateId,
targetTemplateId: params.targetTemplateId || null,
},
};
}
const baseUrl = getPremiumModuleServiceBaseUrl(moduleId);
try {
const result = await fetchModuleJson(baseUrl, '/templates/plan-migration-batch', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(params),
});
const data = result.data || {};
if (data.valid === true && data.allowed === true) {
return {
valid: true,
allowed: true,
sourceTemplateId: typeof data.sourceTemplateId === 'string' ? data.sourceTemplateId : params.sourceTemplateId,
targetTemplateId:
typeof data.targetTemplateId === 'string' ? data.targetTemplateId : String(params.targetTemplateId || ''),
sourceTemplateName:
typeof data.sourceTemplateName === 'string' ? data.sourceTemplateName : String(params.sourceTemplateName || ''),
targetTemplateName:
typeof data.targetTemplateName === 'string' ? data.targetTemplateName : String(params.targetTemplateName || ''),
totalMaterialCount: Number.isFinite(Number(data.totalMaterialCount))
? Number(data.totalMaterialCount)
: params.totalMaterialCount,
previewOnly: data.previewOnly === true,
forceLargeMigration: data.forceLargeMigration === true,
updateMaterialType: data.updateMaterialType !== false,
migrationLimit: Number.isFinite(Number(data.migrationLimit)) ? Number(data.migrationLimit) : 200,
safeLimit: Number.isFinite(Number(data.safeLimit)) ? Number(data.safeLimit) : 200,
maxBatchLimit: Number.isFinite(Number(data.maxBatchLimit)) ? Number(data.maxBatchLimit) : 1000,
batchLimited: data.batchLimited === true,
materialTake: Number.isFinite(Number(data.materialTake)) ? Number(data.materialTake) : null,
migratedAt: typeof data.migratedAt === 'string' ? data.migratedAt : new Date().toISOString(),
};
}
if (data.valid === true && data.allowed === false) {
return {
valid: true,
allowed: false,
statusCode: Number.isFinite(Number(data.statusCode)) ? Number(data.statusCode) : result.status || 400,
error: typeof data.error === 'string' ? data.error : undefined,
errorCode: typeof data.errorCode === 'string' ? data.errorCode : undefined,
response: data.response && typeof data.response === 'object' ? data.response : undefined,
details: data.details && typeof data.details === 'object' ? data.details : undefined,
};
}
return {
valid: false,
allowed: false,
error: typeof data.error === 'string' ? data.error : '付费增强包无法规划模板迁移批次',
errorCode: typeof data.errorCode === 'string' ? data.errorCode : 'TEMPLATE_MIGRATION_BATCH_PLAN_FAILED',
statusCode: Number.isFinite(Number(data.statusCode)) ? Number(data.statusCode) : result.status || 400,
details: data.details && typeof data.details === 'object' ? data.details : undefined,
};
} catch (error) {
return {
valid: false,
allowed: false,
error: error instanceof Error ? error.message : '付费增强包模板迁移批次规划失败',
errorCode: 'PAID_CORE_TEMPLATE_MIGRATION_BATCH_PLAN_FAILED',
statusCode: 500,
details: {
moduleId,
sourceTemplateId: params.sourceTemplateId,
targetTemplateId: params.targetTemplateId || null,
},
};
}
}
export async function prepareTemplateMigrationMetadataWithPaidCore(params: {
sourceTemplateId: string;
sourceTemplateName: string;
targetTemplateId: string;
targetTemplateName: string;
migratedAt: string;
targetFieldKeys: string[];
targetFields?: Array<{
key: string;
label?: string | null;
type?: string | null;
required?: boolean | null;
}>;
templateFields?: Record<string, unknown>;
materialFieldValues?: Record<string, unknown>;
materialStandardFields: Record<string, unknown>;
previewUrls?: string[];
existingMetadata?: string | null;
maxSizeBytes?: number;
}): Promise<TemplateMigrationMetadataPrepareResult> {
const moduleId = PREMIUM_MODULE_IDS.PAID_CORE;
const gate = await isPremiumModuleRunning(moduleId);
if (!gate.available) {
return {
valid: false,
error: gate.message || '付费增强包不可用,无法准备模板迁移数据',
errorCode: 'PAID_CORE_TEMPLATE_MIGRATION_UNAVAILABLE',
details: {
moduleId,
status: gate.status || 'missing',
sourceTemplateId: params.sourceTemplateId,
targetTemplateId: params.targetTemplateId,
},
};
}
const baseUrl = getPremiumModuleServiceBaseUrl(moduleId);
try {
const authorization = await fetchModuleJson(baseUrl, '/authorize', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'customTemplates:migrate',
context: {
sourceTemplateId: params.sourceTemplateId,
targetTemplateId: params.targetTemplateId,
},
}),
});
if (!authorization.ok || authorization.data?.allowed !== true || authorization.data?.moduleId !== moduleId) {
return {
valid: false,
error: authorization.data?.error || '付费增强包未授权执行模板迁移',
errorCode: 'PAID_CORE_TEMPLATE_MIGRATION_UNAUTHORIZED',
details: {
moduleId,
status: 'running',
sourceTemplateId: params.sourceTemplateId,
targetTemplateId: params.targetTemplateId,
},
};
}
} catch (error) {
return {
valid: false,
error: error instanceof Error ? error.message : '付费增强包模板迁移动作授权失败',
errorCode: 'PAID_CORE_TEMPLATE_MIGRATION_UNAUTHORIZED',
details: {
moduleId,
status: 'running',
sourceTemplateId: params.sourceTemplateId,
targetTemplateId: params.targetTemplateId,
},
};
}
try {
const result = await fetchModuleJson(baseUrl, '/templates/prepare-migration-metadata', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(params),
});
const data = result.data || {};
if (
result.ok &&
data.valid === true &&
data.metadataSize &&
typeof data.metadataSize === 'object'
) {
return {
valid: true,
standardData: data.standardData && typeof data.standardData === 'object' ? data.standardData : {},
metadata: typeof data.metadata === 'string' ? data.metadata : null,
metadataSize: {
ok: true,
size: Number(data.metadataSize.size || 0),
maxSize: Number(data.metadataSize.maxSize || 0),
},
templateFields:
data.templateFields && typeof data.templateFields === 'object'
? data.templateFields
: {},
droppedTemplateFieldCount: Number(data.droppedTemplateFieldCount || 0),
droppedStandardFieldCount: Number(data.droppedStandardFieldCount || 0),
droppedTemplateFields:
data.droppedTemplateFields && typeof data.droppedTemplateFields === 'object'
? data.droppedTemplateFields
: {},
droppedStandardFields:
data.droppedStandardFields && typeof data.droppedStandardFields === 'object'
? data.droppedStandardFields
: {},
};
}
return {
valid: false,
error: typeof data.error === 'string' ? data.error : '付费增强包拒绝该模板迁移数据',
errorCode: typeof data.errorCode === 'string' ? data.errorCode : 'TEMPLATE_MIGRATION_METADATA_PREPARE_FAILED',
details: data.details && typeof data.details === 'object' ? data.details : undefined,
};
} catch (error) {
return {
valid: false,
error: error instanceof Error ? error.message : '付费增强包模板迁移数据准备失败',
errorCode: 'PAID_CORE_TEMPLATE_MIGRATION_FAILED',
details: {
moduleId,
sourceTemplateId: params.sourceTemplateId,
targetTemplateId: params.targetTemplateId,
},
};
}
}
export async function buildTemplateMigrationPreviewWithPaidCore(params: {
totalMaterialCount: number;
migratableCount: number;
previewOnly: boolean;
previewLimit?: number;
targetFields: Array<{
key: string;
label?: string | null;
type?: string | null;
required?: boolean | null;
}>;
sourceTemplateFields: Array<{
key: string;
label?: string | null;
type?: string | null;
required?: boolean | null;
}>;
impacts: Array<{
materialId: string;
productModel?: string | null;
droppedTemplateFieldKeys?: string[];
droppedStandardFieldKeys?: string[];
droppedTemplateFields?: Record<string, unknown>;
droppedStandardFields?: Record<string, unknown>;
}>;
}): Promise<TemplateMigrationPreviewBuildResult> {
const moduleId = PREMIUM_MODULE_IDS.PAID_CORE;
const gate = await isPremiumModuleRunning(moduleId);
if (!gate.available) {
return {
valid: false,
error: gate.message || '付费增强包不可用,无法生成模板迁移预览',
errorCode: 'PAID_CORE_TEMPLATE_MIGRATION_PREVIEW_UNAVAILABLE',
details: {
moduleId,
status: gate.status || 'missing',
},
};
}
const baseUrl = getPremiumModuleServiceBaseUrl(moduleId);
try {
const result = await fetchModuleJson(baseUrl, '/templates/build-migration-preview', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(params),
});
const data = result.data || {};
if (
result.ok &&
data.valid === true &&
data.migrationBackup &&
typeof data.migrationBackup === 'object' &&
data.migrationPreview &&
typeof data.migrationPreview === 'object'
) {
const migrationPreview = data.migrationPreview;
const backupFields = migrationPreview.backupFields && typeof migrationPreview.backupFields === 'object'
? migrationPreview.backupFields
: {};
return {
valid: true,
migrationBackup: {
materialsWithDroppedFields: Number(data.migrationBackup.materialsWithDroppedFields || 0),
droppedTemplateFieldCount: Number(data.migrationBackup.droppedTemplateFieldCount || 0),
droppedStandardFieldCount: Number(data.migrationBackup.droppedStandardFieldCount || 0),
},
migrationPreview: {
totalMaterialCount: Number(migrationPreview.totalMaterialCount || 0),
totalMaterials: Number(migrationPreview.totalMaterials || 0),
previewLimited: migrationPreview.previewLimited === true,
previewLimit: Number.isFinite(migrationPreview.previewLimit)
? Number(migrationPreview.previewLimit)
: undefined,
targetCurrentFields: Array.isArray(migrationPreview.targetCurrentFields)
? migrationPreview.targetCurrentFields
: [],
backupFields: {
templateFields: Array.isArray(backupFields.templateFields) ? backupFields.templateFields : [],
standardFields: Array.isArray(backupFields.standardFields) ? backupFields.standardFields : [],
},
samples: Array.isArray(migrationPreview.samples) ? migrationPreview.samples : [],
},
};
}
return {
valid: false,
error: typeof data.error === 'string' ? data.error : '付费增强包拒绝生成模板迁移预览',
errorCode: typeof data.errorCode === 'string' ? data.errorCode : 'TEMPLATE_MIGRATION_PREVIEW_BUILD_FAILED',
details: data.details && typeof data.details === 'object' ? data.details : undefined,
};
} catch (error) {
return {
valid: false,
error: error instanceof Error ? error.message : '付费增强包模板迁移预览生成失败',
errorCode: 'PAID_CORE_TEMPLATE_MIGRATION_PREVIEW_FAILED',
details: { moduleId },
};
}
}
export async function projectPublicTemplateOptionsWithPaidCore(params: {
builtinTemplates?: Array<Record<string, unknown>>;
customTemplates?: Array<Record<string, unknown>>;
materialCounts?: Record<string, number>;
builtinMaterialTypes?: string[];
}): Promise<PublicTemplateOptionsProjectionResult> {
const moduleId = PREMIUM_MODULE_IDS.PAID_CORE;
const gate = await isPremiumModuleRunning(moduleId);
if (!gate.available) {
return {
valid: false,
error: gate.message || '付费增强包不可用,无法生成自定义模板展示列表',
errorCode: 'PAID_CORE_PUBLIC_TEMPLATE_OPTIONS_UNAVAILABLE',
details: {
moduleId,
status: gate.status || 'missing',
},
};
}
const baseUrl = getPremiumModuleServiceBaseUrl(moduleId);
try {
const result = await fetchModuleJson(baseUrl, '/templates/project-public-options', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(params),
});
const data = result.data || {};
if (
result.ok &&
data.valid === true &&
Array.isArray(data.templates) &&
Array.isArray(data.uploadTemplates) &&
Array.isArray(data.materialTypes)
) {
return {
valid: true,
templates: data.templates,
uploadTemplates: data.uploadTemplates,
materialTypes: data.materialTypes.map((item: unknown) => String(item)),
};
}
return {
valid: false,
error: typeof data.error === 'string' ? data.error : '付费增强包拒绝生成自定义模板展示列表',
errorCode: typeof data.errorCode === 'string' ? data.errorCode : 'TEMPLATE_PUBLIC_OPTIONS_PROJECT_FAILED',
details: data.details && typeof data.details === 'object' ? data.details : undefined,
};
} catch (error) {
return {
valid: false,
error: error instanceof Error ? error.message : '付费增强包自定义模板展示列表生成失败',
errorCode: 'PAID_CORE_PUBLIC_TEMPLATE_OPTIONS_FAILED',
details: { moduleId },
};
}
}
export async function calculateTemplateFieldStatsWithPaidCore(params: {
fields: Array<{
key: string;
label?: string | null;
type?: string | null;
}>;
materials: Array<{
id?: string;
metadata?: string | null;
}>;
}): Promise<TemplateFieldStatsCalculationResult> {
const moduleId = PREMIUM_MODULE_IDS.PAID_CORE;
const gate = await isPremiumModuleRunning(moduleId);
if (!gate.available) {
return {
valid: false,
error: gate.message || '付费增强包不可用,无法计算模板字段填写率',
errorCode: 'PAID_CORE_TEMPLATE_FIELD_STATS_UNAVAILABLE',
details: {
moduleId,
status: gate.status || 'missing',
},
};
}
const baseUrl = getPremiumModuleServiceBaseUrl(moduleId);
try {
const result = await fetchModuleJson(baseUrl, '/templates/calculate-field-stats', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(params),
});
const data = result.data || {};
if (result.ok && data.valid === true) {
return {
valid: true,
fieldStats: data.fieldStats && typeof data.fieldStats === 'object' ? data.fieldStats : null,
};
}
return {
valid: false,
error: typeof data.error === 'string' ? data.error : '付费增强包拒绝计算模板字段填写率',
errorCode: typeof data.errorCode === 'string' ? data.errorCode : 'TEMPLATE_FIELD_STATS_CALCULATE_FAILED',
details: data.details && typeof data.details === 'object' ? data.details : undefined,
};
} catch (error) {
return {
valid: false,
error: error instanceof Error ? error.message : '付费增强包模板字段填写率计算失败',
errorCode: 'PAID_CORE_TEMPLATE_FIELD_STATS_FAILED',
details: { moduleId },
};
}
}
export async function planMaterialCustomSearchWithPaidCore(params: {
search: string;
searchableFields: Array<{
key: string;
}>;
}): Promise<MaterialCustomSearchPlanResult> {
const moduleId = PREMIUM_MODULE_IDS.PAID_CORE;
const gate = await isPremiumModuleRunning(moduleId);
if (!gate.available) {
return {
valid: false,
error: gate.message || '付费增强包不可用,无法规划自定义字段搜索',
errorCode: 'PAID_CORE_MATERIAL_CUSTOM_SEARCH_UNAVAILABLE',
details: {
moduleId,
status: gate.status || 'missing',
},
};
}
const baseUrl = getPremiumModuleServiceBaseUrl(moduleId);
try {
const result = await fetchModuleJson(baseUrl, '/materials/plan-custom-search', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(params),
});
const data = result.data || {};
if (result.ok && data.valid === true && Array.isArray(data.customFieldKeys)) {
return {
valid: true,
search: typeof data.search === 'string' ? data.search : params.search,
customFieldKeys: data.customFieldKeys.map((key: unknown) => String(key)).filter(Boolean),
};
}
return {
valid: false,
error: typeof data.error === 'string' ? data.error : '付费增强包拒绝规划自定义字段搜索',
errorCode: typeof data.errorCode === 'string' ? data.errorCode : 'MATERIAL_CUSTOM_SEARCH_PLAN_FAILED',
details: data.details && typeof data.details === 'object' ? data.details : undefined,
};
} catch (error) {
return {
valid: false,
error: error instanceof Error ? error.message : '付费增强包自定义字段搜索规划失败',
errorCode: 'PAID_CORE_MATERIAL_CUSTOM_SEARCH_PLAN_FAILED',
details: { moduleId },
};
}
}
const MATERIAL_LIST_SEARCH_COLUMN_FIELDS = [
'productModel',
'brandName',
'seriesName',
'skuCode',
'materialTexture',
'categoryName',
'sceneName',
'authorName',
];
const MATERIAL_LIST_HOME_FILTER_COLUMN_FIELDS = new Set([
'renderer',
'brandName',
'seriesName',
'skuCode',
'materialTexture',
'renderTime',
'imageResolution',
'colorMode',
'productModel',
'platformName',
'categoryName',
'sceneName',
'authorName',
]);
const HOME_FILTER_COLUMN_FIELDS = new Set([
'categoryName',
'sceneName',
'platformName',
'materialType',
'renderer',
'brandName',
'colorMode',
'renderTime',
'imageResolution',
'seriesName',
'skuCode',
'materialTexture',
'authorName',
]);
function normalizeMaterialListQueryFallback(params: {
search: string;
homeFieldFilters: Array<{ key: string; value: string }>;
}): MaterialListQueryPlanResult {
const homeFieldFilters = params.homeFieldFilters
.map((filter) => ({
key: String(filter.key || '').trim(),
value: String(filter.value || '').trim(),
}))
.filter((filter) => filter.key && filter.value);
return {
valid: true,
search: params.search,
standardSearchFields: params.search ? MATERIAL_LIST_SEARCH_COLUMN_FIELDS : [],
customSearchFieldKeys: [],
standardHomeFilters: homeFieldFilters.filter((filter) =>
MATERIAL_LIST_HOME_FILTER_COLUMN_FIELDS.has(filter.key)
),
customHomeFilters: [],
moduleAvailable: false,
};
}
function normalizeHomeFilterFieldCountsFallback(params: {
fields: Array<{ key: string }>;
}): HomeFilterFieldCountsPlanResult {
return {
valid: true,
fieldPlans: params.fields
.map((field) => String(field.key || '').trim())
.filter(Boolean)
.filter((key) => HOME_FILTER_COLUMN_FIELDS.has(key))
.map((key) => ({ key, source: 'column' })),
moduleAvailable: false,
};
}
export async function planMaterialListQueryWithPaidCore(params: {
search: string;
searchableFields: Array<{
key: string;
}>;
homeFieldFilters: Array<{
key: string;
value: string;
}>;
enabledHomeFilterFields: Array<{
key: string;
}>;
}): Promise<MaterialListQueryPlanResult> {
const moduleId = PREMIUM_MODULE_IDS.PAID_CORE;
const gate = await isPremiumModuleRunning(moduleId);
if (!gate.available) {
return normalizeMaterialListQueryFallback(params);
}
const baseUrl = getPremiumModuleServiceBaseUrl(moduleId);
try {
const result = await fetchModuleJson(baseUrl, '/materials/plan-list-query', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(params),
});
const data = result.data || {};
if (
result.ok &&
data.valid === true &&
Array.isArray(data.standardSearchFields) &&
Array.isArray(data.customSearchFieldKeys) &&
Array.isArray(data.standardHomeFilters) &&
Array.isArray(data.customHomeFilters)
) {
const normalizeFilters = (filters: unknown) =>
(Array.isArray(filters) ? filters : [])
.map((filter) => ({
key: String((filter as any)?.key || '').trim(),
value: String((filter as any)?.value || '').trim(),
}))
.filter((filter) => filter.key && filter.value);
return {
valid: true,
search: typeof data.search === 'string' ? data.search : params.search,
standardSearchFields: data.standardSearchFields.map((key: unknown) => String(key)).filter(Boolean),
customSearchFieldKeys: data.customSearchFieldKeys.map((key: unknown) => String(key)).filter(Boolean),
standardHomeFilters: normalizeFilters(data.standardHomeFilters),
customHomeFilters: normalizeFilters(data.customHomeFilters),
moduleAvailable: true,
};
}
return normalizeMaterialListQueryFallback(params);
} catch {
return normalizeMaterialListQueryFallback(params);
}
}
export async function planHomeFilterFieldCountsWithPaidCore(params: {
fields: Array<{
key: string;
}>;
}): Promise<HomeFilterFieldCountsPlanResult> {
const moduleId = PREMIUM_MODULE_IDS.PAID_CORE;
const gate = await isPremiumModuleRunning(moduleId);
if (!gate.available) {
return normalizeHomeFilterFieldCountsFallback(params);
}
const baseUrl = getPremiumModuleServiceBaseUrl(moduleId);
try {
const result = await fetchModuleJson(baseUrl, '/materials/plan-home-filter-fields', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(params),
});
const data = result.data || {};
if (result.ok && data.valid === true && Array.isArray(data.fieldPlans)) {
return {
valid: true,
fieldPlans: data.fieldPlans
.map((plan: unknown) => ({
key: String((plan as any)?.key || '').trim(),
source: (plan as any)?.source === 'metadata' ? 'metadata' : 'column',
}))
.filter((plan: { key: string; source: 'column' | 'metadata' }) => plan.key),
moduleAvailable: true,
};
}
return normalizeHomeFilterFieldCountsFallback(params);
} catch {
return normalizeHomeFilterFieldCountsFallback(params);
}
}
export async function planHomeFilterCustomFieldCountsWithPaidCore(params: {
fields: Array<{
key: string;
}>;
}): Promise<HomeFilterCustomFieldCountsPlanResult> {
const moduleId = PREMIUM_MODULE_IDS.PAID_CORE;
const gate = await isPremiumModuleRunning(moduleId);
if (!gate.available) {
return {
valid: false,
error: gate.message || '付费增强包不可用,无法规划首页自定义筛选统计',
errorCode: 'PAID_CORE_HOME_FILTER_COUNTS_UNAVAILABLE',
details: {
moduleId,
status: gate.status || 'missing',
},
};
}
const baseUrl = getPremiumModuleServiceBaseUrl(moduleId);
try {
const result = await fetchModuleJson(baseUrl, '/materials/plan-home-filter-counts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(params),
});
const data = result.data || {};
if (result.ok && data.valid === true && Array.isArray(data.customFieldKeys)) {
return {
valid: true,
customFieldKeys: data.customFieldKeys.map((key: unknown) => String(key)).filter(Boolean),
};
}
return {
valid: false,
error: typeof data.error === 'string' ? data.error : '付费增强包拒绝规划首页自定义筛选统计',
errorCode: typeof data.errorCode === 'string' ? data.errorCode : 'HOME_FILTER_COUNTS_PLAN_FAILED',
details: data.details && typeof data.details === 'object' ? data.details : undefined,
};
} catch (error) {
return {
valid: false,
error: error instanceof Error ? error.message : '付费增强包首页自定义筛选统计规划失败',
errorCode: 'PAID_CORE_HOME_FILTER_COUNTS_PLAN_FAILED',
details: { moduleId },
};
}
}
export async function planDepartmentScopeWithPaidCore(params: {
scopeDepartment: string;
targetDepartment?: string | null;
departments: Array<{
name: string;
}> | string[];
}): Promise<DepartmentScopePlanResult> {
const moduleId = PREMIUM_MODULE_IDS.PAID_CORE;
const gate = await isPremiumModuleRunning(moduleId);
if (!gate.available) {
return {
valid: false,
error: gate.message || '付费增强包不可用,无法规划部门管理范围',
errorCode: 'PAID_CORE_DEPARTMENT_SCOPE_UNAVAILABLE',
details: {
moduleId,
status: gate.status || 'missing',
},
};
}
const baseUrl = getPremiumModuleServiceBaseUrl(moduleId);
try {
const result = await fetchModuleJson(baseUrl, '/departments/plan-scope', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(params),
});
const data = result.data || {};
if (result.ok && data.valid === true && Array.isArray(data.scopedDepartments)) {
return {
valid: true,
scopedDepartments: data.scopedDepartments.map((name: unknown) => String(name)).filter(Boolean),
withinScope: data.withinScope === true,
};
}
return {
valid: false,
error: typeof data.error === 'string' ? data.error : '付费增强包拒绝规划部门管理范围',
errorCode: typeof data.errorCode === 'string' ? data.errorCode : 'DEPARTMENT_SCOPE_PLAN_FAILED',
details: data.details && typeof data.details === 'object' ? data.details : undefined,
};
} catch (error) {
return {
valid: false,
error: error instanceof Error ? error.message : '付费增强包部门管理范围规划失败',
errorCode: 'PAID_CORE_DEPARTMENT_SCOPE_PLAN_FAILED',
details: { moduleId },
};
}
}
export async function planDepartmentStructureChangeWithPaidCore(params: {
action: 'create' | 'rename' | 'delete';
target: Record<string, unknown>;
departments: Array<{
id: string;
name: string;
}>;
users?: Array<{
id: string;
department?: string | null;
managedDepartment?: string | null;
}>;
}): Promise<DepartmentStructureChangePlanResult> {
const moduleId = PREMIUM_MODULE_IDS.PAID_CORE;
const gate = await isPremiumModuleRunning(moduleId);
if (!gate.available) {
return {
valid: false,
allowed: false,
error: gate.message || '付费增强包不可用,无法规划部门结构变更',
errorCode: 'PAID_CORE_DEPARTMENT_STRUCTURE_PLAN_UNAVAILABLE',
statusCode: 403,
details: {
moduleId,
status: gate.status || 'missing',
action: params.action,
},
};
}
const baseUrl = getPremiumModuleServiceBaseUrl(moduleId);
try {
const result = await fetchModuleJson(baseUrl, '/departments/plan-structure-change', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(params),
});
const data = result.data || {};
if (
data.valid === true &&
data.allowed === true &&
Array.isArray(data.departmentOps) &&
Array.isArray(data.userOps)
) {
return {
valid: true,
allowed: true,
action: ['create', 'rename', 'delete'].includes(data.action) ? data.action : params.action,
departmentOps: data.departmentOps,
userOps: data.userOps,
summary: data.summary && typeof data.summary === 'object' ? data.summary : undefined,
};
}
if (data.valid === true && data.allowed === false) {
return {
valid: true,
allowed: false,
statusCode: Number.isFinite(Number(data.statusCode)) ? Number(data.statusCode) : result.status || 400,
error: typeof data.error === 'string' ? data.error : undefined,
errorCode: typeof data.errorCode === 'string' ? data.errorCode : undefined,
response: data.response && typeof data.response === 'object' ? data.response : undefined,
details: data.details && typeof data.details === 'object' ? data.details : undefined,
};
}
return {
valid: false,
allowed: false,
error: typeof data.error === 'string' ? data.error : '付费增强包无法规划部门结构变更',
errorCode: typeof data.errorCode === 'string' ? data.errorCode : 'DEPARTMENT_STRUCTURE_CHANGE_PLAN_FAILED',
statusCode: Number.isFinite(Number(data.statusCode)) ? Number(data.statusCode) : result.status || 400,
details: data.details && typeof data.details === 'object' ? data.details : undefined,
};
} catch (error) {
return {
valid: false,
allowed: false,
error: error instanceof Error ? error.message : '付费增强包部门结构变更规划失败',
errorCode: 'PAID_CORE_DEPARTMENT_STRUCTURE_PLAN_FAILED',
statusCode: 500,
details: { moduleId, action: params.action },
};
}
}
export async function detectTemplateStructureChangesWithPaidCore(params: {
existingTemplate: {
materialType: string;
materialKind: string;
fields: Array<{
id: string;
key: string;
type: string;
label: string;
required: boolean;
addToCategory: boolean;
isSearchable: boolean;
isReadOnly: boolean;
validations: string | null;
optionsStatic: string | null;
conditions: string | null;
}>;
};
nextMaterialType?: string;
nextMaterialKind?: string;
nextFields?: FormattedUploadTemplateField[];
}): Promise<TemplateStructureChangeDetectionResult> {
const moduleId = PREMIUM_MODULE_IDS.PAID_CORE;
const gate = await isPremiumModuleRunning(moduleId);
if (!gate.available) {
return {
valid: false,
error: gate.message || '付费增强包不可用,无法判断模板结构变更风险',
field: 'template',
details: {
moduleId,
status: gate.status || 'missing',
},
};
}
const baseUrl = getPremiumModuleServiceBaseUrl(moduleId);
try {
const result = await fetchModuleJson(baseUrl, '/templates/detect-structure-changes', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(params),
});
const data = result.data || {};
if (result.ok && data.valid === true && Array.isArray(data.changes)) {
return {
valid: true,
changes: data.changes,
};
}
return {
valid: false,
error: typeof data.error === 'string' ? data.error : '付费增强包无法判断模板结构变更风险',
field: typeof data.field === 'string' ? data.field : 'template',
details: data.details && typeof data.details === 'object' ? data.details : undefined,
};
} catch (error) {
return {
valid: false,
error: error instanceof Error ? error.message : '付费增强包模板结构变更判断失败',
field: 'template',
details: { moduleId },
};
}
}
export async function updatePremiumModuleRuntimeStatus(status: PremiumModuleRuntimeStatus): Promise<void> {
if (!status.moduleId || !status.image || !status.lastSyncAt) {
throw new Error('授权模块运行状态缺少必要字段');
}
const validStatuses: PremiumModuleRuntimeStatusValue[] = [
'missing',
'pulling',
'running',
'unhealthy',
'expired',
'revoked',
];
if (!validStatuses.includes(status.status)) {
throw new Error('授权模块运行状态无效');
}
const runtimeStatuses = await getCachedPremiumModuleRuntimeStatus();
runtimeStatuses[status.moduleId] = {
moduleId: status.moduleId,
image: status.image,
digest: status.digest || null,
status: status.status,
lastSyncAt: status.lastSyncAt,
lastError: status.lastError,
};
await savePremiumModuleRuntimeStatus(runtimeStatuses);
}