Здесь недоработанный, но уже подготовленный прокт. Нужно дописать бэк и фронт(все на реaкте) и дописать парсер с шаблонами. Для вайбкодинга можно просто написать допиши проект. Все работает грамотно через докер, а не через сплошное хамство в сервер. После доработки: подгружаем docker-compose, инициализируем, докачиваем остальное с гита. Важно, что пока qwen не развернут - иишку подключать через апи.
This commit is contained in:
Generated
+1736
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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}`);
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
import { Router } from 'express';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get('/health', (_req, res) => {
|
||||
res.json({ status: 'ok' });
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "commonjs",
|
||||
"rootDir": "src",
|
||||
"outDir": "dist",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"types": ["node"]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user