NODEDC_1C/llm_normalizer/backend/src/services/capabilitiesRegistry.ts

260 lines
12 KiB
TypeScript
Raw 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 fs from "fs";
import { ASSISTANT_CAPABILITIES_REGISTRY_FILE } from "../config";
export type CapabilityMaturity = "production_ready" | "partial" | "planned" | "deprecated";
export interface CapabilityGroup {
group_code: string;
group_title: string;
description: string;
risk_level: "low" | "medium" | "high";
maturity_status: CapabilityMaturity;
supported_operations: string[];
unsupported_operations: string[];
required_entities: string[];
optional_entities: string[];
typical_queries: string[];
related_routes: string[];
safe_alternatives: string[];
one_c_hints: string[];
}
export interface CapabilityRegistry {
schema_version: string;
updated_at: string;
assistant_mode: "read_only" | "mixed";
groups: CapabilityGroup[];
}
const FALLBACK_REGISTRY: CapabilityRegistry = {
schema_version: "capabilities_registry_fallback_v1",
updated_at: "2026-04-09T00:00:00.000Z",
assistant_mode: "read_only",
groups: [
{
group_code: "vat",
group_title: "НДС",
description: "Срезы и расчёты НДС на базе данных 1С",
risk_level: "high",
maturity_status: "partial",
supported_operations: ["vat_period_snapshot", "vat_payable_forecast"],
unsupported_operations: ["submit_tax_declaration"],
required_entities: ["period", "organization"],
optional_entities: ["counterparty"],
typical_queries: ["Сколько НДС к уплате за период?", "Покажи срез НДС на дату"],
related_routes: [],
safe_alternatives: ["Показать движения по 68/19 за период"],
one_c_hints: ["РегистрБухгалтерии.Хозрасчетный"]
},
{
group_code: "counterparties",
group_title: "Контрагенты",
description: "Документы, операции, договоры и активность по контрагентам",
risk_level: "medium",
maturity_status: "production_ready",
supported_operations: ["list_documents_by_counterparty", "list_contracts_by_counterparty"],
unsupported_operations: ["edit_counterparty_card"],
required_entities: ["counterparty_scope_or_contract"],
optional_entities: ["period", "organization"],
typical_queries: ["Покажи документы по контрагенту", "Какие операции были по банку с контрагентом?"],
related_routes: [],
safe_alternatives: ["Уточнить ИНН или наименование контрагента"],
one_c_hints: ["Справочник.Контрагенты"]
},
{
group_code: "settlements",
group_title: "Долги и расчёты",
description: "Сальдо, хвосты, незакрытые авансы и аналитика по расчётам",
risk_level: "high",
maturity_status: "production_ready",
supported_operations: ["receivables_confirmed_as_of_date", "open_items_by_counterparty_or_contract"],
unsupported_operations: ["close_period"],
required_entities: ["period_or_date"],
optional_entities: ["organization", "account", "counterparty"],
typical_queries: ["Кто нам должен на дату?", "Хвосты покажи по счёту 60 за период"],
related_routes: [],
safe_alternatives: ["Уточнить период, счёт или организацию"],
one_c_hints: ["РегистрБухгалтерии.Хозрасчетный"]
},
{
group_code: "cash",
group_title: "Деньги",
description: "Остатки и движение по денежным счетам и кассе",
risk_level: "medium",
maturity_status: "production_ready",
supported_operations: ["account_balance_snapshot", "bank_operations_by_counterparty"],
unsupported_operations: ["post_bank_statement"],
required_entities: ["date_or_period"],
optional_entities: ["organization", "account", "counterparty"],
typical_queries: ["Какой остаток по счёту 51 на дату?", "Покажи движение денег за месяц"],
related_routes: [],
safe_alternatives: ["Уточнить счёт или период"],
one_c_hints: ["РегистрБухгалтерии.Хозрасчетный"]
},
{
group_code: "inventory",
group_title: "Склад и товары",
description: "Подтверждённые остатки, происхождение и документы по товарным позициям",
risk_level: "medium",
maturity_status: "production_ready",
supported_operations: [
"inventory_on_hand_as_of_date",
"inventory_purchase_provenance_for_item",
"inventory_purchase_documents_for_item"
],
unsupported_operations: ["write_off_inventory"],
required_entities: ["date_or_period"],
optional_entities: ["organization", "warehouse", "item"],
typical_queries: ["Какие товары сейчас лежат на складе?", "Кто поставил эту позицию?"],
related_routes: [],
safe_alternatives: ["Уточнить организацию, дату или позицию"],
one_c_hints: ["РегистрБухгалтерии.Хозрасчетный"]
},
{
group_code: "boundaries",
group_title: "Ограничения",
description: "Операции, которые ассистент не выполняет в этом рантайме",
risk_level: "high",
maturity_status: "production_ready",
supported_operations: ["explain_boundary", "suggest_safe_next_step"],
unsupported_operations: ["configure_1c", "admin_server_actions", "create_or_post_documents"],
required_entities: [],
optional_entities: [],
typical_queries: ["Можешь настроить 1С?"],
related_routes: [],
safe_alternatives: ["Сформировать безопасный план диагностики для 1С или ИТ-админа"],
one_c_hints: []
}
]
};
let cache: { mtimeMs: number; value: CapabilityRegistry } | null = null;
function toRecord(value: unknown): Record<string, unknown> | null {
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
return value as Record<string, unknown>;
}
function toStringSafe(value: unknown): string | null {
if (typeof value !== "string") return null;
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : null;
}
function toArray(value: unknown): unknown[] {
return Array.isArray(value) ? value : [];
}
function readRegistryFromFile(): CapabilityRegistry | null {
if (!fs.existsSync(ASSISTANT_CAPABILITIES_REGISTRY_FILE)) return null;
try {
const raw = fs.readFileSync(ASSISTANT_CAPABILITIES_REGISTRY_FILE, "utf-8");
const parsed = JSON.parse(raw) as unknown;
const root = toRecord(parsed);
if (!root) return null;
const groups = toArray(root.groups)
.map((item) => toRecord(item))
.filter((item): item is Record<string, unknown> => item !== null)
.map((item) => ({
group_code: toStringSafe(item.group_code) ?? "unknown_group",
group_title: toStringSafe(item.group_title) ?? "Группа",
description: toStringSafe(item.description) ?? "",
risk_level: (toStringSafe(item.risk_level) as "low" | "medium" | "high" | null) ?? "medium",
maturity_status:
(toStringSafe(item.maturity_status) as CapabilityMaturity | null) ??
("partial" as CapabilityMaturity),
supported_operations: toArray(item.supported_operations)
.map((v) => toStringSafe(v))
.filter((v): v is string => v !== null),
unsupported_operations: toArray(item.unsupported_operations)
.map((v) => toStringSafe(v))
.filter((v): v is string => v !== null),
required_entities: toArray(item.required_entities)
.map((v) => toStringSafe(v))
.filter((v): v is string => v !== null),
optional_entities: toArray(item.optional_entities)
.map((v) => toStringSafe(v))
.filter((v): v is string => v !== null),
typical_queries: toArray(item.typical_queries)
.map((v) => toStringSafe(v))
.filter((v): v is string => v !== null),
related_routes: toArray(item.related_routes)
.map((v) => toStringSafe(v))
.filter((v): v is string => v !== null),
safe_alternatives: toArray(item.safe_alternatives)
.map((v) => toStringSafe(v))
.filter((v): v is string => v !== null),
one_c_hints: toArray(item.one_c_hints)
.map((v) => toStringSafe(v))
.filter((v): v is string => v !== null)
}));
if (groups.length === 0) return null;
return {
schema_version: toStringSafe(root.schema_version) ?? "capabilities_registry_v1",
updated_at: toStringSafe(root.updated_at) ?? new Date().toISOString(),
assistant_mode: (toStringSafe(root.assistant_mode) as "read_only" | "mixed" | null) ?? "read_only",
groups
};
} catch {
return null;
}
}
export function loadCapabilitiesRegistry(): CapabilityRegistry {
try {
const mtimeMs = fs.existsSync(ASSISTANT_CAPABILITIES_REGISTRY_FILE)
? fs.statSync(ASSISTANT_CAPABILITIES_REGISTRY_FILE).mtimeMs
: -1;
if (cache && cache.mtimeMs === mtimeMs) {
return cache.value;
}
const value = readRegistryFromFile() ?? FALLBACK_REGISTRY;
cache = { mtimeMs, value };
return value;
} catch {
return cache?.value ?? FALLBACK_REGISTRY;
}
}
export function buildCapabilityContractReplyFromRegistry(): string {
return [
"Могу помочь с быстрым анализом данных 1С:",
"",
"- кто должен денег и кому должны;",
"- какой год или месяц был самым денежным;",
"- какие контрагенты дают основной поток;",
"- что лежит на складе и какие остатки стареют;",
"- сколько НДС к уплате за период;",
"- какие документы, оплаты и договоры есть по контрагенту.",
"",
"Примеры запросов:",
"- кто самый доходный клиент за все время",
"- что зависло на складе",
"- кому мы должны на сегодня",
"- какое нетто по СВК за 2020",
"- сколько НДС к уплате за 4 квартал 2019",
"",
"Что не делаю: не настраиваю 1С, не меняю конфигурацию, не создаю и не провожу документы, не выполняю админ-действия на сервере."
].join("\n");
}
export function resolveNearestCapabilityGroup(input: { domain?: string | null; queryClass?: string | null }): CapabilityGroup | null {
const registry = loadCapabilitiesRegistry();
const haystack = `${String(input.domain ?? "")} ${String(input.queryClass ?? "")}`.toLowerCase();
if (!haystack.trim()) return null;
const scoring: Array<{ group: CapabilityGroup; score: number }> = registry.groups.map((group) => {
let score = 0;
const bucket = `${group.group_code} ${group.group_title} ${group.description} ${group.supported_operations.join(" ")}`.toLowerCase();
for (const token of haystack.split(/[\s._/-]+/g).filter(Boolean)) {
if (bucket.includes(token)) score += 1;
}
return { group, score };
});
scoring.sort((a, b) => b.score - a.score);
return scoring[0] && scoring[0].score > 0 ? scoring[0].group : null;
}