ДОМЕНЫ - ВОПРОСЫ - НДС: Стабилизировать VAT MCP probe: добавить задержки, увеличить таймауты и retry на aborted
This commit is contained in:
@@ -91,15 +91,20 @@ const ADDRESS_CONFIRMED_PAYABLES_MIN_LIMIT = 200;
|
||||
const COUNTERPARTY_CATALOG_LOOKUP_LIMIT = 1000;
|
||||
const COUNTERPARTY_CATALOG_CACHE_TTL_MS = 120_000;
|
||||
const VAT_METADATA_PROBE_LIMIT = 100;
|
||||
const VAT_SOURCE_PROBE_MAX_OBJECTS = 6;
|
||||
const VAT_METADATA_PROBE_TYPES = ["РегистрНакопления", "Документ"] as const;
|
||||
const VAT_SOURCE_PROBE_MAX_OBJECTS = 4;
|
||||
const VAT_METADATA_PROBE_TYPES = ["РегистрНакопления"] as const;
|
||||
const VAT_METADATA_PROBE_MASKS = ["ндс", "книгапродаж", "книгапокупок", "счетфактур"] as const;
|
||||
const VAT_METADATA_PROBE_CONCURRENCY = 4;
|
||||
const VAT_METADATA_PROBE_TIMEOUT_MS = 800;
|
||||
const VAT_METADATA_PROBE_RETRY_TIMEOUT_MS = 1_200;
|
||||
const VAT_OBJECT_PROBE_CONCURRENCY = 4;
|
||||
const VAT_OBJECT_PROBE_TIMEOUT_MS = 800;
|
||||
const VAT_OBJECT_PROBE_FALLBACK_TIMEOUT_MS = 800;
|
||||
const VAT_METADATA_PROBE_CONCURRENCY = 2;
|
||||
const VAT_METADATA_PROBE_STAGGER_MS = 90;
|
||||
const VAT_METADATA_PROBE_TIMEOUT_MS = 1_200;
|
||||
const VAT_METADATA_PROBE_RETRY_TIMEOUT_MS = 1_800;
|
||||
const VAT_METADATA_PROBE_RETRY_DELAY_MS = 140;
|
||||
const VAT_OBJECT_PROBE_CONCURRENCY = 1;
|
||||
const VAT_OBJECT_PROBE_STAGGER_MS = 120;
|
||||
const VAT_OBJECT_PROBE_TIMEOUT_MS = 1_500;
|
||||
const VAT_OBJECT_PROBE_ABORT_RETRY_TIMEOUT_MS = 2_200;
|
||||
const VAT_OBJECT_PROBE_ABORT_RETRY_DELAY_MS = 180;
|
||||
const VAT_OBJECT_PROBE_FALLBACK_TIMEOUT_MS = 1_500;
|
||||
const PARTY_ANCHOR_STOPWORDS = new Set([
|
||||
"ооо",
|
||||
"ао",
|
||||
@@ -156,6 +161,28 @@ interface VatMetadataObject {
|
||||
synonym: string | null;
|
||||
objectType: "document" | "register";
|
||||
}
|
||||
const VAT_FALLBACK_METADATA_OBJECTS: VatMetadataObject[] = [
|
||||
{
|
||||
fullName: "РегистрНакопления.НДСЗаписиКнигиПродаж",
|
||||
synonym: "НДС Продажи",
|
||||
objectType: "register"
|
||||
},
|
||||
{
|
||||
fullName: "РегистрНакопления.НДСЗаписиКнигиПокупок",
|
||||
synonym: "НДС Покупки",
|
||||
objectType: "register"
|
||||
},
|
||||
{
|
||||
fullName: "РегистрНакопления.НДСПредъявленный",
|
||||
synonym: "НДС предъявленный",
|
||||
objectType: "register"
|
||||
},
|
||||
{
|
||||
fullName: "РегистрНакопления.НДСВключенныйВСтоимость",
|
||||
synonym: "НДС, включенный в стоимость",
|
||||
objectType: "register"
|
||||
}
|
||||
];
|
||||
const COUNTERPARTY_CATALOG_LOOKUP_QUERY_TEMPLATE = `
|
||||
ВЫБРАТЬ ПЕРВЫЕ __LIMIT__
|
||||
ДАТАВРЕМЯ(2000, 1, 1, 0, 0, 0) КАК Период,
|
||||
@@ -363,6 +390,14 @@ async function mapWithConcurrency<T, R>(
|
||||
return results;
|
||||
}
|
||||
|
||||
function sleepMs(ms: number): Promise<void> {
|
||||
const delayMs = Number.isFinite(ms) ? Math.max(0, Math.trunc(ms)) : 0;
|
||||
if (delayMs <= 0) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return new Promise((resolve) => setTimeout(resolve, delayMs));
|
||||
}
|
||||
|
||||
async function executeVatMetadataProbeRequest(request: {
|
||||
meta_type: string;
|
||||
name_mask: string;
|
||||
@@ -376,6 +411,7 @@ async function executeVatMetadataProbeRequest(request: {
|
||||
return firstAttempt;
|
||||
}
|
||||
|
||||
await sleepMs(VAT_METADATA_PROBE_RETRY_DELAY_MS);
|
||||
const retryLimit = Math.max(20, Math.min(request.limit, Math.trunc(request.limit / 2)));
|
||||
const retryAttempt = await executeAddressMcpMetadata({
|
||||
...request,
|
||||
@@ -420,6 +456,11 @@ function scoreVatMetadataObject(item: VatMetadataObject): number {
|
||||
return score;
|
||||
}
|
||||
|
||||
function vatMetadataObjectPriority(item: VatMetadataObject): number {
|
||||
const idx = VAT_FALLBACK_METADATA_OBJECTS.findIndex((fallback) => fallback.fullName === item.fullName);
|
||||
return idx >= 0 ? idx : VAT_FALLBACK_METADATA_OBJECTS.length + 1;
|
||||
}
|
||||
|
||||
type VatObjectProbeMode = "latest" | "exists";
|
||||
|
||||
function buildVatObjectProbeQuery(object: VatMetadataObject, asOfExpr: string, mode: VatObjectProbeMode = "latest"): string {
|
||||
@@ -494,7 +535,12 @@ async function probeVatDirectSources(filters: AddressFilterSet): Promise<VatDire
|
||||
const metadataResponses = await mapWithConcurrency(
|
||||
metadataRequests,
|
||||
VAT_METADATA_PROBE_CONCURRENCY,
|
||||
(request) => executeVatMetadataProbeRequest(request)
|
||||
async (request, index) => {
|
||||
if (index > 0 && VAT_METADATA_PROBE_STAGGER_MS > 0) {
|
||||
await sleepMs(index * VAT_METADATA_PROBE_STAGGER_MS);
|
||||
}
|
||||
return executeVatMetadataProbeRequest(request);
|
||||
}
|
||||
);
|
||||
|
||||
const metadataOutcomes = metadataResponses.map((response, index) => ({
|
||||
@@ -539,14 +585,36 @@ async function probeVatDirectSources(filters: AddressFilterSet): Promise<VatDire
|
||||
const discoveredMetadataObjects = Array.from(deduplicatedObjects.values())
|
||||
.filter((item) => isVatMetadataObject(item))
|
||||
.sort(
|
||||
(a, b) => scoreVatMetadataObject(b) - scoreVatMetadataObject(a) || a.fullName.localeCompare(b.fullName, "ru")
|
||||
(a, b) =>
|
||||
vatMetadataObjectPriority(a) - vatMetadataObjectPriority(b) ||
|
||||
scoreVatMetadataObject(b) - scoreVatMetadataObject(a) ||
|
||||
a.fullName.localeCompare(b.fullName, "ru")
|
||||
);
|
||||
const metadataObjects = discoveredMetadataObjects.slice(0, VAT_SOURCE_PROBE_MAX_OBJECTS);
|
||||
const mergedMetadataObjectsMap = new Map<string, VatMetadataObject>();
|
||||
for (const item of discoveredMetadataObjects) {
|
||||
mergedMetadataObjectsMap.set(item.fullName, item);
|
||||
}
|
||||
for (const fallbackObject of VAT_FALLBACK_METADATA_OBJECTS) {
|
||||
if (!mergedMetadataObjectsMap.has(fallbackObject.fullName)) {
|
||||
mergedMetadataObjectsMap.set(fallbackObject.fullName, fallbackObject);
|
||||
}
|
||||
}
|
||||
const metadataObjects = Array.from(mergedMetadataObjectsMap.values())
|
||||
.sort(
|
||||
(a, b) =>
|
||||
vatMetadataObjectPriority(a) - vatMetadataObjectPriority(b) ||
|
||||
scoreVatMetadataObject(b) - scoreVatMetadataObject(a) ||
|
||||
a.fullName.localeCompare(b.fullName, "ru")
|
||||
)
|
||||
.slice(0, VAT_SOURCE_PROBE_MAX_OBJECTS);
|
||||
|
||||
const probeRows = await mapWithConcurrency(
|
||||
metadataObjects,
|
||||
VAT_OBJECT_PROBE_CONCURRENCY,
|
||||
async (object): Promise<VatDirectSourceProbeItem> => {
|
||||
async (object, index): Promise<VatDirectSourceProbeItem> => {
|
||||
if (index > 0 && VAT_OBJECT_PROBE_STAGGER_MS > 0) {
|
||||
await sleepMs(index * VAT_OBJECT_PROBE_STAGGER_MS);
|
||||
}
|
||||
let probeResult = await executeAddressMcpQuery({
|
||||
query: buildVatObjectProbeQuery(object, asOfExpr, "latest"),
|
||||
limit: 1,
|
||||
@@ -554,33 +622,41 @@ async function probeVatDirectSources(filters: AddressFilterSet): Promise<VatDire
|
||||
});
|
||||
let fallbackUsed = false;
|
||||
if (probeResult.error) {
|
||||
let latestError: string | null = probeResult.error;
|
||||
if (isAbortErrorMessage(probeResult.error)) {
|
||||
return {
|
||||
fullName: object.fullName,
|
||||
synonym: object.synonym,
|
||||
objectType: object.objectType,
|
||||
status: "error",
|
||||
rowsFetched: probeResult.fetched_rows,
|
||||
error: probeResult.error
|
||||
};
|
||||
await sleepMs(VAT_OBJECT_PROBE_ABORT_RETRY_DELAY_MS);
|
||||
const retryLatestResult = await executeAddressMcpQuery({
|
||||
query: buildVatObjectProbeQuery(object, asOfExpr, "latest"),
|
||||
limit: 1,
|
||||
timeout_ms: VAT_OBJECT_PROBE_ABORT_RETRY_TIMEOUT_MS
|
||||
});
|
||||
if (!retryLatestResult.error) {
|
||||
probeResult = retryLatestResult;
|
||||
latestError = null;
|
||||
} else {
|
||||
probeResult = retryLatestResult;
|
||||
latestError = `${latestError}; retry_latest: ${retryLatestResult.error}`;
|
||||
}
|
||||
}
|
||||
const fallbackResult = await executeAddressMcpQuery({
|
||||
query: buildVatObjectProbeQuery(object, asOfExpr, "exists"),
|
||||
limit: 1,
|
||||
timeout_ms: VAT_OBJECT_PROBE_FALLBACK_TIMEOUT_MS
|
||||
});
|
||||
if (!fallbackResult.error) {
|
||||
probeResult = fallbackResult;
|
||||
fallbackUsed = true;
|
||||
} else {
|
||||
return {
|
||||
fullName: object.fullName,
|
||||
synonym: object.synonym,
|
||||
objectType: object.objectType,
|
||||
status: "error",
|
||||
rowsFetched: probeResult.fetched_rows,
|
||||
error: `${probeResult.error}; fallback: ${fallbackResult.error}`
|
||||
};
|
||||
if (probeResult.error) {
|
||||
const fallbackResult = await executeAddressMcpQuery({
|
||||
query: buildVatObjectProbeQuery(object, asOfExpr, "exists"),
|
||||
limit: 1,
|
||||
timeout_ms: VAT_OBJECT_PROBE_FALLBACK_TIMEOUT_MS
|
||||
});
|
||||
if (!fallbackResult.error) {
|
||||
probeResult = fallbackResult;
|
||||
fallbackUsed = true;
|
||||
} else {
|
||||
return {
|
||||
fullName: object.fullName,
|
||||
synonym: object.synonym,
|
||||
objectType: object.objectType,
|
||||
status: "error",
|
||||
rowsFetched: probeResult.fetched_rows,
|
||||
error: `${latestError ?? probeResult.error}; fallback: ${fallbackResult.error}`
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
const firstRow = probeResult.raw_rows[0] ?? null;
|
||||
@@ -610,7 +686,14 @@ async function probeVatDirectSources(filters: AddressFilterSet): Promise<VatDire
|
||||
}
|
||||
);
|
||||
|
||||
const status: VatDirectSourceProbeSummary["status"] = metadataResponses.every((item) => item.error) ? "error" : "ok";
|
||||
const hasProbeAttempts = probeRows.length > 0;
|
||||
const hasNonErrorProbeResult = probeRows.some((item) => item.status !== "error");
|
||||
const status: VatDirectSourceProbeSummary["status"] =
|
||||
hasProbeAttempts && hasNonErrorProbeResult
|
||||
? "ok"
|
||||
: metadataResponses.every((item) => item.error) && !hasProbeAttempts
|
||||
? "error"
|
||||
: "ok";
|
||||
const allErrors = [
|
||||
...metadataErrors,
|
||||
...probeRows
|
||||
|
||||
@@ -2718,7 +2718,14 @@ export function composeFactualReply(
|
||||
}
|
||||
lines.push("- Сумма расчета выше получена по книгам продаж/покупок; probe использован для контроля полноты VAT-источников.");
|
||||
} else if (vatProbe && vatProbe.status === "error") {
|
||||
lines.push("", "Покрытие VAT-источников через MCP: probe завершился ошибкой, проверьте доступность регистров книг продаж/покупок.");
|
||||
lines.push(
|
||||
"",
|
||||
"Покрытие VAT-источников через MCP: дополнительный probe недоступен (например, timeout metadata).",
|
||||
"Итоговая сумма НДС выше рассчитана по основному маршруту книг продаж/покупок; probe влияет только на диагностику покрытия."
|
||||
);
|
||||
if (vatProbe.errors.length > 0) {
|
||||
lines.push(`- Детали probe: ${vatProbe.errors.slice(0, 2).join("; ")}.`);
|
||||
}
|
||||
}
|
||||
|
||||
if (rows.length === 0) {
|
||||
|
||||
Reference in New Issue
Block a user