АДРЕСНЫЙ РЕЖИМ - локальная подель на декомпозе

This commit is contained in:
2026-04-01 17:55:02 +03:00
parent 4060a5e575
commit 4d59672576
90 changed files with 19595 additions and 785 deletions
@@ -3,6 +3,7 @@
ASSISTANT_MCP_PROXY_URL,
ASSISTANT_MCP_TIMEOUT_MS
} from "../config";
import iconv from "iconv-lite";
interface McpExecuteQueryResponse {
success?: unknown;
@@ -36,8 +37,81 @@ function parseFiniteNumber(value: unknown): number | null {
return null;
}
function textMojibakeScore(value: string): number {
const source = String(value ?? "");
const cyrillic = (source.match(/[А-Яа-яЁё]/g) ?? []).length;
const latin = (source.match(/[A-Za-z]/g) ?? []).length;
const hardMarkers = (source.match(/[Ѓѓ‚„…†‡€‰‹ЉЊЌЋЏ‘’“”•–—™љ›њќћџ]/g) ?? []).length;
const pairMarkers = (source.match(/(?:Р.|С.|Ð.|Ñ.)/g) ?? []).length;
return cyrillic + latin - hardMarkers * 3 - pairMarkers * 2;
}
function looksLikeMojibake(value: string): boolean {
const source = String(value ?? "");
if (!source.trim()) {
return false;
}
if (/[Ѓѓ‚„…†‡€‰‹ЉЊЌЋЏ‘’“”•–—™љ›њќћџ]/.test(source)) {
return true;
}
return (source.match(/(?:Р.|С.|Ð.|Ñ.)/g) ?? []).length >= 2;
}
function decodeUtf8FromWin1251Mojibake(value: string): string {
if (!looksLikeMojibake(value)) {
return value;
}
try {
const bytes = iconv.encode(value, "win1251");
const decoded = bytes.toString("utf8");
return textMojibakeScore(decoded) > textMojibakeScore(value) ? decoded : value;
} catch {
return value;
}
}
function decodeUtf8FromLatin1Mojibake(value: string): string {
if (!looksLikeMojibake(value)) {
return value;
}
try {
const decoded = Buffer.from(value, "latin1").toString("utf8");
return textMojibakeScore(decoded) > textMojibakeScore(value) ? decoded : value;
} catch {
return value;
}
}
function normalizeMojibakeString(value: string): string {
const fromWin1251 = decodeUtf8FromWin1251Mojibake(value);
return decodeUtf8FromLatin1Mojibake(fromWin1251);
}
function normalizeMojibakeValue(value: unknown): unknown {
if (typeof value === "string") {
return normalizeMojibakeString(value);
}
if (Array.isArray(value)) {
return value.map((item) => normalizeMojibakeValue(item));
}
if (value && typeof value === "object") {
const source = value as Record<string, unknown>;
const normalized: Record<string, unknown> = {};
for (const [key, raw] of Object.entries(source)) {
const repairedKey = normalizeMojibakeString(key);
normalized[repairedKey] = normalizeMojibakeValue(raw);
}
return normalized;
}
return value;
}
function normalizeMojibakeRows(rows: Array<Record<string, unknown>>): Array<Record<string, unknown>> {
return rows.map((row) => normalizeMojibakeValue(row) as Record<string, unknown>);
}
function parseRowsFromTextTable(source: string): Array<Record<string, unknown>> {
const normalized = String(source ?? "").replace(/\r/g, "").trim();
const normalized = normalizeMojibakeString(String(source ?? "")).replace(/\r/g, "").trim();
if (!normalized) {
return [];
}
@@ -111,7 +185,7 @@ function parseRowsFromTextTable(source: string): Array<Record<string, unknown>>
rows.push(row);
}
return rows;
return normalizeMojibakeRows(rows);
}
function parseExecutePayload(payload: unknown): AddressMcpQueryResult {
@@ -133,9 +207,11 @@ function parseExecutePayload(payload: unknown): AddressMcpQueryResult {
}
if (Array.isArray(source.data)) {
const rows = source.data
.map((item) => (item && typeof item === "object" ? (item as Record<string, unknown>) : null))
.filter((item): item is Record<string, unknown> => item !== null);
const rows = normalizeMojibakeRows(
source.data
.map((item) => (item && typeof item === "object" ? (item as Record<string, unknown>) : null))
.filter((item): item is Record<string, unknown> => item !== null)
);
return {
ok: true,
rows,
@@ -152,9 +228,11 @@ function parseExecutePayload(payload: unknown): AddressMcpQueryResult {
}
if (source.data && typeof source.data === "object" && Array.isArray((source.data as { rows?: unknown }).rows)) {
const rows = ((source.data as { rows: unknown[] }).rows ?? [])
.map((item) => (item && typeof item === "object" ? (item as Record<string, unknown>) : null))
.filter((item): item is Record<string, unknown> => item !== null);
const rows = normalizeMojibakeRows(
((source.data as { rows: unknown[] }).rows ?? [])
.map((item) => (item && typeof item === "object" ? (item as Record<string, unknown>) : null))
.filter((item): item is Record<string, unknown> => item !== null)
);
return {
ok: true,
rows,