Здесь недоработанный, но уже подготовленный прокт. Нужно дописать бэк и фронт(все на реaкте) и дописать парсер с шаблонами. Для вайбкодинга можно просто написать допиши проект. Все работает грамотно через докер, а не через сплошное хамство в сервер. После доработки: подгружаем docker-compose, инициализируем, докачиваем остальное с гита. Важно, что пока qwen не развернут - иишку подключать через апи.

This commit is contained in:
anker-na-20
2026-05-25 14:37:45 +03:00
commit 6e5d9e978b
34 changed files with 5690 additions and 0 deletions
+1736
View File
File diff suppressed because it is too large Load Diff
+24
View File
@@ -0,0 +1,24 @@
{
"name": "tz-backend",
"version": "0.1.0",
"main": "dist/index.js",
"scripts": {
"dev": "ts-node-dev --respawn --transpile-only src/index.ts",
"build": "tsc",
"start": "node dist/index.js"
},
"dependencies": {
"cors": "^2.8.5",
"express": "^4.19.0",
"multer": "^1.4.5-lts.1",
"uuid": "^9.0.1"
},
"devDependencies": {
"@types/express": "^4.17.21",
"@types/multer": "^1.4.7",
"@types/node": "^20.11.0",
"@types/uuid": "^9.0.6",
"ts-node-dev": "^2.0.0",
"typescript": "^5.6.0"
}
}
+21
View File
@@ -0,0 +1,21 @@
import express from 'express';
import cors from 'cors';
import path from 'path';
import healthRouter from './routes/health';
import jobsRouter from './routes/jobs';
const app = express();
const port = Number(process.env.PORT || 3000);
app.use(cors());
app.use(express.json());
app.use(healthRouter);
app.use(jobsRouter);
// статика на будущее (если нужно)
app.use('/static', express.static(path.join(__dirname, '..', 'public')));
app.listen(port, () => {
console.log(`Backend listening on port ${port}`);
});
+9
View File
@@ -0,0 +1,9 @@
import { Router } from 'express';
const router = Router();
router.get('/health', (_req, res) => {
res.json({ status: 'ok' });
});
export default router;
+56
View File
@@ -0,0 +1,56 @@
import { Router } from 'express';
import multer from 'multer';
import path from 'path';
import fs from 'fs';
import { createJob, listJobs, getJob, processJob } from '../services/jobsService';
const router = Router();
const ROOT_DIR = process.cwd(); // корень backend-проекта
const STORAGE_INPUT_PATH =
process.env.STORAGE_INPUT_PATH || path.join(ROOT_DIR, '..', 'storage', 'input');
const STORAGE_OUTPUT_PATH =
process.env.STORAGE_OUTPUT_PATH || path.join(ROOT_DIR, '..', 'storage', 'output');
const upload = multer({ dest: STORAGE_INPUT_PATH });
router.post('/api/jobs', upload.single('file'), async (req, res) => {
try {
if (!req.file) {
return res.status(400).json({ error: 'file is required' });
}
const templateType = (req.body.templateType as string) || null;
const job = createJob(req.file.originalname, templateType);
const newPath = path.join(STORAGE_INPUT_PATH, `${job.id}.pdf`);
await fs.promises.rename(req.file.path, newPath);
await processJob({ ...job, storedFilePath: newPath });
const updated = getJob(job.id);
res.status(201).json(updated);
} catch (e) {
console.error(e);
res.status(500).json({ error: 'internal_error' });
}
});
router.get('/api/jobs', (_req, res) => {
res.json(listJobs());
});
router.get('/api/jobs/:id', (req, res) => {
const job = getJob(req.params.id);
if (!job) return res.status(404).json({ error: 'not_found' });
res.json(job);
});
router.get('/api/jobs/:id/result', async (req, res) => {
const job = getJob(req.params.id);
if (!job || !job.resultFilePath) {
return res.status(404).json({ error: 'result_not_found' });
}
res.download(job.resultFilePath, `${job.id}.docx`);
});
export default router;
+62
View File
@@ -0,0 +1,62 @@
import { Job, JobStatus } from '../types/jobs';
import { v4 as uuidv4 } from 'uuid';
import path from 'path';
import fs from 'fs';
const jobs = new Map<string, Job>();
const ROOT_DIR = process.cwd(); // корень backend-проекта
const STORAGE_INPUT_PATH =
process.env.STORAGE_INPUT_PATH || path.join(ROOT_DIR, '..', 'storage', 'input');
const STORAGE_OUTPUT_PATH =
process.env.STORAGE_OUTPUT_PATH || path.join(ROOT_DIR, '..', 'storage', 'output');
export function createJob(originalFileName: string, templateType?: string | null): Job {
const id = uuidv4();
const storedFilePath = path.join(STORAGE_INPUT_PATH, `${id}.pdf`);
const job: Job = {
id,
originalFileName,
storedFilePath,
resultFilePath: null,
templateType: templateType || null,
status: 'QUEUED',
errorMessage: null,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
};
jobs.set(id, job);
return job;
}
export function updateJobStatus(id: string, status: JobStatus, fields?: Partial<Job>): Job | null {
const job = jobs.get(id);
if (!job) return null;
const updated: Job = {
...job,
...fields,
status,
updatedAt: new Date().toISOString()
};
jobs.set(id, updated);
return updated;
}
export function getJob(id: string): Job | null {
return jobs.get(id) ?? null;
}
export function listJobs(): Job[] {
return Array.from(jobs.values()).sort((a, b) => b.createdAt.localeCompare(a.createdAt));
}
/**
* Заглушка: "обрабатываем" PDF и делаем фиктивный DOCX.
* В реальности здесь будет вызов n8n / LLM / генератора DOCX.
*/
export async function processJob(job: Job): Promise<Job> {
const resultPath = path.join(STORAGE_OUTPUT_PATH, `${job.id}.docx`);
await fs.promises.mkdir(STORAGE_OUTPUT_PATH, { recursive: true });
await fs.promises.writeFile(resultPath, `Fake DOCX content for job ${job.id}`);
return updateJobStatus(job.id, 'DONE', { resultFilePath: resultPath }) as Job;
}
+13
View File
@@ -0,0 +1,13 @@
export type JobStatus = 'QUEUED' | 'PROCESSING' | 'DONE' | 'ERROR';
export interface Job {
id: string;
originalFileName: string;
storedFilePath: string;
resultFilePath?: string | null;
templateType?: string | null;
status: JobStatus;
errorMessage?: string | null;
createdAt: string;
updatedAt: string;
}
+13
View File
@@ -0,0 +1,13 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"rootDir": "src",
"outDir": "dist",
"strict": true,
"esModuleInterop": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"types": ["node"]
}
}