@besales/mcp 1.0.0-beta.5 → 1.0.0-beta.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -6
- package/dist/auth/oauth-client.d.ts +1 -1
- package/dist/auth/oauth-client.js +2 -0
- package/dist/auth/oauth-client.js.map +1 -1
- package/dist/http/api-client.d.ts +10 -0
- package/dist/http/api-client.js +32 -0
- package/dist/http/api-client.js.map +1 -1
- package/dist/resources/concepts/sandbox.md +8 -1
- package/dist/resources/concepts/triggers.md +1 -0
- package/dist/resources/docs/platforms.md +58 -1
- package/dist/resources/docs/trigger-actions.md +24 -0
- package/dist/resources/workflows/iterate-with-sandbox.md +6 -2
- package/dist/resources/workflows/platform-setup-from-scratch.md +27 -0
- package/dist/schemas/mcp-tools.json +904 -27
- package/dist/tools/definitions/agent-design.d.ts +2 -0
- package/dist/tools/definitions/agent-design.js +15 -4
- package/dist/tools/definitions/agent-design.js.map +1 -1
- package/dist/tools/definitions/agent-settings.d.ts +1 -0
- package/dist/tools/definitions/agent-settings.js +2 -0
- package/dist/tools/definitions/agent-settings.js.map +1 -1
- package/dist/tools/definitions/broadcast-analytics.js +1 -1
- package/dist/tools/definitions/broadcast-analytics.js.map +1 -1
- package/dist/tools/definitions/broadcast-campaigns.d.ts +29 -0
- package/dist/tools/definitions/broadcast-campaigns.js +107 -0
- package/dist/tools/definitions/broadcast-campaigns.js.map +1 -0
- package/dist/tools/definitions/dialogue-export.js +2 -0
- package/dist/tools/definitions/dialogue-export.js.map +1 -1
- package/dist/tools/definitions/follow-up.d.ts +1 -1
- package/dist/tools/definitions/platform-setup-wave.d.ts +1 -1
- package/dist/tools/definitions/platform-setup.d.ts +12 -1
- package/dist/tools/definitions/task-state.d.ts +1 -1
- package/dist/tools/definitions/telegram-outreach-calendar.d.ts +17 -0
- package/dist/tools/definitions/telegram-outreach-calendar.js +68 -0
- package/dist/tools/definitions/telegram-outreach-calendar.js.map +1 -0
- package/dist/tools/definitions/telegram-personal-connect.d.ts +12 -0
- package/dist/tools/definitions/telegram-personal-connect.js +61 -0
- package/dist/tools/definitions/telegram-personal-connect.js.map +1 -0
- package/dist/tools/definitions/telegram-personal-management.d.ts +1 -1
- package/dist/tools/definitions/telegram-personal-profile.d.ts +2 -0
- package/dist/tools/definitions/telegram-personal-profile.js +14 -0
- package/dist/tools/definitions/telegram-personal-profile.js.map +1 -1
- package/dist/tools/definitions/telegram-personal-proxies.d.ts +42 -0
- package/dist/tools/definitions/telegram-personal-proxies.js +204 -0
- package/dist/tools/definitions/telegram-personal-proxies.js.map +1 -0
- package/dist/tools/definitions/telegram-proxy-stock.d.ts +34 -0
- package/dist/tools/definitions/telegram-proxy-stock.js +129 -0
- package/dist/tools/definitions/telegram-proxy-stock.js.map +1 -0
- package/dist/tools/definitions/telegram-session-import.d.ts +5 -0
- package/dist/tools/definitions/telegram-session-import.js +33 -0
- package/dist/tools/definitions/telegram-session-import.js.map +1 -0
- package/dist/tools/definitions/trigger-action-item.schema.d.ts +10 -0
- package/dist/tools/definitions/trigger-action-item.schema.js +28 -11
- package/dist/tools/definitions/trigger-action-item.schema.js.map +1 -1
- package/dist/tools/definitions/trigger-upsert.tool.d.ts +1 -0
- package/dist/tools/definitions/trigger-upsert.tool.js +13 -0
- package/dist/tools/definitions/trigger-upsert.tool.js.map +1 -1
- package/dist/tools/definitions.d.ts +119 -9
- package/dist/tools/definitions.js +13 -0
- package/dist/tools/definitions.js.map +1 -1
- package/dist/tools/operation-recovery.d.ts +1 -1
- package/dist/tools/operation-recovery.js +1 -1
- package/dist/tools/operation-recovery.js.map +1 -1
- package/dist/tools/registry-mapping.cases.js +210 -8
- package/dist/tools/registry-mapping.cases.js.map +1 -1
- package/dist/tools/registry.js +20 -7
- package/dist/tools/registry.js.map +1 -1
- package/dist/tools/result.d.ts +2 -1
- package/dist/tools/result.js +100 -27
- package/dist/tools/result.js.map +1 -1
- package/dist/types/api-contract.gen.d.ts +2220 -216
- package/docs/async-operation-lifecycle.md +13 -2
- package/docs/failure-semantics.md +15 -2
- package/docs/help-navigation.md +2 -0
- package/docs/secret-read-boundary.md +7 -0
- package/docs/telegram-personal-management.md +95 -24
- package/docs/telegram-personal-proxies.md +135 -0
- package/docs/telegram-session-import.md +53 -0
- package/package.json +8 -7
- package/scripts/body-idempotency-contract.mjs +95 -0
- package/scripts/generate-followup-session-contract.mjs +14 -7
- package/scripts/generated-output.mjs +9 -0
- package/scripts/sync-agent-model-schemas.mjs +11 -11
- package/scripts/sync-api-contracts.mjs +99 -40
- package/scripts/sync-crm-stage-pool-schema.mjs +6 -7
- package/scripts/sync-route-map.mjs +189 -35
- package/scripts/sync-sandbox-request-key.mjs +40 -0
- package/scripts/sync-telegram-profile-photo.mjs +57 -0
- package/scripts/sync-telegram-session-import.mjs +39 -0
- package/scripts/sync-tool-inventory.mjs +15 -7
- package/scripts/sync-trigger-argument-schema.mjs +31 -14
- package/scripts/sync-trigger-event-schema.mjs +11 -6
- package/scripts/sync-trigger-step-reference-schema.mjs +21 -0
- package/scripts/sync-wave-id-schemas.mjs +7 -6
- package/scripts/telegram-personal-profile-mutants.mjs +59 -18
- package/scripts/telegram-personal-proxy-mutants.mjs +171 -0
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import { writeGeneratedFile } from './generated-output.mjs';
|
|
1
2
|
// Выпуск схемы нового каталога и подсказок из исполняемых определений, без копии списка моделей.
|
|
2
|
-
import {
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
3
4
|
import { z } from 'zod';
|
|
4
|
-
import { agentModelTools } from '../
|
|
5
|
-
import { agentSettingsTools } from '../
|
|
5
|
+
import { agentModelTools } from '../src/tools/definitions/agent-models.js';
|
|
6
|
+
import { agentSettingsTools } from '../src/tools/definitions/agent-settings.js';
|
|
6
7
|
|
|
7
|
-
import { renderToolInventoryMarkdown, toolInventoryCounts } from '../
|
|
8
|
+
import { renderToolInventoryMarkdown, toolInventoryCounts } from '../src/tools/inventory.js';
|
|
8
9
|
|
|
9
10
|
const target = new URL('../src/schemas/mcp-tools.json', import.meta.url);
|
|
10
11
|
const registry = JSON.parse(readFileSync(target, 'utf8'));
|
|
@@ -36,7 +37,7 @@ registry.summary.total_tools = names.length;
|
|
|
36
37
|
registry.summary.tools_by_category = toolInventoryCounts(names);
|
|
37
38
|
const readmePath = new URL('../README.md', import.meta.url);
|
|
38
39
|
const readme = readFileSync(readmePath, 'utf8');
|
|
39
|
-
|
|
40
|
+
writeGeneratedFile(
|
|
40
41
|
readmePath,
|
|
41
42
|
readme.replace(
|
|
42
43
|
/(<!-- BEGIN GENERATED TOOL INVENTORY -->)[\s\S]*?(<!-- END GENERATED TOOL INVENTORY -->)/u,
|
|
@@ -56,17 +57,16 @@ const route = {
|
|
|
56
57
|
canonicalArgs: args,
|
|
57
58
|
expected: {
|
|
58
59
|
method: request.method,
|
|
59
|
-
pathTemplate: url.pathname.replace(args.workspace_id, ':workspaceId'),
|
|
60
60
|
path: url.pathname,
|
|
61
|
+
pathTemplate: url.pathname.replace(args.workspace_id, ':workspaceId'),
|
|
61
62
|
query: Object.fromEntries(url.searchParams),
|
|
62
63
|
},
|
|
63
64
|
};
|
|
64
65
|
const previousRoute = routeMap.findIndex((item) => item.tool === definition.name);
|
|
65
66
|
if (previousRoute === -1) routeMap.push(route);
|
|
66
|
-
|
|
67
|
-
|
|
67
|
+
// Прочие поля строки (backendTarget) ставит sync-api-contracts — не стирать их, иначе выпуски по очереди меняют файл.
|
|
68
|
+
else routeMap[previousRoute] = { ...routeMap[previousRoute], ...route };
|
|
69
|
+
writeGeneratedFile(routeMapPath, JSON.stringify(routeMap, null, 2) + '\n');
|
|
68
70
|
const serialized = JSON.stringify(registry, null, 2) + '\n';
|
|
69
|
-
|
|
70
|
-
mkdirSync(new URL('../dist/schemas/', import.meta.url), { recursive: true });
|
|
71
|
-
writeFileSync(new URL('../dist/schemas/mcp-tools.json', import.meta.url), serialized);
|
|
71
|
+
writeGeneratedFile(target, serialized);
|
|
72
72
|
console.log('Схемы каталога моделей агента и подсказки выпущены');
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
// Локальный выпуск по явному API checkout: API и Git-индекс остаются только входами.
|
|
2
2
|
import { execFileSync } from 'node:child_process';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
4
|
+
import openapiTS, { astToString, COMMENT_HEADER } from 'openapi-typescript';
|
|
5
|
+
import { pathToFileURL } from 'node:url';
|
|
6
|
+
import { writeGeneratedFile } from './generated-output.mjs';
|
|
5
7
|
import { resolve } from 'node:path';
|
|
6
8
|
import { fileURLToPath } from 'node:url';
|
|
7
9
|
import { z } from 'zod';
|
|
10
|
+
import { shippedRef } from './shipped-ref.mjs';
|
|
8
11
|
import { diffRegistries, lagKey } from './registry-diff.mjs';
|
|
9
12
|
import { syncSchemaDescriptions } from './sync-schema-descriptions.mjs';
|
|
10
13
|
|
|
@@ -14,14 +17,26 @@ if (!contracts) throw new Error('BESALES_API_CONTRACTS_DIR обязателен:
|
|
|
14
17
|
const apiFile = resolve(contracts, 'openapi-besales-api.yaml');
|
|
15
18
|
const apiText = readFileSync(apiFile, 'utf8');
|
|
16
19
|
if (/^(?:<<<<<<<|=======|>>>>>>>)/mu.test(apiText)) throw new Error('OPENAPI_MERGE_CONFLICT');
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
run(['build']);
|
|
20
|
+
const types = COMMENT_HEADER + astToString(await openapiTS(pathToFileURL(apiFile)));
|
|
21
|
+
writeGeneratedFile(new URL('../src/types/api-contract.gen.ts', import.meta.url), types);
|
|
20
22
|
if (process.argv.includes('--types-only')) process.exit(0);
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const
|
|
24
|
-
|
|
23
|
+
process.env.BESALES_MCP_SCHEMA_SOURCE = '1';
|
|
24
|
+
const check = process.argv.includes('--check') ? ['--check'] : [];
|
|
25
|
+
const run = (script) =>
|
|
26
|
+
execFileSync(
|
|
27
|
+
process.execPath,
|
|
28
|
+
[...process.execArgv, resolve(root, 'scripts', script), ...check],
|
|
29
|
+
{ cwd: root, stdio: 'inherit' },
|
|
30
|
+
);
|
|
31
|
+
for (const script of [
|
|
32
|
+
'sync-trigger-argument-schema.mjs',
|
|
33
|
+
'sync-wave-id-schemas.mjs',
|
|
34
|
+
'sync-agent-model-schemas.mjs',
|
|
35
|
+
'sync-contract-ledger-schemas.mjs',
|
|
36
|
+
'generate-followup-session-contract.mjs',
|
|
37
|
+
])
|
|
38
|
+
run(script);
|
|
39
|
+
const { toolDefinitions } = await import('../src/tools/definitions.ts');
|
|
25
40
|
const registryPath = new URL('../src/schemas/mcp-tools.json', import.meta.url);
|
|
26
41
|
const registry = JSON.parse(readFileSync(registryPath, 'utf8'));
|
|
27
42
|
const known = new Map(registry.tools.map((tool) => [tool.name, tool]));
|
|
@@ -29,57 +44,101 @@ registry.tools = toolDefinitions.map((definition) => {
|
|
|
29
44
|
const tool = known.get(definition.name);
|
|
30
45
|
if (!tool) throw new Error(`REGISTRY_TOOL_MISSING:${definition.name}`);
|
|
31
46
|
// Сохранённые ID областей знаний теперь не обязаны быть UUID.
|
|
32
|
-
const derived = z.toJSONSchema(definition.schema, {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
tool.inputSchema.properties.config = { ...config, properties: { ...config.properties, ...current.properties } };
|
|
39
|
-
}
|
|
47
|
+
const derived = z.toJSONSchema(definition.schema, {
|
|
48
|
+
io: 'input',
|
|
49
|
+
unrepresentable: 'any',
|
|
50
|
+
});
|
|
51
|
+
if (derived.properties?.knowledge_space_id)
|
|
52
|
+
tool.inputSchema.properties.knowledge_space_id = derived.properties.knowledge_space_id;
|
|
40
53
|
syncSchemaDescriptions(tool.inputSchema, derived);
|
|
41
54
|
return tool;
|
|
42
55
|
});
|
|
43
|
-
const { reactivationConfigSchema } = await import('../
|
|
56
|
+
const { reactivationConfigSchema } = await import('../src/tools/definitions/follow-up-config.ts');
|
|
44
57
|
const chain = registry.tools.find((tool) => tool.name === 'besales_followup_sequence_upsert');
|
|
45
|
-
const reactivation = z.toJSONSchema(reactivationConfigSchema, {
|
|
58
|
+
const reactivation = z.toJSONSchema(reactivationConfigSchema, {
|
|
59
|
+
io: 'input',
|
|
60
|
+
unrepresentable: 'any',
|
|
61
|
+
});
|
|
46
62
|
function syncEnums(target, derived) {
|
|
47
63
|
if (!target || !derived || typeof target !== 'object' || typeof derived !== 'object') return;
|
|
48
64
|
if (derived.enum) target.enum = derived.enum;
|
|
49
65
|
for (const key of Object.keys(target)) syncEnums(target[key], derived[key]);
|
|
50
66
|
}
|
|
51
67
|
syncEnums(chain.inputSchema.properties.config, reactivation);
|
|
52
|
-
for (const field of ['sessionTimeVariable', 'sessionDayOffsets', 'sessionDurationMinutes'])
|
|
53
|
-
|
|
68
|
+
for (const field of ['sessionTimeVariable', 'sessionDayOffsets', 'sessionDurationMinutes'])
|
|
69
|
+
chain.inputSchema.properties.config.properties[field] = reactivation.properties[field];
|
|
70
|
+
if (!chain.description.includes('45 минут'))
|
|
71
|
+
chain.description +=
|
|
72
|
+
' Окно календарного шага по умолчанию 45 минут; серия эфиров задаётся sessionTimeVariable/sessionDayOffsets/sessionDurationMinutes.';
|
|
54
73
|
const inbound = registry.tools.find((tool) => tool.name === 'besales_inbound_analytics_get');
|
|
55
|
-
const
|
|
56
|
-
const description = baselineRegistry.tools.find((tool) => tool.name === inbound.name).description;
|
|
74
|
+
const description = inbound.description;
|
|
57
75
|
const inboundDefinition = toolDefinitions.find((tool) => tool.name === inbound.name);
|
|
58
|
-
const instructions = inboundDefinition
|
|
76
|
+
const instructions = inboundDefinition
|
|
77
|
+
.enhanceResponse({}, { report: 'money' })
|
|
78
|
+
.instructions.split('\n');
|
|
59
79
|
const moneyInstructions = instructions.find((line) => line.startsWith('Деньги.'));
|
|
60
80
|
if (!moneyInstructions) throw new Error('INBOUND_APPLICATION_DESCRIPTION_MISSING');
|
|
61
|
-
const currentDescription = description.replace(
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
81
|
+
const currentDescription = description.replace(
|
|
82
|
+
'(speed/cycle/quality) ещё не построены: отказ маршрута не означает отсутствие данных.',
|
|
83
|
+
'speed, cycle, quality также построены.',
|
|
84
|
+
);
|
|
85
|
+
inbound.description = currentDescription.includes(moneyInstructions)
|
|
86
|
+
? currentDescription
|
|
87
|
+
: currentDescription + ' ' + moneyInstructions;
|
|
88
|
+
writeGeneratedFile(registryPath, JSON.stringify(registry, null, 2) + '\n');
|
|
89
|
+
// Специализированные схемы накладываются в одном и том же порядке на каждом выпуске.
|
|
90
|
+
for (const script of [
|
|
91
|
+
'sync-crm-stage-pool-schema.mjs',
|
|
92
|
+
'sync-trigger-event-schema.mjs',
|
|
93
|
+
'sync-trigger-step-reference-schema.mjs',
|
|
94
|
+
'sync-sandbox-request-key.mjs',
|
|
95
|
+
'sync-telegram-profile-photo.mjs',
|
|
96
|
+
'sync-tool-inventory.mjs',
|
|
97
|
+
'sync-route-map.mjs',
|
|
98
|
+
])
|
|
99
|
+
run(script);
|
|
100
|
+
|
|
101
|
+
// Обновление долга использует закреплённые входы; отгруженный реф остаётся эталоном в полном checkout.
|
|
102
|
+
const rows = JSON.parse(
|
|
103
|
+
readFileSync(new URL('../src/schemas/mcp-v2-route-map.json', import.meta.url), 'utf8'),
|
|
104
|
+
);
|
|
68
105
|
const lagFile = new URL('../src/schemas/shipped-lag.json', import.meta.url);
|
|
69
106
|
const lag = JSON.parse(readFileSync(lagFile, 'utf8'));
|
|
70
107
|
for (const row of rows) if (lag.routeLag?.[row.tool]) delete lag.routeLag[row.tool];
|
|
71
|
-
|
|
108
|
+
let shippedTools;
|
|
109
|
+
if (process.env.BESALES_API_CONTRACTS_ONLY === '1') {
|
|
110
|
+
const exported = JSON.parse(readFileSync(resolve(contracts, 'mcp-tool-routes.json'), 'utf8'));
|
|
111
|
+
shippedTools = Object.entries(exported.shippedInputSchemas).map(([name, inputSchema]) => ({
|
|
112
|
+
name,
|
|
113
|
+
inputSchema,
|
|
114
|
+
}));
|
|
115
|
+
if (shippedTools.length !== exported.shippedRegistry.toolCount)
|
|
116
|
+
throw new Error('SHIPPED_CONTRACT_EXPORT_INCOMPLETE');
|
|
117
|
+
} else
|
|
118
|
+
shippedTools = JSON.parse(
|
|
119
|
+
execFileSync('git', ['show', shippedRef() + ':src/schemas/mcp-tools.json'], {
|
|
120
|
+
cwd: root,
|
|
121
|
+
encoding: 'utf8',
|
|
122
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
123
|
+
}),
|
|
124
|
+
).tools;
|
|
72
125
|
const finalRegistry = JSON.parse(readFileSync(registryPath, 'utf8'));
|
|
73
|
-
const delta = diffRegistries(
|
|
126
|
+
const delta = diffRegistries(shippedTools, finalRegistry.tools, lag.toolSuccessors);
|
|
74
127
|
const activeKeys = new Set(delta.contractLag.map(lagKey));
|
|
75
128
|
for (const [key] of Object.entries(lag.contractLag)) {
|
|
76
|
-
if (
|
|
129
|
+
if (
|
|
130
|
+
!activeKeys.has(key) &&
|
|
131
|
+
(key.includes('instagram_comment_templates') || key.includes('::personal_url_variable::'))
|
|
132
|
+
)
|
|
133
|
+
delete lag.contractLag[key];
|
|
77
134
|
}
|
|
78
135
|
for (const entry of delta.contractLag) {
|
|
79
|
-
if (entry.path === 'knowledge_space_id#pattern' && !lag.contractLag[lagKey(entry)])
|
|
80
|
-
|
|
81
|
-
|
|
136
|
+
if (entry.path === 'knowledge_space_id#pattern' && !lag.contractLag[lagKey(entry)])
|
|
137
|
+
lag.contractLag[lagKey(entry)] = {
|
|
138
|
+
owner: 'DELIBERATE',
|
|
139
|
+
severity: 'DELIBERATE',
|
|
140
|
+
reason:
|
|
141
|
+
'Сохранённые идентификаторы области знаний проверяются по структурной форме UUID без требований к версии и варианту; значение передаётся неизменным (night-spaceid).',
|
|
142
|
+
};
|
|
82
143
|
}
|
|
83
|
-
|
|
84
|
-
execFileSync(process.execPath, [resolve(root, 'scripts/sync-tool-inventory.mjs')], { cwd: root, stdio: 'inherit' });
|
|
85
|
-
run(['build']);
|
|
144
|
+
writeGeneratedFile(lagFile, JSON.stringify(lag, null, 2) + '\n');
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { writeGeneratedFile } from './generated-output.mjs';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
2
3
|
import { resolve } from 'node:path';
|
|
3
4
|
import { pathToFileURL } from 'node:url';
|
|
4
5
|
import openapiTS from 'openapi-typescript';
|
|
@@ -6,12 +7,12 @@ import { z } from 'zod';
|
|
|
6
7
|
import {
|
|
7
8
|
triggerActionItemSchema,
|
|
8
9
|
followupSequenceConfigSchema,
|
|
9
|
-
} from '../
|
|
10
|
+
} from '../src/tools/definitions/platform-setup.js';
|
|
10
11
|
import {
|
|
11
12
|
crmMoveStageConfigSchema,
|
|
12
13
|
stagePoolFields,
|
|
13
|
-
} from '../
|
|
14
|
-
import { CRM_STAGE_ACTION_POOL_MAX_SIZE } from '../
|
|
14
|
+
} from '../src/tools/definitions/crm-stage-pool.schema.js';
|
|
15
|
+
import { CRM_STAGE_ACTION_POOL_MAX_SIZE } from '../src/shared/constants/crm-stage-pool.constants.js';
|
|
15
16
|
|
|
16
17
|
// Каталог ссылки берётся из принятого API. Отставание соседних полей этим выпуском не снимается.
|
|
17
18
|
const contractsDir = process.env.BESALES_API_CONTRACTS_DIR?.trim() || '../besales-api/contracts';
|
|
@@ -80,6 +81,4 @@ const hint = stagePoolFields.assign_responsible_operator_ids.description;
|
|
|
80
81
|
for (const tool of [trigger, chain])
|
|
81
82
|
if (!tool.description.includes(hint)) tool.description += ` ${hint}`;
|
|
82
83
|
const serialized = `${JSON.stringify(registry, null, 2)}\n`;
|
|
83
|
-
|
|
84
|
-
mkdirSync(new URL('../dist/schemas/', import.meta.url), { recursive: true });
|
|
85
|
-
writeFileSync(new URL('../dist/schemas/mcp-tools.json', import.meta.url), serialized);
|
|
84
|
+
writeGeneratedFile(target, serialized);
|
|
@@ -1,51 +1,156 @@
|
|
|
1
1
|
// Пересобирает только карту и локальные доказательства маршрутов; API остаётся входом.
|
|
2
2
|
import { execFileSync } from 'node:child_process';
|
|
3
3
|
import { createHash } from 'node:crypto';
|
|
4
|
-
import {
|
|
4
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
5
5
|
import { resolve } from 'node:path';
|
|
6
|
+
import { load } from 'js-yaml';
|
|
6
7
|
import { sampleFor, resetSampleUid } from './sample-args.mjs';
|
|
7
8
|
const contracts = process.env.BESALES_API_CONTRACTS_DIR?.trim();
|
|
8
9
|
if (!contracts) throw new Error('BESALES_API_CONTRACTS_DIR обязателен: contracts/ выбранного API');
|
|
9
10
|
const apiFile = resolve(contracts, 'openapi-besales-api.yaml');
|
|
10
11
|
const apiText = readFileSync(apiFile, 'utf8');
|
|
11
12
|
if (/^(?:<<<<<<<|=======|>>>>>>>)/mu.test(apiText)) throw new Error('OPENAPI_MERGE_CONFLICT');
|
|
12
|
-
const { toolDefinitions } = await import('../
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
|
|
13
|
+
const { toolDefinitions } = await import('../src/tools/definitions.js');
|
|
14
|
+
const { mappingCases } = await import('../src/tools/registry-mapping.cases.js');
|
|
15
|
+
const registry = JSON.parse(
|
|
16
|
+
readFileSync(new URL('../src/schemas/mcp-tools.json', import.meta.url), 'utf8'),
|
|
17
|
+
);
|
|
16
18
|
const known = new Map(registry.tools.map((tool) => [tool.name, tool]));
|
|
17
19
|
const routeFile = new URL('../src/schemas/mcp-v2-route-map.json', import.meta.url);
|
|
18
20
|
const original = readFileSync(routeFile, 'utf8');
|
|
19
21
|
const rows = JSON.parse(original);
|
|
20
22
|
// Справочники не принимают фильтр периода: прежний образец mode=all стал недопустим.
|
|
21
23
|
for (const row of rows) {
|
|
22
|
-
if (
|
|
24
|
+
if (
|
|
25
|
+
[
|
|
26
|
+
'besales_broadcast_analytics_get@segments_catalog',
|
|
27
|
+
'besales_broadcast_analytics_get@funnel_products',
|
|
28
|
+
].includes(row.tool)
|
|
29
|
+
)
|
|
30
|
+
delete row.canonicalArgs.mode;
|
|
23
31
|
}
|
|
24
32
|
const definitions = new Map(toolDefinitions.map((definition) => [definition.name, definition]));
|
|
25
33
|
const snapshot = readFileSync(new URL('../src/types/api-contract.gen.ts', import.meta.url), 'utf8');
|
|
26
|
-
const paths = [...snapshot.matchAll(/^ {4}"(\/[^"]*)":\s*\{\n([\s\S]*?)^ {4}\};$/gm)]
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
34
|
+
const paths = [...snapshot.matchAll(/^ {4}"(\/[^"]*)":\s*\{\n([\s\S]*?)^ {4}\};$/gm)]
|
|
35
|
+
.map((block) => ({
|
|
36
|
+
path: block[1],
|
|
37
|
+
methods: new Set(
|
|
38
|
+
[...block[2].matchAll(/^ {8}([a-z]+):[ \t]*(\S[^\n]*)/gm)]
|
|
39
|
+
.filter((match) => !/^never\b/u.test(match[2]))
|
|
40
|
+
.map((match) => match[1].toUpperCase()),
|
|
41
|
+
),
|
|
42
|
+
regex: new RegExp(
|
|
43
|
+
'^' +
|
|
44
|
+
block[1]
|
|
45
|
+
.split('/')
|
|
46
|
+
.map((part) =>
|
|
47
|
+
part.startsWith('{') ? '[^/]+' : part.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&'),
|
|
48
|
+
)
|
|
49
|
+
.join('/') +
|
|
50
|
+
'$',
|
|
51
|
+
),
|
|
52
|
+
}))
|
|
53
|
+
.sort((a, b) => (a.path.match(/\{/gu)?.length ?? 0) - (b.path.match(/\{/gu)?.length ?? 0));
|
|
30
54
|
// Маршрут намеренно исключён из публичного OpenAPI, но зарегистрирован в текущем API.
|
|
31
|
-
const privateController = resolve(
|
|
55
|
+
const privateController = resolve(
|
|
56
|
+
contracts,
|
|
57
|
+
'../src/control/channel/crm-webhook-url-reveal.controller.ts',
|
|
58
|
+
);
|
|
32
59
|
const privateModule = resolve(contracts, '../src/control/channel/channel.module.ts');
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
60
|
+
let privatePath;
|
|
61
|
+
let privateEvidence;
|
|
62
|
+
if (process.env.BESALES_API_CONTRACTS_ONLY === '1') {
|
|
63
|
+
// Явный режим закреплённого снимка: это доказательство из экспортов, а не проверка Nest.
|
|
64
|
+
const exported = JSON.parse(readFileSync(resolve(contracts, 'mcp-tool-routes.json'), 'utf8'));
|
|
65
|
+
const exportedRows = JSON.parse(
|
|
66
|
+
readFileSync(resolve(contracts, 'mcp-v2-route-map.json'), 'utf8'),
|
|
67
|
+
);
|
|
68
|
+
const tool = exported.tools.find((row) => row.name === 'besales_crm_webhook_url_reveal');
|
|
69
|
+
const route = exportedRows.find((row) => row.tool === 'besales_crm_webhook_url_reveal');
|
|
70
|
+
if (
|
|
71
|
+
tool?.kind !== 'api' ||
|
|
72
|
+
tool.method !== 'POST' ||
|
|
73
|
+
tool.samplePath !== '/crm-webhook-url-reveals' ||
|
|
74
|
+
route?.expected.method !== tool.method ||
|
|
75
|
+
route.expected.pathTemplate !== tool.samplePath
|
|
76
|
+
)
|
|
77
|
+
throw new Error('PRIVATE_CRM_CONTRACT_MISSING');
|
|
78
|
+
privatePath = '/api/v2' + tool.samplePath;
|
|
79
|
+
privateEvidence = {
|
|
80
|
+
source: 'pinned-contract-export',
|
|
81
|
+
file: resolve(contracts, 'mcp-tool-routes.json'),
|
|
82
|
+
};
|
|
83
|
+
} else {
|
|
84
|
+
if (!existsSync(privateController) || !existsSync(privateModule))
|
|
85
|
+
throw new Error(
|
|
86
|
+
'PRIVATE_CRM_SOURCE_MISSING: use BESALES_API_CONTRACTS_ONLY=1 only with pinned exports',
|
|
87
|
+
);
|
|
88
|
+
const controllerSource = readFileSync(privateController, 'utf8');
|
|
89
|
+
const moduleSource = readFileSync(privateModule, 'utf8');
|
|
90
|
+
const controller = /@Controller\('([^']+)'\)/u.exec(controllerSource);
|
|
91
|
+
if (
|
|
92
|
+
!controller ||
|
|
93
|
+
!/@Post\(\)/u.test(controllerSource) ||
|
|
94
|
+
!/@HiddenFromPublicDocs/u.test(controllerSource) ||
|
|
95
|
+
!/controllers:\s*\[[\s\S]*?CrmWebhookUrlRevealController/u.test(moduleSource)
|
|
96
|
+
)
|
|
97
|
+
throw new Error('PRIVATE_CRM_ROUTE_NOT_REGISTERED');
|
|
98
|
+
privatePath = '/api/v2/' + controller[1];
|
|
99
|
+
privateEvidence = {
|
|
100
|
+
source: 'controller',
|
|
101
|
+
controller: privateController,
|
|
102
|
+
sha256: createHash('sha256').update(controllerSource).digest('hex'),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
paths.push({
|
|
106
|
+
path: privatePath,
|
|
107
|
+
methods: new Set(['POST']),
|
|
108
|
+
regex: new RegExp('^' + privatePath + '$'),
|
|
109
|
+
private: true,
|
|
110
|
+
});
|
|
111
|
+
// Setup-сессии и смена прокси скрыты из публичного описания как носители секретов.
|
|
112
|
+
// Их существование проверяется по app-контракту того же закреплённого ядра.
|
|
113
|
+
const customerTools = new Set([
|
|
114
|
+
'besales_platform_telegram_personal_proxy_import_link',
|
|
115
|
+
'besales_platform_telegram_personal_proxy_import_status',
|
|
116
|
+
'besales_platform_telegram_personal_account_proxy_change',
|
|
117
|
+
'besales_platform_telegram_personal_proxy_stock_import_link',
|
|
118
|
+
'besales_platform_telegram_personal_proxy_stock_import_status',
|
|
119
|
+
'besales_platform_telegram_personal_connect_link',
|
|
120
|
+
'besales_platform_telegram_personal_session_import_link',
|
|
121
|
+
]);
|
|
122
|
+
const appFile = resolve(contracts, 'openapi-app.yaml');
|
|
123
|
+
const appText = readFileSync(appFile, 'utf8');
|
|
124
|
+
if (/^(?:<<<<<<<|=======|>>>>>>>)/mu.test(appText)) throw new Error('APP_OPENAPI_MERGE_CONFLICT');
|
|
125
|
+
const appPaths = Object.entries(load(appText).paths).map(([path, item]) => ({
|
|
126
|
+
path,
|
|
127
|
+
methods: new Set(Object.keys(item).map((method) => method.toUpperCase())),
|
|
128
|
+
regex: new RegExp(
|
|
129
|
+
'^' +
|
|
130
|
+
path
|
|
131
|
+
.split('/')
|
|
132
|
+
.map((part) =>
|
|
133
|
+
part.startsWith('{') ? '[^/]+' : part.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&'),
|
|
134
|
+
)
|
|
135
|
+
.join('/') +
|
|
136
|
+
'$',
|
|
137
|
+
),
|
|
138
|
+
private: true,
|
|
139
|
+
}));
|
|
140
|
+
const initialRows = JSON.parse(original);
|
|
40
141
|
const initialByName = new Map(initialRows.map((row) => [row.tool, row]));
|
|
41
142
|
const errors = [];
|
|
42
143
|
const changes = [];
|
|
43
144
|
for (const definition of toolDefinitions) {
|
|
44
|
-
if (!definition.createRequest || rows.some((row) => row.tool.split('@')[0] === definition.name))
|
|
145
|
+
if (!definition.createRequest || rows.some((row) => row.tool.split('@')[0] === definition.name))
|
|
146
|
+
continue;
|
|
45
147
|
resetSampleUid();
|
|
46
148
|
const schema = known.get(definition.name).inputSchema;
|
|
47
|
-
const args =
|
|
48
|
-
|
|
149
|
+
const args =
|
|
150
|
+
mappingCases.find((row) => row.name === definition.name)?.input ??
|
|
151
|
+
sampleFor(definition.name, schema, true);
|
|
152
|
+
if (definition.name === 'besales_feedback_document_access_update')
|
|
153
|
+
args.expected_document_snapshot = 'a'.repeat(64);
|
|
49
154
|
rows.push({ tool: definition.name, canonicalArgs: args });
|
|
50
155
|
}
|
|
51
156
|
for (const row of rows) {
|
|
@@ -55,40 +160,89 @@ for (const row of rows) {
|
|
|
55
160
|
const request = definition.createRequest(input);
|
|
56
161
|
const url = new URL(request.path, 'https://api.test');
|
|
57
162
|
const method = request.method ?? 'POST';
|
|
58
|
-
const
|
|
163
|
+
const candidates = customerTools.has(definition.name) ? [...paths, ...appPaths] : paths;
|
|
164
|
+
const route = candidates.find(
|
|
165
|
+
(candidate) =>
|
|
166
|
+
candidate.regex.test('/api/v2' + url.pathname) && candidate.methods.has(method),
|
|
167
|
+
);
|
|
59
168
|
if (!route) throw new Error(`API_ROUTE_MISSING:${method} ${url.pathname}`);
|
|
60
169
|
const query = {};
|
|
61
170
|
for (const key of new Set(url.searchParams.keys())) {
|
|
62
171
|
const values = url.searchParams.getAll(key);
|
|
63
172
|
query[key] = values.length === 1 ? values[0] : values;
|
|
64
173
|
}
|
|
65
|
-
const expected = {
|
|
174
|
+
const expected = {
|
|
175
|
+
method,
|
|
176
|
+
path: url.pathname,
|
|
177
|
+
pathTemplate: route.path.slice('/api/v2'.length).replace(/\{([^}]+)\}/gu, ':$1'),
|
|
178
|
+
};
|
|
66
179
|
if (Object.keys(query).length) expected.query = query;
|
|
67
180
|
if (request.body instanceof FormData) {
|
|
68
181
|
const fields = {};
|
|
69
182
|
let file;
|
|
70
183
|
for (const [key, value] of request.body.entries()) {
|
|
71
184
|
if (typeof value === 'string') fields[key] = value;
|
|
72
|
-
else
|
|
185
|
+
else
|
|
186
|
+
file = {
|
|
187
|
+
fieldName: key,
|
|
188
|
+
filename: value.name,
|
|
189
|
+
contentType: value.type,
|
|
190
|
+
content: await value.text(),
|
|
191
|
+
};
|
|
73
192
|
}
|
|
74
193
|
expected.multipart = { fields, file };
|
|
75
194
|
} else if (request.body !== undefined) expected.body = request.body;
|
|
76
195
|
const before = initialByName.get(row.tool)?.expected;
|
|
77
|
-
if (before && (before.method !== method || before.pathTemplate !== expected.pathTemplate))
|
|
78
|
-
|
|
196
|
+
if (before && (before.method !== method || before.pathTemplate !== expected.pathTemplate))
|
|
197
|
+
changes.push({
|
|
198
|
+
tool: row.tool,
|
|
199
|
+
before: before.method + ' ' + before.pathTemplate,
|
|
200
|
+
after: method + ' ' + expected.pathTemplate,
|
|
201
|
+
});
|
|
202
|
+
if (row.tool.startsWith('besales_dialogue_audit_run_start@'))
|
|
203
|
+
expected.body.idempotencyKey = '00000000-0000-4000-8000-000000000090';
|
|
79
204
|
row.expected = expected;
|
|
80
|
-
if (route.private) {
|
|
81
|
-
|
|
82
|
-
|
|
205
|
+
if (route.private) {
|
|
206
|
+
delete row.backendTarget;
|
|
207
|
+
row.contractVisibility = 'CUSTOMER';
|
|
208
|
+
} else row.backendTarget = 'besales-api';
|
|
209
|
+
} catch (error) {
|
|
210
|
+
errors.push({ tool: row.tool, error: String(error) });
|
|
211
|
+
}
|
|
83
212
|
}
|
|
84
|
-
const artifactDir = new URL('../.codex-artifacts-
|
|
213
|
+
const artifactDir = new URL('../.codex-artifacts-prBETA9ASM/', import.meta.url);
|
|
85
214
|
mkdirSync(artifactDir, { recursive: true });
|
|
86
|
-
const payload = {
|
|
215
|
+
const payload = {
|
|
216
|
+
privateRoutes: [{ method: 'POST', path: privatePath, ...privateEvidence }],
|
|
217
|
+
source: 'working-tree',
|
|
218
|
+
apiOpenApi: apiFile,
|
|
219
|
+
apiSha256: createHash('sha256').update(apiText).digest('hex'),
|
|
220
|
+
appOpenApi: appFile,
|
|
221
|
+
appSha256: createHash('sha256').update(appText).digest('hex'),
|
|
222
|
+
apiHead: execFileSync('git', ['-C', contracts, 'log', '-1', '--format=%H'], {
|
|
223
|
+
encoding: 'utf8',
|
|
224
|
+
}).trim(),
|
|
225
|
+
totalTools: toolDefinitions.length,
|
|
226
|
+
localTools: toolDefinitions.filter((tool) => !tool.createRequest).map((tool) => tool.name),
|
|
227
|
+
checkedApiTools: new Set(rows.map((row) => row.tool.split('@')[0])).size,
|
|
228
|
+
records: rows.length,
|
|
229
|
+
changes,
|
|
230
|
+
errors,
|
|
231
|
+
};
|
|
87
232
|
writeFileSync(new URL('route-audit.json', artifactDir), JSON.stringify(payload, null, 2) + '\n');
|
|
88
|
-
writeFileSync(
|
|
89
|
-
|
|
233
|
+
writeFileSync(
|
|
234
|
+
new URL('mcp-tool-routes.json', artifactDir),
|
|
235
|
+
JSON.stringify({ ...payload, tools: rows }, null, 2) + '\n',
|
|
236
|
+
);
|
|
237
|
+
if (errors.length) {
|
|
238
|
+
console.error(JSON.stringify(errors, null, 2));
|
|
239
|
+
throw new Error(`API_ROUTE_AUDIT_FAILED:${errors.length}`);
|
|
240
|
+
}
|
|
90
241
|
const output = JSON.stringify(rows, null, 2) + '\n';
|
|
91
242
|
if (process.argv.includes('--check')) {
|
|
92
|
-
if (original !== output)
|
|
243
|
+
if (original !== output)
|
|
244
|
+
throw new Error('Карта разошлась с определениями: выполните sync-route-map.mjs');
|
|
93
245
|
} else writeFileSync(routeFile, output);
|
|
94
|
-
console.log(
|
|
246
|
+
console.log(
|
|
247
|
+
`API route audit: ${payload.checkedApiTools} API tools, ${rows.length} variants; missing 0`,
|
|
248
|
+
);
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// node --loader ./src/utils/setup-sessions-loader.mjs scripts/sync-sandbox-request-key.mjs [--check]
|
|
2
|
+
// Узкий выпуск из исходников: существующие поля и метаданные реестра остаются на месте.
|
|
3
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { agentDesignTools } from '../src/tools/definitions/agent-design.js';
|
|
6
|
+
|
|
7
|
+
const name = 'besales_sandbox_run_start';
|
|
8
|
+
const definition = agentDesignTools.find((tool) => tool.name === name);
|
|
9
|
+
if (!definition?.createRequest) throw new Error('SANDBOX_DEFINITION_MISSING');
|
|
10
|
+
const registryFile = new URL('../src/schemas/mcp-tools.json', import.meta.url);
|
|
11
|
+
const routesFile = new URL('../src/schemas/mcp-v2-route-map.json', import.meta.url);
|
|
12
|
+
const registry = JSON.parse(readFileSync(registryFile, 'utf8'));
|
|
13
|
+
const routes = JSON.parse(readFileSync(routesFile, 'utf8'));
|
|
14
|
+
const contract = registry.tools.find((tool) => tool.name === name);
|
|
15
|
+
const route = routes.find((entry) => entry.tool === name);
|
|
16
|
+
if (!contract || !route) throw new Error('SANDBOX_CONTRACT_MISSING');
|
|
17
|
+
const derived = z.toJSONSchema(definition.schema, {
|
|
18
|
+
target: 'draft-7',
|
|
19
|
+
io: 'input',
|
|
20
|
+
});
|
|
21
|
+
contract.inputSchema.properties.request_key = derived.properties.request_key;
|
|
22
|
+
route.canonicalArgs.request_key = '00000000-0000-4000-8000-000000000090';
|
|
23
|
+
const input = definition.schema.parse(route.canonicalArgs);
|
|
24
|
+
route.expected.body = definition.createRequest(input).body;
|
|
25
|
+
const note = definition
|
|
26
|
+
.enhanceResponse({}, input)
|
|
27
|
+
.instructions.split('\n')
|
|
28
|
+
.find((line) => line.startsWith('Сохрани request_key.'));
|
|
29
|
+
if (!note) throw new Error('SANDBOX_KEY_INSTRUCTIONS_MISSING');
|
|
30
|
+
contract.description = contract.description.replace(/ Сохрани request_key\..*$/u, '') + ' ' + note;
|
|
31
|
+
for (const [file, value] of [
|
|
32
|
+
[registryFile, registry],
|
|
33
|
+
[routesFile, routes],
|
|
34
|
+
]) {
|
|
35
|
+
const output = JSON.stringify(value, null, 2) + '\n';
|
|
36
|
+
if (process.argv.includes('--check')) {
|
|
37
|
+
if (readFileSync(file, 'utf8') !== output)
|
|
38
|
+
throw new Error('SANDBOX_KEY_CONTRACT_DRIFT: ' + file.pathname);
|
|
39
|
+
} else writeFileSync(file, output);
|
|
40
|
+
}
|