260306:1535 20260306:1500 refactor tags
Some checks failed
Build and Deploy / deploy (push) Failing after 8m12s
Some checks failed
Build and Deploy / deploy (push) Failing after 8m12s
This commit is contained in:
@@ -5,6 +5,14 @@ import { Type } from 'class-transformer';
|
|||||||
import { ApiPropertyOptional } from '@nestjs/swagger';
|
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
|
|
||||||
export class SearchTagDto {
|
export class SearchTagDto {
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: 'ID โครงการ (ใช้กรอง Tag ของแต่ละโปรเจกต์)',
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@Type(() => Number)
|
||||||
|
@IsInt()
|
||||||
|
project_id?: number;
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: 'คำค้นหา (ชื่อ Tag หรือ คำอธิบาย)' })
|
@ApiPropertyOptional({ description: 'คำค้นหา (ชื่อ Tag หรือ คำอธิบาย)' })
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
|
|||||||
@@ -4,22 +4,37 @@ import {
|
|||||||
PrimaryGeneratedColumn,
|
PrimaryGeneratedColumn,
|
||||||
CreateDateColumn,
|
CreateDateColumn,
|
||||||
UpdateDateColumn,
|
UpdateDateColumn,
|
||||||
|
DeleteDateColumn,
|
||||||
|
Unique,
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
|
|
||||||
@Entity('tags')
|
@Entity('tags')
|
||||||
|
@Unique('ux_tag_project', ['project_id', 'tag_name'])
|
||||||
export class Tag {
|
export class Tag {
|
||||||
@PrimaryGeneratedColumn()
|
@PrimaryGeneratedColumn()
|
||||||
id!: number; // เพิ่ม !
|
id!: number; // เพิ่ม !
|
||||||
|
|
||||||
@Column({ length: 100, unique: true })
|
@Column({ type: 'int', nullable: true })
|
||||||
|
project_id!: number | null; // เพิ่ม !
|
||||||
|
|
||||||
|
@Column({ length: 100 })
|
||||||
tag_name!: string; // เพิ่ม !
|
tag_name!: string; // เพิ่ม !
|
||||||
|
|
||||||
|
@Column({ length: 30, default: 'default' })
|
||||||
|
color_code!: string; // เพิ่ม !
|
||||||
|
|
||||||
@Column({ type: 'text', nullable: true })
|
@Column({ type: 'text', nullable: true })
|
||||||
description!: string; // เพิ่ม !
|
description!: string | null; // เพิ่ม !
|
||||||
|
|
||||||
@CreateDateColumn()
|
@CreateDateColumn()
|
||||||
created_at!: Date; // เพิ่ม !
|
created_at!: Date; // เพิ่ม !
|
||||||
|
|
||||||
@UpdateDateColumn()
|
@UpdateDateColumn()
|
||||||
updated_at!: Date; // เพิ่ม !
|
updated_at!: Date; // เพิ่ม !
|
||||||
|
|
||||||
|
@Column({ type: 'int', nullable: true })
|
||||||
|
created_by!: number | null; // เพิ่ม !
|
||||||
|
|
||||||
|
@DeleteDateColumn()
|
||||||
|
deleted_at!: Date | null; // เพิ่ม !
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import { SaveNumberFormatDto } from './dto/save-number-format.dto'; // [New]
|
|||||||
|
|
||||||
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
||||||
import { RequirePermission } from '../../common/decorators/require-permission.decorator';
|
import { RequirePermission } from '../../common/decorators/require-permission.decorator';
|
||||||
|
import { CurrentUser } from '../../common/decorators/current-user.decorator';
|
||||||
|
|
||||||
@ApiTags('Master Data')
|
@ApiTags('Master Data')
|
||||||
@Controller('master')
|
@Controller('master')
|
||||||
@@ -216,8 +217,11 @@ export class MasterController {
|
|||||||
@Post('tags')
|
@Post('tags')
|
||||||
@RequirePermission('master_data.tag.manage')
|
@RequirePermission('master_data.tag.manage')
|
||||||
@ApiOperation({ summary: 'Create a new tag' })
|
@ApiOperation({ summary: 'Create a new tag' })
|
||||||
createTag(@Body() dto: CreateTagDto) {
|
createTag(
|
||||||
return this.masterService.createTag(dto);
|
@CurrentUser() user: { userId: number },
|
||||||
|
@Body() dto: CreateTagDto
|
||||||
|
) {
|
||||||
|
return this.masterService.createTag(dto, user.userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Patch('tags/:id')
|
@Patch('tags/:id')
|
||||||
|
|||||||
@@ -253,11 +253,21 @@ export class MasterService {
|
|||||||
// ... (Tag Logic เดิม คงไว้ตามปกติ) ...
|
// ... (Tag Logic เดิม คงไว้ตามปกติ) ...
|
||||||
async findAllTags(query?: SearchTagDto) {
|
async findAllTags(query?: SearchTagDto) {
|
||||||
const qb = this.tagRepo.createQueryBuilder('tag');
|
const qb = this.tagRepo.createQueryBuilder('tag');
|
||||||
if (query?.search) {
|
|
||||||
qb.where('tag.tag_name LIKE :search OR tag.description LIKE :search', {
|
if (query?.project_id) {
|
||||||
search: `%${query.search}%`,
|
qb.andWhere('tag.project_id = :projectId', {
|
||||||
|
projectId: query.project_id,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (query?.search) {
|
||||||
|
qb.andWhere(
|
||||||
|
'(tag.tag_name LIKE :search OR tag.description LIKE :search)',
|
||||||
|
{
|
||||||
|
search: `%${query.search}%`,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
qb.orderBy('tag.tag_name', 'ASC');
|
qb.orderBy('tag.tag_name', 'ASC');
|
||||||
if (query?.page && query?.limit) {
|
if (query?.page && query?.limit) {
|
||||||
const page = query.page;
|
const page = query.page;
|
||||||
@@ -278,8 +288,11 @@ export class MasterService {
|
|||||||
return tag;
|
return tag;
|
||||||
}
|
}
|
||||||
|
|
||||||
async createTag(dto: CreateTagDto) {
|
async createTag(dto: CreateTagDto, userId: number) {
|
||||||
const tag = this.tagRepo.create(dto);
|
const tag = this.tagRepo.create({
|
||||||
|
...dto,
|
||||||
|
created_by: userId,
|
||||||
|
});
|
||||||
return this.tagRepo.save(tag);
|
return this.tagRepo.save(tag);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,14 +2,52 @@
|
|||||||
|
|
||||||
import { GenericCrudTable } from "@/components/admin/reference/generic-crud-table";
|
import { GenericCrudTable } from "@/components/admin/reference/generic-crud-table";
|
||||||
import { masterDataService } from "@/lib/services/master-data.service";
|
import { masterDataService } from "@/lib/services/master-data.service";
|
||||||
|
import { projectService } from "@/lib/services/project.service";
|
||||||
import { CreateTagDto } from "@/types/dto/master/tag.dto";
|
import { CreateTagDto } from "@/types/dto/master/tag.dto";
|
||||||
import { ColumnDef } from "@tanstack/react-table";
|
import { ColumnDef } from "@tanstack/react-table";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
|
||||||
export default function TagsPage() {
|
export default function TagsPage() {
|
||||||
const columns: ColumnDef<any>[] = [
|
const { data: projectsData } = useQuery({
|
||||||
|
queryKey: ["projects"],
|
||||||
|
queryFn: () => projectService.getAll(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const projectOptions = [
|
||||||
|
{ label: "Global (All Projects)", value: "" },
|
||||||
|
...(projectsData || []).map((p: Record<string, unknown>) => ({
|
||||||
|
label: p.project_name || p.project_code || `Project ${p.id}`,
|
||||||
|
value: String(p.id),
|
||||||
|
})),
|
||||||
|
];
|
||||||
|
|
||||||
|
const columns: ColumnDef<Record<string, unknown>>[] = [
|
||||||
|
{
|
||||||
|
accessorKey: "project_id",
|
||||||
|
header: "Project",
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const pId = row.original.project_id;
|
||||||
|
if (!pId) return <span className="text-muted-foreground italic">Global</span>;
|
||||||
|
const p = (projectsData || []).find((proj: Record<string, unknown>) => proj.id === pId);
|
||||||
|
return p ? (p.project_name || p.project_code || `Project ${pId}`) as React.ReactNode : pId as React.ReactNode;
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "tag_name",
|
accessorKey: "tag_name",
|
||||||
header: "Tag Name",
|
header: "Tag Name",
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const color = row.original.color_code || 'default';
|
||||||
|
const isHex = color.startsWith('#');
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span
|
||||||
|
className="w-3 h-3 rounded-full border border-border"
|
||||||
|
style={{ backgroundColor: isHex ? color : (color === 'default' ? '#e2e8f0' : color) }}
|
||||||
|
/>
|
||||||
|
{row.original.tag_name}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "description",
|
accessorKey: "description",
|
||||||
@@ -17,24 +55,47 @@ export default function TagsPage() {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const formatPayload = (data: Record<string, unknown>) => {
|
||||||
|
const payload = { ...data };
|
||||||
|
if (!payload.project_id || payload.project_id === "") {
|
||||||
|
payload.project_id = null;
|
||||||
|
} else {
|
||||||
|
payload.project_id = Number(payload.project_id);
|
||||||
|
}
|
||||||
|
return payload;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GenericCrudTable
|
<GenericCrudTable
|
||||||
title="Tags"
|
title="Tags"
|
||||||
description="Manage system tags."
|
description="Manage system tags, multi-tenant capable."
|
||||||
entityName="Tag"
|
entityName="Tag"
|
||||||
queryKey={["tags"]}
|
queryKey={["tags"]}
|
||||||
fetchFn={() => masterDataService.getTags()}
|
fetchFn={() => masterDataService.getTags()}
|
||||||
createFn={(data: Record<string, unknown>) => masterDataService.createTag(data as unknown as CreateTagDto)}
|
createFn={(data: Record<string, unknown>) => masterDataService.createTag(formatPayload(data) as unknown as CreateTagDto)}
|
||||||
updateFn={(id, data) => masterDataService.updateTag(id, data)}
|
updateFn={(id, data) => masterDataService.updateTag(id, formatPayload(data))}
|
||||||
deleteFn={(id) => masterDataService.deleteTag(id)}
|
deleteFn={(id) => masterDataService.deleteTag(id)}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
fields={[
|
fields={[
|
||||||
|
{
|
||||||
|
name: "project_id",
|
||||||
|
label: "Project Scope",
|
||||||
|
type: "select",
|
||||||
|
options: projectOptions,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "tag_name",
|
name: "tag_name",
|
||||||
label: "Tag Name",
|
label: "Tag Name",
|
||||||
type: "text",
|
type: "text",
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "color_code",
|
||||||
|
label: "Color Code (Hex or Name)",
|
||||||
|
type: "text",
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "description",
|
name: "description",
|
||||||
label: "Description",
|
label: "Description",
|
||||||
|
|||||||
@@ -1,9 +1,15 @@
|
|||||||
// File: src/types/dto/master/tag.dto.ts
|
// File: src/types/dto/master/tag.dto.ts
|
||||||
|
|
||||||
export interface CreateTagDto {
|
export interface CreateTagDto {
|
||||||
|
/** ID โครงการ (NULL = Global) */
|
||||||
|
project_id?: number | null;
|
||||||
|
|
||||||
/** ชื่อ Tag (เช่น 'URGENT') */
|
/** ชื่อ Tag (เช่น 'URGENT') */
|
||||||
tag_name: string;
|
tag_name: string;
|
||||||
|
|
||||||
|
/** รหัสสี หรือชื่อคลาสสำหรับ UI */
|
||||||
|
color_code?: string;
|
||||||
|
|
||||||
/** คำอธิบาย */
|
/** คำอธิบาย */
|
||||||
description?: string;
|
description?: string;
|
||||||
}
|
}
|
||||||
@@ -11,6 +17,9 @@ export interface CreateTagDto {
|
|||||||
export type UpdateTagDto = Partial<CreateTagDto>;
|
export type UpdateTagDto = Partial<CreateTagDto>;
|
||||||
|
|
||||||
export interface SearchTagDto {
|
export interface SearchTagDto {
|
||||||
|
/** ID โครงการ (ใช้กรอง Tag ของแต่ละโปรเจกต์) */
|
||||||
|
project_id?: number;
|
||||||
|
|
||||||
/** คำค้นหา (ชื่อ Tag หรือ คำอธิบาย) */
|
/** คำค้นหา (ชื่อ Tag หรือ คำอธิบาย) */
|
||||||
search?: string;
|
search?: string;
|
||||||
|
|
||||||
|
|||||||
@@ -434,31 +434,38 @@ SET NULL - INDEX (is_active) - INDEX (email) ** Relationships **: - Parent: orga
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 3.6 tags
|
### 3.6 tags (UPDATE v1.8.0)
|
||||||
|
|
||||||
**Purpose**: Master table for document tagging system
|
**Purpose**: Master table for document tagging system (Supports multi-tenant per project)
|
||||||
|
|
||||||
| Column Name | Data Type | Constraints | Description |
|
| Column Name | Data Type | Constraints | Description |
|
||||||
| ----------- | ------------ | ----------------------------------- | ------------------------- |
|
| -------------- | --------------- | ----------------------------------- | --------------------------------------- |
|
||||||
| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique tag ID |
|
| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique tag ID |
|
||||||
| tag_name | VARCHAR(100) | NOT NULL, UNIQUE | Tag name |
|
| **project_id** | **INT** | **NULL, FK** | **[NEW] Project scope (NULL = Global)** |
|
||||||
| description | TEXT | NULL | Tag description |
|
| tag_name | VARCHAR(100) | NOT NULL | Tag name |
|
||||||
| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | Record creation timestamp |
|
| **color_code** | **VARCHAR(30)** | **DEFAULT 'default'** | **[NEW] UI Color/Class Code** |
|
||||||
| updated_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP ON UPDATE | Last update timestamp |
|
| description | TEXT | NULL | Tag description |
|
||||||
|
| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | Record creation timestamp |
|
||||||
|
| updated_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP ON UPDATE | Last update timestamp |
|
||||||
|
| **created_by** | **INT** | **NULL, FK** | **[NEW] User who created the tag** |
|
||||||
|
| **deleted_at** | **DATETIME** | **NULL** | **[NEW] Soft delete timestamp** |
|
||||||
|
|
||||||
**Indexes**:
|
**Indexes**:
|
||||||
|
|
||||||
* PRIMARY KEY (id)
|
* PRIMARY KEY (id)
|
||||||
* UNIQUE (tag_name)
|
* **FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE**
|
||||||
* INDEX (tag_name) - For autocomplete
|
* **FOREIGN KEY (created_by) REFERENCES users(user_id) ON DELETE SET NULL**
|
||||||
|
* **UNIQUE KEY (project_id, tag_name)**
|
||||||
|
* INDEX (deleted_at)
|
||||||
|
|
||||||
**Relationships**:
|
**Relationships**:
|
||||||
|
|
||||||
|
* Parent: projects, users
|
||||||
* Referenced by: correspondence_tags
|
* Referenced by: correspondence_tags
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 3.7 correspondence_tags
|
### 3.7 correspondence_tags (UPDATE v1.8.0)
|
||||||
|
|
||||||
**Purpose**: Junction table linking correspondences to tags (M:N)
|
**Purpose**: Junction table linking correspondences to tags (M:N)
|
||||||
|
|
||||||
@@ -472,7 +479,7 @@ SET NULL - INDEX (is_active) - INDEX (email) ** Relationships **: - Parent: orga
|
|||||||
* PRIMARY KEY (correspondence_id, tag_id)
|
* PRIMARY KEY (correspondence_id, tag_id)
|
||||||
* FOREIGN KEY (correspondence_id) REFERENCES correspondences(id) ON DELETE CASCADE
|
* FOREIGN KEY (correspondence_id) REFERENCES correspondences(id) ON DELETE CASCADE
|
||||||
* FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE
|
* FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE
|
||||||
* INDEX (tag_id)
|
* **INDEX idx_tag_lookup (tag_id) - For reverse lookup (Find documents by tag)**
|
||||||
|
|
||||||
**Relationships**:
|
**Relationships**:
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,9 @@
|
|||||||
-- รัน: mysql < 01-schema-drop.sql แล้วจึงรัน 02-schema-tables.sql
|
-- รัน: mysql < 01-schema-drop.sql แล้วจึงรัน 02-schema-tables.sql
|
||||||
-- ==========================================================
|
-- ==========================================================
|
||||||
SET NAMES utf8mb4;
|
SET NAMES utf8mb4;
|
||||||
|
|
||||||
SET time_zone = '+07:00';
|
SET time_zone = '+07:00';
|
||||||
|
|
||||||
SET FOREIGN_KEY_CHECKS = 0;
|
SET FOREIGN_KEY_CHECKS = 0;
|
||||||
|
|
||||||
-- =====================================================
|
-- =====================================================
|
||||||
@@ -342,11 +344,21 @@ CREATE TABLE correspondence_revisions (
|
|||||||
-- ตาราง Master เก็บ Tags ทั้งหมดที่ใช้ในระบบ
|
-- ตาราง Master เก็บ Tags ทั้งหมดที่ใช้ในระบบ
|
||||||
CREATE TABLE tags (
|
CREATE TABLE tags (
|
||||||
id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง',
|
id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง',
|
||||||
tag_name VARCHAR(100) NOT NULL UNIQUE COMMENT 'ชื่อ Tag',
|
project_id INT NULL COMMENT 'ID โครงการ (NULL = Global Tag)',
|
||||||
|
tag_name VARCHAR(100) NOT NULL COMMENT 'ชื่อ Tag',
|
||||||
|
color_code VARCHAR(30) DEFAULT 'default' COMMENT 'รหัสสี หรือชื่อคลาสสำหรับ UI',
|
||||||
description TEXT COMMENT 'คำอธิบายแท็ก',
|
description TEXT COMMENT 'คำอธิบายแท็ก',
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้าง',
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้าง',
|
||||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'วันที่แก้ไขล่าสุด '
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'วันที่แก้ไขล่าสุด',
|
||||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง Master เก็บ Tags ทั้งหมดที่ใช้ในระบบ';
|
created_by INT COMMENT 'ผู้สร้าง',
|
||||||
|
deleted_at DATETIME NULL COMMENT 'ลบแบบ Soft Delete',
|
||||||
|
-- Constraints & Indexes
|
||||||
|
FOREIGN KEY (project_id) REFERENCES projects (id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (created_by) REFERENCES users (user_id) ON DELETE
|
||||||
|
SET NULL,
|
||||||
|
UNIQUE KEY ux_tag_project (project_id, tag_name),
|
||||||
|
INDEX idx_tags_deleted_at (deleted_at)
|
||||||
|
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง Master เก็บ Tags ย่อยตาม Project';
|
||||||
|
|
||||||
-- ตารางเชื่อมระหว่าง correspondences และ tags (M:N)
|
-- ตารางเชื่อมระหว่าง correspondences และ tags (M:N)
|
||||||
CREATE TABLE correspondence_tags (
|
CREATE TABLE correspondence_tags (
|
||||||
@@ -354,8 +366,9 @@ CREATE TABLE correspondence_tags (
|
|||||||
tag_id INT COMMENT 'ID ของ Tag',
|
tag_id INT COMMENT 'ID ของ Tag',
|
||||||
PRIMARY KEY (correspondence_id, tag_id),
|
PRIMARY KEY (correspondence_id, tag_id),
|
||||||
FOREIGN KEY (correspondence_id) REFERENCES correspondences (id) ON DELETE CASCADE,
|
FOREIGN KEY (correspondence_id) REFERENCES correspondences (id) ON DELETE CASCADE,
|
||||||
FOREIGN KEY (tag_id) REFERENCES tags (id) ON DELETE CASCADE
|
FOREIGN KEY (tag_id) REFERENCES tags (id) ON DELETE CASCADE,
|
||||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตารางเชื่อมระหว่าง correspondences และ tags (M :N)';
|
INDEX idx_tag_lookup (tag_id)
|
||||||
|
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตารางเชื่อมระหว่าง correspondences และ tags (M:N)';
|
||||||
|
|
||||||
-- ตารางเชื่อมการอ้างอิงระหว่างเอกสาร (M:N)
|
-- ตารางเชื่อมการอ้างอิงระหว่างเอกสาร (M:N)
|
||||||
CREATE TABLE correspondence_references (
|
CREATE TABLE correspondence_references (
|
||||||
@@ -1340,5 +1353,3 @@ CREATE TABLE workflow_histories (
|
|||||||
CREATE INDEX idx_wf_hist_instance ON workflow_histories (instance_id);
|
CREATE INDEX idx_wf_hist_instance ON workflow_histories (instance_id);
|
||||||
|
|
||||||
CREATE INDEX idx_wf_hist_user ON workflow_histories (action_by_user_id);
|
CREATE INDEX idx_wf_hist_user ON workflow_histories (action_by_user_id);
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,48 +1,41 @@
|
|||||||
{
|
{
|
||||||
"name": "LCBP3 Migration Workflow v1.8.0",
|
"name": "LCBP3 Migration Workflow v1.8.0",
|
||||||
"meta": {
|
|
||||||
"instanceId": "lcbp3-migration-free"
|
|
||||||
},
|
|
||||||
"settings": {
|
|
||||||
"executionOrder": "v1"
|
|
||||||
},
|
|
||||||
"nodes": [
|
"nodes": [
|
||||||
{
|
{
|
||||||
"parameters": {},
|
"parameters": {},
|
||||||
"id": "trigger-manual",
|
"id": "c41e7a06-5115-48e8-a8ce-821bb3e4d2dc",
|
||||||
"name": "Manual Trigger",
|
"name": "Manual Trigger",
|
||||||
"type": "n8n-nodes-base.manualTrigger",
|
"type": "n8n-nodes-base.manualTrigger",
|
||||||
"typeVersion": 1,
|
"typeVersion": 1,
|
||||||
"position": [
|
"position": [
|
||||||
0,
|
4640,
|
||||||
0
|
3696
|
||||||
],
|
],
|
||||||
"notes": "กดรันด้วยตนเอง"
|
"notes": "กดรันด้วยตนเอง"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"jsCode": "// ============================================\n// CONFIGURATION - แก้ไขค่าที่นี่\n// ============================================\nconst CONFIG = {\n // Ollama Settings\n OLLAMA_HOST: 'http://192.168.20.100:11434',\n OLLAMA_MODEL_PRIMARY: 'llama3.2:3b',\n OLLAMA_MODEL_FALLBACK: 'mistral:7b-instruct-q4_K_M',\n \n // Backend Settings\n BACKEND_URL: 'https://api.np-dms.work',\n MIGRATION_TOKEN: 'Bearer YOUR_MIGRATION_TOKEN_HERE',\n \n // Batch Settings\n BATCH_SIZE: 10,\n BATCH_ID: 'migration_20260226',\n DELAY_MS: 2000,\n \n // Thresholds\n CONFIDENCE_HIGH: 0.85,\n CONFIDENCE_LOW: 0.60,\n MAX_RETRY: 3,\n FALLBACK_THRESHOLD: 5,\n \n // Paths\n STAGING_PATH: '/home/node/.n8n-files/staging_ai',\n LOG_PATH: '/home/node/.n8n-files/migration_logs',\n \n // Database\n DB_HOST: '192.168.1.100',\n DB_PORT: 3306,\n DB_NAME: 'lcbp3_db',\n DB_USER: 'migration_bot',\n DB_PASSWORD: 'YOUR_DB_PASSWORD_HERE'\n};\n\nreturn [{ json: { config_loaded: true, timestamp: new Date().toISOString(), config: CONFIG } }];"
|
"jsCode": "// ============================================\n// CONFIGURATION - แก้ไขค่าที่นี่\n// ============================================\nconst CONFIG = {\n // Ollama Settings\n OLLAMA_HOST: 'http://192.168.20.100:11434',\n OLLAMA_MODEL_PRIMARY: 'llama3.2:3b',\n OLLAMA_MODEL_FALLBACK: 'mistral:7b-instruct-q4_K_M',\n \n // Backend Settings\n BACKEND_URL: 'https://backend.np-dms.work',\n MIGRATION_TOKEN: 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Im1pZ3JhdGlvbl9ib3QiLCJzdWIiOjUsInNjb3BlIjoiR2xvYmFsIiwiaWF0IjoxNzcyNzc0MzI5LCJleHAiOjQ5Mjg1MzQzMjl9.TtA8zoHy7G9J5jPgYQPv7yw-9X--B_hl-Nv-c9V4PaA',\n \n // Batch Settings\n BATCH_SIZE: 10,\n BATCH_ID: 'migration_20260226',\n DELAY_MS: 2000,\n \n // Thresholds\n CONFIDENCE_HIGH: 0.85,\n CONFIDENCE_LOW: 0.60,\n MAX_RETRY: 3,\n FALLBACK_THRESHOLD: 5,\n \n // Source Definitions - แก้ไขโฟลเดอร์และไฟล์ทำงานที่นี่\n EXCEL_FILE: '/home/node/.n8n-files/staging_ai/C22024.xlsx',\n SOURCE_PDF_DIR: '/home/node/.n8n-files/staging_ai/Incoming/08 ผรม.2/2567',\n LOG_PATH: '/home/node/.n8n-files/migration_logs',\n \n // Database\n DB_HOST: '192.168.10.8',\n DB_PORT: 3306,\n DB_NAME: 'lcbp3',\n DB_USER: 'migration_bot',\n DB_PASSWORD: 'Center2025'\n};\n\nreturn [{ json: { config_loaded: true, timestamp: new Date().toISOString(), config: CONFIG } }];"
|
||||||
},
|
},
|
||||||
"id": "config-setter",
|
"id": "bc8c9b9d-284d-4ce5-b7ff-d5b4bb36e748",
|
||||||
"name": "Set Configuration",
|
"name": "Set Configuration",
|
||||||
"type": "n8n-nodes-base.code",
|
"type": "n8n-nodes-base.code",
|
||||||
"typeVersion": 2,
|
"typeVersion": 2,
|
||||||
"position": [
|
"position": [
|
||||||
200,
|
4832,
|
||||||
0
|
3696
|
||||||
],
|
],
|
||||||
"notes": "กำหนดค่า Configuration ทั้งหมด - แก้ไขที่นี่ก่อนรัน"
|
"notes": "กำหนดค่า Configuration ทั้งหมด - แก้ไขที่นี่ก่อนรัน"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"method": "GET",
|
|
||||||
"url": "={{$('Set Configuration').first().json.config.BACKEND_URL}}/api/master/correspondence-types",
|
"url": "={{$('Set Configuration').first().json.config.BACKEND_URL}}/api/master/correspondence-types",
|
||||||
"sendHeaders": true,
|
"sendHeaders": true,
|
||||||
"headerParameters": {
|
"headerParameters": {
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"name": "Authorization",
|
"name": "Authorization",
|
||||||
"value": "={{$workflow.staticData.config.MIGRATION_TOKEN}}"
|
"value": "={{$('Set Configuration').first().json.config.MIGRATION_TOKEN}}"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -50,150 +43,165 @@
|
|||||||
"timeout": 10000
|
"timeout": 10000
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"id": "preflight-categories",
|
"id": "ccb5fea4-773d-4584-a14c-88845f4c2bc3",
|
||||||
"name": "Fetch Categories",
|
"name": "Fetch Categories",
|
||||||
"type": "n8n-nodes-base.httpRequest",
|
"type": "n8n-nodes-base.httpRequest",
|
||||||
"typeVersion": 4.1,
|
"typeVersion": 4.1,
|
||||||
"position": [
|
"position": [
|
||||||
400,
|
5040,
|
||||||
0
|
3696
|
||||||
],
|
],
|
||||||
"notes": "ดึง Categories จาก Backend"
|
"notes": "ดึง Categories จาก Backend"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"method": "GET",
|
|
||||||
"url": "={{$('Set Configuration').first().json.config.BACKEND_URL}}/health",
|
"url": "={{$('Set Configuration').first().json.config.BACKEND_URL}}/health",
|
||||||
"options": {
|
"options": {
|
||||||
"timeout": 5000
|
"timeout": 5000
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"id": "preflight-health",
|
"id": "0fe2cc93-7d88-4290-8170-2863e087afd3",
|
||||||
"name": "Check Backend Health",
|
"name": "Check Backend Health",
|
||||||
"type": "n8n-nodes-base.httpRequest",
|
"type": "n8n-nodes-base.httpRequest",
|
||||||
"typeVersion": 4.1,
|
"typeVersion": 4.1,
|
||||||
"position": [
|
"position": [
|
||||||
400,
|
5008,
|
||||||
200
|
3328
|
||||||
],
|
],
|
||||||
"notes": "ตรวจสอบ Backend พร้อมใช้งาน",
|
"onError": "continueErrorOutput",
|
||||||
"onError": "continueErrorOutput"
|
"notes": "ตรวจสอบ Backend พร้อมใช้งาน"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"jsCode": "const fs = require('fs');\nconst config = $('Set Configuration').first().json.config;\n\n// Check file mount\ntry {\n const files = fs.readdirSync(config.STAGING_PATH);\n if (files.length === 0) throw new Error('staging_ai is empty');\n \n // Check write permission to log path\n fs.writeFileSync(`${config.LOG_PATH}/.preflight_ok`, new Date().toISOString());\n \n // Store categories\n const categories = $input.first().json.categories || \n ['Correspondence','RFA','Drawing','Transmittal','Report','Other'];\n $('File Mount Check').first().json.system_categories = categories;\n \n return [{ json: { \n preflight_ok: true, \n file_count: files.length,\n system_categories: categories,\n timestamp: new Date().toISOString()\n }}];\n} catch (err) {\n throw new Error(`Pre-flight check failed: ${err.message}`);\n}"
|
"jsCode": "const fs = require('fs');\nconst config = $('Set Configuration').first().json.config;\n\n// Check file mount and inputs\ntry {\n if (!fs.existsSync(config.EXCEL_FILE)) {\n throw new Error(`Excel file not found at: ${config.EXCEL_FILE}`);\n }\n if (!fs.existsSync(config.SOURCE_PDF_DIR)) {\n throw new Error(`PDF Source directory not found at: ${config.SOURCE_PDF_DIR}`);\n }\n \n const files = fs.readdirSync(config.SOURCE_PDF_DIR);\n \n // Check write permission to log path\n fs.writeFileSync(`${config.LOG_PATH}/.preflight_ok`, new Date().toISOString());\n \n // Grab categories out of the previous node (Fetch Categories) if available\n // otherwise use fallback array\n let categories = ['Correspondence','RFA','Drawing','Transmittal','Report','Other'];\n try {\n const upstreamData = $('Fetch Categories').first()?.json?.data;\n if (upstreamData && Array.isArray(upstreamData)) {\n categories = upstreamData.map(c => c.name || c.type || c); // very loose mapping depending on API response\n }\n } catch(e) {}\n \n return [{ json: { \n preflight_ok: true, \n pdf_count_in_source: files.length,\n excel_target: config.EXCEL_FILE,\n system_categories: categories,\n timestamp: new Date().toISOString()\n }}];\n} catch (err) {\n throw new Error(`Pre-flight check failed: ${err.message}`);\n}"
|
||||||
},
|
},
|
||||||
"id": "preflight-check",
|
"id": "5bdb31ca-9588-404d-92ce-3438bdd9835b",
|
||||||
"name": "File Mount Check",
|
"name": "File Mount Check",
|
||||||
"type": "n8n-nodes-base.code",
|
"type": "n8n-nodes-base.code",
|
||||||
"typeVersion": 2,
|
"typeVersion": 2,
|
||||||
"position": [
|
"position": [
|
||||||
600,
|
5248,
|
||||||
0
|
3392
|
||||||
],
|
],
|
||||||
"notes": "ตรวจสอบ File System และเก็บ Categories"
|
"notes": "ตรวจสอบ File System มีไฟล์ Excel และ Folder ตามตั้งค่า"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"operation": "executeQuery",
|
"operation": "executeQuery",
|
||||||
"host": "={{$('Set Configuration').first().json.config.DB_HOST}}",
|
|
||||||
"port": "={{$('Set Configuration').first().json.config.DB_PORT}}",
|
|
||||||
"database": "={{$('Set Configuration').first().json.config.DB_NAME}}",
|
|
||||||
"user": "={{$('Set Configuration').first().json.config.DB_USER}}",
|
|
||||||
"password": "={{$('Set Configuration').first().json.config.DB_PASSWORD}}",
|
|
||||||
"query": "SELECT last_processed_index, status FROM migration_progress WHERE batch_id = '{{$('Set Configuration').first().json.config.BATCH_ID}}' LIMIT 1",
|
"query": "SELECT last_processed_index, status FROM migration_progress WHERE batch_id = '{{$('Set Configuration').first().json.config.BATCH_ID}}' LIMIT 1",
|
||||||
"options": {}
|
"options": {}
|
||||||
},
|
},
|
||||||
"id": "checkpoint-read",
|
"id": "2907a4ca-2a46-45ef-8920-9684d00ffda7",
|
||||||
"name": "Read Checkpoint",
|
"name": "Read Checkpoint",
|
||||||
"type": "n8n-nodes-base.mySql",
|
"type": "n8n-nodes-base.mySql",
|
||||||
"typeVersion": 2.4,
|
"typeVersion": 2.4,
|
||||||
"position": [
|
"position": [
|
||||||
800,
|
5504,
|
||||||
0
|
3376
|
||||||
],
|
],
|
||||||
"notes": "อ่านตำแหน่งล่าสุดที่ประมวลผล",
|
"alwaysOutputData": true,
|
||||||
"onError": "continueErrorOutput"
|
"credentials": {
|
||||||
|
"mySql": {
|
||||||
|
"id": "CHHfbKhMacNo03V4",
|
||||||
|
"name": "MySQL account"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"onError": "continueErrorOutput",
|
||||||
|
"notes": "อ่านตำแหน่งล่าสุดที่ประมวลผล"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"operation": "toData",
|
"fileSelector": "={{ $json.excel_target }}",
|
||||||
"binaryProperty": "data",
|
"options": {}
|
||||||
"options": {
|
|
||||||
"sheetName": "Sheet1"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"id": "excel-reader",
|
"id": "f035d28b-413b-4386-bbef-d242cd22aa8f",
|
||||||
|
"name": "Read Excel Binary",
|
||||||
|
"type": "n8n-nodes-base.readWriteFile",
|
||||||
|
"typeVersion": 1,
|
||||||
|
"position": [
|
||||||
|
5040,
|
||||||
|
4112
|
||||||
|
],
|
||||||
|
"notes": "ดึงไฟล์ Excel ขึ้นมาไว้ในหน่วยความจำ"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"id": "35727d79-e3c2-4fdf-8bc3-064914393cf7",
|
||||||
"name": "Read Excel",
|
"name": "Read Excel",
|
||||||
"type": "n8n-nodes-base.spreadsheetFile",
|
"type": "n8n-nodes-base.spreadsheetFile",
|
||||||
"typeVersion": 2,
|
"typeVersion": 2,
|
||||||
"position": [
|
"position": [
|
||||||
800,
|
5264,
|
||||||
200
|
3968
|
||||||
],
|
],
|
||||||
"notes": "อ่านไฟล์ Excel รายการเอกสาร"
|
"notes": "แปลงข้อมูล Excel เป็น JSON Data"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"jsCode": "const checkpoint = $input.first().json[0] || { last_processed_index: 0, status: 'NEW' };\nconst startIndex = checkpoint.last_processed_index || 0;\nconst config = $('Set Configuration').first().json.config;\n\nconst allItems = $('Read Excel').all()[0].json.data || [];\nconst remaining = allItems.slice(startIndex);\nconst currentBatch = remaining.slice(0, config.BATCH_SIZE);\n\n// Encoding Normalization\nconst normalize = (str) => {\n if (!str) return '';\n return String(str).normalize('NFC').trim();\n};\n\nreturn currentBatch.map((item, i) => ({\n json: {\n document_number: normalize(item.document_number || item['Document Number']),\n title: normalize(item.title || item.Title || item['Subject']),\n legacy_number: normalize(item.legacy_number || item['Legacy Number']),\n excel_revision: item.revision || item.Revision || 1,\n original_index: startIndex + i,\n batch_id: config.BATCH_ID,\n file_name: `${normalize(item.document_number)}.pdf`\n }\n}));"
|
"jsCode": "const cpJson = $input.first()?.json || {};\nconst startIndex = cpJson.last_processed_index || 0;\nconst config = $('Set Configuration').first().json.config;\n\nconst allItems = $('Read Excel').all().map(i => i.json);\nconst remaining = allItems.slice(startIndex);\nconst currentBatch = remaining.slice(0, config.BATCH_SIZE);\n\n// Encoding Normalization\nconst normalize = (str) => {\n if (!str) return '';\n return String(str).normalize('NFC').trim();\n};\n\nreturn currentBatch.map((item, i) => {\n const docNum = item.document_number || item['Document Number'] || item['Corr. No.'];\n return {\n json: {\n document_number: normalize(docNum),\n title: normalize(item.title || item.Title || item['Subject']),\n legacy_number: normalize(item.legacy_number || item['Legacy Number'] || item['Response Doc.'] || ''),\n excel_revision: item.revision || item.Revision || item.rev || 1,\n original_index: startIndex + i,\n batch_id: config.BATCH_ID,\n file_name: `${normalize(docNum)}.pdf`\n }\n };\n});"
|
||||||
},
|
},
|
||||||
"id": "batch-processor",
|
"id": "49c98c75-456b-4a1d-a203-a5b2bf19fd15",
|
||||||
"name": "Process Batch + Encoding",
|
"name": "Process Batch + Encoding",
|
||||||
"type": "n8n-nodes-base.code",
|
"type": "n8n-nodes-base.code",
|
||||||
"typeVersion": 2,
|
"typeVersion": 2,
|
||||||
"position": [
|
"position": [
|
||||||
1000,
|
5712,
|
||||||
0
|
3360
|
||||||
],
|
],
|
||||||
|
"alwaysOutputData": true,
|
||||||
"notes": "ตัด Batch + Normalize UTF-8"
|
"notes": "ตัด Batch + Normalize UTF-8"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"jsCode": "const fs = require('fs');\nconst path = require('path');\nconst config = $('Set Configuration').first().json.config;\n\nconst items = $input.all();\nconst validated = [];\nconst errors = [];\n\nfor (const item of items) {\n const docNum = item.json.document_number;\n \n // Sanitize filename\n const safeName = path.basename(String(docNum).replace(/[^a-zA-Z0-9\\-_.]/g, '_')).normalize('NFC');\n const filePath = path.resolve(config.STAGING_PATH, `${safeName}.pdf`);\n \n // Path traversal check\n if (!filePath.startsWith(config.STAGING_PATH)) {\n errors.push({\n ...item,\n json: { ...item.json, error: 'Path traversal detected', error_type: 'SECURITY', file_exists: false }\n });\n continue;\n }\n \n try {\n if (fs.existsSync(filePath)) {\n const stats = fs.statSync(filePath);\n validated.push({\n ...item,\n json: { ...item.json, file_exists: true, file_size: stats.size, file_path: filePath }\n });\n } else {\n errors.push({\n ...item,\n json: { ...item.json, error: `File not found: ${safeName}.pdf`, error_type: 'FILE_NOT_FOUND', file_exists: false }\n });\n }\n } catch (err) {\n errors.push({\n ...item,\n json: { ...item.json, error: err.message, error_type: 'FILE_ERROR', file_exists: false }\n });\n }\n}\n\n// Output 0: Validated, Output 1: Errors\nreturn [validated, errors];"
|
"jsCode": "const fs = require('fs');\nconst path = require('path');\nconst config = $('Set Configuration').first().json.config;\n\nconst items = $input.all();\nif (!items || items.length === 0) return [];\n\nconst validated = [];\nconst errors = [];\n\nfor (const item of items) {\n const docNum = item.json?.document_number;\n if (!docNum) continue;\n \n // Sanitize filename\n const safeName = path.basename(String(docNum).replace(/[^a-zA-Z0-9\\-_.]/g, '_')).normalize('NFC');\n const filePath = path.resolve(config.SOURCE_PDF_DIR, `${safeName}.pdf`);\n \n // Path traversal check\n if (!filePath.startsWith(path.resolve(config.SOURCE_PDF_DIR))) {\n errors.push({\n ...item,\n json: { ...item.json, file_valid: false, error: 'Path traversal detected in Source PDF path', error_type: 'SECURITY', file_exists: false }\n });\n continue;\n }\n \n try {\n if (fs.existsSync(filePath)) {\n const stats = fs.statSync(filePath);\n validated.push({\n ...item,\n json: { ...item.json, file_valid: true, file_exists: true, file_size: stats.size, file_path: filePath }\n });\n } else {\n errors.push({\n ...item,\n json: { ...item.json, file_valid: false, error: `File not found: ${safeName}.pdf in ${config.SOURCE_PDF_DIR}`, error_type: 'FILE_NOT_FOUND', file_exists: false }\n });\n }\n } catch (err) {\n errors.push({\n ...item,\n json: { ...item.json, file_valid: false, error: err.message, error_type: 'FILE_ERROR', file_exists: false }\n });\n }\n}\n\nreturn [...validated, ...errors];"
|
||||||
},
|
},
|
||||||
"id": "file-validator",
|
"id": "51e91c88-98cd-4df4-81ac-e452b25e5c06",
|
||||||
"name": "File Validator",
|
"name": "File Validator",
|
||||||
"type": "n8n-nodes-base.code",
|
"type": "n8n-nodes-base.code",
|
||||||
"typeVersion": 2,
|
"typeVersion": 2,
|
||||||
"position": [
|
"position": [
|
||||||
1200,
|
5904,
|
||||||
0
|
3264
|
||||||
],
|
],
|
||||||
"notes": "ตรวจสอบไฟล์ PDF มีอยู่จริง + Sanitize path"
|
"notes": "ตรวจสอบไฟล์ PDF ตัวชี้ใน Directory จาก Config"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"operation": "executeQuery",
|
"operation": "executeQuery",
|
||||||
"host": "={{$('Set Configuration').first().json.config.DB_HOST}}",
|
|
||||||
"port": "={{$('Set Configuration').first().json.config.DB_PORT}}",
|
|
||||||
"database": "={{$('Set Configuration').first().json.config.DB_NAME}}",
|
|
||||||
"user": "={{$('Set Configuration').first().json.config.DB_USER}}",
|
|
||||||
"password": "={{$('Set Configuration').first().json.config.DB_PASSWORD}}",
|
|
||||||
"query": "SELECT is_fallback_active, recent_error_count FROM migration_fallback_state WHERE batch_id = '{{$('Set Configuration').first().json.config.BATCH_ID}}' LIMIT 1",
|
"query": "SELECT is_fallback_active, recent_error_count FROM migration_fallback_state WHERE batch_id = '{{$('Set Configuration').first().json.config.BATCH_ID}}' LIMIT 1",
|
||||||
"options": {}
|
"options": {}
|
||||||
},
|
},
|
||||||
"id": "fallback-check",
|
"id": "88c205b6-9b94-4a4f-ad53-ab3cad6fde27",
|
||||||
"name": "Check Fallback State",
|
"name": "Check Fallback State",
|
||||||
"type": "n8n-nodes-base.mySql",
|
"type": "n8n-nodes-base.mySql",
|
||||||
"typeVersion": 2.4,
|
"typeVersion": 2.4,
|
||||||
"position": [
|
"position": [
|
||||||
1400,
|
6032,
|
||||||
-200
|
3488
|
||||||
],
|
],
|
||||||
"notes": "ตรวจสอบว่าต้องใช้ Fallback Model หรือไม่",
|
"alwaysOutputData": true,
|
||||||
"onError": "continueErrorOutput"
|
"credentials": {
|
||||||
|
"mySql": {
|
||||||
|
"id": "CHHfbKhMacNo03V4",
|
||||||
|
"name": "MySQL account"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"onError": "continueErrorOutput",
|
||||||
|
"notes": "ตรวจสอบว่าต้องใช้ Fallback Model หรือไม่"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"jsCode": "const config = $('Set Configuration').first().json.config;\nconst fallbackState = $input.first().json[0] || { is_fallback_active: false, recent_error_count: 0 };\n\nconst isFallback = fallbackState.is_fallback_active || false;\nconst model = isFallback ? config.OLLAMA_MODEL_FALLBACK : config.OLLAMA_MODEL_PRIMARY;\n\nconst systemCategories = $('File Mount Check').first().json.system_categories || \n ['Correspondence','RFA','Drawing','Transmittal','Report','Other'];\n\nconst items = $('File Validator').all();\n\nreturn items.map(item => {\n const systemPrompt = `You are a Document Controller for a large construction project.\nYour task is to validate document metadata.\nYou MUST respond ONLY with valid JSON. No explanation, no markdown, no extra text.\nIf there are no issues, \"detected_issues\" must be an empty array [].`;\n\n const userPrompt = `Validate this document metadata and respond in JSON:\n\nDocument Number: ${item.json.document_number}\nTitle: ${item.json.title}\nExpected Pattern: [ORG]-[TYPE]-[SEQ] e.g. \"TCC-COR-0001\"\nCategory List (MUST match system enum exactly): ${JSON.stringify(systemCategories)}\n\nRespond ONLY with this exact JSON structure:\n{\n \"is_valid\": true | false,\n \"confidence\": 0.0 to 1.0,\n \"suggested_category\": \"<one from Category List>\",\n \"detected_issues\": [\"<issue1>\"],\n \"suggested_title\": \"<corrected title or null>\"\n}`;\n\n return {\n json: {\n ...item.json,\n active_model: model,\n is_fallback: isFallback,\n system_categories: systemCategories,\n ollama_payload: {\n model: model,\n prompt: `${systemPrompt}\\n\\n${userPrompt}`,\n stream: false,\n format: 'json'\n }\n }\n };\n});"
|
"jsCode": "const config = $('Set Configuration').first().json.config;\nconst fallbackState = $input.first().json[0] || { is_fallback_active: false, recent_error_count: 0 };\n\nconst isFallback = fallbackState.is_fallback_active || false;\nconst model = isFallback ? config.OLLAMA_MODEL_FALLBACK : config.OLLAMA_MODEL_PRIMARY;\n\n// Safely pull categories from the first Check node\nlet systemCategories = ['Correspondence','RFA','Drawing','Transmittal','Report','Other'];\ntry { systemCategories = $('File Mount Check').first().json.system_categories || systemCategories; } catch (e) {}\n\nconst items = $('File Validator').all();\n\nreturn items.map(item => {\n const systemPrompt = `You are a Document Controller for a large construction project.\nYour task is to validate document metadata.\nYou MUST respond ONLY with valid JSON. No explanation, no markdown, no extra text.\nIf there are no issues, \"detected_issues\" must be an empty array [].`;\n\n const userPrompt = `Validate this document metadata and respond in JSON:\n\nDocument Number: ${item.json.document_number}\nTitle: ${item.json.title}\nExpected Pattern: [ORG]-[TYPE]-[SEQ] e.g. \"TCC-COR-0001\"\nCategory List (MUST match system enum exactly): ${JSON.stringify(systemCategories)}\n\nRespond ONLY with this exact JSON structure:\n{\n \"is_valid\": true | false,\n \"confidence\": 0.0 to 1.0,\n \"suggested_category\": \"<one from Category List>\",\n \"detected_issues\": [\"<issue1>\"],\n \"suggested_title\": \"<corrected title or null>\"\n}`;\n\n return {\n json: {\n ...item.json,\n active_model: model,\n is_fallback: isFallback,\n system_categories: systemCategories,\n ollama_payload: {\n model: model,\n prompt: `${systemPrompt}\\n\\n${userPrompt}`,\n stream: false,\n format: 'json'\n }\n }\n };\n});"
|
||||||
},
|
},
|
||||||
"id": "prompt-builder",
|
"id": "9f82950f-7533-4cbd-8e1e-8e441c1cb2a5",
|
||||||
"name": "Build AI Prompt",
|
"name": "Build AI Prompt",
|
||||||
"type": "n8n-nodes-base.code",
|
"type": "n8n-nodes-base.code",
|
||||||
"typeVersion": 2,
|
"typeVersion": 2,
|
||||||
"position": [
|
"position": [
|
||||||
1400,
|
6032,
|
||||||
0
|
3696
|
||||||
],
|
],
|
||||||
"notes": "สร้าง Prompt โดยใช้ Categories จาก System"
|
"notes": "สร้าง Prompt โดยใช้ Categories จาก System"
|
||||||
},
|
},
|
||||||
@@ -208,13 +216,13 @@
|
|||||||
"timeout": 30000
|
"timeout": 30000
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"id": "ollama-call",
|
"id": "ae9b6be5-284c-44db-b7f0-b4839a59230e",
|
||||||
"name": "Ollama AI Analysis",
|
"name": "Ollama AI Analysis",
|
||||||
"type": "n8n-nodes-base.httpRequest",
|
"type": "n8n-nodes-base.httpRequest",
|
||||||
"typeVersion": 4.1,
|
"typeVersion": 4.1,
|
||||||
"position": [
|
"position": [
|
||||||
1600,
|
6240,
|
||||||
0
|
3696
|
||||||
],
|
],
|
||||||
"notes": "เรียก Ollama วิเคราะห์เอกสาร"
|
"notes": "เรียก Ollama วิเคราะห์เอกสาร"
|
||||||
},
|
},
|
||||||
@@ -222,48 +230,49 @@
|
|||||||
"parameters": {
|
"parameters": {
|
||||||
"jsCode": "const items = $input.all();\nconst parsed = [];\nconst parseErrors = [];\n\nfor (const item of items) {\n try {\n let raw = item.json.response || '';\n \n // Clean markdown\n raw = raw.replace(/```json/gi, '').replace(/```/g, '').trim();\n const result = JSON.parse(raw);\n \n // Schema Validation\n if (typeof result.is_valid !== 'boolean') throw new Error('is_valid must be boolean');\n if (typeof result.confidence !== 'number' || result.confidence < 0 || result.confidence > 1) {\n throw new Error('confidence must be float 0.0-1.0');\n }\n if (!Array.isArray(result.detected_issues)) throw new Error('detected_issues must be array');\n \n // Enum Validation\n const systemCategories = item.json.system_categories || [];\n if (!systemCategories.includes(result.suggested_category)) {\n throw new Error(`Category \"${result.suggested_category}\" not in system enum`);\n }\n \n parsed.push({\n ...item,\n json: { ...item.json, ai_result: result, parse_error: null }\n });\n } catch (err) {\n parseErrors.push({\n ...item,\n json: {\n ...item.json,\n ai_result: null,\n parse_error: err.message,\n raw_ai_response: item.json.response,\n error_type: 'AI_PARSE_ERROR'\n }\n });\n }\n}\n\nreturn [parsed, parseErrors];"
|
"jsCode": "const items = $input.all();\nconst parsed = [];\nconst parseErrors = [];\n\nfor (const item of items) {\n try {\n let raw = item.json.response || '';\n \n // Clean markdown\n raw = raw.replace(/```json/gi, '').replace(/```/g, '').trim();\n const result = JSON.parse(raw);\n \n // Schema Validation\n if (typeof result.is_valid !== 'boolean') throw new Error('is_valid must be boolean');\n if (typeof result.confidence !== 'number' || result.confidence < 0 || result.confidence > 1) {\n throw new Error('confidence must be float 0.0-1.0');\n }\n if (!Array.isArray(result.detected_issues)) throw new Error('detected_issues must be array');\n \n // Enum Validation\n const systemCategories = item.json.system_categories || [];\n if (!systemCategories.includes(result.suggested_category)) {\n throw new Error(`Category \"${result.suggested_category}\" not in system enum`);\n }\n \n parsed.push({\n ...item,\n json: { ...item.json, ai_result: result, parse_error: null }\n });\n } catch (err) {\n parseErrors.push({\n ...item,\n json: {\n ...item.json,\n ai_result: null,\n parse_error: err.message,\n raw_ai_response: item.json.response,\n error_type: 'AI_PARSE_ERROR'\n }\n });\n }\n}\n\nreturn [parsed, parseErrors];"
|
||||||
},
|
},
|
||||||
"id": "json-parser",
|
"id": "281dc950-a3b6-4412-a0b4-76663b8c37ea",
|
||||||
"name": "Parse & Validate AI Response",
|
"name": "Parse & Validate AI Response",
|
||||||
"type": "n8n-nodes-base.code",
|
"type": "n8n-nodes-base.code",
|
||||||
"typeVersion": 2,
|
"typeVersion": 2,
|
||||||
"position": [
|
"position": [
|
||||||
1800,
|
6432,
|
||||||
0
|
3696
|
||||||
],
|
],
|
||||||
"notes": "Parse JSON + Validate Schema + Enum Check"
|
"notes": "Parse JSON + Validate Schema + Enum Check"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"operation": "executeQuery",
|
"operation": "executeQuery",
|
||||||
"host": "={{$('Set Configuration').first().json.config.DB_HOST}}",
|
|
||||||
"port": "={{$('Set Configuration').first().json.config.DB_PORT}}",
|
|
||||||
"database": "={{$('Set Configuration').first().json.config.DB_NAME}}",
|
|
||||||
"user": "={{$('Set Configuration').first().json.config.DB_USER}}",
|
|
||||||
"password": "={{$('Set Configuration').first().json.config.DB_PASSWORD}}",
|
|
||||||
"query": "INSERT INTO migration_fallback_state (batch_id, recent_error_count, is_fallback_active) VALUES ('{{$('Set Configuration').first().json.config.BATCH_ID}}', 1, FALSE) ON DUPLICATE KEY UPDATE recent_error_count = recent_error_count + 1, is_fallback_active = CASE WHEN recent_error_count + 1 >= {{$('Set Configuration').first().json.config.FALLBACK_THRESHOLD}} THEN TRUE ELSE is_fallback_active END, updated_at = NOW()",
|
"query": "INSERT INTO migration_fallback_state (batch_id, recent_error_count, is_fallback_active) VALUES ('{{$('Set Configuration').first().json.config.BATCH_ID}}', 1, FALSE) ON DUPLICATE KEY UPDATE recent_error_count = recent_error_count + 1, is_fallback_active = CASE WHEN recent_error_count + 1 >= {{$('Set Configuration').first().json.config.FALLBACK_THRESHOLD}} THEN TRUE ELSE is_fallback_active END, updated_at = NOW()",
|
||||||
"options": {}
|
"options": {}
|
||||||
},
|
},
|
||||||
"id": "fallback-update",
|
"id": "41904cb6-b6f3-4a32-9dd5-c44e8e0cefab",
|
||||||
"name": "Update Fallback State",
|
"name": "Update Fallback State",
|
||||||
"type": "n8n-nodes-base.mySql",
|
"type": "n8n-nodes-base.mySql",
|
||||||
"typeVersion": 2.4,
|
"typeVersion": 2.4,
|
||||||
"position": [
|
"position": [
|
||||||
2000,
|
6640,
|
||||||
200
|
3888
|
||||||
],
|
],
|
||||||
|
"credentials": {
|
||||||
|
"mySql": {
|
||||||
|
"id": "CHHfbKhMacNo03V4",
|
||||||
|
"name": "MySQL account"
|
||||||
|
}
|
||||||
|
},
|
||||||
"notes": "เพิ่ม Error count และตรวจสอบ Fallback threshold"
|
"notes": "เพิ่ม Error count และตรวจสอบ Fallback threshold"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"jsCode": "const config = $('Set Configuration').first().json.config;\nconst items = $('Parse & Validate AI Response').all();\n\nconst autoIngest = [];\nconst reviewQueue = [];\nconst rejectLog = [];\nconst errorLog = [];\n\nfor (const item of items) {\n if (item.json.parse_error || !item.json.ai_result) {\n errorLog.push(item);\n continue;\n }\n \n const ai = item.json.ai_result;\n \n // Revision Drift Protection (ถ้ามีข้อมูลจาก DB)\n if (item.json.current_db_revision !== undefined) {\n const expectedRev = item.json.current_db_revision + 1;\n if (parseInt(item.json.excel_revision) !== expectedRev) {\n reviewQueue.push({\n ...item,\n json: { ...item.json, review_reason: `Revision drift: Excel=${item.json.excel_revision}, Expected=${expectedRev}` }\n });\n continue;\n }\n }\n \n // Confidence Routing\n if (ai.confidence >= config.CONFIDENCE_HIGH && ai.is_valid === true) {\n autoIngest.push(item);\n } else if (ai.confidence >= config.CONFIDENCE_LOW) {\n reviewQueue.push({\n ...item,\n json: { ...item.json, review_reason: `Confidence ${ai.confidence.toFixed(2)} < ${config.CONFIDENCE_HIGH}` }\n });\n } else {\n rejectLog.push({\n ...item,\n json: { ...item.json, reject_reason: ai.is_valid === false ? 'AI marked invalid' : `Confidence ${ai.confidence.toFixed(2)} < ${config.CONFIDENCE_LOW}` }\n });\n }\n}\n\n// Output 0: Auto, 1: Review, 2: Reject, 3: Error\nreturn [autoIngest, reviewQueue, rejectLog, errorLog];"
|
"jsCode": "const config = $('Set Configuration').first().json.config;\nconst items = $('Parse & Validate AI Response').all();\n\nconst autoIngest = [];\nconst reviewQueue = [];\nconst rejectLog = [];\nconst errorLog = [];\n\nfor (const item of items) {\n if (item.json.parse_error || !item.json.ai_result) {\n errorLog.push(item);\n continue;\n }\n \n const ai = item.json.ai_result;\n \n // Revision Drift Protection (ถ้ามีข้อมูลจาก DB)\n if (item.json.current_db_revision !== undefined) {\n const expectedRev = item.json.current_db_revision + 1;\n if (parseInt(item.json.excel_revision) !== expectedRev) {\n reviewQueue.push({\n ...item,\n json: { ...item.json, review_reason: `Revision drift: Excel=${item.json.excel_revision}, Expected=${expectedRev}` }\n });\n continue;\n }\n }\n \n // Confidence Routing\n if (ai.confidence >= config.CONFIDENCE_HIGH && ai.is_valid === true) {\n autoIngest.push(item);\n } else if (ai.confidence >= config.CONFIDENCE_LOW) {\n reviewQueue.push({\n ...item,\n json: { ...item.json, review_reason: `Confidence ${ai.confidence.toFixed(2)} < ${config.CONFIDENCE_HIGH}` }\n });\n } else {\n rejectLog.push({\n ...item,\n json: { ...item.json, reject_reason: ai.is_valid === false ? 'AI marked invalid' : `Confidence ${ai.confidence.toFixed(2)} < ${config.CONFIDENCE_LOW}` }\n });\n }\n}\n\n// Output 0: Auto, 1: Review, 2: Reject, 3: Error\nreturn [autoIngest, reviewQueue, rejectLog, errorLog];"
|
||||||
},
|
},
|
||||||
"id": "confidence-router",
|
"id": "897dfc43-9f4f-4a9b-8727-64f3483ac56a",
|
||||||
"name": "Confidence Router",
|
"name": "Confidence Router",
|
||||||
"type": "n8n-nodes-base.code",
|
"type": "n8n-nodes-base.code",
|
||||||
"typeVersion": 2,
|
"typeVersion": 2,
|
||||||
"position": [
|
"position": [
|
||||||
2000,
|
6640,
|
||||||
0
|
3696
|
||||||
],
|
],
|
||||||
"notes": "แยกตาม Confidence: Auto(≥0.85) / Review(≥0.60) / Reject(<0.60)"
|
"notes": "แยกตาม Confidence: Auto(≥0.85) / Review(≥0.60) / Reject(<0.60)"
|
||||||
},
|
},
|
||||||
@@ -276,11 +285,11 @@
|
|||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"name": "Authorization",
|
"name": "Authorization",
|
||||||
"value": "={{$workflow.staticData.config.MIGRATION_TOKEN}}"
|
"value": "={{$('Set Configuration').first().json.config.MIGRATION_TOKEN}}"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Idempotency-Key",
|
"name": "Idempotency-Key",
|
||||||
"value": "={{$json.document_number}}:{{$workflow.staticData.config.BATCH_ID}}"
|
"value": "={{$json.document_number}}:{{$('Set Configuration').first().json.config.BATCH_ID}}"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -291,13 +300,13 @@
|
|||||||
"timeout": 30000
|
"timeout": 30000
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"id": "backend-import",
|
"id": "49762c5d-0cb3-4acf-97f7-7e22905148dc",
|
||||||
"name": "Import to Backend",
|
"name": "Import to Backend",
|
||||||
"type": "n8n-nodes-base.httpRequest",
|
"type": "n8n-nodes-base.httpRequest",
|
||||||
"typeVersion": 4.1,
|
"typeVersion": 4.1,
|
||||||
"position": [
|
"position": [
|
||||||
2200,
|
6832,
|
||||||
-200
|
3488
|
||||||
],
|
],
|
||||||
"notes": "ส่งข้อมูลเข้า LCBP3 Backend พร้อม Idempotency-Key"
|
"notes": "ส่งข้อมูลเข้า LCBP3 Backend พร้อม Idempotency-Key"
|
||||||
},
|
},
|
||||||
@@ -305,69 +314,71 @@
|
|||||||
"parameters": {
|
"parameters": {
|
||||||
"jsCode": "const item = $input.first();\nconst shouldCheckpoint = item.json.original_index % 10 === 0;\n\nreturn [{\n json: {\n ...item.json,\n should_update_checkpoint: shouldCheckpoint,\n checkpoint_index: item.json.original_index,\n import_status: 'success',\n timestamp: new Date().toISOString()\n }\n}];"
|
"jsCode": "const item = $input.first();\nconst shouldCheckpoint = item.json.original_index % 10 === 0;\n\nreturn [{\n json: {\n ...item.json,\n should_update_checkpoint: shouldCheckpoint,\n checkpoint_index: item.json.original_index,\n import_status: 'success',\n timestamp: new Date().toISOString()\n }\n}];"
|
||||||
},
|
},
|
||||||
"id": "checkpoint-flag",
|
"id": "7fd03017-f08c-4e93-9486-36069f91ce57",
|
||||||
"name": "Flag Checkpoint",
|
"name": "Flag Checkpoint",
|
||||||
"type": "n8n-nodes-base.code",
|
"type": "n8n-nodes-base.code",
|
||||||
"typeVersion": 2,
|
"typeVersion": 2,
|
||||||
"position": [
|
"position": [
|
||||||
2400,
|
7040,
|
||||||
-200
|
3488
|
||||||
],
|
],
|
||||||
"notes": "กำหนดว่าจะบันทึก Checkpoint หรือไม่ (ทุก 10 records)"
|
"notes": "กำหนดว่าจะบันทึก Checkpoint หรือไม่ (ทุก 10 records)"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"operation": "executeQuery",
|
"operation": "executeQuery",
|
||||||
"host": "={{$('Set Configuration').first().json.config.DB_HOST}}",
|
|
||||||
"port": "={{$('Set Configuration').first().json.config.DB_PORT}}",
|
|
||||||
"database": "={{$('Set Configuration').first().json.config.DB_NAME}}",
|
|
||||||
"user": "={{$('Set Configuration').first().json.config.DB_USER}}",
|
|
||||||
"password": "={{$('Set Configuration').first().json.config.DB_PASSWORD}}",
|
|
||||||
"query": "INSERT INTO migration_progress (batch_id, last_processed_index, status) VALUES ('{{$('Set Configuration').first().json.config.BATCH_ID}}', {{$json.checkpoint_index}}, 'RUNNING') ON DUPLICATE KEY UPDATE last_processed_index = {{$json.checkpoint_index}}, updated_at = NOW()",
|
"query": "INSERT INTO migration_progress (batch_id, last_processed_index, status) VALUES ('{{$('Set Configuration').first().json.config.BATCH_ID}}', {{$json.checkpoint_index}}, 'RUNNING') ON DUPLICATE KEY UPDATE last_processed_index = {{$json.checkpoint_index}}, updated_at = NOW()",
|
||||||
"options": {}
|
"options": {}
|
||||||
},
|
},
|
||||||
"id": "checkpoint-save",
|
"id": "27b26d7b-2b57-479f-81ca-8d9319a45a7d",
|
||||||
"name": "Save Checkpoint",
|
"name": "Save Checkpoint",
|
||||||
"type": "n8n-nodes-base.mySql",
|
"type": "n8n-nodes-base.mySql",
|
||||||
"typeVersion": 2.4,
|
"typeVersion": 2.4,
|
||||||
"position": [
|
"position": [
|
||||||
2600,
|
7232,
|
||||||
-200
|
3488
|
||||||
],
|
],
|
||||||
|
"credentials": {
|
||||||
|
"mySql": {
|
||||||
|
"id": "CHHfbKhMacNo03V4",
|
||||||
|
"name": "MySQL account"
|
||||||
|
}
|
||||||
|
},
|
||||||
"notes": "บันทึกความคืบหน้าลง Database"
|
"notes": "บันทึกความคืบหน้าลง Database"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"operation": "executeQuery",
|
"operation": "executeQuery",
|
||||||
"host": "={{$('Set Configuration').first().json.config.DB_HOST}}",
|
|
||||||
"port": "={{$('Set Configuration').first().json.config.DB_PORT}}",
|
|
||||||
"database": "={{$('Set Configuration').first().json.config.DB_NAME}}",
|
|
||||||
"user": "={{$('Set Configuration').first().json.config.DB_USER}}",
|
|
||||||
"password": "={{$('Set Configuration').first().json.config.DB_PASSWORD}}",
|
|
||||||
"query": "INSERT INTO migration_review_queue (document_number, title, original_title, ai_suggested_category, ai_confidence, ai_issues, review_reason, status, created_at) VALUES ('{{$json.document_number}}', '{{$json.ai_result.suggested_title || $json.title}}', '{{$json.title}}', '{{$json.ai_result.suggested_category}}', {{$json.ai_result.confidence}}, '{{JSON.stringify($json.ai_result.detected_issues)}}', '{{$json.review_reason}}', 'PENDING', NOW()) ON DUPLICATE KEY UPDATE status = 'PENDING', review_reason = '{{$json.review_reason}}', created_at = NOW()",
|
"query": "INSERT INTO migration_review_queue (document_number, title, original_title, ai_suggested_category, ai_confidence, ai_issues, review_reason, status, created_at) VALUES ('{{$json.document_number}}', '{{$json.ai_result.suggested_title || $json.title}}', '{{$json.title}}', '{{$json.ai_result.suggested_category}}', {{$json.ai_result.confidence}}, '{{JSON.stringify($json.ai_result.detected_issues)}}', '{{$json.review_reason}}', 'PENDING', NOW()) ON DUPLICATE KEY UPDATE status = 'PENDING', review_reason = '{{$json.review_reason}}', created_at = NOW()",
|
||||||
"options": {}
|
"options": {}
|
||||||
},
|
},
|
||||||
"id": "review-queue-insert",
|
"id": "5d9547a9-36c8-434d-93e2-405be47d4e43",
|
||||||
"name": "Insert Review Queue",
|
"name": "Insert Review Queue",
|
||||||
"type": "n8n-nodes-base.mySql",
|
"type": "n8n-nodes-base.mySql",
|
||||||
"typeVersion": 2.4,
|
"typeVersion": 2.4,
|
||||||
"position": [
|
"position": [
|
||||||
2200,
|
6832,
|
||||||
0
|
3696
|
||||||
],
|
],
|
||||||
|
"credentials": {
|
||||||
|
"mySql": {
|
||||||
|
"id": "CHHfbKhMacNo03V4",
|
||||||
|
"name": "MySQL account"
|
||||||
|
}
|
||||||
|
},
|
||||||
"notes": "บันทึกรายการที่ต้องตรวจสอบโดยคน (ไม่สร้าง Correspondence)"
|
"notes": "บันทึกรายการที่ต้องตรวจสอบโดยคน (ไม่สร้าง Correspondence)"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"jsCode": "const fs = require('fs');\nconst item = $input.first();\nconst config = $('Set Configuration').first().json.config;\n\nconst csvPath = `${config.LOG_PATH}/reject_log.csv`;\nconst header = 'timestamp,document_number,title,reject_reason,ai_confidence,ai_issues\\n';\nconst esc = (s) => `\"${String(s || '').replace(/\"/g, '\"\"')}\"`;\n\nif (!fs.existsSync(csvPath)) {\n fs.writeFileSync(csvPath, header, 'utf8');\n}\n\nconst line = [\n new Date().toISOString(),\n esc(item.json.document_number),\n esc(item.json.title),\n esc(item.json.reject_reason),\n item.json.ai_result?.confidence ?? 'N/A',\n esc(JSON.stringify(item.json.ai_result?.detected_issues || []))\n].join(',') + '\\n';\n\nfs.appendFileSync(csvPath, line, 'utf8');\n\nreturn [$input.first()];"
|
"jsCode": "const fs = require('fs');\nconst item = $input.first();\nconst config = $('Set Configuration').first().json.config;\n\nconst csvPath = `${config.LOG_PATH}/reject_log.csv`;\nconst header = 'timestamp,document_number,title,reject_reason,ai_confidence,ai_issues\\n';\nconst esc = (s) => `\"${String(s || '').replace(/\"/g, '\"\"')}\"`;\n\nif (!fs.existsSync(csvPath)) {\n fs.writeFileSync(csvPath, header, 'utf8');\n}\n\nconst line = [\n new Date().toISOString(),\n esc(item.json.document_number),\n esc(item.json.title),\n esc(item.json.reject_reason),\n item.json.ai_result?.confidence ?? 'N/A',\n esc(JSON.stringify(item.json.ai_result?.detected_issues || []))\n].join(',') + '\\n';\n\nfs.appendFileSync(csvPath, line, 'utf8');\n\nreturn [$input.first()];"
|
||||||
},
|
},
|
||||||
"id": "reject-logger",
|
"id": "e933dc6a-885c-4607-916f-f28c655ceac4",
|
||||||
"name": "Log Reject to CSV",
|
"name": "Log Reject to CSV",
|
||||||
"type": "n8n-nodes-base.code",
|
"type": "n8n-nodes-base.code",
|
||||||
"typeVersion": 2,
|
"typeVersion": 2,
|
||||||
"position": [
|
"position": [
|
||||||
2200,
|
6832,
|
||||||
200
|
3888
|
||||||
],
|
],
|
||||||
"notes": "บันทึกรายการที่ถูกปฏิเสธลง CSV"
|
"notes": "บันทึกรายการที่ถูกปฏิเสธลง CSV"
|
||||||
},
|
},
|
||||||
@@ -375,35 +386,36 @@
|
|||||||
"parameters": {
|
"parameters": {
|
||||||
"jsCode": "const fs = require('fs');\nconst items = $input.all();\nconst config = $('Set Configuration').first().json.config;\n\nconst csvPath = `${config.LOG_PATH}/error_log.csv`;\nconst header = 'timestamp,document_number,error_type,error_message,raw_ai_response\\n';\nconst esc = (s) => `\"${String(s || '').replace(/\"/g, '\"\"')}\"`;\n\nif (!fs.existsSync(csvPath)) {\n fs.writeFileSync(csvPath, header, 'utf8');\n}\n\nfor (const item of items) {\n const line = [\n new Date().toISOString(),\n esc(item.json.document_number),\n esc(item.json.error_type || 'UNKNOWN'),\n esc(item.json.error || item.json.parse_error),\n esc(item.json.raw_ai_response || '')\n ].join(',') + '\\n';\n \n fs.appendFileSync(csvPath, line, 'utf8');\n}\n\nreturn items;"
|
"jsCode": "const fs = require('fs');\nconst items = $input.all();\nconst config = $('Set Configuration').first().json.config;\n\nconst csvPath = `${config.LOG_PATH}/error_log.csv`;\nconst header = 'timestamp,document_number,error_type,error_message,raw_ai_response\\n';\nconst esc = (s) => `\"${String(s || '').replace(/\"/g, '\"\"')}\"`;\n\nif (!fs.existsSync(csvPath)) {\n fs.writeFileSync(csvPath, header, 'utf8');\n}\n\nfor (const item of items) {\n const line = [\n new Date().toISOString(),\n esc(item.json.document_number),\n esc(item.json.error_type || 'UNKNOWN'),\n esc(item.json.error || item.json.parse_error),\n esc(item.json.raw_ai_response || '')\n ].join(',') + '\\n';\n \n fs.appendFileSync(csvPath, line, 'utf8');\n}\n\nreturn items;"
|
||||||
},
|
},
|
||||||
"id": "error-logger-csv",
|
"id": "cda3d253-a14d-4ec5-adaa-3e7b276be1f2",
|
||||||
"name": "Log Error to CSV",
|
"name": "Log Error to CSV",
|
||||||
"type": "n8n-nodes-base.code",
|
"type": "n8n-nodes-base.code",
|
||||||
"typeVersion": 2,
|
"typeVersion": 2,
|
||||||
"position": [
|
"position": [
|
||||||
1400,
|
6032,
|
||||||
400
|
4096
|
||||||
],
|
],
|
||||||
"notes": "บันทึก Error ลง CSV (จาก File Validator)"
|
"notes": "บันทึก Error ลง CSV (จาก File Validator)"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"operation": "executeQuery",
|
"operation": "executeQuery",
|
||||||
"host": "={{$('Set Configuration').first().json.config.DB_HOST}}",
|
|
||||||
"port": "={{$('Set Configuration').first().json.config.DB_PORT}}",
|
|
||||||
"database": "={{$('Set Configuration').first().json.config.DB_NAME}}",
|
|
||||||
"user": "={{$('Set Configuration').first().json.config.DB_USER}}",
|
|
||||||
"password": "={{$('Set Configuration').first().json.config.DB_PASSWORD}}",
|
|
||||||
"query": "INSERT INTO migration_errors (batch_id, document_number, error_type, error_message, raw_ai_response, created_at) VALUES ('{{$('Set Configuration').first().json.config.BATCH_ID}}', '{{$json.document_number}}', '{{$json.error_type || \"UNKNOWN\"}}', '{{$json.error || $json.parse_error}}', '{{$json.raw_ai_response || \"\"}}', NOW())",
|
"query": "INSERT INTO migration_errors (batch_id, document_number, error_type, error_message, raw_ai_response, created_at) VALUES ('{{$('Set Configuration').first().json.config.BATCH_ID}}', '{{$json.document_number}}', '{{$json.error_type || \"UNKNOWN\"}}', '{{$json.error || $json.parse_error}}', '{{$json.raw_ai_response || \"\"}}', NOW())",
|
||||||
"options": {}
|
"options": {}
|
||||||
},
|
},
|
||||||
"id": "error-logger-db",
|
"id": "1ee11a28-e339-42ac-9066-0ff6dac30920",
|
||||||
"name": "Log Error to DB",
|
"name": "Log Error to DB",
|
||||||
"type": "n8n-nodes-base.mySql",
|
"type": "n8n-nodes-base.mySql",
|
||||||
"typeVersion": 2.4,
|
"typeVersion": 2.4,
|
||||||
"position": [
|
"position": [
|
||||||
2000,
|
6640,
|
||||||
400
|
4096
|
||||||
],
|
],
|
||||||
|
"credentials": {
|
||||||
|
"mySql": {
|
||||||
|
"id": "CHHfbKhMacNo03V4",
|
||||||
|
"name": "MySQL account"
|
||||||
|
}
|
||||||
|
},
|
||||||
"notes": "บันทึก Error ลง MariaDB"
|
"notes": "บันทึก Error ลง MariaDB"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -411,17 +423,19 @@
|
|||||||
"amount": "={{$('Set Configuration').first().json.config.DELAY_MS}}",
|
"amount": "={{$('Set Configuration').first().json.config.DELAY_MS}}",
|
||||||
"unit": "milliseconds"
|
"unit": "milliseconds"
|
||||||
},
|
},
|
||||||
"id": "delay-node",
|
"id": "0bd637f6-6260-44ab-a27e-d7f4cb372ce4",
|
||||||
"name": "Delay",
|
"name": "Delay",
|
||||||
"type": "n8n-nodes-base.wait",
|
"type": "n8n-nodes-base.wait",
|
||||||
"typeVersion": 1,
|
"typeVersion": 1,
|
||||||
"position": [
|
"position": [
|
||||||
2800,
|
7440,
|
||||||
0
|
3696
|
||||||
],
|
],
|
||||||
|
"webhookId": "38e97a99-4dcc-4b63-977a-a02945a1c369",
|
||||||
"notes": "หน่วงเวลาระหว่าง Batches"
|
"notes": "หน่วงเวลาระหว่าง Batches"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"pinData": {},
|
||||||
"connections": {
|
"connections": {
|
||||||
"Manual Trigger": {
|
"Manual Trigger": {
|
||||||
"main": [
|
"main": [
|
||||||
@@ -435,6 +449,17 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"Set Configuration": {
|
"Set Configuration": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Check Backend Health",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Check Backend Health": {
|
||||||
"main": [
|
"main": [
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
@@ -457,6 +482,28 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"File Mount Check": {
|
"File Mount Check": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Read Excel Binary",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Read Excel Binary": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Read Excel",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Read Excel": {
|
||||||
"main": [
|
"main": [
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
@@ -493,14 +540,18 @@
|
|||||||
"main": [
|
"main": [
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"node": "Build AI Prompt",
|
"node": "Check Fallback State",
|
||||||
"type": "main",
|
"type": "main",
|
||||||
"index": 0
|
"index": 0
|
||||||
}
|
}
|
||||||
],
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Check Fallback State": {
|
||||||
|
"main": [
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"node": "Log Error to CSV",
|
"node": "Build AI Prompt",
|
||||||
"type": "main",
|
"type": "main",
|
||||||
"index": 0
|
"index": 0
|
||||||
}
|
}
|
||||||
@@ -537,13 +588,6 @@
|
|||||||
"type": "main",
|
"type": "main",
|
||||||
"index": 0
|
"index": 0
|
||||||
}
|
}
|
||||||
],
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"node": "Update Fallback State",
|
|
||||||
"type": "main",
|
|
||||||
"index": 0
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -555,24 +599,25 @@
|
|||||||
"type": "main",
|
"type": "main",
|
||||||
"index": 0
|
"index": 0
|
||||||
}
|
}
|
||||||
],
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Insert Review Queue": {
|
||||||
|
"main": [
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"node": "Insert Review Queue",
|
"node": "Delay",
|
||||||
"type": "main",
|
"type": "main",
|
||||||
"index": 0
|
"index": 0
|
||||||
}
|
}
|
||||||
],
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Log Reject to CSV": {
|
||||||
|
"main": [
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"node": "Log Reject to CSV",
|
"node": "Delay",
|
||||||
"type": "main",
|
|
||||||
"index": 0
|
|
||||||
}
|
|
||||||
],
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"node": "Log Error to DB",
|
|
||||||
"type": "main",
|
"type": "main",
|
||||||
"index": 0
|
"index": 0
|
||||||
}
|
}
|
||||||
@@ -601,6 +646,17 @@
|
|||||||
]
|
]
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"Save Checkpoint": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Delay",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
"Log Error to CSV": {
|
"Log Error to CSV": {
|
||||||
"main": [
|
"main": [
|
||||||
[
|
[
|
||||||
@@ -611,6 +667,40 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"Log Error to DB": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Delay",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Delay": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Read Checkpoint",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"active": false,
|
||||||
|
"settings": {
|
||||||
|
"executionOrder": "v1",
|
||||||
|
"availableInMCP": false
|
||||||
|
},
|
||||||
|
"versionId": "c52d7a07-398b-495e-b384-fb4f02ef3fed",
|
||||||
|
"meta": {
|
||||||
|
"templateCredsSetupCompleted": true,
|
||||||
|
"instanceId": "9e70e47c1eaf3bac72f497ddfbde0983f840f7d0f059537f7e37dd70de18ecb7"
|
||||||
|
},
|
||||||
|
"id": "u7CLP05AyFb8Um0P",
|
||||||
|
"tags": []
|
||||||
}
|
}
|
||||||
@@ -65,6 +65,7 @@ services:
|
|||||||
N8N_RESTRICT_FILE_ACCESS_TO: "/home/node/.n8n-files"
|
N8N_RESTRICT_FILE_ACCESS_TO: "/home/node/.n8n-files"
|
||||||
N8N_BLOCK_FILE_ACCESS_TO_N8N_FILES: "false"
|
N8N_BLOCK_FILE_ACCESS_TO_N8N_FILES: "false"
|
||||||
GENERIC_TIMEZONE: "Asia/Bangkok"
|
GENERIC_TIMEZONE: "Asia/Bangkok"
|
||||||
|
NODE_FUNCTION_ALLOW_BUILTIN: "*"
|
||||||
# DB Setup
|
# DB Setup
|
||||||
DB_TYPE: postgresdb
|
DB_TYPE: postgresdb
|
||||||
DB_POSTGRESDB_DATABASE: n8n
|
DB_POSTGRESDB_DATABASE: n8n
|
||||||
@@ -75,7 +76,7 @@ services:
|
|||||||
# Data Prune
|
# Data Prune
|
||||||
EXECUTIONS_DATA_PRUNE: 'true'
|
EXECUTIONS_DATA_PRUNE: 'true'
|
||||||
EXECUTIONS_DATA_MAX_AGE: 168
|
EXECUTIONS_DATA_MAX_AGE: 168
|
||||||
EXECUTIONS_DATA_PRUNE_TIMEOUT: 60
|
# EXECUTIONS_DATA_PRUNE_TIMEOUT: 60
|
||||||
|
|
||||||
ports:
|
ports:
|
||||||
- "5678:5678"
|
- "5678:5678"
|
||||||
|
|||||||
Reference in New Issue
Block a user