yandex-direct-mcp-plus 1.3.0

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 (177) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +186 -0
  3. package/dist/app/index.d.ts +2 -0
  4. package/dist/app/index.js +15 -0
  5. package/dist/app/registry.d.ts +2 -0
  6. package/dist/app/registry.js +83 -0
  7. package/dist/app/server.d.ts +4 -0
  8. package/dist/app/server.js +30 -0
  9. package/dist/shared/api/client.d.ts +1 -0
  10. package/dist/shared/api/client.js +16 -0
  11. package/dist/shared/api/errors.d.ts +2 -0
  12. package/dist/shared/api/errors.js +21 -0
  13. package/dist/shared/api/fetch.d.ts +5 -0
  14. package/dist/shared/api/fetch.js +62 -0
  15. package/dist/shared/api/json.d.ts +2 -0
  16. package/dist/shared/api/json.js +13 -0
  17. package/dist/shared/api/parse.d.ts +2 -0
  18. package/dist/shared/api/parse.js +10 -0
  19. package/dist/shared/api/reports.d.ts +6 -0
  20. package/dist/shared/api/reports.js +45 -0
  21. package/dist/shared/api/v4.d.ts +1 -0
  22. package/dist/shared/api/v4.js +18 -0
  23. package/dist/shared/config/endpoints.d.ts +3 -0
  24. package/dist/shared/config/endpoints.js +8 -0
  25. package/dist/shared/config/enums.d.ts +30 -0
  26. package/dist/shared/config/enums.js +137 -0
  27. package/dist/shared/config/env.d.ts +2 -0
  28. package/dist/shared/config/env.js +14 -0
  29. package/dist/shared/config/limits.d.ts +58 -0
  30. package/dist/shared/config/limits.js +54 -0
  31. package/dist/shared/lib/campaign-type.d.ts +12 -0
  32. package/dist/shared/lib/campaign-type.js +20 -0
  33. package/dist/shared/lib/date.d.ts +3 -0
  34. package/dist/shared/lib/date.js +17 -0
  35. package/dist/shared/lib/format.d.ts +3 -0
  36. package/dist/shared/lib/format.js +51 -0
  37. package/dist/shared/lib/id.d.ts +4 -0
  38. package/dist/shared/lib/id.js +21 -0
  39. package/dist/shared/lib/money.d.ts +6 -0
  40. package/dist/shared/lib/money.js +21 -0
  41. package/dist/shared/lib/pagination.d.ts +9 -0
  42. package/dist/shared/lib/pagination.js +26 -0
  43. package/dist/shared/lib/report.d.ts +1 -0
  44. package/dist/shared/lib/report.js +5 -0
  45. package/dist/shared/lib/tool.d.ts +24 -0
  46. package/dist/shared/lib/tool.js +20 -0
  47. package/dist/tools/account/handler.d.ts +3 -0
  48. package/dist/tools/account/handler.js +12 -0
  49. package/dist/tools/account/schema.d.ts +4 -0
  50. package/dist/tools/account/schema.js +7 -0
  51. package/dist/tools/account/tool.d.ts +1 -0
  52. package/dist/tools/account/tool.js +11 -0
  53. package/dist/tools/ad-extensions/handler.d.ts +5 -0
  54. package/dist/tools/ad-extensions/handler.js +35 -0
  55. package/dist/tools/ad-extensions/schema.d.ts +14 -0
  56. package/dist/tools/ad-extensions/schema.js +37 -0
  57. package/dist/tools/ad-extensions/tool.d.ts +3 -0
  58. package/dist/tools/ad-extensions/tool.js +27 -0
  59. package/dist/tools/ad-groups/handler.d.ts +5 -0
  60. package/dist/tools/ad-groups/handler.js +33 -0
  61. package/dist/tools/ad-groups/schema.d.ts +14 -0
  62. package/dist/tools/ad-groups/schema.js +27 -0
  63. package/dist/tools/ad-groups/tool.d.ts +3 -0
  64. package/dist/tools/ad-groups/tool.js +27 -0
  65. package/dist/tools/ad-images/handler.d.ts +5 -0
  66. package/dist/tools/ad-images/handler.js +47 -0
  67. package/dist/tools/ad-images/schema.d.ts +13 -0
  68. package/dist/tools/ad-images/schema.js +37 -0
  69. package/dist/tools/ad-images/tool.d.ts +1 -0
  70. package/dist/tools/ad-images/tool.js +11 -0
  71. package/dist/tools/ads/handler.d.ts +7 -0
  72. package/dist/tools/ads/handler.js +54 -0
  73. package/dist/tools/ads/schema.d.ts +27 -0
  74. package/dist/tools/ads/schema.js +68 -0
  75. package/dist/tools/ads/tool.d.ts +5 -0
  76. package/dist/tools/ads/tool.js +43 -0
  77. package/dist/tools/audience-targets/handler.d.ts +6 -0
  78. package/dist/tools/audience-targets/handler.js +96 -0
  79. package/dist/tools/audience-targets/schema.d.ts +29 -0
  80. package/dist/tools/audience-targets/schema.js +84 -0
  81. package/dist/tools/audience-targets/tool.d.ts +2 -0
  82. package/dist/tools/audience-targets/tool.js +19 -0
  83. package/dist/tools/bid-adjustments/handler.d.ts +4 -0
  84. package/dist/tools/bid-adjustments/handler.js +43 -0
  85. package/dist/tools/bid-adjustments/schema.d.ts +16 -0
  86. package/dist/tools/bid-adjustments/schema.js +50 -0
  87. package/dist/tools/bid-adjustments/tool.d.ts +2 -0
  88. package/dist/tools/bid-adjustments/tool.js +19 -0
  89. package/dist/tools/businesses/handler.d.ts +3 -0
  90. package/dist/tools/businesses/handler.js +26 -0
  91. package/dist/tools/businesses/schema.d.ts +6 -0
  92. package/dist/tools/businesses/schema.js +12 -0
  93. package/dist/tools/businesses/tool.d.ts +1 -0
  94. package/dist/tools/businesses/tool.js +11 -0
  95. package/dist/tools/campaigns/handler.d.ts +11 -0
  96. package/dist/tools/campaigns/handler.js +200 -0
  97. package/dist/tools/campaigns/schema.d.ts +46 -0
  98. package/dist/tools/campaigns/schema.js +109 -0
  99. package/dist/tools/campaigns/tool.d.ts +7 -0
  100. package/dist/tools/campaigns/tool.js +61 -0
  101. package/dist/tools/changes/handler.d.ts +5 -0
  102. package/dist/tools/changes/handler.js +33 -0
  103. package/dist/tools/changes/schema.d.ts +9 -0
  104. package/dist/tools/changes/schema.js +35 -0
  105. package/dist/tools/changes/tool.d.ts +1 -0
  106. package/dist/tools/changes/tool.js +11 -0
  107. package/dist/tools/dictionaries/handler.d.ts +5 -0
  108. package/dist/tools/dictionaries/handler.js +66 -0
  109. package/dist/tools/dictionaries/schema.d.ts +9 -0
  110. package/dist/tools/dictionaries/schema.js +23 -0
  111. package/dist/tools/dictionaries/tool.d.ts +2 -0
  112. package/dist/tools/dictionaries/tool.js +19 -0
  113. package/dist/tools/dynamic-targets/handler.d.ts +6 -0
  114. package/dist/tools/dynamic-targets/handler.js +96 -0
  115. package/dist/tools/dynamic-targets/schema.d.ts +33 -0
  116. package/dist/tools/dynamic-targets/schema.js +91 -0
  117. package/dist/tools/dynamic-targets/tool.d.ts +2 -0
  118. package/dist/tools/dynamic-targets/tool.js +19 -0
  119. package/dist/tools/feeds/handler.d.ts +3 -0
  120. package/dist/tools/feeds/handler.js +29 -0
  121. package/dist/tools/feeds/schema.d.ts +6 -0
  122. package/dist/tools/feeds/schema.js +12 -0
  123. package/dist/tools/feeds/tool.d.ts +1 -0
  124. package/dist/tools/feeds/tool.js +11 -0
  125. package/dist/tools/keywords/handler.d.ts +6 -0
  126. package/dist/tools/keywords/handler.js +53 -0
  127. package/dist/tools/keywords/schema.d.ts +21 -0
  128. package/dist/tools/keywords/schema.js +41 -0
  129. package/dist/tools/keywords/tool.d.ts +4 -0
  130. package/dist/tools/keywords/tool.js +35 -0
  131. package/dist/tools/negative-keywords/handler.d.ts +8 -0
  132. package/dist/tools/negative-keywords/handler.js +135 -0
  133. package/dist/tools/negative-keywords/merge.d.ts +2 -0
  134. package/dist/tools/negative-keywords/merge.js +36 -0
  135. package/dist/tools/negative-keywords/schema.d.ts +37 -0
  136. package/dist/tools/negative-keywords/schema.js +139 -0
  137. package/dist/tools/negative-keywords/shared-set-link.d.ts +3 -0
  138. package/dist/tools/negative-keywords/shared-set-link.js +21 -0
  139. package/dist/tools/negative-keywords/tool.d.ts +6 -0
  140. package/dist/tools/negative-keywords/tool.js +51 -0
  141. package/dist/tools/retargeting/handler.d.ts +4 -0
  142. package/dist/tools/retargeting/handler.js +47 -0
  143. package/dist/tools/retargeting/schema.d.ts +19 -0
  144. package/dist/tools/retargeting/schema.js +57 -0
  145. package/dist/tools/retargeting/tool.d.ts +2 -0
  146. package/dist/tools/retargeting/tool.js +19 -0
  147. package/dist/tools/search-queries/handler.d.ts +3 -0
  148. package/dist/tools/search-queries/handler.js +28 -0
  149. package/dist/tools/search-queries/schema.d.ts +7 -0
  150. package/dist/tools/search-queries/schema.js +17 -0
  151. package/dist/tools/search-queries/tool.d.ts +1 -0
  152. package/dist/tools/search-queries/tool.js +11 -0
  153. package/dist/tools/sitelinks/handler.d.ts +4 -0
  154. package/dist/tools/sitelinks/handler.js +28 -0
  155. package/dist/tools/sitelinks/schema.d.ts +13 -0
  156. package/dist/tools/sitelinks/schema.js +35 -0
  157. package/dist/tools/sitelinks/tool.d.ts +2 -0
  158. package/dist/tools/sitelinks/tool.js +19 -0
  159. package/dist/tools/statistics/handler.d.ts +3 -0
  160. package/dist/tools/statistics/handler.js +20 -0
  161. package/dist/tools/statistics/schema.d.ts +7 -0
  162. package/dist/tools/statistics/schema.js +17 -0
  163. package/dist/tools/statistics/tool.d.ts +1 -0
  164. package/dist/tools/statistics/tool.js +11 -0
  165. package/dist/tools/time-targeting/handler.d.ts +6 -0
  166. package/dist/tools/time-targeting/handler.js +124 -0
  167. package/dist/tools/time-targeting/schema.d.ts +19 -0
  168. package/dist/tools/time-targeting/schema.js +73 -0
  169. package/dist/tools/time-targeting/tool.d.ts +2 -0
  170. package/dist/tools/time-targeting/tool.js +20 -0
  171. package/dist/tools/vcards/handler.d.ts +4 -0
  172. package/dist/tools/vcards/handler.js +105 -0
  173. package/dist/tools/vcards/schema.d.ts +27 -0
  174. package/dist/tools/vcards/schema.js +43 -0
  175. package/dist/tools/vcards/tool.d.ts +2 -0
  176. package/dist/tools/vcards/tool.js +19 -0
  177. package/package.json +121 -0
@@ -0,0 +1,91 @@
1
+ import { z } from "zod";
2
+ import { AUDIENCE_TARGET_STATES, DYNAMIC_TARGET_ACTIONS, STRATEGY_PRIORITIES, WEBPAGE_CONDITION_OPERANDS, WEBPAGE_CONDITION_OPERATORS } from "#shared/config/enums";
3
+ import { MAX_AD_GROUPS_PER_CALL, MAX_CAMPAIGNS_PER_AUDIENCE_CALL, MAX_IDS_PER_CALL } from "#shared/config/limits";
4
+ import { idField } from "#shared/lib/id";
5
+ import { rublesField } from "#shared/lib/money";
6
+ import { pageFields } from "#shared/lib/pagination";
7
+ export const listDynamicTargetsSchema = z.object({
8
+ dynamic_target_ids: z
9
+ .array(idField("ID динамической цели"))
10
+ .check(z.maxLength(MAX_IDS_PER_CALL, { error: `Не больше ${MAX_IDS_PER_CALL} целей за вызов` }))
11
+ .optional()
12
+ .meta({ description: "Конкретные динамические цели" }),
13
+ ad_group_ids: z
14
+ .array(idField("ID группы объявлений"))
15
+ .check(z.maxLength(MAX_AD_GROUPS_PER_CALL, { error: `Не больше ${MAX_AD_GROUPS_PER_CALL} групп за вызов` }))
16
+ .optional()
17
+ .meta({ description: "Цели выбранных групп" }),
18
+ campaign_ids: z
19
+ .array(idField("ID кампании"))
20
+ .check(z.maxLength(MAX_CAMPAIGNS_PER_AUDIENCE_CALL, {
21
+ error: `Не больше ${MAX_CAMPAIGNS_PER_AUDIENCE_CALL} кампаний за вызов`
22
+ }))
23
+ .optional()
24
+ .meta({ description: "Цели выбранных кампаний" }),
25
+ states: z
26
+ .array(z.literal(AUDIENCE_TARGET_STATES))
27
+ .optional()
28
+ .meta({ description: "Фильтр по состоянию: ON или SUSPENDED" }),
29
+ ...pageFields
30
+ });
31
+ // Условие нацеливания: по какому свойству страницы отбирать и с каким оператором.
32
+ const webpageCondition = z.object({
33
+ operand: z.literal(WEBPAGE_CONDITION_OPERANDS).meta({
34
+ description: "Свойство страницы: URL, DOMAIN, PAGE_TITLE, PAGE_CONTENT или OFFERS_LIST_URL"
35
+ }),
36
+ operator: z.literal(WEBPAGE_CONDITION_OPERATORS).meta({
37
+ description: "Как сравнивать: EQUALS_ANY, NOT_EQUALS_ALL, CONTAINS_ANY или NOT_CONTAINS_ALL"
38
+ }),
39
+ arguments: z
40
+ .array(z.string().check(z.minLength(1, { error: "Аргумент условия не может быть пустым" })))
41
+ .check(z.minLength(1, { error: "Условие без аргументов" }))
42
+ .meta({ description: "Значения, с которыми сравнивается свойство страницы" })
43
+ });
44
+ const dynamicTarget = z.object({
45
+ ad_group_id: idField("ID группы динамических объявлений"),
46
+ name: z
47
+ .string()
48
+ .check(z.minLength(1, { error: "Название не может быть пустым" }))
49
+ .meta({ description: "Название цели" }),
50
+ conditions: z
51
+ .array(webpageCondition)
52
+ .check(z.minLength(1, { error: "Цель без условий" }))
53
+ .meta({ description: "Условия отбора страниц; между собой соединяются логическим И" }),
54
+ bid: rublesField("Ставка на поиске в рублях; 0 — снять ставку", { allowZero: true }).optional(),
55
+ context_bid: rublesField("Ставка в сетях в рублях; 0 — снять ставку", { allowZero: true }).optional(),
56
+ strategy_priority: z
57
+ .literal(STRATEGY_PRIORITIES)
58
+ .optional()
59
+ .meta({ description: "Приоритет для автоматических стратегий" })
60
+ });
61
+ const dynamicBid = z.object({
62
+ dynamic_target_id: idField("ID динамической цели").optional(),
63
+ ad_group_id: idField("ID группы: ставка применится ко всем её целям").optional(),
64
+ campaign_id: idField("ID кампании: ставка применится ко всем её целям").optional(),
65
+ bid: rublesField("Ставка на поиске в рублях; 0 — снять ставку", { allowZero: true }).optional(),
66
+ context_bid: rublesField("Ставка в сетях в рублях; 0 — снять ставку", { allowZero: true }).optional(),
67
+ strategy_priority: z
68
+ .literal(STRATEGY_PRIORITIES)
69
+ .optional()
70
+ .meta({ description: "Приоритет для автоматических стратегий" })
71
+ });
72
+ export const manageDynamicTargetsSchema = z.object({
73
+ action: z.literal(DYNAMIC_TARGET_ACTIONS).meta({
74
+ description: "Что сделать: add, set_bids, suspend, resume или delete (необратимо)"
75
+ }),
76
+ targets: z
77
+ .array(dynamicTarget)
78
+ .check(z.minLength(1, { error: "Список целей пуст" }), z.maxLength(MAX_AD_GROUPS_PER_CALL, { error: `Не больше ${MAX_AD_GROUPS_PER_CALL} целей за вызов` }))
79
+ .optional()
80
+ .meta({ description: "Цели для добавления; обязателен при action=add" }),
81
+ bids: z
82
+ .array(dynamicBid)
83
+ .check(z.minLength(1, { error: "Список ставок пуст" }), z.maxLength(MAX_IDS_PER_CALL, { error: `Не больше ${MAX_IDS_PER_CALL} ставок за вызов` }))
84
+ .optional()
85
+ .meta({ description: "Новые ставки и приоритеты; обязателен при action=set_bids" }),
86
+ dynamic_target_ids: z
87
+ .array(idField("ID динамической цели"))
88
+ .check(z.minLength(1, { error: "Список целей пуст" }), z.maxLength(MAX_IDS_PER_CALL, { error: `Не больше ${MAX_IDS_PER_CALL} целей за вызов` }))
89
+ .optional()
90
+ .meta({ description: "Цели для suspend, resume или delete" })
91
+ });
@@ -0,0 +1,2 @@
1
+ export declare const listDynamicTargetsTool: import("#shared/lib/tool").ToolDescriptor;
2
+ export declare const manageDynamicTargetsTool: import("#shared/lib/tool").ToolDescriptor;
@@ -0,0 +1,19 @@
1
+ import { DESTRUCTIVE, defineTool, READ } from "#shared/lib/tool";
2
+ import { handleListDynamicTargets, handleManageDynamicTargets } from "./handler.js";
3
+ import { listDynamicTargetsSchema, manageDynamicTargetsSchema } from "./schema.js";
4
+ export const listDynamicTargetsTool = defineTool({
5
+ name: "list_dynamic_targets",
6
+ title: "Динамические цели",
7
+ description: "Получить условия нацеливания динамических текстовых объявлений.",
8
+ annotations: READ,
9
+ schema: listDynamicTargetsSchema,
10
+ handler: handleListDynamicTargets
11
+ });
12
+ export const manageDynamicTargetsTool = defineTool({
13
+ name: "manage_dynamic_targets",
14
+ title: "Управление динамическими целями",
15
+ description: "Создать, изменить ставки, остановить, возобновить или удалить динамические цели.",
16
+ annotations: DESTRUCTIVE,
17
+ schema: manageDynamicTargetsSchema,
18
+ handler: handleManageDynamicTargets
19
+ });
@@ -0,0 +1,3 @@
1
+ import type { z } from "zod";
2
+ import type { listFeedsSchema } from "./schema.js";
3
+ export declare function handleListFeeds(params: z.infer<typeof listFeedsSchema>): Promise<string>;
@@ -0,0 +1,29 @@
1
+ import { apiPost } from "#shared/api/client";
2
+ import { formatResult } from "#shared/lib/format";
3
+ import { apiIds } from "#shared/lib/id";
4
+ import { buildPage } from "#shared/lib/pagination";
5
+ const LIST_FIELDS = [
6
+ "Id",
7
+ "Name",
8
+ "BusinessType",
9
+ "SourceType",
10
+ "FilterSchema",
11
+ "UpdatedAt",
12
+ "CampaignIds",
13
+ "NumberOfItems",
14
+ "Status",
15
+ "TitleAndTextSources"
16
+ ];
17
+ export async function handleListFeeds(params) {
18
+ const request = {
19
+ FieldNames: LIST_FIELDS,
20
+ FileFeedFieldNames: ["Filename"],
21
+ UrlFeedFieldNames: ["Login", "Url", "RemoveUtmTags"]
22
+ };
23
+ if (params.feed_ids?.length)
24
+ request.SelectionCriteria = { Ids: apiIds(params.feed_ids) };
25
+ const page = buildPage(params);
26
+ if (page)
27
+ request.Page = page;
28
+ return formatResult(await apiPost("feeds", "get", request), { money: false });
29
+ }
@@ -0,0 +1,6 @@
1
+ import { z } from "zod";
2
+ export declare const listFeedsSchema: z.ZodObject<{
3
+ limit: z.ZodOptional<z.ZodInt>;
4
+ offset: z.ZodOptional<z.ZodInt>;
5
+ feed_ids: z.ZodOptional<z.ZodArray<z.ZodString>>;
6
+ }, z.core.$strip>;
@@ -0,0 +1,12 @@
1
+ import { z } from "zod";
2
+ import { MAX_IDS_PER_CALL } from "#shared/config/limits";
3
+ import { idField } from "#shared/lib/id";
4
+ import { pageFields } from "#shared/lib/pagination";
5
+ export const listFeedsSchema = z.object({
6
+ feed_ids: z
7
+ .array(idField("ID фида"))
8
+ .check(z.maxLength(MAX_IDS_PER_CALL, { error: `За один вызов допустимо не больше ${MAX_IDS_PER_CALL} фидов` }))
9
+ .optional()
10
+ .meta({ description: "Конкретные фиды; без них возвращаются все фиды аккаунта" }),
11
+ ...pageFields
12
+ });
@@ -0,0 +1 @@
1
+ export declare const listFeedsTool: import("#shared/lib/tool").ToolDescriptor;
@@ -0,0 +1,11 @@
1
+ import { defineTool, READ } from "#shared/lib/tool";
2
+ import { handleListFeeds } from "./handler.js";
3
+ import { listFeedsSchema } from "./schema.js";
4
+ export const listFeedsTool = defineTool({
5
+ name: "list_feeds",
6
+ title: "Список фидов",
7
+ description: "Получить товарные фиды, их источники, статусы обработки и связанные кампании.",
8
+ annotations: READ,
9
+ schema: listFeedsSchema,
10
+ handler: handleListFeeds
11
+ });
@@ -0,0 +1,6 @@
1
+ import type { z } from "zod";
2
+ import type { addKeywordsSchema, listKeywordsSchema, manageKeywordsSchema, setKeywordBidsSchema } from "./schema.js";
3
+ export declare function handleListKeywords(params: z.infer<typeof listKeywordsSchema>): Promise<string>;
4
+ export declare function handleAddKeywords(params: z.infer<typeof addKeywordsSchema>): Promise<string>;
5
+ export declare function handleManageKeywords(params: z.infer<typeof manageKeywordsSchema>): Promise<string>;
6
+ export declare function handleSetKeywordBids(params: z.infer<typeof setKeywordBidsSchema>): Promise<string>;
@@ -0,0 +1,53 @@
1
+ import { apiPost } from "#shared/api/client";
2
+ import { formatResult } from "#shared/lib/format";
3
+ import { apiId, apiIds } from "#shared/lib/id";
4
+ import { buildPage } from "#shared/lib/pagination";
5
+ const LIST_FIELDS = ["Id", "Keyword", "CampaignId", "AdGroupId", "Status", "State", "Bid", "ContextBid"];
6
+ function selectBidTarget(params) {
7
+ const targets = [];
8
+ if (params.keyword_ids?.length)
9
+ targets.push({ field: "KeywordId", ids: params.keyword_ids });
10
+ if (params.ad_group_ids?.length)
11
+ targets.push({ field: "AdGroupId", ids: params.ad_group_ids });
12
+ if (params.campaign_ids?.length)
13
+ targets.push({ field: "CampaignId", ids: params.campaign_ids });
14
+ if (targets.length !== 1) {
15
+ throw new Error("Укажите ровно один уровень целей: keyword_ids ИЛИ ad_group_ids ИЛИ campaign_ids.");
16
+ }
17
+ return targets[0];
18
+ }
19
+ export async function handleListKeywords(params) {
20
+ const requestParams = {
21
+ SelectionCriteria: { AdGroupIds: apiIds(params.ad_group_ids) },
22
+ FieldNames: LIST_FIELDS
23
+ };
24
+ const page = buildPage(params);
25
+ if (page)
26
+ requestParams.Page = page;
27
+ return formatResult(await apiPost("keywords", "get", requestParams));
28
+ }
29
+ export async function handleAddKeywords(params) {
30
+ const data = await apiPost("keywords", "add", {
31
+ Keywords: params.keywords.map((keyword) => ({ AdGroupId: apiId(params.ad_group_id), Keyword: keyword }))
32
+ });
33
+ return formatResult(data);
34
+ }
35
+ export async function handleManageKeywords(params) {
36
+ const data = await apiPost("keywords", params.action, {
37
+ SelectionCriteria: { Ids: apiIds(params.keyword_ids) }
38
+ });
39
+ return formatResult(data);
40
+ }
41
+ export async function handleSetKeywordBids(params) {
42
+ const target = selectBidTarget(params);
43
+ if (params.bid === undefined && params.context_bid === undefined) {
44
+ throw new Error("Укажите bid и/или context_bid (в рублях).");
45
+ }
46
+ const amounts = {};
47
+ if (params.bid !== undefined)
48
+ amounts.Bid = params.bid;
49
+ if (params.context_bid !== undefined)
50
+ amounts.ContextBid = params.context_bid;
51
+ const bids = target.ids.map((id) => ({ [target.field]: apiId(id), ...amounts }));
52
+ return formatResult(await apiPost("bids", "set", { Bids: bids }));
53
+ }
@@ -0,0 +1,21 @@
1
+ import { z } from "zod";
2
+ export declare const listKeywordsSchema: z.ZodObject<{
3
+ limit: z.ZodOptional<z.ZodInt>;
4
+ offset: z.ZodOptional<z.ZodInt>;
5
+ ad_group_ids: z.ZodArray<z.ZodString>;
6
+ }, z.core.$strip>;
7
+ export declare const addKeywordsSchema: z.ZodObject<{
8
+ ad_group_id: z.ZodString;
9
+ keywords: z.ZodArray<z.ZodString>;
10
+ }, z.core.$strip>;
11
+ export declare const manageKeywordsSchema: z.ZodObject<{
12
+ keyword_ids: z.ZodArray<z.ZodString>;
13
+ action: z.ZodLiteral<"delete" | "resume" | "suspend">;
14
+ }, z.core.$strip>;
15
+ export declare const setKeywordBidsSchema: z.ZodObject<{
16
+ keyword_ids: z.ZodOptional<z.ZodArray<z.ZodString>>;
17
+ ad_group_ids: z.ZodOptional<z.ZodArray<z.ZodString>>;
18
+ campaign_ids: z.ZodOptional<z.ZodArray<z.ZodString>>;
19
+ bid: z.ZodOptional<z.ZodPipe<z.ZodNumber, z.ZodTransform<number, number>>>;
20
+ context_bid: z.ZodOptional<z.ZodPipe<z.ZodNumber, z.ZodTransform<number, number>>>;
21
+ }, z.core.$strip>;
@@ -0,0 +1,41 @@
1
+ import { z } from "zod";
2
+ import { KEYWORD_ACTIONS } from "#shared/config/enums";
3
+ import { idField } from "#shared/lib/id";
4
+ import { rublesField } from "#shared/lib/money";
5
+ import { pageFields } from "#shared/lib/pagination";
6
+ export const listKeywordsSchema = z.object({
7
+ ad_group_ids: z
8
+ .array(idField("ID группы объявлений"))
9
+ .check(z.minLength(1, { error: "Укажите хотя бы одну группу" }))
10
+ .meta({ description: "Группы, ключевые фразы которых нужно выбрать" }),
11
+ ...pageFields
12
+ });
13
+ export const addKeywordsSchema = z.object({
14
+ ad_group_id: idField("ID группы, в которую добавляются фразы"),
15
+ keywords: z
16
+ .array(z.string().check(z.minLength(1, { error: "Ключевая фраза не может быть пустой" })))
17
+ .check(z.minLength(1, { error: "Список фраз пуст" }))
18
+ .meta({ description: "Ключевые фразы; минус-слова внутри фразы записываются через дефис" })
19
+ });
20
+ export const manageKeywordsSchema = z.object({
21
+ keyword_ids: z
22
+ .array(idField("ID ключевой фразы"))
23
+ .check(z.minLength(1, { error: "Список фраз пуст" }))
24
+ .meta({ description: "Фразы, над которыми выполняется действие" }),
25
+ action: z.literal(KEYWORD_ACTIONS).meta({ description: "Действие: suspend, resume или delete (необратимо)" })
26
+ });
27
+ // Ставка ставится ровно на один уровень целей: фразы, группы или кампании.
28
+ // Проверку «ровно один» делает хендлер — она про сочетание полей, а не про поле.
29
+ export const setKeywordBidsSchema = z.object({
30
+ keyword_ids: z.array(idField("ID ключевой фразы")).optional().meta({ description: "Ставки на уровне фраз" }),
31
+ ad_group_ids: z
32
+ .array(idField("ID группы объявлений"))
33
+ .optional()
34
+ .meta({ description: "Ставки на все фразы указанных групп" }),
35
+ campaign_ids: z
36
+ .array(idField("ID кампании"))
37
+ .optional()
38
+ .meta({ description: "Ставки на все фразы указанных кампаний" }),
39
+ bid: rublesField("Ставка на поиске в рублях").optional(),
40
+ context_bid: rublesField("Ставка в сетях (РСЯ) в рублях").optional()
41
+ });
@@ -0,0 +1,4 @@
1
+ export declare const listKeywordsTool: import("#shared/lib/tool").ToolDescriptor;
2
+ export declare const addKeywordsTool: import("#shared/lib/tool").ToolDescriptor;
3
+ export declare const manageKeywordsTool: import("#shared/lib/tool").ToolDescriptor;
4
+ export declare const setKeywordBidsTool: import("#shared/lib/tool").ToolDescriptor;
@@ -0,0 +1,35 @@
1
+ import { DESTRUCTIVE, defineTool, IDEMPOTENT, READ, WRITE } from "#shared/lib/tool";
2
+ import { handleAddKeywords, handleListKeywords, handleManageKeywords, handleSetKeywordBids } from "./handler.js";
3
+ import { addKeywordsSchema, listKeywordsSchema, manageKeywordsSchema, setKeywordBidsSchema } from "./schema.js";
4
+ export const listKeywordsTool = defineTool({
5
+ name: "list_keywords",
6
+ title: "Список ключевых слов",
7
+ description: "Ключевые фразы в группах объявлений: фразы, ставки (руб), статусы.",
8
+ annotations: READ,
9
+ schema: listKeywordsSchema,
10
+ handler: handleListKeywords
11
+ });
12
+ export const addKeywordsTool = defineTool({
13
+ name: "add_keywords",
14
+ title: "Добавить ключевые слова",
15
+ description: "Добавить ключевые фразы в группу объявлений.",
16
+ annotations: WRITE,
17
+ schema: addKeywordsSchema,
18
+ handler: handleAddKeywords
19
+ });
20
+ export const manageKeywordsTool = defineTool({
21
+ name: "manage_keywords",
22
+ title: "Управление ключевыми словами",
23
+ description: "Действие над ключевыми фразами: suspend/resume/delete. ⚠️ delete необратимо.",
24
+ annotations: DESTRUCTIVE,
25
+ schema: manageKeywordsSchema,
26
+ handler: handleManageKeywords
27
+ });
28
+ export const setKeywordBidsTool = defineTool({
29
+ name: "set_keyword_bids",
30
+ title: "Установить ставки",
31
+ description: "Установить ставки (поиск/сети, в рублях) на уровне фраз, групп или кампаний (сервис Bids).",
32
+ annotations: IDEMPOTENT,
33
+ schema: setKeywordBidsSchema,
34
+ handler: handleSetKeywordBids
35
+ });
@@ -0,0 +1,8 @@
1
+ import type { z } from "zod";
2
+ import type { getCampaignNegativeKeywordsSchema, linkNegativeKeywordSetsSchema, listNegativeKeywordSharedSetsSchema, manageNegativeKeywordSharedSetsSchema, setAdGroupNegativeKeywordsSchema, setCampaignNegativeKeywordsSchema } from "./schema.js";
3
+ export declare function handleGetCampaignNegativeKeywords(params: z.infer<typeof getCampaignNegativeKeywordsSchema>): Promise<string>;
4
+ export declare function handleSetCampaignNegativeKeywords(params: z.infer<typeof setCampaignNegativeKeywordsSchema>): Promise<string>;
5
+ export declare function handleSetAdGroupNegativeKeywords(params: z.infer<typeof setAdGroupNegativeKeywordsSchema>): Promise<string>;
6
+ export declare function handleListNegativeKeywordSharedSets(params: z.infer<typeof listNegativeKeywordSharedSetsSchema>): Promise<string>;
7
+ export declare function handleManageNegativeKeywordSharedSets(params: z.infer<typeof manageNegativeKeywordSharedSetsSchema>): Promise<string>;
8
+ export declare function handleLinkNegativeKeywordSets(params: z.infer<typeof linkNegativeKeywordSetsSchema>): Promise<string>;
@@ -0,0 +1,135 @@
1
+ import { apiPost } from "#shared/api/client";
2
+ import { formatResult } from "#shared/lib/format";
3
+ import { apiId, apiIds } from "#shared/lib/id";
4
+ import { buildPage } from "#shared/lib/pagination";
5
+ import { mergeNegativeKeywords } from "./merge.js";
6
+ import { buildCampaignLink } from "./shared-set-link.js";
7
+ // В домене нет денег: money: false бережёт от конвертации полей, которые ими не являются.
8
+ const NO_MONEY = { money: false };
9
+ export async function handleGetCampaignNegativeKeywords(params) {
10
+ const data = await apiPost("campaigns", "get", {
11
+ SelectionCriteria: { Ids: apiIds(params.campaign_ids) },
12
+ FieldNames: ["Id", "Name", "NegativeKeywords"]
13
+ });
14
+ return formatResult(data, NO_MONEY);
15
+ }
16
+ // NegativeKeywords обязано быть в FieldNames: без него Директ вернёт существующую
17
+ // кампанию вообще без этого поля, слияние даст один входящий список, а update пройдёт
18
+ // успешно — прежние минус-фразы исчезнут молча. Отсутствие сущности в ответе — ошибка,
19
+ // а не пустой список: сливать не с чем.
20
+ async function readExistingKeywords(service, collection, label, id) {
21
+ const data = await apiPost(service, "get", {
22
+ SelectionCriteria: { Ids: apiIds([id]) },
23
+ FieldNames: ["Id", "NegativeKeywords"]
24
+ });
25
+ const found = data?.result?.[collection];
26
+ const entity = Array.isArray(found) ? found[0] : undefined;
27
+ if (!entity)
28
+ throw new Error(`${label} ${id} не найдена или недоступна — объединять минус-фразы не с чем.`);
29
+ // У сущности без минус-фраз поле приходит как null, а не пропущенным ключом и не пустым
30
+ // Items (пробой 05.09.2026) — тип обязан это допускать, иначе `?.` выглядит перестраховкой.
31
+ return entity.NegativeKeywords?.Items ?? [];
32
+ }
33
+ // Коллекция очищается значением null, а не пустым Items: { Items: [] } Директ отклоняет
34
+ // ошибкой 8000 «Количество элементов в массиве … должно быть не менее 1», { Items: null } —
35
+ // «Items не может иметь значение null». В WSDL это видно как nillable="true" на самом поле;
36
+ // подтверждено сетевым тестом и для NegativeKeywords, и для NegativeKeywordSharedSetIds.
37
+ // Правило одно на домен и живёт здесь одним местом: «очистить» — это null, всегда.
38
+ const itemsOrNull = (items) => (items.length > 0 ? { Items: items } : null);
39
+ // replace остаётся одним вызовом: читать нечего, прежний список и так затирается.
40
+ async function resolveItems(params, readExisting) {
41
+ if (params.mode === "replace")
42
+ return params.negative_keywords;
43
+ return mergeNegativeKeywords(await readExisting(), params.negative_keywords, params.mode);
44
+ }
45
+ export async function handleSetCampaignNegativeKeywords(params) {
46
+ const items = await resolveItems(params, () => readExistingKeywords("campaigns", "Campaigns", "Кампания", params.campaign_id));
47
+ const data = await apiPost("campaigns", "update", {
48
+ Campaigns: [{ Id: apiId(params.campaign_id), NegativeKeywords: itemsOrNull(items) }]
49
+ });
50
+ return formatResult(data);
51
+ }
52
+ export async function handleSetAdGroupNegativeKeywords(params) {
53
+ const items = await resolveItems(params, () => readExistingKeywords("adgroups", "AdGroups", "Группа объявлений", params.ad_group_id));
54
+ const data = await apiPost("adgroups", "update", {
55
+ AdGroups: [{ Id: apiId(params.ad_group_id), NegativeKeywords: itemsOrNull(items) }]
56
+ });
57
+ return formatResult(data);
58
+ }
59
+ export async function handleListNegativeKeywordSharedSets(params) {
60
+ const request = { FieldNames: ["Id", "Name", "NegativeKeywords", "Associated"] };
61
+ if (params.set_ids?.length)
62
+ request.SelectionCriteria = { Ids: apiIds(params.set_ids) };
63
+ const page = buildPage(params);
64
+ if (page)
65
+ request.Page = page;
66
+ return formatResult(await apiPost("negativekeywordsharedsets", "get", request), NO_MONEY);
67
+ }
68
+ function buildUpdateItem(set) {
69
+ if (set.name === undefined && set.negative_keywords === undefined) {
70
+ throw new Error("Для каждого update_sets укажите name и/или negative_keywords.");
71
+ }
72
+ const item = { Id: apiId(set.set_id) };
73
+ if (set.name !== undefined)
74
+ item.Name = set.name;
75
+ if (set.negative_keywords !== undefined)
76
+ item.NegativeKeywords = set.negative_keywords;
77
+ return item;
78
+ }
79
+ export async function handleManageNegativeKeywordSharedSets(params) {
80
+ if (params.action === "add") {
81
+ if (!params.add_sets?.length)
82
+ throw new Error("Для action=add передайте add_sets.");
83
+ const data = await apiPost("negativekeywordsharedsets", "add", {
84
+ NegativeKeywordSharedSets: params.add_sets.map((set) => ({
85
+ Name: set.name,
86
+ NegativeKeywords: set.negative_keywords
87
+ }))
88
+ });
89
+ return formatResult(data, NO_MONEY);
90
+ }
91
+ if (params.action === "update") {
92
+ if (!params.update_sets?.length)
93
+ throw new Error("Для action=update передайте update_sets.");
94
+ const data = await apiPost("negativekeywordsharedsets", "update", {
95
+ NegativeKeywordSharedSets: params.update_sets.map(buildUpdateItem)
96
+ });
97
+ return formatResult(data, NO_MONEY);
98
+ }
99
+ if (!params.set_ids?.length)
100
+ throw new Error("Для action=delete передайте set_ids.");
101
+ const data = await apiPost("negativekeywordsharedsets", "delete", {
102
+ SelectionCriteria: { Ids: apiIds(params.set_ids) }
103
+ });
104
+ return formatResult(data, NO_MONEY);
105
+ }
106
+ // Типы читаются одним вызовом на все кампании сразу, а не по одной: имя объекта настроек
107
+ // зависит от типа, но сам запрос от этого не дробится.
108
+ async function readCampaignTypes(campaignIds) {
109
+ const data = await apiPost("campaigns", "get", {
110
+ SelectionCriteria: { Ids: apiIds(campaignIds) },
111
+ FieldNames: ["Id", "Type"]
112
+ });
113
+ const campaigns = data.result?.Campaigns ?? [];
114
+ return new Map(campaigns.map((campaign) => [String(campaign.Id), campaign.Type]));
115
+ }
116
+ // Привязка наборов перезаписывается целиком: список, отправленный здесь, становится
117
+ // единственным для объекта. Кампании и группы — два разных вызова API, и оба выполняются,
118
+ // если заданы оба: ответы форматируются отдельно, чтобы per-item ошибки не потерялись.
119
+ export async function handleLinkNegativeKeywordSets(params) {
120
+ const sharedSetIds = itemsOrNull(apiIds(params.set_ids));
121
+ const sections = [];
122
+ if (params.campaign_ids?.length) {
123
+ const types = await readCampaignTypes(params.campaign_ids);
124
+ const campaigns = params.campaign_ids.map((campaignId) => buildCampaignLink(campaignId, types.get(campaignId), sharedSetIds));
125
+ sections.push(formatResult(await apiPost("campaigns", "update", { Campaigns: campaigns }), NO_MONEY));
126
+ }
127
+ if (params.ad_group_ids?.length) {
128
+ const adGroups = params.ad_group_ids.map((adGroupId) => ({
129
+ Id: apiId(adGroupId),
130
+ NegativeKeywordSharedSetIds: sharedSetIds
131
+ }));
132
+ sections.push(formatResult(await apiPost("adgroups", "update", { AdGroups: adGroups }), NO_MONEY));
133
+ }
134
+ return sections.join("\n\n");
135
+ }
@@ -0,0 +1,2 @@
1
+ export type NegativeKeywordsMode = "replace" | "add" | "remove";
2
+ export declare function mergeNegativeKeywords(existing: string[], incoming: string[], mode: NegativeKeywordsMode): string[];
@@ -0,0 +1,36 @@
1
+ // Слияние минус-фраз для режимов add и remove. Директ перезаписывает
2
+ // NegativeKeywords.Items целиком, поэтому объединять обязан сценарий — а здесь это
3
+ // чистая функция, чтобы правило сравнения проверялось обычным тестом, а не двумя
4
+ // подменами fetch подряд.
5
+ // Сравнивать надо ровно так, как сравнивает Директ: строже — remove не найдёт фразу и
6
+ // вернёт успех, ничего не удалив; слабее — add допишет дубль, который Директ отбросит.
7
+ // Тождественны для него: регистр («СКИДКА» ≡ «скидка»), ё и е («дёшево» ≡ «дешево»),
8
+ // краевые и повторные пробелы («весло двойное» ≡ «весло двойное»), операторы
9
+ // закрепления «!» и «+» («!стекло» ≡ «стекло», «+ремонт» ≡ «ремонт»).
10
+ //
11
+ // Смысл операторов описан в справке (keywords/symbols-and-operators): «+» и «!» фиксируют
12
+ // стоп-слова и словоформу. Чего в справке нет — что Директ приписывает «!» сам: присланное
13
+ // «своими руками» хранится как «!своими руками». Не игнорируй мы оператор, пользователь не
14
+ // смог бы удалить фразу, которую сам же и написал. Это и равенство пар выше подтверждены
15
+ // пробоем 05.09.2026 на кампании-полигоне; сетевой тест рядом держит их проверенными.
16
+ const normalize = (keyword) => keyword.toLowerCase().replaceAll("ё", "е").replaceAll(/[!+]/g, "").replaceAll(/\s+/g, " ").trim();
17
+ export function mergeNegativeKeywords(existing, incoming, mode) {
18
+ if (mode === "replace")
19
+ return incoming;
20
+ const incomingKeys = new Set(incoming.map(normalize));
21
+ if (mode === "remove")
22
+ return existing.filter((keyword) => !incomingKeys.has(normalize(keyword)));
23
+ // add: написание уже сохранённых фраз не трогаем, новые дописываем в конец. Порядок —
24
+ // наш внутренний, наружу он не доживает: Директ хранит список отсортированным по
25
+ // кодовым точкам и при схлопывании дубля оставляет написание первого вхождения.
26
+ const merged = [...existing];
27
+ const seen = new Set(existing.map(normalize));
28
+ for (const keyword of incoming) {
29
+ const key = normalize(keyword);
30
+ if (seen.has(key))
31
+ continue;
32
+ seen.add(key);
33
+ merged.push(keyword.trim());
34
+ }
35
+ return merged;
36
+ }
@@ -0,0 +1,37 @@
1
+ import { z } from "zod";
2
+ export declare const getCampaignNegativeKeywordsSchema: z.ZodObject<{
3
+ campaign_ids: z.ZodArray<z.ZodString>;
4
+ }, z.core.$strip>;
5
+ export declare const setCampaignNegativeKeywordsSchema: z.ZodObject<{
6
+ campaign_id: z.ZodString;
7
+ negative_keywords: z.ZodArray<z.ZodString>;
8
+ mode: z.ZodLiteral<"add" | "remove" | "replace">;
9
+ }, z.core.$strip>;
10
+ export declare const setAdGroupNegativeKeywordsSchema: z.ZodObject<{
11
+ ad_group_id: z.ZodString;
12
+ negative_keywords: z.ZodArray<z.ZodString>;
13
+ mode: z.ZodLiteral<"add" | "remove" | "replace">;
14
+ }, z.core.$strip>;
15
+ export declare const listNegativeKeywordSharedSetsSchema: z.ZodObject<{
16
+ limit: z.ZodOptional<z.ZodInt>;
17
+ offset: z.ZodOptional<z.ZodInt>;
18
+ set_ids: z.ZodOptional<z.ZodArray<z.ZodString>>;
19
+ }, z.core.$strip>;
20
+ export declare const manageNegativeKeywordSharedSetsSchema: z.ZodObject<{
21
+ action: z.ZodLiteral<"add" | "delete" | "update">;
22
+ add_sets: z.ZodOptional<z.ZodArray<z.ZodObject<{
23
+ name: z.ZodString;
24
+ negative_keywords: z.ZodArray<z.ZodString>;
25
+ }, z.core.$strip>>>;
26
+ update_sets: z.ZodOptional<z.ZodArray<z.ZodObject<{
27
+ set_id: z.ZodString;
28
+ name: z.ZodOptional<z.ZodString>;
29
+ negative_keywords: z.ZodOptional<z.ZodArray<z.ZodString>>;
30
+ }, z.core.$strip>>>;
31
+ set_ids: z.ZodOptional<z.ZodArray<z.ZodString>>;
32
+ }, z.core.$strip>;
33
+ export declare const linkNegativeKeywordSetsSchema: z.ZodObject<{
34
+ ad_group_ids: z.ZodOptional<z.ZodArray<z.ZodString>>;
35
+ campaign_ids: z.ZodOptional<z.ZodArray<z.ZodString>>;
36
+ set_ids: z.ZodArray<z.ZodString>;
37
+ }, z.core.$strip>;