@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.
Files changed (95) hide show
  1. package/README.md +41 -6
  2. package/dist/auth/oauth-client.d.ts +1 -1
  3. package/dist/auth/oauth-client.js +2 -0
  4. package/dist/auth/oauth-client.js.map +1 -1
  5. package/dist/http/api-client.d.ts +10 -0
  6. package/dist/http/api-client.js +32 -0
  7. package/dist/http/api-client.js.map +1 -1
  8. package/dist/resources/concepts/sandbox.md +8 -1
  9. package/dist/resources/concepts/triggers.md +1 -0
  10. package/dist/resources/docs/platforms.md +58 -1
  11. package/dist/resources/docs/trigger-actions.md +24 -0
  12. package/dist/resources/workflows/iterate-with-sandbox.md +6 -2
  13. package/dist/resources/workflows/platform-setup-from-scratch.md +27 -0
  14. package/dist/schemas/mcp-tools.json +904 -27
  15. package/dist/tools/definitions/agent-design.d.ts +2 -0
  16. package/dist/tools/definitions/agent-design.js +15 -4
  17. package/dist/tools/definitions/agent-design.js.map +1 -1
  18. package/dist/tools/definitions/agent-settings.d.ts +1 -0
  19. package/dist/tools/definitions/agent-settings.js +2 -0
  20. package/dist/tools/definitions/agent-settings.js.map +1 -1
  21. package/dist/tools/definitions/broadcast-analytics.js +1 -1
  22. package/dist/tools/definitions/broadcast-analytics.js.map +1 -1
  23. package/dist/tools/definitions/broadcast-campaigns.d.ts +29 -0
  24. package/dist/tools/definitions/broadcast-campaigns.js +107 -0
  25. package/dist/tools/definitions/broadcast-campaigns.js.map +1 -0
  26. package/dist/tools/definitions/dialogue-export.js +2 -0
  27. package/dist/tools/definitions/dialogue-export.js.map +1 -1
  28. package/dist/tools/definitions/follow-up.d.ts +1 -1
  29. package/dist/tools/definitions/platform-setup-wave.d.ts +1 -1
  30. package/dist/tools/definitions/platform-setup.d.ts +12 -1
  31. package/dist/tools/definitions/task-state.d.ts +1 -1
  32. package/dist/tools/definitions/telegram-outreach-calendar.d.ts +17 -0
  33. package/dist/tools/definitions/telegram-outreach-calendar.js +68 -0
  34. package/dist/tools/definitions/telegram-outreach-calendar.js.map +1 -0
  35. package/dist/tools/definitions/telegram-personal-connect.d.ts +12 -0
  36. package/dist/tools/definitions/telegram-personal-connect.js +61 -0
  37. package/dist/tools/definitions/telegram-personal-connect.js.map +1 -0
  38. package/dist/tools/definitions/telegram-personal-management.d.ts +1 -1
  39. package/dist/tools/definitions/telegram-personal-profile.d.ts +2 -0
  40. package/dist/tools/definitions/telegram-personal-profile.js +14 -0
  41. package/dist/tools/definitions/telegram-personal-profile.js.map +1 -1
  42. package/dist/tools/definitions/telegram-personal-proxies.d.ts +42 -0
  43. package/dist/tools/definitions/telegram-personal-proxies.js +204 -0
  44. package/dist/tools/definitions/telegram-personal-proxies.js.map +1 -0
  45. package/dist/tools/definitions/telegram-proxy-stock.d.ts +34 -0
  46. package/dist/tools/definitions/telegram-proxy-stock.js +129 -0
  47. package/dist/tools/definitions/telegram-proxy-stock.js.map +1 -0
  48. package/dist/tools/definitions/telegram-session-import.d.ts +5 -0
  49. package/dist/tools/definitions/telegram-session-import.js +33 -0
  50. package/dist/tools/definitions/telegram-session-import.js.map +1 -0
  51. package/dist/tools/definitions/trigger-action-item.schema.d.ts +10 -0
  52. package/dist/tools/definitions/trigger-action-item.schema.js +28 -11
  53. package/dist/tools/definitions/trigger-action-item.schema.js.map +1 -1
  54. package/dist/tools/definitions/trigger-upsert.tool.d.ts +1 -0
  55. package/dist/tools/definitions/trigger-upsert.tool.js +13 -0
  56. package/dist/tools/definitions/trigger-upsert.tool.js.map +1 -1
  57. package/dist/tools/definitions.d.ts +119 -9
  58. package/dist/tools/definitions.js +13 -0
  59. package/dist/tools/definitions.js.map +1 -1
  60. package/dist/tools/operation-recovery.d.ts +1 -1
  61. package/dist/tools/operation-recovery.js +1 -1
  62. package/dist/tools/operation-recovery.js.map +1 -1
  63. package/dist/tools/registry-mapping.cases.js +210 -8
  64. package/dist/tools/registry-mapping.cases.js.map +1 -1
  65. package/dist/tools/registry.js +20 -7
  66. package/dist/tools/registry.js.map +1 -1
  67. package/dist/tools/result.d.ts +2 -1
  68. package/dist/tools/result.js +100 -27
  69. package/dist/tools/result.js.map +1 -1
  70. package/dist/types/api-contract.gen.d.ts +2220 -216
  71. package/docs/async-operation-lifecycle.md +13 -2
  72. package/docs/failure-semantics.md +15 -2
  73. package/docs/help-navigation.md +2 -0
  74. package/docs/secret-read-boundary.md +7 -0
  75. package/docs/telegram-personal-management.md +95 -24
  76. package/docs/telegram-personal-proxies.md +135 -0
  77. package/docs/telegram-session-import.md +53 -0
  78. package/package.json +8 -7
  79. package/scripts/body-idempotency-contract.mjs +95 -0
  80. package/scripts/generate-followup-session-contract.mjs +14 -7
  81. package/scripts/generated-output.mjs +9 -0
  82. package/scripts/sync-agent-model-schemas.mjs +11 -11
  83. package/scripts/sync-api-contracts.mjs +99 -40
  84. package/scripts/sync-crm-stage-pool-schema.mjs +6 -7
  85. package/scripts/sync-route-map.mjs +189 -35
  86. package/scripts/sync-sandbox-request-key.mjs +40 -0
  87. package/scripts/sync-telegram-profile-photo.mjs +57 -0
  88. package/scripts/sync-telegram-session-import.mjs +39 -0
  89. package/scripts/sync-tool-inventory.mjs +15 -7
  90. package/scripts/sync-trigger-argument-schema.mjs +31 -14
  91. package/scripts/sync-trigger-event-schema.mjs +11 -6
  92. package/scripts/sync-trigger-step-reference-schema.mjs +21 -0
  93. package/scripts/sync-wave-id-schemas.mjs +7 -6
  94. package/scripts/telegram-personal-profile-mutants.mjs +59 -18
  95. package/scripts/telegram-personal-proxy-mutants.mjs +171 -0
@@ -1,10 +1,11 @@
1
+ import { writeGeneratedFile } from './generated-output.mjs';
1
2
  // Выпуск схемы нового каталога и подсказок из исполняемых определений, без копии списка моделей.
2
- import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
3
+ import { readFileSync } from 'node:fs';
3
4
  import { z } from 'zod';
4
- import { agentModelTools } from '../dist/tools/definitions/agent-models.js';
5
- import { agentSettingsTools } from '../dist/tools/definitions/agent-settings.js';
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 '../dist/tools/inventory.js';
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
- writeFileSync(
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
- else routeMap[previousRoute] = route;
67
- writeFileSync(routeMapPath, JSON.stringify(routeMap, null, 2) + '\n');
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
- writeFileSync(target, serialized);
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 { shippedRef } from './shipped-ref.mjs';
4
- import { copyFileSync, readFileSync, writeFileSync } from 'node:fs';
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 run = (args) => execFileSync('yarn', args, { cwd: root, stdio: 'inherit' });
18
- run(['exec', 'openapi-typescript', apiFile, '-o', 'src/types/api-contract.gen.ts']);
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
- for (const script of ['schemas:sync:trigger-arguments', 'schemas:sync:crm-stage-pool', 'schemas:sync:wave-ids', 'schemas:sync:agent-models']) run([script]);
22
- for (const script of ['sync-contract-ledger-schemas.mjs', 'generate-followup-session-contract.mjs']) execFileSync(process.execPath, [resolve(root, 'scripts', script)], { cwd: root, stdio: 'inherit' });
23
- const { toolDefinitions } = await import('../dist/tools/definitions.js');
24
- copyFileSync(new URL('../src/schemas/mcp-v2-route-map.json', import.meta.url), new URL('../dist/schemas/mcp-v2-route-map.json', import.meta.url));
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, { io: 'input', unrepresentable: 'any' });
33
- if (derived.properties?.knowledge_space_id) tool.inputSchema.properties.knowledge_space_id = derived.properties.knowledge_space_id;
34
- if (definition.name === 'besales_followup_sequence_upsert' && !tool.inputSchema.properties.config.properties.steps) {
35
- const baseline = JSON.parse(execFileSync('git', ['show', 'HEAD:src/schemas/mcp-tools.json'], { encoding: 'utf8' }));
36
- const config = baseline.tools.find((entry) => entry.name === definition.name).inputSchema.properties.config;
37
- const current = tool.inputSchema.properties.config;
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('../dist/tools/definitions/follow-up-config.js');
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, { io: 'input', unrepresentable: 'any' });
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']) chain.inputSchema.properties.config.properties[field] = reactivation.properties[field];
53
- if (!chain.description.includes('45 минут')) chain.description += ' Окно календарного шага по умолчанию 45 минут; серия эфиров задаётся sessionTimeVariable/sessionDayOffsets/sessionDurationMinutes.';
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 baselineRegistry = JSON.parse(execFileSync('git', ['show', 'HEAD:src/schemas/mcp-tools.json'], { encoding: 'utf8' }));
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.enhanceResponse({}, { report: 'money' }).instructions.split('\n');
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('(speed/cycle/quality) ещё не построены: отказ маршрута не означает отсутствие данных.', 'speed, cycle, quality также построены.');
62
- inbound.description = currentDescription.includes(moneyInstructions) ? currentDescription : currentDescription + ' ' + moneyInstructions;
63
- writeFileSync(registryPath, JSON.stringify(registry, null, 2) + '\n');
64
- run(['schemas:sync:crm-stage-pool']);
65
- execFileSync(process.execPath, [resolve(root, 'scripts/sync-route-map.mjs')], { cwd: root, stdio: 'inherit' });
66
- const rows = JSON.parse(readFileSync(new URL('../src/schemas/mcp-v2-route-map.json', import.meta.url), 'utf8'));
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
- const shipped = JSON.parse(execFileSync('git', ['show', `${shippedRef()}:src/schemas/mcp-tools.json`], { encoding: 'utf8', maxBuffer: 16 * 1024 * 1024 }));
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(shipped.tools, finalRegistry.tools, lag.toolSuccessors);
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 (!activeKeys.has(key) && (key.includes('instagram_comment_templates') || key.includes('::personal_url_variable::'))) delete lag.contractLag[key];
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)]) lag.contractLag[lagKey(entry)] = {
80
- owner: 'DELIBERATE', severity: 'DELIBERATE', reason: 'Сохранённые идентификаторы области знаний проверяются по структурной форме UUID без требований к версии и варианту; значение передаётся неизменным (night-spaceid).',
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
- writeFileSync(lagFile, JSON.stringify(lag, null, 2) + '\n');
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 { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
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 '../dist/tools/definitions/platform-setup.js';
10
+ } from '../src/tools/definitions/platform-setup.js';
10
11
  import {
11
12
  crmMoveStageConfigSchema,
12
13
  stagePoolFields,
13
- } from '../dist/tools/definitions/crm-stage-pool.schema.js';
14
- import { CRM_STAGE_ACTION_POOL_MAX_SIZE } from '../dist/shared/constants/crm-stage-pool.constants.js';
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
- writeFileSync(target, serialized);
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 { copyFileSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
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('../dist/tools/definitions.js');
13
- copyFileSync(new URL('../src/schemas/mcp-v2-route-map.json', import.meta.url), new URL('../dist/schemas/mcp-v2-route-map.json', import.meta.url));
14
- const { mappingCases } = await import('../dist/tools/registry-mapping.cases.js');
15
- const registry = JSON.parse(readFileSync(new URL('../src/schemas/mcp-tools.json', import.meta.url), 'utf8'));
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 (['besales_broadcast_analytics_get@segments_catalog', 'besales_broadcast_analytics_get@funnel_products'].includes(row.tool)) delete row.canonicalArgs.mode;
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)].map((block) => ({
27
- path: block[1], methods: new Set([...block[2].matchAll(/^ {8}([a-z]+):[ \t]*(\S[^\n]*)/gm)].filter((match) => !/^never\b/u.test(match[2])).map((match) => match[1].toUpperCase())),
28
- regex: new RegExp('^' + block[1].split('/').map((part) => part.startsWith('{') ? '[^/]+' : part.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&')).join('/') + '$'),
29
- })).sort((a, b) => (a.path.match(/\{/gu)?.length ?? 0) - (b.path.match(/\{/gu)?.length ?? 0));
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(contracts, '../src/control/channel/crm-webhook-url-reveal.controller.ts');
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
- const controllerSource = readFileSync(privateController, 'utf8');
34
- const moduleSource = readFileSync(privateModule, 'utf8');
35
- const controller = /@Controller\('([^']+)'\)/u.exec(controllerSource);
36
- if (!controller || !/@Post\(\)/u.test(controllerSource) || !/@HiddenFromPublicDocs/u.test(controllerSource) || !/controllers:\s*\[[\s\S]*?CrmWebhookUrlRevealController/u.test(moduleSource)) throw new Error('PRIVATE_CRM_ROUTE_NOT_REGISTERED');
37
- const privatePath = '/api/v2/' + controller[1];
38
- paths.push({ path: privatePath, methods: new Set(['POST']), regex: new RegExp('^' + privatePath + '$'), private: true });
39
- const initialRows = JSON.parse(execFileSync('git', ['show', 'HEAD:src/schemas/mcp-v2-route-map.json'], { encoding: 'utf8' }));
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)) continue;
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 = mappingCases.find((row) => row.name === definition.name)?.input ?? sampleFor(definition.name, schema, true);
48
- if (definition.name === 'besales_feedback_document_access_update') args.expected_document_snapshot = 'a'.repeat(64);
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 route = paths.find((candidate) => candidate.regex.test('/api/v2' + url.pathname) && candidate.methods.has(method));
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 = { method, path: url.pathname, pathTemplate: route.path.slice('/api/v2'.length).replace(/\{([^}]+)\}/gu, ':$1') };
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 file = { fieldName: key, filename: value.name, contentType: value.type, content: await value.text() };
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)) changes.push({ tool: row.tool, before: before.method + ' ' + before.pathTemplate, after: method + ' ' + expected.pathTemplate });
78
- if (row.tool.startsWith('besales_dialogue_audit_run_start@')) expected.body.idempotencyKey = '00000000-0000-4000-8000-000000000090';
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) { delete row.backendTarget; row.contractVisibility = 'CUSTOMER'; }
81
- else row.backendTarget = 'besales-api';
82
- } catch (error) { errors.push({ tool: row.tool, error: String(error) }); }
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-bridge-train2/', import.meta.url);
213
+ const artifactDir = new URL('../.codex-artifacts-prBETA9ASM/', import.meta.url);
85
214
  mkdirSync(artifactDir, { recursive: true });
86
- const payload = { privateRoutes: [{ method: 'POST', path: privatePath, controller: privateController, sha256: createHash('sha256').update(controllerSource).digest('hex') }], source: 'working-tree', apiOpenApi: apiFile, apiSha256: createHash('sha256').update(apiText).digest('hex'), apiHead: execFileSync('git', ['-C', resolve(contracts, '..'), 'rev-parse', 'HEAD'], { encoding: 'utf8' }).trim(), totalTools: toolDefinitions.length, localTools: toolDefinitions.filter((tool) => !tool.createRequest).map((tool) => tool.name), checkedApiTools: new Set(rows.map((row) => row.tool.split('@')[0])).size, records: rows.length, changes, errors };
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(new URL('mcp-tool-routes.json', artifactDir), JSON.stringify({ ...payload, tools: rows }, null, 2) + '\n');
89
- if (errors.length) { console.error(JSON.stringify(errors, null, 2)); throw new Error(`API_ROUTE_AUDIT_FAILED:${errors.length}`); }
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) throw new Error('Карта разошлась с определениями: выполните sync-route-map.mjs');
243
+ if (original !== output)
244
+ throw new Error('Карта разошлась с определениями: выполните sync-route-map.mjs');
93
245
  } else writeFileSync(routeFile, output);
94
- console.log(`API route audit: ${payload.checkedApiTools} API tools, ${rows.length} variants; missing 0`);
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
+ }