2498 lines
78 KiB
TypeScript
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);
|
|
}
|