import { createRequire } from 'module'; import PocketBase from 'pocketbase'; const require = createRequire(import.meta.url); let runtimeConfig = {}; try { runtimeConfig = require('../pocket-base/bai_api_pb_hooks/bai_api_shared/config/runtime.js'); } catch (_error) { runtimeConfig = {}; } const PB_URL = (process.env.PB_URL || 'https://bai-api.blv-oa.com/pb').replace(/\/+$/, ''); const AUTH_TOKEN = process.env.POCKETBASE_AUTH_TOKEN || runtimeConfig.POCKETBASE_AUTH_TOKEN || ''; if (!AUTH_TOKEN) { console.error('❌ 缺少 POCKETBASE_AUTH_TOKEN,无法执行建表。'); process.exit(1); } const pb = new PocketBase(PB_URL); const collections = [ { name: 'tbl_attachments', type: 'base', listRule: '', viewRule: '', fields: [ { name: 'attachments_id', type: 'text', required: true }, { name: 'attachments_link', type: 'file', maxSelect: 0, maxSize: 4294967296, mimeTypes: [] }, { name: 'attachments_filename', type: 'text' }, { name: 'attachments_filetype', type: 'text' }, { name: 'attachments_size', type: 'number' }, { name: 'attachments_owner', type: 'text' }, { name: 'attachments_md5', type: 'text' }, { name: 'attachments_ocr', type: 'text' }, { name: 'attachments_status', type: 'text' }, { name: 'attachments_remark', type: 'text' }, ], indexes: [ 'CREATE UNIQUE INDEX idx_tbl_attachments_attachments_id ON tbl_attachments (attachments_id)', 'CREATE INDEX idx_tbl_attachments_attachments_owner ON tbl_attachments (attachments_owner)', 'CREATE INDEX idx_tbl_attachments_attachments_status ON tbl_attachments (attachments_status)', ], }, { name: 'tbl_document', type: 'base', listRule: '', viewRule: '', fields: [ { name: 'document_id', type: 'text', required: true }, { name: 'document_create', type: 'autodate', onCreate: true, onUpdate: false }, { name: 'document_effect_date', type: 'date' }, { name: 'document_expiry_date', type: 'date' }, { name: 'document_type', type: 'text', required: true }, { name: 'document_title', type: 'text', required: true }, { name: 'document_subtitle', type: 'text' }, { name: 'document_summary', type: 'text' }, { name: 'document_content', type: 'text' }, { name: 'document_image', type: 'text' }, { name: 'document_video', type: 'text' }, { name: 'document_file', type: 'text' }, { name: 'document_owner', type: 'text' }, { name: 'document_relation_model', type: 'text' }, { name: 'document_keywords', type: 'text' }, { name: 'document_share_count', type: 'number' }, { name: 'document_download_count', type: 'number' }, { name: 'document_favorite_count', type: 'number' }, { name: 'document_status', type: 'text' }, { name: 'document_embedding_status', type: 'text' }, { name: 'document_embedding_error', type: 'text' }, { name: 'document_embedding_lasttime', type: 'date' }, { name: 'document_vector_version', type: 'text' }, { name: 'document_product_categories', type: 'text' }, { name: 'document_application_scenarios', type: 'text' }, { name: 'document_hotel_type', type: 'text' }, { name: 'document_remark', type: 'text' }, ], indexes: [ 'CREATE UNIQUE INDEX idx_tbl_document_document_id ON tbl_document (document_id)', 'CREATE INDEX idx_tbl_document_document_create ON tbl_document (document_create)', 'CREATE INDEX idx_tbl_document_document_owner ON tbl_document (document_owner)', 'CREATE INDEX idx_tbl_document_document_type ON tbl_document (document_type)', 'CREATE INDEX idx_tbl_document_document_status ON tbl_document (document_status)', 'CREATE INDEX idx_tbl_document_document_embedding_status ON tbl_document (document_embedding_status)', 'CREATE INDEX idx_tbl_document_document_effect_date ON tbl_document (document_effect_date)', 'CREATE INDEX idx_tbl_document_document_expiry_date ON tbl_document (document_expiry_date)', ], }, { name: 'tbl_document_operation_history', type: 'base', fields: [ { name: 'doh_id', type: 'text', required: true }, { name: 'doh_document_id', type: 'text', required: true }, { name: 'doh_operation_type', type: 'text' }, { name: 'doh_user_id', type: 'text' }, { name: 'doh_current_count', type: 'number' }, { name: 'doh_remark', type: 'text' }, ], indexes: [ 'CREATE UNIQUE INDEX idx_tbl_document_operation_history_doh_id ON tbl_document_operation_history (doh_id)', 'CREATE INDEX idx_tbl_document_operation_history_doh_document_id ON tbl_document_operation_history (doh_document_id)', 'CREATE INDEX idx_tbl_document_operation_history_doh_user_id ON tbl_document_operation_history (doh_user_id)', 'CREATE INDEX idx_tbl_document_operation_history_doh_operation_type ON tbl_document_operation_history (doh_operation_type)', ], }, ]; function normalizeFieldPayload(field, existingField) { const payload = existingField ? Object.assign({}, existingField) : { name: field.name, type: field.type, }; if (existingField && existingField.id) { payload.id = existingField.id; } payload.name = field.name; payload.type = field.type; if (typeof field.required !== 'undefined') { payload.required = field.required; } if (field.type === 'file') { payload.maxSelect = typeof field.maxSelect === 'number' ? field.maxSelect : 0; payload.maxSize = typeof field.maxSize === 'number' ? field.maxSize : 0; payload.mimeTypes = Array.isArray(field.mimeTypes) && field.mimeTypes.length ? field.mimeTypes : null; } if (field.type === 'autodate') { payload.onCreate = typeof field.onCreate === 'boolean' ? field.onCreate : true; payload.onUpdate = typeof field.onUpdate === 'boolean' ? field.onUpdate : false; } return payload; } function buildCollectionPayload(collectionData, existingCollection) { if (!existingCollection) { return { name: collectionData.name, type: collectionData.type, listRule: Object.prototype.hasOwnProperty.call(collectionData, 'listRule') ? collectionData.listRule : null, viewRule: Object.prototype.hasOwnProperty.call(collectionData, 'viewRule') ? collectionData.viewRule : null, createRule: Object.prototype.hasOwnProperty.call(collectionData, 'createRule') ? collectionData.createRule : null, updateRule: Object.prototype.hasOwnProperty.call(collectionData, 'updateRule') ? collectionData.updateRule : null, deleteRule: Object.prototype.hasOwnProperty.call(collectionData, 'deleteRule') ? collectionData.deleteRule : null, fields: collectionData.fields.map((field) => normalizeFieldPayload(field, null)), indexes: collectionData.indexes, }; } const targetFieldMap = new Map(collectionData.fields.map((field) => [field.name, field])); const fields = (existingCollection.fields || []).map((existingField) => { const targetField = targetFieldMap.get(existingField.name); if (!targetField) { return existingField; } targetFieldMap.delete(existingField.name); return normalizeFieldPayload(targetField, existingField); }); for (const field of targetFieldMap.values()) { fields.push(normalizeFieldPayload(field, null)); } return { name: collectionData.name, type: collectionData.type, listRule: Object.prototype.hasOwnProperty.call(collectionData, 'listRule') ? collectionData.listRule : existingCollection.listRule, viewRule: Object.prototype.hasOwnProperty.call(collectionData, 'viewRule') ? collectionData.viewRule : existingCollection.viewRule, createRule: Object.prototype.hasOwnProperty.call(collectionData, 'createRule') ? collectionData.createRule : existingCollection.createRule, updateRule: Object.prototype.hasOwnProperty.call(collectionData, 'updateRule') ? collectionData.updateRule : existingCollection.updateRule, deleteRule: Object.prototype.hasOwnProperty.call(collectionData, 'deleteRule') ? collectionData.deleteRule : existingCollection.deleteRule, fields: fields, indexes: collectionData.indexes, }; } function normalizeFieldList(fields) { return (fields || []).map((field) => ({ name: field.name, type: field.type, required: !!field.required, })); } async function createOrUpdateCollection(collectionData) { console.log(`🔄 正在处理表: ${collectionData.name} ...`); try { const existing = await pb.collections.getOne(collectionData.name); await pb.collections.update(existing.id, buildCollectionPayload(collectionData, existing)); console.log(`♻️ ${collectionData.name} 已存在,已按最新结构更新。`); } catch (error) { if (error.status === 404) { try { await pb.collections.create(buildCollectionPayload(collectionData, null)); console.log(`✅ ${collectionData.name} 创建完成。`); return; } catch (createError) { console.error(`❌ 创建集合 ${collectionData.name} 失败:`, { status: createError.status, message: createError.message, response: createError.response?.data, }); throw createError; } } console.error(`❌ 处理集合 ${collectionData.name} 失败:`, { status: error.status, message: error.message, response: error.response?.data, }); throw error; } } async function verifyCollections(targetCollections) { console.log('\n🔍 开始校验文档相关表结构与索引...'); for (const target of targetCollections) { const remote = await pb.collections.getOne(target.name); const remoteFields = normalizeFieldList(remote.fields); const targetFields = normalizeFieldList(target.fields); const remoteFieldMap = new Map(remoteFields.map((field) => [field.name, field.type])); const remoteRequiredMap = new Map(remoteFields.map((field) => [field.name, field.required])); const missingFields = []; const mismatchedTypes = []; const mismatchedRequired = []; for (const field of targetFields) { if (!remoteFieldMap.has(field.name)) { missingFields.push(field.name); continue; } if (remoteFieldMap.get(field.name) !== field.type) { mismatchedTypes.push(`${field.name}:${remoteFieldMap.get(field.name)}!=${field.type}`); } if (remoteRequiredMap.get(field.name) !== !!field.required) { mismatchedRequired.push(`${field.name}:${remoteRequiredMap.get(field.name)}!=${!!field.required}`); } } const remoteIndexes = new Set(remote.indexes || []); const missingIndexes = target.indexes.filter((indexSql) => !remoteIndexes.has(indexSql)); if (remote.type !== target.type) { throw new Error(`${target.name} 类型不匹配,期望 ${target.type},实际 ${remote.type}`); } if (!missingFields.length && !mismatchedTypes.length && !mismatchedRequired.length && !missingIndexes.length) { console.log(`✅ ${target.name} 校验通过。`); continue; } console.log(`❌ ${target.name} 校验失败:`); if (missingFields.length) { console.log(` - 缺失字段: ${missingFields.join(', ')}`); } if (mismatchedTypes.length) { console.log(` - 字段类型不匹配: ${mismatchedTypes.join(', ')}`); } if (mismatchedRequired.length) { console.log(` - 字段必填属性不匹配: ${mismatchedRequired.join(', ')}`); } if (missingIndexes.length) { console.log(` - 缺失索引: ${missingIndexes.join(' | ')}`); } throw new Error(`${target.name} 结构与预期不一致`); } } async function init() { try { console.log(`🔄 正在连接 PocketBase: ${PB_URL}`); pb.authStore.save(AUTH_TOKEN, null); console.log('✅ 已使用 POCKETBASE_AUTH_TOKEN 载入认证状态。'); for (const collectionData of collections) { await createOrUpdateCollection(collectionData); } await verifyCollections(collections); console.log('\n🎉 文档相关表结构初始化并校验完成!'); } catch (error) { console.error('❌ 初始化失败:', error.response?.data || error.message); process.exitCode = 1; } } init();