DI2 внедрение, таск 1: серверная интеграция Data-Insight2 (server/finance-di2)
- Копия DI2-сервера в server/finance-di2/ с правками: schema/db-client/cache/table-config/override-tables/google-auth/audit-agent/routes - db-client — обёртка над существующим пулом server/finance/db-client (второй пул не создаётся, добавлен экспорт isConnectionError) - ai-config.ts — чистое IO конфигов из ai-agent.ts без Telegram-поллинга; роуты /api/ai/toggle и /api/ai/status удалены, без compression/startAuditScheduler/autoStartIfEnabled - Обёртка registerDi2Routes с auth-gate (authenticateToken + finance.manage), регистрация строго перед registerFinanceRoutes - Статика /di2 из dist/public-di2 перед веткой vite/static (dev и prod) - Фикс предсуществующего бага DI2: buildGlobalExclusionConditions без cats в /api/profitability
This commit is contained in:
192
server/finance-di2/ai-config.ts
Normal file
192
server/finance-di2/ai-config.ts
Normal file
@@ -0,0 +1,192 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
// Вырезано из Data-Insight2 (модуль ИИ-агента) — только чистое IO конфигов и лога.
|
||||
// Telegram-поллинг, cron и сетевые вызовы намеренно не переносятся.
|
||||
|
||||
const AI_CONFIG_PATH = path.join("data", "ai-config.json");
|
||||
const AI_LOG_PATH = path.join("data", "ai-query-log.json");
|
||||
const MAX_LOG_ENTRIES = 500;
|
||||
|
||||
export interface AiLogEntry {
|
||||
timestamp: string;
|
||||
chatId: string;
|
||||
chatName: string;
|
||||
question: string;
|
||||
answer: string;
|
||||
sqlQueries: string[];
|
||||
}
|
||||
|
||||
function loadAiLog(): AiLogEntry[] {
|
||||
try {
|
||||
if (fs.existsSync(AI_LOG_PATH)) {
|
||||
return JSON.parse(fs.readFileSync(AI_LOG_PATH, "utf-8"));
|
||||
}
|
||||
} catch {}
|
||||
return [];
|
||||
}
|
||||
|
||||
export function saveLogEntry(entry: AiLogEntry): void {
|
||||
try {
|
||||
const log = loadAiLog();
|
||||
log.push(entry);
|
||||
if (log.length > MAX_LOG_ENTRIES) {
|
||||
log.splice(0, log.length - MAX_LOG_ENTRIES);
|
||||
}
|
||||
fs.writeFileSync(AI_LOG_PATH, JSON.stringify(log, null, 2));
|
||||
} catch (err) {
|
||||
console.error("[AI Log] Failed to save:", err);
|
||||
}
|
||||
}
|
||||
|
||||
export function getAiLog(days: number = 90): AiLogEntry[] {
|
||||
const log = loadAiLog();
|
||||
const cutoff = new Date();
|
||||
cutoff.setDate(cutoff.getDate() - days);
|
||||
return log.filter(e => new Date(e.timestamp) >= cutoff);
|
||||
}
|
||||
|
||||
export interface AiConfig {
|
||||
apiUrl: string;
|
||||
apiKey: string;
|
||||
model: string;
|
||||
systemPrompt: string;
|
||||
enabled: boolean;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface AiPlanningConfig {
|
||||
apiUrl: string;
|
||||
apiKey: string;
|
||||
model: string;
|
||||
systemPrompt: string;
|
||||
name: string;
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
const DEFAULT_SYSTEM_PROMPT = `Ты — финансовый аналитик компании ОмегаПром. Отвечай на русском языке. У тебя есть доступ к двум источникам данных:
|
||||
|
||||
1. **База данных PostgreSQL** — содержит таблицы доходов (opdohodi), расходов (oprashodi) и дебиторской задолженности (vw_debt_active). Используй инструмент query_database для SQL-запросов (только SELECT).
|
||||
|
||||
2. **Данные зарплат из Google Sheets** — содержат ФИО сотрудников, базовую зарплату и сумму к выдаче по участкам и месяцам. Используй инструмент get_salary_data.
|
||||
|
||||
ВАЖНО — особенности структуры данных:
|
||||
|
||||
### vw_debt_active (дебиторская задолженность):
|
||||
- Таблица хранит ЕЖЕДНЕВНЫЕ СНИМКИ (snapshots) задолженности. Поле "period" — дата снимка. Каждый period содержит полный срез всех долгов на эту дату.
|
||||
- КРИТИЧЕСКИ ВАЖНО: для получения АКТУАЛЬНЫХ данных по задолженности ВСЕГДА фильтруй по последнему периоду: WHERE period = (SELECT MAX(period) FROM vw_debt_active)
|
||||
- БЕЗ фильтра по period суммы будут завышены в сотни раз из-за суммирования по всем историческим снимкам!
|
||||
- Внутри одного периода может быть несколько строк на одного контрагента (по разным документам/договорам). Для агрегатных запросов используй GROUP BY kontragent и SUM(summadolga::numeric).
|
||||
|
||||
### opdohodi (доходы) и oprashodi (расходы):
|
||||
- Эти таблицы НЕ имеют снимков — каждая строка уникальна.
|
||||
- Для суммарных запросов по доходам: GROUP BY zakazchik и SUM(summa::numeric)
|
||||
- Для суммарных запросов по расходам: GROUP BY и SUM(summadokumenta::numeric)
|
||||
- Суммы (summadolga, summa, summadokumenta) хранятся как text — для математических операций приводи к numeric: ::numeric
|
||||
- Даты в БД хранятся в формате timestamp
|
||||
|
||||
Примеры правильных запросов:
|
||||
- Топ 5 должников: SELECT kontragent, SUM(summadolga::numeric) as total FROM vw_debt_active WHERE period = (SELECT MAX(period) FROM vw_debt_active) GROUP BY kontragent ORDER BY total DESC LIMIT 5
|
||||
- Общая дебиторка: SELECT SUM(summadolga::numeric) as total FROM vw_debt_active WHERE period = (SELECT MAX(period) FROM vw_debt_active)
|
||||
- Просроченная дебиторка: SELECT SUM(summaprosrocheno::numeric) as total FROM vw_debt_active WHERE period = (SELECT MAX(period) FROM vw_debt_active)
|
||||
- Задолженность по классам просрочки: SELECT klassprosrochki, SUM(summadolga::numeric) as total FROM vw_debt_active WHERE period = (SELECT MAX(period) FROM vw_debt_active) GROUP BY klassprosrochki ORDER BY total DESC
|
||||
- Доходы по контрагентам: SELECT zakazchik, SUM(summa::numeric) as total FROM opdohodi GROUP BY zakazchik ORDER BY total DESC LIMIT 10
|
||||
- Расходы за период: SELECT SUM(summadokumenta::numeric) as total FROM oprashodi WHERE data >= '2025-01-01' AND data < '2025-04-01'
|
||||
|
||||
Правила:
|
||||
- Генерируй только SELECT запросы, никогда INSERT/UPDATE/DELETE
|
||||
- ВСЕГДА используй GROUP BY и агрегатные функции для "топ", "сколько всего", "общая сумма" и подобных запросов
|
||||
- Для vw_debt_active ВСЕГДА добавляй WHERE period = (SELECT MAX(period) FROM vw_debt_active) если нужны актуальные данные
|
||||
- Суммы в рублях, форматируй с разделителем тысяч
|
||||
- Если не уверен в структуре данных, сначала сделай запрос для проверки
|
||||
- Отвечай кратко и по делу`;
|
||||
|
||||
const DEFAULT_CONFIG: AiConfig = {
|
||||
apiUrl: "https://openrouter.ai/api/v1",
|
||||
apiKey: "",
|
||||
model: "deepseek/deepseek-chat",
|
||||
systemPrompt: DEFAULT_SYSTEM_PROMPT,
|
||||
enabled: false,
|
||||
name: "Ассистент",
|
||||
};
|
||||
|
||||
export function loadAiConfig(): AiConfig {
|
||||
try {
|
||||
if (fs.existsSync(AI_CONFIG_PATH)) {
|
||||
const raw = JSON.parse(fs.readFileSync(AI_CONFIG_PATH, "utf-8"));
|
||||
return { ...DEFAULT_CONFIG, ...raw };
|
||||
}
|
||||
} catch {}
|
||||
return { ...DEFAULT_CONFIG };
|
||||
}
|
||||
|
||||
export function saveAiConfig(cfg: AiConfig): void {
|
||||
fs.writeFileSync(AI_CONFIG_PATH, JSON.stringify(cfg, null, 2));
|
||||
}
|
||||
|
||||
const AI_PLANNING_CONFIG_PATH = path.join("data", "ai-planning-config.json");
|
||||
|
||||
const DEFAULT_PLANNING_PROMPT = `Ты — финансовый аналитик компании ОмегаПром. Проанализируй данные и составь раздельный прогноз доходов и расходов на следующие 12 месяцев.
|
||||
|
||||
ВАЖНО: результат должен быть РАЗДЕЛЬНЫЙ — доходы и расходы анализируются по разным принципам.
|
||||
|
||||
## ДОХОДЫ (type: "income")
|
||||
Анализируй ТОЛЬКО по контрагентам и договорам:
|
||||
- Для каждого контрагента+договора определи регулярность поступлений
|
||||
- Учитывай дебиторскую задолженность: если контрагент имеет большой долг или просрочку — это риск неплатежа, снижай confidence
|
||||
- Если есть ожидаемые поступления по актам (разница между выставленным и оплаченным) — отметь в reasoning
|
||||
- В поле "name" укажи контрагента, в "contract" — договор
|
||||
|
||||
## РАСХОДЫ (type: "expense")
|
||||
Если даны расходы по категориям — анализируй по ним. Если по контрагентам — анализируй по контрагентам.
|
||||
- Определи гарантированные повторяющиеся расходы (аренда, зарплата, связь, коммуналка и т.д.)
|
||||
- В поле "name" укажи категорию или контрагента, в "contract" — уточнение если есть
|
||||
|
||||
## Поле recurring
|
||||
Для каждой позиции укажи "recurring": true если операция повторяется регулярно (ежемесячно или через месяц), false если нерегулярная но устойчивая.
|
||||
|
||||
Верни ТОЛЬКО JSON-массив без markdown-обёртки, без \`\`\`json, просто чистый JSON:
|
||||
[
|
||||
{
|
||||
"type": "income" или "expense",
|
||||
"name": "название контрагента или категории",
|
||||
"contract": "договор или уточнение",
|
||||
"monthlyAmount": среднемесячная_сумма_число,
|
||||
"confidence": "high" или "medium" или "low",
|
||||
"recurring": true или false,
|
||||
"reasoning": "краткое пояснение на русском почему включил и какой тренд"
|
||||
}
|
||||
]
|
||||
|
||||
Правила:
|
||||
- Включай только операции с confidence medium или high
|
||||
- Для high: операция повторяется минимум 3 месяца подряд с похожими суммами
|
||||
- Для medium: операция повторяется 2+ месяца или имеет нерегулярный но устойчивый паттерн
|
||||
- monthlyAmount — прогнозируемая среднемесячная сумма (число без форматирования)
|
||||
- Не включай разовые операции
|
||||
- Сначала все доходы (отсортированные по сумме убывания), потом все расходы (отсортированные по сумме убывания)
|
||||
- Верни максимум 50 позиций
|
||||
- Ответ — ТОЛЬКО JSON массив, никакого текста до или после`;
|
||||
|
||||
const DEFAULT_PLANNING_CONFIG: AiPlanningConfig = {
|
||||
apiUrl: "https://openrouter.ai/api/v1",
|
||||
apiKey: "",
|
||||
model: "deepseek/deepseek-chat",
|
||||
systemPrompt: DEFAULT_PLANNING_PROMPT,
|
||||
name: "Аналитик",
|
||||
enabled: true,
|
||||
};
|
||||
|
||||
export function loadAiPlanningConfig(): AiPlanningConfig {
|
||||
try {
|
||||
if (fs.existsSync(AI_PLANNING_CONFIG_PATH)) {
|
||||
const raw = JSON.parse(fs.readFileSync(AI_PLANNING_CONFIG_PATH, "utf-8"));
|
||||
return { ...DEFAULT_PLANNING_CONFIG, ...raw };
|
||||
}
|
||||
} catch {}
|
||||
return { ...DEFAULT_PLANNING_CONFIG };
|
||||
}
|
||||
|
||||
export function saveAiPlanningConfig(cfg: AiPlanningConfig): void {
|
||||
fs.writeFileSync(AI_PLANNING_CONFIG_PATH, JSON.stringify(cfg, null, 2));
|
||||
}
|
||||
Reference in New Issue
Block a user