diff --git a/backend/src/modules/migration/dto/enqueue-migration.dto.ts b/backend/src/modules/migration/dto/enqueue-migration.dto.ts new file mode 100644 index 0000000..945ed64 --- /dev/null +++ b/backend/src/modules/migration/dto/enqueue-migration.dto.ts @@ -0,0 +1,74 @@ +import { + IsString, + IsNotEmpty, + IsOptional, + IsNumber, + IsBoolean, + IsArray, +} from 'class-validator'; + +export class EnqueueMigrationDto { + @IsString() + @IsNotEmpty() + document_number!: string; + + @IsString() + @IsOptional() + title?: string; + + @IsString() + @IsOptional() + original_title?: string; + + @IsString() + @IsOptional() + category?: string; + + @IsString() + @IsOptional() + ai_summary?: string; + + @IsNumber() + @IsOptional() + project_id?: number; + + @IsNumber() + @IsOptional() + sender_org_id?: number; + + @IsNumber() + @IsOptional() + receiver_org_id?: number; + + @IsString() + @IsOptional() + issued_date?: string; + + @IsString() + @IsOptional() + received_date?: string; + + @IsString() + @IsOptional() + remarks?: string; + + @IsArray() + @IsOptional() + extracted_tags?: any[]; + + @IsNumber() + @IsOptional() + temp_attachment_id?: number; + + @IsBoolean() + @IsOptional() + is_valid?: boolean; + + @IsNumber() + @IsOptional() + confidence?: number; + + @IsArray() + @IsOptional() + ai_issues?: any[]; +} diff --git a/backend/src/modules/migration/dto/import-correspondence.dto.ts b/backend/src/modules/migration/dto/import-correspondence.dto.ts index 18b37ca..917685b 100644 --- a/backend/src/modules/migration/dto/import-correspondence.dto.ts +++ b/backend/src/modules/migration/dto/import-correspondence.dto.ts @@ -20,8 +20,12 @@ export class ImportCorrespondenceDto { category!: string; @IsString() - @IsNotEmpty() - source_file_path!: string; + @IsOptional() + source_file_path?: string; + + @IsNumber() + @IsOptional() + temp_attachment_id?: number; @IsNumber() @IsOptional() diff --git a/backend/src/modules/migration/entities/migration-review-queue.entity.ts b/backend/src/modules/migration/entities/migration-review-queue.entity.ts index 1c931dd..b51b32d 100644 --- a/backend/src/modules/migration/entities/migration-review-queue.entity.ts +++ b/backend/src/modules/migration/entities/migration-review-queue.entity.ts @@ -56,6 +56,33 @@ export class MigrationReviewQueue { @Column({ name: 'reviewed_at', type: 'timestamp', nullable: true }) reviewedAt?: Date; + @Column({ name: 'project_id', type: 'int', nullable: true }) + projectId?: number; + + @Column({ name: 'sender_organization_id', type: 'int', nullable: true }) + senderOrganizationId?: number; + + @Column({ name: 'receiver_organization_id', type: 'int', nullable: true }) + receiverOrganizationId?: number; + + @Column({ name: 'received_date', type: 'date', nullable: true }) + receivedDate?: Date; + + @Column({ name: 'issued_date', type: 'date', nullable: true }) + issuedDate?: Date; + + @Column({ type: 'text', nullable: true }) + remarks?: string; + + @Column({ name: 'ai_summary', type: 'text', nullable: true }) + aiSummary?: string; + + @Column({ name: 'extracted_tags', type: 'json', nullable: true }) + extractedTags?: any; + + @Column({ name: 'temp_attachment_id', type: 'int', nullable: true }) + tempAttachmentId?: number; + @CreateDateColumn({ name: 'created_at' }) createdAt!: Date; } diff --git a/backend/src/modules/migration/migration.controller.ts b/backend/src/modules/migration/migration.controller.ts index 91bb290..2948966 100644 --- a/backend/src/modules/migration/migration.controller.ts +++ b/backend/src/modules/migration/migration.controller.ts @@ -1,6 +1,7 @@ import { Controller, Post, Body, Headers, UseGuards, Get, Param, Query, Res, ParseIntPipe } from '@nestjs/common'; import { MigrationService } from './migration.service'; import { ImportCorrespondenceDto } from './dto/import-correspondence.dto'; +import { EnqueueMigrationDto } from './dto/enqueue-migration.dto'; import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard'; import { CurrentUser } from '../../common/decorators/current-user.decorator'; import { ApiTags, ApiOperation, ApiBearerAuth, ApiHeader, ApiQuery, ApiParam } from '@nestjs/swagger'; @@ -30,6 +31,13 @@ export class MigrationController { return this.migrationService.importCorrespondence(dto, idempotencyKey, userId); } + @Post('queue') + @UseGuards(JwtAuthGuard) + @ApiOperation({ summary: 'Enqueue a record into the staging migration review queue' }) + async enqueueRecord(@Body() dto: EnqueueMigrationDto) { + return this.migrationService.enqueueRecord(dto); + } + @Get('queue') @UseGuards(JwtAuthGuard) @ApiOperation({ summary: 'Get migration review queue' }) diff --git a/backend/src/modules/migration/migration.service.ts b/backend/src/modules/migration/migration.service.ts index 7edda80..76fc1ba 100644 --- a/backend/src/modules/migration/migration.service.ts +++ b/backend/src/modules/migration/migration.service.ts @@ -8,6 +8,7 @@ import { import { InjectRepository } from '@nestjs/typeorm'; import { Repository, DataSource } from 'typeorm'; import { ImportCorrespondenceDto } from './dto/import-correspondence.dto'; +import { EnqueueMigrationDto } from './dto/enqueue-migration.dto'; import { ImportTransaction } from './entities/import-transaction.entity'; import { Correspondence } from '../correspondence/entities/correspondence.entity'; import { CorrespondenceRevision } from '../correspondence/entities/correspondence-revision.entity'; @@ -21,6 +22,7 @@ import { } from './entities/migration-review-queue.entity'; import { MigrationError } from './entities/migration-error.entity'; import { MigrationQueueQueryDto } from './dto/migration-queue-query.dto'; +import { Attachment } from '../../common/file-storage/entities/attachment.entity'; import { createReadStream, existsSync } from 'fs'; import * as path from 'path'; @Injectable() @@ -177,7 +179,20 @@ export class MigrationService { // 4. File Handling let attachmentId: number | null = null; - if (dto.source_file_path) { + if (dto.temp_attachment_id) { + attachmentId = dto.temp_attachment_id; + try { + // Mark attachment as permanent + await queryRunner.manager.update( + Attachment, + { id: attachmentId }, + { isTemporary: false } + ); + } catch (fileError: unknown) { + const errMsg = fileError instanceof Error ? fileError.message : String(fileError); + this.logger.warn(`Failed to update temp_file [id:${attachmentId}]: ${errMsg}`); + } + } else if (dto.source_file_path) { try { const attachment = await this.fileStorageService.importStagingFile( dto.source_file_path, @@ -360,6 +375,63 @@ export class MigrationService { await queryRunner.release(); } } + + async enqueueRecord(dto: EnqueueMigrationDto) { + if (!dto.document_number) { + throw new BadRequestException('document_number is required'); + } + + // Determine status based on confidence policy in ADR-017 + let autoStatus = MigrationReviewStatus.PENDING; + if (dto.is_valid === false || (dto.confidence != null && dto.confidence < 0.60)) { + autoStatus = MigrationReviewStatus.REJECTED; + } + + // Upsert or create new queue item + let queueItem = await this.reviewQueueRepo.findOne({ + where: { documentNumber: dto.document_number }, + }); + + if (!queueItem) { + queueItem = this.reviewQueueRepo.create({ + documentNumber: dto.document_number, + }); + } + + queueItem.title = dto.title; + queueItem.originalTitle = dto.original_title; + queueItem.aiSuggestedCategory = dto.category; + queueItem.aiConfidence = dto.confidence; + queueItem.aiIssues = dto.ai_issues; + queueItem.projectId = dto.project_id; + queueItem.senderOrganizationId = dto.sender_org_id; + queueItem.receiverOrganizationId = dto.receiver_org_id; + queueItem.remarks = dto.remarks; + queueItem.aiSummary = dto.ai_summary; + queueItem.extractedTags = dto.extracted_tags; + queueItem.tempAttachmentId = dto.temp_attachment_id; + queueItem.status = autoStatus; + + if (dto.issued_date) { + const parsed = new Date(dto.issued_date); + if (!isNaN(parsed.getTime())) queueItem.issuedDate = parsed; + } + if (dto.received_date) { + const parsed = new Date(dto.received_date); + if (!isNaN(parsed.getTime())) queueItem.receivedDate = parsed; + } + + await this.reviewQueueRepo.save(queueItem); + + this.logger.log(`Enqueued document [${dto.document_number}] to staging queue with status [${autoStatus}]`); + + return { + message: 'Document enqueued successfully', + id: queueItem.id, + status: autoStatus, + }; + } + async getReviewQueue(query: MigrationQueueQueryDto) { const { page = 1, limit = 10, status } = query; const skip = (page - 1) * limit; diff --git a/specs/03-Data-and-Storage/03-04-legacy-data-migration.md b/specs/03-Data-and-Storage/03-04-legacy-data-migration.md index 16dca38..cc1fca6 100644 --- a/specs/03-Data-and-Storage/03-04-legacy-data-migration.md +++ b/specs/03-Data-and-Storage/03-04-legacy-data-migration.md @@ -206,206 +206,102 @@ n8n ต้องเก็บ categories นี้ไว้ใน Workflow Variab #### Node 1: Data Reader & Checkpoint -- อ่าน Checkpoint จาก **MariaDB Node แยก** (ไม่ใช่ async call ใน Code Node) -- Batch ทีละ **10–20 แถว** ตาม `$env.MIGRATION_BATCH_SIZE` -- ติด `original_index` ทุก Item +#### Node 1: Data Reader & Checkpoint -**Encoding Normalization:** -```javascript -// Normalize ข้อมูลจาก Excel เป็น UTF-8 NFC ก่อนประมวลผล -const normalize = (str) => { - if (!str) return ''; - return Buffer.from(str, 'utf8').toString('utf8').normalize('NFC'); -}; +- อ่าน Checkpoint จาก **MariaDB Node แยก** +- Batch ทีละ **50–100 แถว** ตาม `$env.MIGRATION_BATCH_SIZE` (ควรจำกัด Batch Size ป้องกัน DB Connection Overload) +- ติด `original_index` ทุก Item และ Normalize Encoding (UTF-8 NFC) สำหรับ ชื่อไฟล์ และ เลขเอกสารเก่า -return items.map(item => ({ - ...item, - json: { - ...item.json, - document_number: normalize(item.json.document_number), - title: normalize(item.json.title), - // Mapping เลขอ้างอิงเก่า (Legacy Number) เพื่อนำไปเก็บใน details JSON - legacy_document_number: item.json.document_number - } -})); -``` +#### Node 2: DB Lookup & Data Augmentation -#### Node 2: File Validator & Sanitizer +- **Task:** ให้ n8n นำข้อมูลจาก Excel (เช่น รหัสโปรเจ็กต์, รหัสผู้ส่ง) ยิงคำสั่ง Query ไปยัง MariaDB เพื่อแปลงเป็น `id` +- **Queries:** + 1. แปลง `project_code` -> `project_id` + 2. แปลง `sender_code` -> `sender_organization_id` + 3. แปลง `receiver_code` -> `receiver_organization_id` + 4. หา Tags ที่มีอยู่ในโปรเจ็กต์: `SELECT * FROM tags WHERE project_id = {{project_id}}` +- **Output:** n8n เก็บ `project_id`, `organization_ids` และ `existing_tags_json` ไว้ในแต่ละ item +- *ถ้าหารหัสโปรเจ็กต์ไม่เจอ ให้ส่งเข้า Error Log ไม่ทำต่อ* -- ตรวจสอบไฟล์ PDF มีอยู่จริงบน NAS -- Normalize ชื่อไฟล์เป็น **UTF-8 NFC** -- Path Traversal Guard: resolved path ต้องอยู่ใน `/share/np-dms/staging_ai` เท่านั้น -- **Output 0** → valid → Node 3 -- **Output 1** → error → Node 5D (ไม่หายเงียบ) +#### Node 3: File Processor (Extract PDF Text & Temp Upload) -#### Node 3: AI Analysis (Sequential เท่านั้น) +- ตรวจสอบไฟล์ PDF มีอยู่จริงบน NAS `/share/np-dms/staging_ai` +- **Extract PDF Text:** ใช้ Apache Tika สกัดข้อความจากเอกสาร +- **Two-Phase Storage (Upload):** + - n8n ยิง `POST /api/storage/upload` ส่งไฟล์ PDF เข้า Backend + - Backend อัพโหลดไฟล์, กำหนด `is_temporary = TRUE` + - Backend ส่งคืน `attachment_id` ให้ n8n (จะเรียกว่า `temp_attachment_id`) + +#### Node 4: AI Analysis (Sequential เท่านั้น) **System Prompt:** ```text You are a Document Controller for a large construction project. -Your task is to validate document metadata and suggest relevant tags. -You MUST respond ONLY with valid JSON. No explanation, no markdown, no extra text. -If there are no issues, "detected_issues" must be an empty array []. +Your task is to validate document metadata, summarize content, and suggest relevant tags. +You MUST respond ONLY with valid JSON. No explanation, no markdown. ``` -**User Prompt (Category List มาจาก Backend ไม่ hardcode):** +**User Prompt:** ```text -Validate this document metadata and respond in JSON: - +Validate and summarize this document. Respond in JSON. Document Number: {{$json.document_number}} Title: {{$json.title}} -Expected Pattern: [ORG]-[TYPE]-[SEQ] e.g. "TCC-COR-0001" -Category List (MUST match system enum exactly): {{$workflow.variables.system_categories}} +Extracted Text: {{$json.extracted_text}} -Analyze the document and suggest relevant tags based on: -1. Document content/title keywords (e.g., "Foundation", "Structure", "Electrical", "Safety") -2. Document type indicators (e.g., "Drawing", "Report", "Inspection") -3. Organization codes present in document number -4. Any discipline or phase indicators +Existing Project Tags: {{$json.existing_tags_json}} + +Analyze the content to provide: +1. Validation of Subject/Dates with PDF text. +2. A 4-5 sentence summary. +3. Suggest tags. Select from Existing Project Tags if applicable. If no existing tag fits, suggest a NEW one (set is_new: true). Respond ONLY with this exact JSON structure: { "is_valid": true | false, "confidence": 0.0 to 1.0, - "suggested_category": "", - "detected_issues": [""], - "suggested_title": "", - "suggested_tags": ["", ""], - "tag_confidence": 0.0 to 1.0 + "category": "Correspondence", + "summary": "<4-5 sentence summary>", + "suggested_tags": [ + {"name": "Structural", "description": "...", "is_new": false} + ], + "detected_issues": [] } ``` -**JSON Validation (ตรวจ Category ตรง Enum + Tag Normalization):** -```javascript -const systemCategories = $workflow.variables.system_categories; -if (!systemCategories.includes(result.suggested_category)) { - throw new Error(`Category "${result.suggested_category}" not in system enum: ${systemCategories.join(', ')}`); -} +#### Node 5: Staging Ingestion (Insert to Review Queue) -// Tag Validation -if (!Array.isArray(result.suggested_tags)) { - result.suggested_tags = []; -} -// Normalize: trim, lowercase, remove duplicates -result.suggested_tags = [...new Set(result.suggested_tags.map(t => String(t).trim()).filter(t => t.length > 0))]; +ข้อมูลทั้งหมดที่ผ่าน n8n และ AI Model **จะต้องไม่ถูกอัพเดทเข้าตารางหลักอัตโนมัติ** แต่จะถูกบังคับนำเข้าตาราง Staging `migration_review_queue` แทน เพื่อรอมนุษย์จัดการผ่าน Frontend UI -// Tag confidence validation -if (typeof result.tag_confidence !== 'number' || result.tag_confidence < 0 || result.tag_confidence > 1) { - result.tag_confidence = 0.5; -} -``` +**Status Routing Policy:** +- `confidence >= 0.85` และ `is_valid = true` -> Status **`PENDING`** (พร้อมรับ Batch Import) +- `confidence >= 0.60` และ `< 0.85` -> Status **`PENDING`** (ติด Flag ให้ระวัง) +- `confidence < 0.60` หรือ `is_valid = false` -> Status **`REJECTED`** +- Parse Error / AI ไม่ตอบ -> **Error Log** (Node ถัดไป) -#### Node 3.5: Fallback Model Manager - -- อัปเดต `migration_fallback_state` ทุกครั้งที่เกิด Parse Error -- Auto-switch ไป `OLLAMA_MODEL_FALLBACK` เมื่อ Error ≥ `FALLBACK_ERROR_THRESHOLD` -- ส่ง Alert Email เมื่อ Fallback ถูก Activate - -#### Node 4: Confidence Router (4 outputs) - -| เงื่อนไข | การดำเนินการ | -| ------------------------------------------ | -------------------------------- | -| `confidence >= 0.85` และ `is_valid = true` | **Output 0** → Auto Ingest | -| `confidence >= 0.60` และ `< 0.85` | **Output 1** → Review Queue | -| `confidence < 0.60` หรือ `is_valid = false` | **Output 2** → Reject Log | -| Parse Error / AI ไม่ตอบ | **Output 3** → Error Log | -| Fallback: Error > 5 ใน 10 Request | สลับ Model / หยุด Workflow + Alert | - -**Revision Drift Protection:** -```javascript -// ถ้า Excel มี revision column — ตรวจสอบก่อน route -if (item.json.excel_revision !== undefined) { - const expectedRevision = (item.json.current_db_revision || 0) + 1; - if (parseInt(item.json.excel_revision) !== expectedRevision) { - item.json.review_reason = `Revision drift: Excel=${item.json.excel_revision}, Expected=${expectedRevision}`; - reviewQueue.push(item); - continue; - } -} -``` - -#### Node 5A: Auto Ingest — Backend API - -> ⚠️ **Storage Enforcement:** n8n ส่งแค่ `source_file_path` — Backend จะ generate UUID, enforce path strategy (`/share/np-dms/staging_ai/...`), และ move file atomically ผ่าน StorageService - -```http -POST /api/correspondences/import -Authorization: Bearer -Idempotency-Key: : -Content-Type: application/json -``` - -**Backend Tag Handling Logic:** - -เมื่อ Backend รับ Payload พร้อม `ai_tags` ระบบจะ: - -1. **Validate Tags:** ตรวจสอบว่า tag name อยู่ในรูปแบบที่ถูกต้อง (ไม่ว่าง, ไม่มีอักขระพิเศษ) -2. **Create Missing Tags:** ถ้า Tag ไม่มีอยู่ใน `tags` table → สร้างใหม่โดยอัตโนมัติ - ```sql - INSERT INTO tags (tag_name, created_by, created_at) - VALUES ('', (SELECT user_id FROM users WHERE username = 'migration_bot'), NOW()) - ON DUPLICATE KEY UPDATE id=LAST_INSERT_ID(id); - ``` -3. **Link Document Tags:** บันทึกความสัมพันธ์ใน `correspondence_tags` - ```sql - INSERT INTO correspondence_tags (correspondence_id, tag_id) - SELECT LAST_INSERT_ID(), tag_id FROM tags WHERE tag_name IN (); - ``` -4. **Tag Confidence Logging:** บันทึก `tag_confidence` ลงใน `details` JSON ของ Revision - -Payload: -```json -{ - "document_number": "{{document_number}}", - "title": "{{ai_result.suggested_title || title}}", - "category": "{{ai_result.suggested_category}}", - "source_file_path": "{{file_path}}", - "ai_confidence": "{{ai_result.confidence}}", - "ai_issues": "{{ai_result.detected_issues}}", - "ai_tags": "{{ai_result.suggested_tags}}", - "tag_confidence": "{{ai_result.tag_confidence}}", - "migrated_by": "SYSTEM_IMPORT", - "batch_id": "{{$env.MIGRATION_BATCH_ID}}" -} -``` - -**Audit Log ที่ Backend ต้องสร้าง:** -```json -{ - "action": "IMPORT", - "source": "MIGRATION", - "batch_id": "migration_20260226", - "created_by": "SYSTEM_IMPORT", - "metadata": { - "migration": true, - "batch_id": "migration_20260226", - "ai_confidence": 0.91 - } -} -``` - -**Checkpoint Update (ทุก 10 Records — ผ่าน IF Node + MariaDB Node):** +**Insert into staging:** ```sql -INSERT INTO migration_progress (batch_id, last_processed_index, status) -VALUES ('{{$env.MIGRATION_BATCH_ID}}', {{checkpoint_index}}, 'RUNNING') -ON DUPLICATE KEY UPDATE - last_processed_index = {{checkpoint_index}}, - updated_at = NOW(); +INSERT INTO migration_review_queue ( + document_number, title, project_id, sender_organization_id, receiver_organization_id, + received_date, issued_date, remarks, ai_suggested_category, ai_confidence, + ai_issues, ai_summary, extracted_tags, temp_attachment_id, status +) VALUES ( ... ) +ON DUPLICATE KEY UPDATE status = VALUES(status), ai_summary = VALUES(ai_summary); ``` -#### Node 5B: Review Queue +#### Node 6: Error Log & Reject Log -> ⚠️ **`migration_review_queue` เป็น Temporary Table เท่านั้น** — ห้ามสร้าง Correspondence record จนกว่า Admin จะ Approve +- Parse Error → เขียนลงไฟล์ `/share/np-dms/n8n/migration_logs/error_log.csv` +- ทุก 10-50 ราบการอัพเดท MariaDB `migration_progress` เพื่อเป็น Checkpoint. -Approval Flow: -``` -Review → Admin Approve → POST /api/correspondences/import (เหมือน Auto Ingest) -Admin Reject → ลบออกจาก queue ไม่สร้าง record -``` +--- -#### Node 5C: Reject Log → `/share/np-dms/n8n/migration_logs/reject_log.csv` +### Phase 4: Frontend Management & Final Commit (UI -> Backend API) -#### Node 5D: Error Log → `/share/np-dms/n8n/migration_logs/error_log.csv` + MariaDB +1. หน้าจอ **Frontend Management UI** ดึงข้อมูลจาก `migration_review_queue` +2. Admin สามารถ Browse & Edit ข้อมูล +3. **Tag Review:** Admin สามารถพิจารณา Tags ที่เป็น `is_new: true` ว่าควรตีตก หรือเปลี่ยนไปแมตช์ของเดิม +4. Admin กดปุ่ม **Execute Import** ส่งให้ Backend รัน Final Commit. +5. Backend ยิงคำสั่งสร้าง Correspondence, นำ `temp_attachment_id` ไปผูกกับ Revision, ปรับเป็น `is_temporary = FALSE` และสร้าง/เชื่อม Tags จริง. --- diff --git a/specs/03-Data-and-Storage/03-05-n8n-migration-setup-guide.md b/specs/03-Data-and-Storage/03-05-n8n-migration-setup-guide.md index adbee8b..bfd6fa0 100644 --- a/specs/03-Data-and-Storage/03-05-n8n-migration-setup-guide.md +++ b/specs/03-Data-and-Storage/03-05-n8n-migration-setup-guide.md @@ -35,8 +35,8 @@ │ └──────┬──────┘ │ │ │ │ │ ┌──────▼──────┐ ┌──────────────┐ ┌──────────────┐ │ -│ │Pre-flight │───▶│Fetch Categories│──▶│File Validator│ │ -│ │Checks │ │from Backend │ │+ Sanitize │ │ +│ │Pre-flight │───▶│DB Lookup & │──▶│File Upload & │ │ +│ │Checks │ │Data Fetch │ │Temp Storage │ │ │ └─────────────┘ └──────────────┘ └──────┬───────┘ │ │ │ │ │ ┌────────────────────────────┤ │ @@ -57,9 +57,9 @@ │ ┌─────────┘ │ └─────────┐ │ │ ▼ ▼ ▼ │ │ ┌──────┐ ┌──────────┐ ┌────────┐ │ -│ │Auto │ │ Review │ │Reject │ │ -│ │Ingest│ │ Queue │ │Log │ │ -│ │+Chkpt│ │(DB only) │ │(CSV) │ │ +│ │Review│ │ Review │ │Reject │ │ +│ │Queue │ │ Queue │ │Log │ │ +│ │(AUTO)│ │(FLAGGED) │ │(CSV) │ │ │ └──────┘ └──────────┘ └────────┘ │ │ │ └─────────────────────────────────────────────────────────────┘ @@ -227,70 +227,43 @@ mysql -h -u migration_bot -p lcbp3_production < lcbp3-v1.8.0-migration - เก็บค่า Config ทั้งหมดใน `$workflow.staticData.config` - อ่านผ่าน `$workflow.staticData.config.KEY` ใน Node อื่น -### Node 1-2: Pre-flight Checks -- ตรวจสอบ Backend Health -- ดึง Categories จาก `/api/master/correspondence-types` -- ดึง Tags ที่มีอยู่แล้วจาก `/api/tags` (สำหรับ AI Tag Extraction) -- ตรวจ File Mount (Read-only) -- เก็บ Categories และ Existing Tags ใน `$workflow.staticData.systemCategories` +### Node 1: Pre-flight Checks & Data Reader +- ตรวจสอบ Backend Health และ Ollama Ping +- อ่าน Checkpoint (`last_processed_index`) จาก `migration_progress` +- Batch ข้อมูลจาก Excel ตามตาราง `BATCH_SIZE` ปกติ (50-100) +- Normalize ข้อมูล UTF-8 (NFC) และสร้าง `original_index` -### Node 3: Read Checkpoint -- อ่าน `last_processed_index` จาก `migration_progress` -- ถ้าไม่มี เริ่มจาก 0 +### Node 2: DB Lookup & Categories Fetch +- ดึง Categories จาก `/api/meta/categories` เพื่อเตรียม Prompt +- Query ทะลวง DB: แปลงรหัสใน Excel (`project_code`, `sender`, `receiver`) ให้เป็น IDs จาก MariaDB +- Query ดึง Master Tags ของโปรเจ็กต์: `SELECT tag_name, description FROM tags WHERE project_id = ...` +- Output: แปลง ID เรียบร้อยและเตรียม `existing_tags_json` ให้ Ollama -### Node 4: Process Batch -- อ่าน Excel -- Normalize UTF-8 (NFC) -- ตัด Batch ตาม `BATCH_SIZE` +### Node 3: Text Extraction & Temp Upload +- ใช้ **Apache Tika** (ผ่าน `Extract PDF Text` node หรือ HTTP Request) สกัดข้อความ (OCR/Text) ออกจาก PDF ใน staging +- แนบไฟล์ไปยัง Backend: ยิง HTTP Request **`POST /api/storage/upload`** ของ Backend +- รอรับผลลัพธ์เป็น `temp_attachment_id` (หมายความว่าไฟล์นี้เข้าข่าย Temporary ถูกเก็บจัดการใน NAS เรียบร้อยแล้ว) +- Output: ไฟล์พร้อมใช้งาน, ได้เนื้อหา Text มาเตรียม prompt -### Node 5: File Validator -- Sanitize filename (replace special chars) -- Path traversal check -- ตรวจสอบไฟล์มีอยู่จริง -- **Output 2 ทาง**: Valid → AI, Error → Log +### Node 4: AI Analysis +- วาง System Prompt บังคับ Output JSON +- โยน Metadata (Title, Date, DB Lookups) พร้อม Extracted PDF Text คุยกับ **Ollama `llama3.2:3b`** +- ให้ AI วิเคราะห์ และสรุปเป็น `ai_summary` +- ให้ AI แนะนำ Tags ใหม่หรือเลือก Tags เดิมจาก `existing_tags_json` -### Node 6: Build AI Prompt -- ดึง Categories จาก `staticData` (ไม่ hardcode) -- เลือก Model ตาม Fallback State -- สร้าง Prompt ตาม Template พร้อม **Tag Extraction Instructions** -- AI จะวิเคราะห์ Title และ Document Number เพื่อสกัด Tags ที่เกี่ยวข้อง +### Node 5: Parse & Validate +- Schema Validation (ดูให้แน่ใจว่า AI ตอบ `is_valid`, `confidence`, `summary`, `suggested_tags`) +- Normalizing categories, trimming tags (`is_new: true / false` flag สำคัญมาก) +- จัดชุดค่า Status ใหม่ -### Node 7: Ollama AI Analysis -- เรียก `POST /api/generate` -- Timeout 30 วินาที -- Retry 3 ครั้ง (n8n built-in) -- AI Response รวม `suggested_tags` และ `tag_confidence` +### Node 6: Confidence Router & Staging Ingest +**แยกสาย 4 สาย:** +1. **PENDING (Auto Ready):** (`confidence ≥ 0.85` && `is_valid = true`) → INSERT เข้า `migration_review_queue` +2. **PENDING (Flagged):** (`confidence 0.60 - 0.84`) → INSERT เข้า `migration_review_queue` พร้อม Highlight/Remarks ให้ Admin ดูละเอียด +3. **REJECTED:** (`confidence < 0.60` หรือ `is_valid = false`) → INSERT เข้า `migration_review_queue` สถานะรอแก้แบบ Manual +4. **Error/Parse Fail:** ไปลง CSV Reject Log + DB `migration_errors` -### Node 8: Parse & Validate -- Parse JSON Response -- Schema Validation (is_valid, confidence, detected_issues) -- **Tag Validation**: Normalize tags (trim, lowercase, deduplicate) -- Enum Validation (ตรวจ Category ว่าอยู่ใน List หรือไม่) -- **Output 2 ทาง**: Success → Router, Error → Fallback - -### Node 9: Confidence Router -- **4 Outputs**: - 1. Auto Ingest (confidence ≥ 0.85 && is_valid) - 2. Review Queue (0.60 ≤ confidence < 0.85) - 3. Reject Log (confidence < 0.60 หรือ is_valid = false) - 4. Error Log (parse error) - -### Node 10A: Auto Ingest -- POST `/api/migration/import` -- Header: `Idempotency-Key: {doc_num}:{batch_id}` -- Payload รวม **ai_tags** และ **tag_confidence** -- Backend จะสร้าง Tags ที่ยังไม่มี และผูกกับเอกสารอัตโนมัติ -- บันทึก Checkpoint ทุก 10 records - -### Node 10B: Review Queue -- INSERT เข้า `migration_review_queue` เท่านั้น -- ยังไม่สร้าง Correspondence - -### Node 10C: Reject Log -- เขียน CSV ที่ `/home/node/.n8n-files/migration_logs/reject_log.csv` - -### Node 10D: Error Log -- เขียน CSV + INSERT เข้า `migration_errors` +**สำคัญมาก:** *n8n จะทำหน้าที่สูบข้อมูลและจัดเตรียมเข้า `migration_review_queue` เท่านั้น จะไม่มีการข้ามขั้นตอนไป Import ลงตารางหลัก `correspondences` อัตโนมัติ (Final Commit ต้องทำบน Frontend UI)* --- diff --git a/specs/03-Data-and-Storage/03-06-migration-business-scope.md b/specs/03-Data-and-Storage/03-06-migration-business-scope.md index 9cc9170..3c77198 100644 --- a/specs/03-Data-and-Storage/03-06-migration-business-scope.md +++ b/specs/03-Data-and-Storage/03-06-migration-business-scope.md @@ -189,6 +189,7 @@ T+1 เดือน: | Dry Run 2 AI Category Accuracy | ≥ 90% (Manual Spot-check 50 docs) | Human Review | | Idempotency Test: รัน Batch ซ้ำ | 0 Duplicate Records | SQL Count | | Organization Mapping ครบ | 100% | Lookup Table review | +| Frontend Review UI พร้อมใช้งาน | ✅ | UAT Passed สำหรับหน้าจออนุมัติ | | Migration Bot Token Active + Whitelisted | ✅ | API Test | | Staging NAS Space: ≥ 500GB free | ✅ | QNAP Dashboard | @@ -202,7 +203,7 @@ T+1 เดือน: |-------|---------| | Tier 1 Migration: 100% เสร็จ + Verified | ✅ | | Tier 2 Migration: ≥ 90% เสร็จ + Verified | ✅ | -| Review Queue: ≤ 5% ค้างอยู่ (Critical Tier 1 = 0%) | ✅ | +| Review Queue (รวมการพิจารณา AI New Tags): ≤ 5% ค้างอยู่ (Critical Tier 1 = 0%) | ✅ | | Migration Bot Token: REVOKED | ✅ | | Integrity Queries ผ่านทั้งหมด | ✅ | | Legacy System ยังเข้าถึงได้ (Read-only Fallback) | ✅ | @@ -229,7 +230,7 @@ T+1 เดือน: | **Organization Lookup Table** | Superadmin (NAP) | สร้างก่อน T-6 | | **Tier 1 Document List** | Document Control ทุก Org | ยืนยัน T-5 | | **Daily Monitoring (n8n Runs)** | Nattanin P. | T-3 ถึง Go-Live | -| **Admin Review Queue** | Document Control (สค.) | ทุกเช้าวันทำงาน | +| **Admin Review Queue & AI Tag Approval** | Document Control (สค.) | ทุกเช้าวันทำงาน (บังคับตรวจสอบ New Tags) | | **Post-migration Verification** | Nattanin P. | After each Gate | | **Legacy System Archival** | กทท. IT + NAP | T+30 | diff --git a/specs/03-Data-and-Storage/lcbp3-v1.8.0-migration.sql b/specs/03-Data-and-Storage/lcbp3-v1.8.0-migration.sql index 67e04fd..a8d1fad 100644 --- a/specs/03-Data-and-Storage/lcbp3-v1.8.0-migration.sql +++ b/specs/03-Data-and-Storage/lcbp3-v1.8.0-migration.sql @@ -25,9 +25,18 @@ CREATE TABLE IF NOT EXISTS migration_review_queue ( document_number VARCHAR(100) NOT NULL, title TEXT, original_title TEXT, + project_id INT NULL COMMENT 'Project ID จาก Lookups', + sender_organization_id INT NULL COMMENT 'Sender ID จาก Lookups', + receiver_organization_id INT NULL COMMENT 'Receiver ID จาก Lookups', + received_date DATE NULL COMMENT 'วันที่รับเอกสาร', + issued_date DATE NULL COMMENT 'วันที่ออกเอกสาร', + remarks TEXT COMMENT 'หมายเหตุจากหน้างาน (response)', ai_suggested_category VARCHAR(50), ai_confidence DECIMAL(4, 3), ai_issues JSON, + ai_summary TEXT COMMENT 'สรุปเนื้อหาจาก AI (4-5 บรรทัด)', + extracted_tags JSON COMMENT 'Tag ที่ AI นำเสนอหรือจับคู่ได้', + temp_attachment_id INT NULL COMMENT 'ID ของไฟล์ชั่วคราวจาก Two-Phase Storage', review_reason VARCHAR(255), STATUS ENUM('PENDING', 'APPROVED', 'REJECTED') DEFAULT 'PENDING', reviewed_by VARCHAR(100), diff --git a/specs/06-Decision-Records/ADR-017-ollama-data-migration.md b/specs/06-Decision-Records/ADR-017-ollama-data-migration.md index 00a21cc..c9b931d 100644 --- a/specs/06-Decision-Records/ADR-017-ollama-data-migration.md +++ b/specs/06-Decision-Records/ADR-017-ollama-data-migration.md @@ -85,14 +85,15 @@ | Component | รายละเอียด | | ---------------------- | ------------------------------------------------------------------------------- | | Migration Orchestrator | n8n (Docker บน QNAP NAS) | -| AI Model Primary | Ollama `llama3.2:3b` | +| AI Model Primary | Ollama `llama3.2:3b` (Validation, Summarization, Tagging) | | AI Model Fallback | Ollama `mistral:7b-instruct-q4_K_M` | | Hardware | QNAP NAS (Orchestrator) + Desktop Desk-5439 (AI Processing, RTX 2060 SUPER 8GB) | -| Data Ingestion | RESTful API + Migration Token (7 วัน) + Idempotency-Key Header | -| Concurrency | Sequential — 1 Request/ครั้ง, Delay 2 วินาที | -| Checkpoint | MariaDB `migration_progress` | +| DB Lookup (n8n) | n8n ทำการ Query `project_id`, `organization_id` และดึง `Tags` จาก DB ให้ AI | +| Data Ingestion | 1. Staging ลง `migration_review_queue` -> 2. กดยืนยันผ่าน Frontend Management UI -> 3. Final Commit ผ่าน API | +| Concurrency (n8n) | Sequential — Batch Size 50-100 ป้องกัน DB Connection Overload | +| Checkpoint | MariaDB `migration_progress` และการใช้ `ON DUPLICATE KEY UPDATE` ใน Staging | | Fallback | Auto-switch Model เมื่อ Error ≥ Threshold | -| Storage | Backend StorageService เท่านั้น — ห้าม move file โดยตรง | +| Storage | Two-Phase Storage: 1. `POST /api/storage/upload` (Temp) -> 2. Commit ภายหลัง | | Expected Runtime | ~16.6 ชั่วโมง (~3–4 คืน) สำหรับ 20,000 records | --- @@ -105,31 +106,42 @@ "confidence": 0.92, "suggested_category": "Correspondence", "detected_issues": [], - "suggested_title": null + "suggested_title": null, + "summary": "This document outlines the revised design specifications for the electrical subsystem in phase 2...", + "suggested_tags": [ + { "name": "Electrical", "description": "Electrical engineering design documents.", "is_new": false }, + { "name": "Phase2-Specs", "description": "Specific requirements for Phase 2 implementation.", "is_new": true } + ] } ``` | Field | Type | คำอธิบาย | | -------------------- | ------------------------- | --------------------------- | -| `is_valid` | boolean | เอกสารผ่านการตรวจสอบหรือไม่ | +| `is_valid` | boolean | เอกสารผ่านการตรวจสอบหรือไม่ (เปรียบเทียบ subject vs pdf) | | `confidence` | float (0.0–1.0) | ความมั่นใจของ AI | | `suggested_category` | string (enum จาก Backend) | หมวดหมู่ที่ AI แนะนำ | | `detected_issues` | string[] | รายการปัญหา (array ว่างถ้าไม่มี) | | `suggested_title` | string \| null | Title ที่แก้ไขแล้ว หรือ null | +| `summary` | string | สรุปเนื้อหา 4-5 ประโยค สำหรับใส่ใน `body` | +| `suggested_tags` | array of objects | รายการ Tags ที่จับคู่ได้ หรือ แนะนำให้สร้างใหม่ (`is_new: true`) | > ⚠️ **Patch:** `suggested_category` ต้องตรงกับ System Enum จาก `GET /api/meta/categories` เท่านั้น — ห้าม hardcode Category List ใน Prompt --- -## Confidence Threshold Policy +## Confidence Threshold Policy (Staging Logic) -| ระดับ Confidence | การดำเนินการ | +**ข้อมูลทุกชุดจาก n8n จะต้องถูกส่งเข้าตาราง `migration_review_queue` เสมอ** โดยจัดสถานะเบื้องต้นตาม Confidence: + +| ระดับ Confidence | สถานะใน Review Queue | | ------------------------------- | --------------------------------------- | -| `>= 0.85` และ `is_valid = true` | Auto Ingest เข้าระบบ | -| `0.60–0.84` | ส่งไป Human Review Queue | -| `< 0.60` หรือ `is_valid = false` | ส่งไป Reject Log รอ Manual Fix | +| `>= 0.85` และ `is_valid = true` | `PENDING` (พร้อมให้ Admin เลือก Batch Import) | +| `0.60–0.84` | `PENDING` (ไฮไลต์แจ้งให้ Admin ตรวจสอบข้อมูลก่อน) | +| `< 0.60` หรือ `is_valid = false` | `REJECTED` (รอให้ Admin แก้ไขข้อมูล Manual) | | AI Parse Error | ส่งไป Error Log + Trigger Fallback Logic | -| Revision Drift | ส่งไป Review Queue พร้อม reason | +| Revision Drift | `PENDING` พร้อมระบุ reason: "Revision drift" | + +> ⚠️ **Tag Review:** ข้อมูลใดที่มี `is_new: true` ใน `suggested_tags` จะถูกบังคับให้ Admin ตรวจสอบบน Frontend UI ก่อน เพื่อป้องกัน AI สร้าง Tags ขยะซ้ำซ้อน --- @@ -161,32 +173,45 @@ Hard Rules: --- -## Storage Governance (Patch) +## Storage Governance (Two-Phase Storage) **ข้อห้าม:** ``` ❌ mv /data/dms/staging_ai/TCC-COR-0001.pdf /final/path/... ``` -**ข้อบังคับ:** +**ข้อบังคับ (Two-Phase Strategy):** + +**Phase 1: Temp Upload (โดย n8n)** ``` -✅ POST /api/correspondences/import - body: { source_file_path: "/data/dms/staging_ai/TCC-COR-0001.pdf", ... } +✅ POST /api/storage/upload + (Upload ไฟล์ PDF ได้ผลลัพธ์เป็น attachment_id เช่น 1024) + *ไฟล์จะถูกระบุเป็น `is_temporary = TRUE`* ``` -Backend จะ: -1. Generate UUID -2. Enforce path strategy: `/data/dms/uploads/YYYY/MM/{uuid}.pdf` -3. Move file atomically ผ่าน StorageService -4. Create revision folder ถ้าจำเป็น +**Phase 2: Final Commit (โดย Frontend UI -> Backend API)** +``` +✅ POST /api/migration/commit_batch + body: { queue_ids: [1, 2, 3] } +``` + +Backend จะทำหน้าที่: +1. อ่านข้อมูลจาก `migration_review_queue` ซึ่งมี `temp_attachment_id` อยู่ +2. นำ `temp_attachment_id` ไปเชื่อมกับเอกสาร (Link to `correspondence_attachments`) +3. เปลี่ยนสถานะอัพเดต `is_temporary = FALSE` +4. Move ไฟล์ไปที่ `/data/dms/uploads/YYYY/MM/{uuid}.pdf` ผ่าน StorageService อย่างถูกต้อง --- -## Review Queue Contract +## Review Queue Contract & Frontend UI -- `migration_review_queue` เป็น **Temporary Table เท่านั้น** — ไม่ใช่ Business Schema -- ห้ามสร้าง Correspondence record จนกว่า Admin จะ Approve -- Approval Flow: `Review → Admin Approve → POST /api/correspondences/import` +- `migration_review_queue` เป็น **Staging Table หลัก** (ไม่ auto-ingest ข้ามขั้นตอนนี้) +- ห้ามสร้าง Correspondence record จนกว่า Admin จะสั่ง Execute การ Import จากหน้าจอ +- **Approval Flow:** + 1. N8N Insert เข้า `migration_review_queue` (พร้อม `temp_attachment_id`) + 2. Admin Review บน Frontend UI (ให้ความสำคัญกับการเช็ค `is_new: true` Tags) + 3. Admin เลือก Rows แล้วกด **"Execute Import"** + 4. Frontend ส่งคำสั่ง `POST /api/migration/commit_batch` ถือว่าเป็นการ Ingest ลงตาราง Business Schema จริง ---