Добавлена возможность подключаться из вне

This commit is contained in:
2026-02-10 13:03:46 +05:00
parent 5a419156ed
commit baa670b4fd
185 changed files with 48524 additions and 11342 deletions

View File

@@ -7,8 +7,8 @@ NODE_ENV=development
JWT_SECRET=change-this-in-production-to-random-string
# AI Providers
AI_PROVIDER=iieasy
# iieasy | lmstudio
AI_PROVIDER=ollama
# iieasy | lmstudio | ollama
# iieasy.ru API
IIEASY_API_URL=https://ai.iieasy.ru/v1
@@ -19,6 +19,10 @@ IIEASY_MODEL=google/gemma-3n-e4b
LMSTUDIO_API_URL=http://localhost:1234/v1
LMSTUDIO_MODEL=local-model
# Ollama
OLLAMA_API_URL=http://192.168.88.160:11434
OLLAMA_MODEL=gemma3n:e4b
# Default company settings
DEFAULT_EXECUTOR=ООО "ГеоВектор"
DEFAULT_VAT_RATE=20

View File

@@ -7,7 +7,7 @@ NODE_ENV=development
# AI Providers
AI_PROVIDER=iieasy
# iieasy | lmstudio
# iieasy | lmstudio | ollama
# iieasy.ru API
IIEASY_API_URL=https://ai.iieasy.ru/v1
@@ -18,6 +18,10 @@ IIEASY_MODEL=google/gemma-3n-e4b
LMSTUDIO_API_URL=http://localhost:1234/v1
LMSTUDIO_MODEL=local-model
# Ollama
OLLAMA_API_URL=http://192.168.88.160:11434
OLLAMA_MODEL=gemma3n:e4b
# Default company settings
DEFAULT_EXECUTOR=ООО "ГеоВектор"
DEFAULT_VAT_RATE=20

View File

@@ -1,5 +0,0 @@
# Шрифты для PDF
Для генерации PDF с кириллицей используются шрифты PT Sans из npm-пакета `@fontsource/pt-sans` (файлы в `node_modules/@fontsource/pt-sans/files/`).
При необходимости можно положить сюда свои TTF/WOFF (например, PTSans-Regular.ttf и PTSans-Bold.ttf) — тогда в `pdf.service.ts` нужно указать путь к этой папке через `path.join(process.cwd(), 'fonts', '...')`.

View File

@@ -276,6 +276,7 @@
"integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/body-parser": "*",
"@types/express-serve-static-core": "^4.17.33",
@@ -344,6 +345,7 @@
"integrity": "sha512-WJtwWJu7UdlvzEAUm484QNg5eAoq5QR08KDNx7g45Usrs2NtOPiX8ugDqmKdXkyL03rBqU5dYNYVQetEpBHq2g==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"undici-types": "~6.21.0"
}
@@ -2633,6 +2635,7 @@
"devOptional": true,
"hasInstallScript": true,
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@prisma/engines": "5.22.0"
},
@@ -3374,6 +3377,7 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"

0
backend/node_modules/.prisma/client/default.d.ts generated vendored Executable file → Normal file
View File

0
backend/node_modules/.prisma/client/default.js generated vendored Executable file → Normal file
View File

0
backend/node_modules/.prisma/client/edge.d.ts generated vendored Executable file → Normal file
View File

25
backend/node_modules/.prisma/client/edge.js generated vendored Executable file → Normal file

File diff suppressed because one or more lines are too long

12
backend/node_modules/.prisma/client/index-browser.js generated vendored Executable file → Normal file
View File

@@ -219,10 +219,13 @@ exports.Prisma.EstimateScalarFieldEnum = {
totalLaboratory: 'totalLaboratory',
subtotal: 'subtotal',
regionalCoef: 'regionalCoef',
regionalCoefDocRef: 'regionalCoefDocRef',
inflationIndex: 'inflationIndex',
inflationDocRef: 'inflationDocRef',
companyCoef: 'companyCoef',
companyCoefDocRef: 'companyCoefDocRef',
executorCoef: 'executorCoef',
executorCoefDocRef: 'executorCoefDocRef',
withVat: 'withVat',
totalWithoutVat: 'totalWithoutVat',
vatRate: 'vatRate',
@@ -249,6 +252,14 @@ exports.Prisma.EstimateShareScalarFieldEnum = {
createdAt: 'createdAt'
};
exports.Prisma.EstimateShareNoteScalarFieldEnum = {
id: 'id',
shareId: 'shareId',
authorId: 'authorId',
content: 'content',
createdAt: 'createdAt'
};
exports.Prisma.EstimateItemScalarFieldEnum = {
id: 'id',
estimateId: 'estimateId',
@@ -355,6 +366,7 @@ exports.Prisma.ModelName = {
Estimate: 'Estimate',
EstimateVersion: 'EstimateVersion',
EstimateShare: 'EstimateShare',
EstimateShareNote: 'EstimateShareNote',
EstimateItem: 'EstimateItem',
EstimateTotal: 'EstimateTotal',
Setting: 'Setting',

1875
backend/node_modules/.prisma/client/index.d.ts generated vendored Executable file → Normal file

File diff suppressed because it is too large Load Diff

29
backend/node_modules/.prisma/client/index.js generated vendored Executable file → Normal file

File diff suppressed because one or more lines are too long

Binary file not shown.

2
backend/node_modules/.prisma/client/package.json generated vendored Executable file → Normal file
View File

@@ -1,5 +1,5 @@
{
"name": "prisma-client-4b58c981519ea022802510544edb111bbd4ef5e4d3b50b2269f96083d9bf4ae3",
"name": "prisma-client-aa42ad392173f3ebbcce688c9d47fd7d65145cf816fa2f07c9d3255147711e22",
"main": "index.js",
"types": "index.d.ts",
"browser": "index-browser.js",

View File

@@ -9,7 +9,7 @@ datasource db {
url = env("DATABASE_URL")
}
// Справочники базовых цен
// Справочники базовых цен (СБЦ)
model PriceBook {
id String @id @default(uuid())
code String @unique // SBC-GEODESY-2004, SBC-GEOLOGY-1999
@@ -114,8 +114,9 @@ model User {
estimates Estimate[]
chatSessions ChatSession[]
ownedShares EstimateShare[] @relation("ShareOwner")
receivedShares EstimateShare[] @relation("ShareReceiver")
ownedShares EstimateShare[] @relation("ShareOwner")
receivedShares EstimateShare[] @relation("ShareReceiver")
shareNotes EstimateShareNote[]
}
// Направления изысканий
@@ -146,13 +147,17 @@ model Estimate {
subtotal Decimal? @db.Decimal(14, 2) // Итого по изысканиям
// Коэффициенты и пересчет
regionalCoef Decimal? @db.Decimal(6, 4) // Районный коэффициент
inflationIndex Decimal? @db.Decimal(10, 4) // Индекс перевода в текущие цены
inflationDocRef String? // Ссылка на документ с индексом
companyCoef Decimal? @db.Decimal(6, 4) // Коэффициент компании (Газпром и т.д.)
executorCoef Decimal? @db.Decimal(6, 4) // Коэффициент исполнителя
regionalCoef Decimal? @db.Decimal(6, 4) // Районный коэффициент
regionalCoefDocRef String? // Описание (С районным коэффициентом и т.д.)
inflationIndex Decimal? @db.Decimal(10, 4) // Индекс перевода в текущие цены
inflationDocRef String? // Ссылка на документ (Письмо Минстроя и т.д.)
companyCoef Decimal? @db.Decimal(6, 4) // Коэффициент компании (Газпром и т.д.)
companyCoefDocRef String? // Описание (Коэффициент ОАО «Газпром» №544 и т.д.)
executorCoef Decimal? @db.Decimal(6, 4) // Коэффициент исполнителя
executorCoefDocRef String? // Описание (Коэффициент ООО «ГеоВектор» и т.д.)
// Итоги
withVat Boolean @default(true) // Смета с НДС (true) или без НДС (false)
totalWithoutVat Decimal? @db.Decimal(14, 2) // Итого без НДС
vatRate Decimal? @db.Decimal(4, 2) // Ставка НДС (18, 20, 7)
vatAmount Decimal? @db.Decimal(14, 2) // Сумма НДС
@@ -162,11 +167,26 @@ model Estimate {
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade)
direction SurveyDirection @relation(fields: [directionId], references: [id])
owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade)
direction SurveyDirection @relation(fields: [directionId], references: [id])
items EstimateItem[]
totals EstimateTotal[]
shares EstimateShare[]
versions EstimateVersion[]
}
// История версий сметы (снимок при сохранении/пересчёте)
model EstimateVersion {
id String @id @default(uuid())
estimateId String
versionNumber Int // Порядковый номер версии по смете
snapshot Json // Полный снимок: estimate + items + totals
createdAt DateTime @default(now())
estimate Estimate @relation(fields: [estimateId], references: [id], onDelete: Cascade)
@@index([estimateId])
@@index([estimateId, createdAt])
}
// Шаринг сметы с другим пользователем
@@ -177,14 +197,29 @@ model EstimateShare {
sharedWithId String // С кем поделились
createdAt DateTime @default(now())
estimate Estimate @relation(fields: [estimateId], references: [id], onDelete: Cascade)
sharedWith User @relation("ShareReceiver", fields: [sharedWithId], references: [id], onDelete: Cascade)
owner User @relation("ShareOwner", fields: [ownerId], references: [id], onDelete: Cascade)
estimate Estimate @relation(fields: [estimateId], references: [id], onDelete: Cascade)
sharedWith User @relation("ShareReceiver", fields: [sharedWithId], references: [id], onDelete: Cascade)
owner User @relation("ShareOwner", fields: [ownerId], references: [id], onDelete: Cascade)
notes EstimateShareNote[]
@@unique([estimateId, sharedWithId])
@@index([sharedWithId])
}
// Заметки к шарингу сметы (кто что написал)
model EstimateShareNote {
id String @id @default(uuid())
shareId String
authorId String
content String @db.Text
createdAt DateTime @default(now())
share EstimateShare @relation(fields: [shareId], references: [id], onDelete: Cascade)
author User @relation(fields: [authorId], references: [id], onDelete: Cascade)
@@index([shareId])
}
// Позиции сметы
model EstimateItem {
id String @id @default(uuid())

0
backend/node_modules/.prisma/client/wasm.d.ts generated vendored Executable file → Normal file
View File

12
backend/node_modules/.prisma/client/wasm.js generated vendored Executable file → Normal file
View File

@@ -219,10 +219,13 @@ exports.Prisma.EstimateScalarFieldEnum = {
totalLaboratory: 'totalLaboratory',
subtotal: 'subtotal',
regionalCoef: 'regionalCoef',
regionalCoefDocRef: 'regionalCoefDocRef',
inflationIndex: 'inflationIndex',
inflationDocRef: 'inflationDocRef',
companyCoef: 'companyCoef',
companyCoefDocRef: 'companyCoefDocRef',
executorCoef: 'executorCoef',
executorCoefDocRef: 'executorCoefDocRef',
withVat: 'withVat',
totalWithoutVat: 'totalWithoutVat',
vatRate: 'vatRate',
@@ -249,6 +252,14 @@ exports.Prisma.EstimateShareScalarFieldEnum = {
createdAt: 'createdAt'
};
exports.Prisma.EstimateShareNoteScalarFieldEnum = {
id: 'id',
shareId: 'shareId',
authorId: 'authorId',
content: 'content',
createdAt: 'createdAt'
};
exports.Prisma.EstimateItemScalarFieldEnum = {
id: 'id',
estimateId: 'estimateId',
@@ -355,6 +366,7 @@ exports.Prisma.ModelName = {
Estimate: 'Estimate',
EstimateVersion: 'EstimateVersion',
EstimateShare: 'EstimateShare',
EstimateShareNote: 'EstimateShareNote',
EstimateItem: 'EstimateItem',
EstimateTotal: 'EstimateTotal',
Setting: 'Setting',

Binary file not shown.

Binary file not shown.

0
backend/node_modules/bcrypt/.editorconfig generated vendored Executable file → Normal file
View File

0
backend/node_modules/bcrypt/.github/workflows/ci.yaml generated vendored Executable file → Normal file
View File

0
backend/node_modules/bcrypt/.travis.yml generated vendored Executable file → Normal file
View File

0
backend/node_modules/bcrypt/CHANGELOG.md generated vendored Executable file → Normal file
View File

0
backend/node_modules/bcrypt/ISSUE_TEMPLATE.md generated vendored Executable file → Normal file
View File

0
backend/node_modules/bcrypt/LICENSE generated vendored Executable file → Normal file
View File

0
backend/node_modules/bcrypt/Makefile generated vendored Executable file → Normal file
View File

0
backend/node_modules/bcrypt/README.md generated vendored Executable file → Normal file
View File

0
backend/node_modules/bcrypt/SECURITY.md generated vendored Executable file → Normal file
View File

0
backend/node_modules/bcrypt/appveyor.yml generated vendored Executable file → Normal file
View File

0
backend/node_modules/bcrypt/bcrypt.js generated vendored Executable file → Normal file
View File

0
backend/node_modules/bcrypt/binding.gyp generated vendored Executable file → Normal file
View File

0
backend/node_modules/bcrypt/examples/async_compare.js generated vendored Executable file → Normal file
View File

0
backend/node_modules/bcrypt/examples/forever_gen_salt.js generated vendored Executable file → Normal file
View File

Binary file not shown.

0
backend/node_modules/bcrypt/package.json generated vendored Executable file → Normal file
View File

0
backend/node_modules/bcrypt/promises.js generated vendored Executable file → Normal file
View File

0
backend/node_modules/bcrypt/src/bcrypt.cc generated vendored Executable file → Normal file
View File

0
backend/node_modules/bcrypt/src/bcrypt_node.cc generated vendored Executable file → Normal file
View File

0
backend/node_modules/bcrypt/src/blowfish.cc generated vendored Executable file → Normal file
View File

0
backend/node_modules/bcrypt/src/node_blf.h generated vendored Executable file → Normal file
View File

0
backend/node_modules/bcrypt/test/async.test.js generated vendored Executable file → Normal file
View File

0
backend/node_modules/bcrypt/test/implementation.test.js generated vendored Executable file → Normal file
View File

0
backend/node_modules/bcrypt/test/promise.test.js generated vendored Executable file → Normal file
View File

0
backend/node_modules/bcrypt/test/repetitions.test.js generated vendored Executable file → Normal file
View File

0
backend/node_modules/bcrypt/test/sync.test.js generated vendored Executable file → Normal file
View File

Binary file not shown.

View File

@@ -309,6 +309,7 @@
"integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/body-parser": "*",
"@types/express-serve-static-core": "^4.17.33",
@@ -377,6 +378,7 @@
"integrity": "sha512-WJtwWJu7UdlvzEAUm484QNg5eAoq5QR08KDNx7g45Usrs2NtOPiX8ugDqmKdXkyL03rBqU5dYNYVQetEpBHq2g==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"undici-types": "~6.21.0"
}
@@ -2681,6 +2683,7 @@
"devOptional": true,
"hasInstallScript": true,
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@prisma/engines": "5.22.0"
},
@@ -3422,6 +3425,7 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"

View File

@@ -25,12 +25,12 @@
"cors": "^2.8.5",
"dotenv": "^16.4.1",
"express": "^4.18.2",
"jsonwebtoken": "^9.0.2",
"multer": "^1.4.5-lts.1",
"pdf-lib": "^1.17.1",
"pdfkit": "^0.14.0",
"uuid": "^9.0.1",
"xlsx": "^0.18.5",
"jsonwebtoken": "^9.0.2"
"xlsx": "^0.18.5"
},
"devDependencies": {
"@types/bcrypt": "^5.0.2",

View File

@@ -19,8 +19,8 @@ const app = express();
const prisma = new PrismaClient();
const PORT = process.env.PORT || 5000;
// Middleware — фронт на :3500, прокси Vite тоже
app.use(cors({ origin: ['http://localhost:3500', 'http://127.0.0.1:3500'], credentials: true }));
// Middleware — CORS: разрешаем запросы с любого origin (для внутренней сети)
app.use(cors({ origin: true, credentials: true }));
app.use(cookieParser());
app.use(express.json({ limit: '50mb' }));
app.use(express.urlencoded({ extended: true, limit: '50mb' }));

View File

@@ -22,6 +22,8 @@ export class AIService {
private iieasyModel: string;
private lmstudioUrl: string;
private lmstudioModel: string;
private ollamaUrl: string;
private ollamaModel: string;
constructor() {
this.provider = process.env.AI_PROVIDER || 'iieasy';
@@ -30,9 +32,11 @@ export class AIService {
this.iieasyModel = process.env.IIEASY_MODEL || 'google/gemma-3n-e4b';
this.lmstudioUrl = process.env.LMSTUDIO_API_URL || 'http://localhost:1234/v1';
this.lmstudioModel = process.env.LMSTUDIO_MODEL || 'local-model';
this.ollamaUrl = (process.env.OLLAMA_API_URL || 'http://localhost:11434').replace(/\/+$/, '');
this.ollamaModel = process.env.OLLAMA_MODEL || 'gemma3n:e4b';
}
setProvider(provider: 'iieasy' | 'lmstudio') {
setProvider(provider: 'iieasy' | 'lmstudio' | 'ollama') {
this.provider = provider;
}
@@ -45,11 +49,9 @@ export class AIService {
allMessages.push(...messages);
if (this.provider === 'lmstudio') {
return this.chatLMStudio(allMessages);
} else {
return this.chatIIEasy(allMessages);
}
if (this.provider === 'ollama') return this.chatOllama(allMessages);
if (this.provider === 'lmstudio') return this.chatLMStudio(allMessages);
return this.chatIIEasy(allMessages);
}
private async chatIIEasy(messages: ChatMessage[]): Promise<AIResponse> {
@@ -117,6 +119,44 @@ export class AIService {
}
}
private async chatOllama(messages: ChatMessage[]): Promise<AIResponse> {
// Ollama API docs: POST /api/chat { model, messages, stream:false, options:{} }
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 120_000);
try {
const response = await fetch(`${this.ollamaUrl}/api/chat`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
signal: controller.signal,
body: JSON.stringify({
model: this.ollamaModel,
messages,
stream: false,
options: {
temperature: 0.7,
num_predict: 4096,
},
}),
});
if (!response.ok) {
const error = await response.text();
throw new Error(`Ollama API error: ${response.status} - ${error}`);
}
const data: any = await response.json();
return {
content: data?.message?.content || '',
};
} catch (error: any) {
const msg = error?.name === 'AbortError' ? 'timeout' : error?.message;
console.error('Ollama API error:', error);
throw new Error(`AI service error: ${msg}`);
} finally {
clearTimeout(timeout);
}
}
async extractEstimateData(
text: string,
previousData?: Partial<{

View File

@@ -88,6 +88,15 @@ function getScriptPath(): string {
return candidates[0];
}
/** Python для запуска скрипта: предпочитаем venv в каталоге pdf_generator (Ubuntu 24.04). */
function getPythonCommand(): string {
const scriptPath = getScriptPath();
const pdfGenDir = path.dirname(scriptPath);
const venvPython = path.join(pdfGenDir, 'venv', 'bin', 'python3');
if (fs.existsSync(venvPython)) return venvPython;
return process.platform === 'win32' ? 'python' : 'python3';
}
/**
* Сгенерировать PDF сметы через Python (ReportLab, кириллица).
* При ошибке возвращает null — тогда используется Node (PDFKit).
@@ -113,7 +122,7 @@ export async function generateEstimatePdfWithPython(estimate: any): Promise<Buff
return null;
}
const pythonCmd = process.platform === 'win32' ? 'python' : 'python3';
const pythonCmd = getPythonCommand();
const args = [scriptPath, tempFile];
return new Promise((resolve) => {