АДРЕСНЫЙ РЕЖИМ - M2.3b тюнинг account-scope и диагностика стадий адресного рантайма
This commit is contained in:
@@ -0,0 +1,252 @@
|
||||
import {
|
||||
ASSISTANT_MCP_CHANNEL,
|
||||
ASSISTANT_MCP_PROXY_URL,
|
||||
ASSISTANT_MCP_TIMEOUT_MS
|
||||
} from "../config";
|
||||
|
||||
interface McpExecuteQueryResponse {
|
||||
success?: unknown;
|
||||
data?: unknown;
|
||||
error?: unknown;
|
||||
}
|
||||
|
||||
export interface AddressMcpQueryResult {
|
||||
ok: boolean;
|
||||
rows: Array<Record<string, unknown>>;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
function toStringValue(value: unknown): string {
|
||||
if (value === null || value === undefined) {
|
||||
return "";
|
||||
}
|
||||
return String(value);
|
||||
}
|
||||
|
||||
function parseFiniteNumber(value: unknown): number | null {
|
||||
if (typeof value === "number" && Number.isFinite(value)) {
|
||||
return value;
|
||||
}
|
||||
if (typeof value === "string") {
|
||||
const parsed = Number(value.replace(",", ".").trim());
|
||||
if (Number.isFinite(parsed)) {
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function parseRowsFromTextTable(source: string): Array<Record<string, unknown>> {
|
||||
const normalized = String(source ?? "").replace(/\r/g, "").trim();
|
||||
if (!normalized) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const headerMatch = normalized.match(/\{([^}]*)\}:/);
|
||||
if (!headerMatch) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const columns = String(headerMatch[1] ?? "")
|
||||
.split(",")
|
||||
.map((item) => item.replace(/^"+|"+$/g, "").trim())
|
||||
.filter(Boolean);
|
||||
|
||||
const body = normalized.slice((headerMatch.index ?? 0) + headerMatch[0].length).trim();
|
||||
if (!body) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const rows: Array<Record<string, unknown>> = [];
|
||||
const lines = body
|
||||
.split("\n")
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
for (const line of lines) {
|
||||
const values: string[] = [];
|
||||
const matcher = /"([^"]*)"|([^,]+)/g;
|
||||
let match: RegExpExecArray | null = null;
|
||||
while ((match = matcher.exec(line)) !== null) {
|
||||
const raw = match[1] !== undefined ? match[1] : match[2];
|
||||
const value = String(raw ?? "").trim();
|
||||
if (value.length > 0) {
|
||||
values.push(value);
|
||||
}
|
||||
}
|
||||
|
||||
if (values.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const row: Record<string, unknown> = {};
|
||||
for (let index = 0; index < columns.length; index += 1) {
|
||||
const key = columns[index] ?? `column_${index + 1}`;
|
||||
const raw = values[index] ?? "";
|
||||
const parsed = parseFiniteNumber(raw);
|
||||
row[key] = parsed ?? raw;
|
||||
}
|
||||
|
||||
if (values[0]) row.Period = values[0];
|
||||
if (values[1]) row.Registrator = values[1];
|
||||
if (values[2]) row.AccountDt = values[2];
|
||||
if (values[3]) row.AccountKt = values[3];
|
||||
if (values[4]) row.Amount = parseFiniteNumber(values[4]) ?? values[4];
|
||||
rows.push(row);
|
||||
}
|
||||
|
||||
return rows;
|
||||
}
|
||||
|
||||
function parseExecutePayload(payload: unknown): AddressMcpQueryResult {
|
||||
if (!payload || typeof payload !== "object") {
|
||||
return {
|
||||
ok: false,
|
||||
rows: [],
|
||||
error: "MCP payload is empty or malformed"
|
||||
};
|
||||
}
|
||||
|
||||
const source = payload as McpExecuteQueryResponse;
|
||||
if (source.success !== true) {
|
||||
return {
|
||||
ok: false,
|
||||
rows: [],
|
||||
error: toStringValue(source.error).trim() || "MCP execute_query returned success=false"
|
||||
};
|
||||
}
|
||||
|
||||
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);
|
||||
return {
|
||||
ok: true,
|
||||
rows,
|
||||
error: null
|
||||
};
|
||||
}
|
||||
|
||||
if (typeof source.data === "string") {
|
||||
return {
|
||||
ok: true,
|
||||
rows: parseRowsFromTextTable(source.data),
|
||||
error: null
|
||||
};
|
||||
}
|
||||
|
||||
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);
|
||||
return {
|
||||
ok: true,
|
||||
rows,
|
||||
error: null
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
rows: [],
|
||||
error: null
|
||||
};
|
||||
}
|
||||
|
||||
function buildMcpUrl(endpoint: string): string {
|
||||
const normalizedEndpoint = endpoint.startsWith("/") ? endpoint : `/${endpoint}`;
|
||||
const separator = normalizedEndpoint.includes("?") ? "&" : "?";
|
||||
return `${ASSISTANT_MCP_PROXY_URL}${normalizedEndpoint}${separator}channel=${encodeURIComponent(ASSISTANT_MCP_CHANNEL)}`;
|
||||
}
|
||||
|
||||
function escapeRegExp(value: string): string {
|
||||
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
|
||||
function filterRowsByAccountScope(
|
||||
rows: Array<Record<string, unknown>>,
|
||||
accountScope: string[]
|
||||
): Array<Record<string, unknown>> {
|
||||
if (accountScope.length === 0) {
|
||||
return rows;
|
||||
}
|
||||
const matchers = accountScope.map((account) => new RegExp(`\\b${escapeRegExp(account)}(?:\\.\\d{1,2})?\\b`, "i"));
|
||||
return rows.filter((row) => {
|
||||
const searchable = Object.values(row)
|
||||
.map((item) => String(item ?? ""))
|
||||
.join(" ");
|
||||
return matchers.some((matcher) => matcher.test(searchable));
|
||||
});
|
||||
}
|
||||
|
||||
export async function executeAddressMcpQuery(input: {
|
||||
query: string;
|
||||
limit: number;
|
||||
account_scope?: string[];
|
||||
}): Promise<{
|
||||
fetched_rows: number;
|
||||
matched_rows: number;
|
||||
raw_rows: Array<Record<string, unknown>>;
|
||||
rows: Array<Record<string, unknown>>;
|
||||
error: string | null;
|
||||
}> {
|
||||
const endpoint = buildMcpUrl("/api/execute_query");
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), Math.max(300, ASSISTANT_MCP_TIMEOUT_MS));
|
||||
try {
|
||||
const response = await fetch(endpoint, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json; charset=utf-8"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
query: input.query,
|
||||
limit: input.limit
|
||||
}),
|
||||
signal: controller.signal
|
||||
});
|
||||
|
||||
const responseText = await response.text();
|
||||
if (!response.ok) {
|
||||
return {
|
||||
fetched_rows: 0,
|
||||
matched_rows: 0,
|
||||
raw_rows: [],
|
||||
rows: [],
|
||||
error: `MCP HTTP ${response.status}: ${responseText.slice(0, 240)}`
|
||||
};
|
||||
}
|
||||
|
||||
const payload = responseText.trim() ? (JSON.parse(responseText) as unknown) : {};
|
||||
const parsed = parseExecutePayload(payload);
|
||||
if (!parsed.ok) {
|
||||
return {
|
||||
fetched_rows: 0,
|
||||
matched_rows: 0,
|
||||
raw_rows: [],
|
||||
rows: [],
|
||||
error: parsed.error
|
||||
};
|
||||
}
|
||||
|
||||
const filtered = filterRowsByAccountScope(parsed.rows, Array.isArray(input.account_scope) ? input.account_scope : []);
|
||||
return {
|
||||
fetched_rows: parsed.rows.length,
|
||||
matched_rows: filtered.length,
|
||||
raw_rows: parsed.rows,
|
||||
rows: filtered,
|
||||
error: null
|
||||
};
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
return {
|
||||
fetched_rows: 0,
|
||||
matched_rows: 0,
|
||||
raw_rows: [],
|
||||
rows: [],
|
||||
error: `MCP fetch failed: ${message}`
|
||||
};
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user