klg-asutk-app/inbox-server/index.js
Yuriy b147d16798 MVP: заглушки, auth, .env.example, связь с бэкендом, главная КЛГ
- Заполнены заглушки: user-friendly-messages, health, aria, keyboard
- backend: core/auth.py, /api/v1/stats; cached-api → backend-client при USE_MOCK_DATA=false
- .env.example, middleware auth (skip при USE_MOCK_DATA), убраны неиспользуемые deps
- Страницы: airworthiness, maintenance, defects, modifications; AircraftAddModal, Sidebar
- Главная страница: REFLY — Контроль лётной годности (вместо Numerology App)
- Линт/скрипты: eslintrc, security, cleanup, logs, api inbox/knowledge

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-13 16:43:53 +03:00

480 lines
14 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import express from "express";
import multer from "multer";
import path from "path";
import fs from "fs";
import { fileURLToPath } from "url";
import { randomUUID } from "crypto";
import { createHash } from "crypto";
import Database from "better-sqlite3";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const app = express();
const PORT = process.env.PORT || 3001;
// Пути
const DATA_DIR = path.join(__dirname, "../data");
const INBOX_DIR = path.join(DATA_DIR, "ai-inbox");
const DB_PATH = path.join(DATA_DIR, "db", "inbox.db");
// Создаём директории если их нет
[INBOX_DIR, path.join(DATA_DIR, "db")].forEach((dir) => {
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
});
// Инициализация БД
const db = new Database(DB_PATH);
db.exec(`
CREATE TABLE IF NOT EXISTS file_registry (
id TEXT PRIMARY KEY,
original_name TEXT NOT NULL,
stored_path TEXT NOT NULL,
mime TEXT NOT NULL,
size INTEGER NOT NULL,
sha256 TEXT NOT NULL,
created_at TEXT NOT NULL,
status TEXT DEFAULT 'pending'
);
CREATE TABLE IF NOT EXISTS ai_extraction_run (
id TEXT PRIMARY KEY,
file_id TEXT NOT NULL,
started_at TEXT NOT NULL,
completed_at TEXT,
status TEXT DEFAULT 'running',
error TEXT,
FOREIGN KEY (file_id) REFERENCES file_registry(id)
);
CREATE TABLE IF NOT EXISTS ai_extraction_field (
id TEXT PRIMARY KEY,
run_id TEXT NOT NULL,
field_code TEXT NOT NULL,
value TEXT,
confidence REAL,
provenance TEXT,
FOREIGN KEY (run_id) REFERENCES ai_extraction_run(id)
);
CREATE TABLE IF NOT EXISTS tmc_request_draft (
id TEXT PRIMARY KEY,
file_id TEXT NOT NULL,
extraction_run_id TEXT,
status TEXT DEFAULT 'draft',
data TEXT NOT NULL,
created_at TEXT NOT NULL,
updated_at TEXT,
FOREIGN KEY (file_id) REFERENCES file_registry(id),
FOREIGN KEY (extraction_run_id) REFERENCES ai_extraction_run(id)
);
`);
// Настройка multer для загрузки файлов
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, INBOX_DIR);
},
filename: (req, file, cb) => {
// Формат: uuid_originalName
const uuid = randomUUID();
const sanitizedName = file.originalname.replace(/[^a-zA-Z0-9._-]/g, '_');
cb(null, `${uuid}_${sanitizedName}`);
},
});
const upload = multer({
storage,
limits: { fileSize: 50 * 1024 * 1024 }, // 50MB
fileFilter: (req, file, cb) => {
const allowedTypes = [
"application/pdf",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"application/msword",
];
if (allowedTypes.includes(file.mimetype)) {
cb(null, true);
} else {
cb(new Error("Неподдерживаемый тип файла. Разрешены только PDF и DOCX"));
}
},
});
app.use(express.json());
app.use((req, res, next) => {
res.header("Access-Control-Allow-Origin", process.env.CORS_ORIGIN || "http://localhost:3000");
res.header("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
res.header("Access-Control-Allow-Headers", "Content-Type");
if (req.method === "OPTIONS") {
res.sendStatus(200);
} else {
next();
}
});
// Загрузка промптов
function loadPrompts() {
const promptsDir = path.join(__dirname, "../prompts");
const systemPrompt = fs.readFileSync(path.join(promptsDir, "system.md"), "utf8");
const policyPrompt = fs.readFileSync(path.join(promptsDir, "policy.md"), "utf8");
let domainPrompts = {};
const domainDir = path.join(promptsDir, "domain");
if (fs.existsSync(domainDir)) {
const domainFiles = fs.readdirSync(domainDir);
domainFiles.forEach(file => {
if (file.endsWith(".md")) {
const domain = file.replace(".md", "");
domainPrompts[domain] = fs.readFileSync(path.join(domainDir, file), "utf8");
}
});
}
return { systemPrompt, policyPrompt, domainPrompts };
}
// Получить список файлов
app.get("/api/inbox/files", (req, res) => {
try {
const files = db
.prepare("SELECT * FROM file_registry ORDER BY created_at DESC")
.all();
res.json(files);
} catch (error) {
console.error("Error fetching files:", error);
res.status(500).json({ error: "Failed to fetch files" });
}
});
// Загрузить файл
app.post("/api/inbox/upload", upload.single("file"), (req, res) => {
if (!req.file) {
return res.status(400).json({ error: "No file uploaded" });
}
try {
// Вычисляем SHA256
const fileBuffer = fs.readFileSync(req.file.path);
const sha256 = createHash("sha256").update(fileBuffer).digest("hex");
// Извлекаем UUID из имени файла (формат: uuid_originalName)
const uuid = req.file.filename.split("_")[0];
const storedPath = path.join("ai-inbox", req.file.filename);
const stmt = db.prepare(`
INSERT INTO file_registry (id, original_name, stored_path, mime, size, sha256, created_at, status)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`);
const createdAt = new Date().toISOString();
const fileRecord = {
id: uuid,
originalName: req.file.originalname,
storedPath,
mime: req.file.mimetype,
size: req.file.size,
sha256,
createdAt,
status: "pending",
};
stmt.run(
fileRecord.id,
fileRecord.originalName,
fileRecord.storedPath,
fileRecord.mime,
fileRecord.size,
fileRecord.sha256,
fileRecord.createdAt,
fileRecord.status
);
res.json(fileRecord);
} catch (error) {
console.error("Error saving file:", error);
res.status(500).json({ error: "Failed to save file" });
}
});
// Скачать файл
app.get("/api/inbox/files/:id/download", (req, res) => {
try {
const file = db
.prepare("SELECT * FROM file_registry WHERE id = ?")
.get(req.params.id);
if (!file) {
return res.status(404).json({ error: "File not found" });
}
// stored_path может быть относительным или абсолютным
const filePath = file.stored_path.startsWith("/")
? file.stored_path
: path.join(DATA_DIR, file.stored_path);
if (!fs.existsSync(filePath)) {
return res.status(404).json({ error: "File not found on disk" });
}
res.download(filePath, file.original_name);
} catch (error) {
console.error("Error downloading file:", error);
res.status(500).json({ error: "Failed to download file" });
}
});
// Удалить файл
app.delete("/api/inbox/files/:id", (req, res) => {
try {
const file = db
.prepare("SELECT * FROM file_registry WHERE id = ?")
.get(req.params.id);
if (!file) {
return res.status(404).json({ error: "File not found" });
}
// Удаляем файл с диска
const filePath = file.stored_path.startsWith("/")
? file.stored_path
: path.join(DATA_DIR, file.stored_path);
if (fs.existsSync(filePath)) {
fs.unlinkSync(filePath);
}
// Удаляем запись из БД
db.prepare("DELETE FROM file_registry WHERE id = ?").run(req.params.id);
res.json({ success: true });
} catch (error) {
console.error("Error deleting file:", error);
res.status(500).json({ error: "Failed to delete file" });
}
});
// Запуск Extract
app.post("/api/inbox/files/:id/extract", async (req, res) => {
try {
const file = db
.prepare("SELECT * FROM file_registry WHERE id = ?")
.get(req.params.id);
if (!file) {
return res.status(404).json({ error: "File not found" });
}
const runId = randomUUID();
const startedAt = new Date().toISOString();
// Создаём запись о запуске
db.prepare(
"INSERT INTO ai_extraction_run (id, file_id, started_at, status) VALUES (?, ?, ?, ?)"
).run(runId, file.id, startedAt, "running");
// Загружаем промпты
const prompts = loadPrompts();
// Логируем использование промптов
// console.log(`[Extract] Run ${runId} started for file ${file.id}`);
console.log(`[Extract] Using prompts: system.md, policy.md`);
if (Object.keys(prompts.domainPrompts).length > 0) {
console.log(`[Extract] Domain prompts: ${Object.keys(prompts.domainPrompts).join(", ")}`);
}
// TODO: Здесь будет вызов AI для извлечения данных
// Пока симулируем извлечение с задержкой
setTimeout(() => {
// Пример извлечённых полей с provenance
const sampleFields = [
{
code: "doc_number",
value: "DOC-2026-001",
confidence: 0.95,
provenance: "Страница 1, верхний колонтитул"
},
{
code: "doc_date",
value: "2026-01-25",
confidence: 0.90,
provenance: "Страница 1, строка 2"
},
{
code: "organization",
value: "MURA MENASA FZCO",
confidence: 0.88,
provenance: "Страница 1, блок подписи"
},
];
// Сохраняем поля
const fieldStmt = db.prepare(
"INSERT INTO ai_extraction_field (id, run_id, field_code, value, confidence, provenance) VALUES (?, ?, ?, ?, ?, ?)"
);
sampleFields.forEach((field, index) => {
const fieldId = randomUUID();
fieldStmt.run(fieldId, runId, field.code, field.value, field.confidence, field.provenance);
});
// Обновляем статус запуска
db.prepare(
"UPDATE ai_extraction_run SET completed_at = ?, status = ? WHERE id = ?"
).run(new Date().toISOString(), "completed", runId);
console.log(`[Extract] Run ${runId} completed successfully`);
}, 2000);
res.json({ runId, status: "running", startedAt });
} catch (error) {
console.error("Error starting extraction:", error);
res.status(500).json({ error: "Failed to start extraction" });
}
});
// Получить результаты Extract
app.get("/api/inbox/extractions/:runId", (req, res) => {
try {
const run = db
.prepare("SELECT * FROM ai_extraction_run WHERE id = ?")
.get(req.params.runId);
if (!run) {
return res.status(404).json({ error: "Extraction run not found" });
}
const fields = db
.prepare("SELECT * FROM ai_extraction_field WHERE run_id = ?")
.all(req.params.runId);
res.json({ run, fields });
} catch (error) {
console.error("Error fetching extraction:", error);
res.status(500).json({ error: "Failed to fetch extraction" });
}
});
// Apply - создание draft сущности
app.post("/api/inbox/files/:id/apply", (req, res) => {
try {
const file = db
.prepare("SELECT * FROM file_registry WHERE id = ?")
.get(req.params.id);
if (!file) {
return res.status(404).json({ error: "File not found" });
}
// Находим последний успешный запуск Extract
const run = db
.prepare(
"SELECT * FROM ai_extraction_run WHERE file_id = ? AND status = 'completed' ORDER BY completed_at DESC LIMIT 1"
)
.get(req.params.id);
if (!run) {
return res.status(400).json({ error: "No completed extraction found for this file" });
}
// Получаем извлечённые поля
const fields = db
.prepare("SELECT * FROM ai_extraction_field WHERE run_id = ?")
.all(run.id);
// Создаём draft сущность в tmc_request_draft
const draftId = randomUUID();
const createdAt = new Date().toISOString();
// Преобразуем поля в JSON для хранения
const draftData = {
sourceFile: file.original_name,
extractionRunId: run.id,
fields: fields.map((f) => ({
code: f.field_code,
value: f.value,
confidence: f.confidence,
provenance: f.provenance,
})),
};
// Сохраняем draft в БД
const draftStmt = db.prepare(`
INSERT INTO tmc_request_draft (id, file_id, extraction_run_id, status, data, created_at)
VALUES (?, ?, ?, ?, ?, ?)
`);
draftStmt.run(
draftId,
file.id,
run.id,
"draft",
JSON.stringify(draftData),
createdAt
);
console.log(`[Apply] Draft ${draftId} created from file ${file.id}, extraction ${run.id}`);
res.json({
success: true,
draftId,
message: "Черновик создан успешно. Требуется подтверждение для активации.",
status: "draft",
createdAt,
fields: fields.map((f) => ({
code: f.field_code,
value: f.value,
confidence: f.confidence,
provenance: f.provenance,
})),
});
} catch (error) {
console.error("Error applying extraction:", error);
res.status(500).json({ error: "Failed to apply extraction" });
}
});
// Получить список draft сущностей
app.get("/api/tmc/drafts", (req, res) => {
try {
const drafts = db
.prepare("SELECT * FROM tmc_request_draft ORDER BY created_at DESC")
.all();
// Парсим JSON данные
const draftsWithData = drafts.map(draft => ({
...draft,
data: JSON.parse(draft.data),
}));
res.json(draftsWithData);
} catch (error) {
console.error("Error fetching drafts:", error);
res.status(500).json({ error: "Failed to fetch drafts" });
}
});
// Получить draft по ID
app.get("/api/tmc/drafts/:id", (req, res) => {
try {
const draft = db
.prepare("SELECT * FROM tmc_request_draft WHERE id = ?")
.get(req.params.id);
if (!draft) {
return res.status(404).json({ error: "Draft not found" });
}
draft.data = JSON.parse(draft.data);
res.json(draft);
} catch (error) {
console.error("Error fetching draft:", error);
res.status(500).json({ error: "Failed to fetch draft" });
}
});
app.listen(PORT, () => {
console.log(`🚀 API Server running on http://localhost:${PORT}`);
});