@optima-chat/ads-cli 0.5.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 (182) hide show
  1. package/.claude/CLAUDE.md +279 -0
  2. package/dist/commands/account/check.d.ts +6 -0
  3. package/dist/commands/account/check.d.ts.map +1 -0
  4. package/dist/commands/account/check.js +179 -0
  5. package/dist/commands/account/check.js.map +1 -0
  6. package/dist/commands/account/create.d.ts +6 -0
  7. package/dist/commands/account/create.d.ts.map +1 -0
  8. package/dist/commands/account/create.js +172 -0
  9. package/dist/commands/account/create.js.map +1 -0
  10. package/dist/commands/account/index.d.ts +6 -0
  11. package/dist/commands/account/index.d.ts.map +1 -0
  12. package/dist/commands/account/index.js +11 -0
  13. package/dist/commands/account/index.js.map +1 -0
  14. package/dist/commands/ad/create.d.ts +6 -0
  15. package/dist/commands/ad/create.d.ts.map +1 -0
  16. package/dist/commands/ad/create.js +77 -0
  17. package/dist/commands/ad/create.js.map +1 -0
  18. package/dist/commands/ad/delete.d.ts +6 -0
  19. package/dist/commands/ad/delete.d.ts.map +1 -0
  20. package/dist/commands/ad/delete.js +50 -0
  21. package/dist/commands/ad/delete.js.map +1 -0
  22. package/dist/commands/ad/index.d.ts +6 -0
  23. package/dist/commands/ad/index.d.ts.map +1 -0
  24. package/dist/commands/ad/index.js +17 -0
  25. package/dist/commands/ad/index.js.map +1 -0
  26. package/dist/commands/ad/info.d.ts +6 -0
  27. package/dist/commands/ad/info.d.ts.map +1 -0
  28. package/dist/commands/ad/info.js +90 -0
  29. package/dist/commands/ad/info.js.map +1 -0
  30. package/dist/commands/ad/list.d.ts +6 -0
  31. package/dist/commands/ad/list.d.ts.map +1 -0
  32. package/dist/commands/ad/list.js +94 -0
  33. package/dist/commands/ad/list.js.map +1 -0
  34. package/dist/commands/ad/update.d.ts +6 -0
  35. package/dist/commands/ad/update.d.ts.map +1 -0
  36. package/dist/commands/ad/update.js +42 -0
  37. package/dist/commands/ad/update.js.map +1 -0
  38. package/dist/commands/ad-group/create.d.ts +6 -0
  39. package/dist/commands/ad-group/create.d.ts.map +1 -0
  40. package/dist/commands/ad-group/create.js +59 -0
  41. package/dist/commands/ad-group/create.js.map +1 -0
  42. package/dist/commands/ad-group/delete.d.ts +6 -0
  43. package/dist/commands/ad-group/delete.d.ts.map +1 -0
  44. package/dist/commands/ad-group/delete.js +49 -0
  45. package/dist/commands/ad-group/delete.js.map +1 -0
  46. package/dist/commands/ad-group/index.d.ts +6 -0
  47. package/dist/commands/ad-group/index.d.ts.map +1 -0
  48. package/dist/commands/ad-group/index.js +17 -0
  49. package/dist/commands/ad-group/index.js.map +1 -0
  50. package/dist/commands/ad-group/info.d.ts +6 -0
  51. package/dist/commands/ad-group/info.d.ts.map +1 -0
  52. package/dist/commands/ad-group/info.js +59 -0
  53. package/dist/commands/ad-group/info.js.map +1 -0
  54. package/dist/commands/ad-group/list.d.ts +6 -0
  55. package/dist/commands/ad-group/list.d.ts.map +1 -0
  56. package/dist/commands/ad-group/list.js +83 -0
  57. package/dist/commands/ad-group/list.js.map +1 -0
  58. package/dist/commands/ad-group/update.d.ts +6 -0
  59. package/dist/commands/ad-group/update.d.ts.map +1 -0
  60. package/dist/commands/ad-group/update.js +56 -0
  61. package/dist/commands/ad-group/update.js.map +1 -0
  62. package/dist/commands/auth/index.d.ts +6 -0
  63. package/dist/commands/auth/index.d.ts.map +1 -0
  64. package/dist/commands/auth/index.js +13 -0
  65. package/dist/commands/auth/index.js.map +1 -0
  66. package/dist/commands/auth/login.d.ts +10 -0
  67. package/dist/commands/auth/login.d.ts.map +1 -0
  68. package/dist/commands/auth/login.js +154 -0
  69. package/dist/commands/auth/login.js.map +1 -0
  70. package/dist/commands/auth/logout.d.ts +6 -0
  71. package/dist/commands/auth/logout.d.ts.map +1 -0
  72. package/dist/commands/auth/logout.js +28 -0
  73. package/dist/commands/auth/logout.js.map +1 -0
  74. package/dist/commands/auth/status.d.ts +6 -0
  75. package/dist/commands/auth/status.d.ts.map +1 -0
  76. package/dist/commands/auth/status.js +47 -0
  77. package/dist/commands/auth/status.js.map +1 -0
  78. package/dist/commands/campaign/create.d.ts +6 -0
  79. package/dist/commands/campaign/create.d.ts.map +1 -0
  80. package/dist/commands/campaign/create.js +58 -0
  81. package/dist/commands/campaign/create.js.map +1 -0
  82. package/dist/commands/campaign/delete.d.ts +6 -0
  83. package/dist/commands/campaign/delete.d.ts.map +1 -0
  84. package/dist/commands/campaign/delete.js +49 -0
  85. package/dist/commands/campaign/delete.js.map +1 -0
  86. package/dist/commands/campaign/index.d.ts +6 -0
  87. package/dist/commands/campaign/index.d.ts.map +1 -0
  88. package/dist/commands/campaign/index.js +17 -0
  89. package/dist/commands/campaign/index.js.map +1 -0
  90. package/dist/commands/campaign/info.d.ts +6 -0
  91. package/dist/commands/campaign/info.d.ts.map +1 -0
  92. package/dist/commands/campaign/info.js +57 -0
  93. package/dist/commands/campaign/info.js.map +1 -0
  94. package/dist/commands/campaign/list.d.ts +6 -0
  95. package/dist/commands/campaign/list.d.ts.map +1 -0
  96. package/dist/commands/campaign/list.js +71 -0
  97. package/dist/commands/campaign/list.js.map +1 -0
  98. package/dist/commands/campaign/update.d.ts +6 -0
  99. package/dist/commands/campaign/update.d.ts.map +1 -0
  100. package/dist/commands/campaign/update.js +56 -0
  101. package/dist/commands/campaign/update.js.map +1 -0
  102. package/dist/commands/config.d.ts +6 -0
  103. package/dist/commands/config.d.ts.map +1 -0
  104. package/dist/commands/config.js +104 -0
  105. package/dist/commands/config.js.map +1 -0
  106. package/dist/commands/keyword/add.d.ts +6 -0
  107. package/dist/commands/keyword/add.d.ts.map +1 -0
  108. package/dist/commands/keyword/add.js +47 -0
  109. package/dist/commands/keyword/add.js.map +1 -0
  110. package/dist/commands/keyword/delete.d.ts +6 -0
  111. package/dist/commands/keyword/delete.d.ts.map +1 -0
  112. package/dist/commands/keyword/delete.js +50 -0
  113. package/dist/commands/keyword/delete.js.map +1 -0
  114. package/dist/commands/keyword/index.d.ts +6 -0
  115. package/dist/commands/keyword/index.d.ts.map +1 -0
  116. package/dist/commands/keyword/index.js +15 -0
  117. package/dist/commands/keyword/index.js.map +1 -0
  118. package/dist/commands/keyword/list.d.ts +6 -0
  119. package/dist/commands/keyword/list.d.ts.map +1 -0
  120. package/dist/commands/keyword/list.js +77 -0
  121. package/dist/commands/keyword/list.js.map +1 -0
  122. package/dist/commands/keyword/update.d.ts +6 -0
  123. package/dist/commands/keyword/update.d.ts.map +1 -0
  124. package/dist/commands/keyword/update.js +51 -0
  125. package/dist/commands/keyword/update.js.map +1 -0
  126. package/dist/commands/query.d.ts +6 -0
  127. package/dist/commands/query.d.ts.map +1 -0
  128. package/dist/commands/query.js +71 -0
  129. package/dist/commands/query.js.map +1 -0
  130. package/dist/config.d.ts +32 -0
  131. package/dist/config.d.ts.map +1 -0
  132. package/dist/config.js +10 -0
  133. package/dist/config.js.map +1 -0
  134. package/dist/index.d.ts +7 -0
  135. package/dist/index.d.ts.map +1 -0
  136. package/dist/index.js +75 -0
  137. package/dist/index.js.map +1 -0
  138. package/dist/lib/api-client.d.ts +100 -0
  139. package/dist/lib/api-client.d.ts.map +1 -0
  140. package/dist/lib/api-client.js +186 -0
  141. package/dist/lib/api-client.js.map +1 -0
  142. package/dist/lib/callback-server.d.ts +32 -0
  143. package/dist/lib/callback-server.d.ts.map +1 -0
  144. package/dist/lib/callback-server.js +132 -0
  145. package/dist/lib/callback-server.js.map +1 -0
  146. package/dist/lib/google-ads-client.d.ts +177 -0
  147. package/dist/lib/google-ads-client.d.ts.map +1 -0
  148. package/dist/lib/google-ads-client.js +826 -0
  149. package/dist/lib/google-ads-client.js.map +1 -0
  150. package/dist/lib/oauth2-manager.d.ts +27 -0
  151. package/dist/lib/oauth2-manager.d.ts.map +1 -0
  152. package/dist/lib/oauth2-manager.js +123 -0
  153. package/dist/lib/oauth2-manager.js.map +1 -0
  154. package/dist/lib/token-store.d.ts +43 -0
  155. package/dist/lib/token-store.d.ts.map +1 -0
  156. package/dist/lib/token-store.js +103 -0
  157. package/dist/lib/token-store.js.map +1 -0
  158. package/dist/utils/config.d.ts +9 -0
  159. package/dist/utils/config.d.ts.map +1 -0
  160. package/dist/utils/config.js +34 -0
  161. package/dist/utils/config.js.map +1 -0
  162. package/dist/utils/customer-id.d.ts +10 -0
  163. package/dist/utils/customer-id.d.ts.map +1 -0
  164. package/dist/utils/customer-id.js +29 -0
  165. package/dist/utils/customer-id.js.map +1 -0
  166. package/dist/utils/env-updater.d.ts +8 -0
  167. package/dist/utils/env-updater.d.ts.map +1 -0
  168. package/dist/utils/env-updater.js +45 -0
  169. package/dist/utils/env-updater.js.map +1 -0
  170. package/dist/utils/errors.d.ts +32 -0
  171. package/dist/utils/errors.d.ts.map +1 -0
  172. package/dist/utils/errors.js +68 -0
  173. package/dist/utils/errors.js.map +1 -0
  174. package/dist/utils/logger.d.ts +10 -0
  175. package/dist/utils/logger.d.ts.map +1 -0
  176. package/dist/utils/logger.js +25 -0
  177. package/dist/utils/logger.js.map +1 -0
  178. package/dist/utils/optima-config.d.ts +9 -0
  179. package/dist/utils/optima-config.d.ts.map +1 -0
  180. package/dist/utils/optima-config.js +83 -0
  181. package/dist/utils/optima-config.js.map +1 -0
  182. package/package.json +67 -0
@@ -0,0 +1,826 @@
1
+ /**
2
+ * Google Ads Client - 封装 Google Ads API
3
+ */
4
+ import { GoogleAdsApi, enums, toMicros } from 'google-ads-api';
5
+ import { loadConfig } from '../utils/config.js';
6
+ import { OAuth2Manager } from './oauth2-manager.js';
7
+ import { GoogleAdsError } from '../utils/errors.js';
8
+ /**
9
+ * Google Ads Client 封装类
10
+ */
11
+ export class GoogleAdsClient {
12
+ api;
13
+ oauth;
14
+ config;
15
+ constructor() {
16
+ this.config = loadConfig();
17
+ this.oauth = new OAuth2Manager();
18
+ // 初始化 Google Ads API
19
+ this.api = new GoogleAdsApi({
20
+ client_id: this.config.clientId,
21
+ client_secret: this.config.clientSecret,
22
+ developer_token: this.config.developerToken,
23
+ });
24
+ }
25
+ /**
26
+ * 获取 Customer 对象(带认证)
27
+ */
28
+ async getCustomer(customerId) {
29
+ try {
30
+ // 获取 refresh token
31
+ const refreshToken = await this.oauth.getRefreshToken();
32
+ // 创建 Customer 对象
33
+ const customer = this.api.Customer({
34
+ customer_id: customerId,
35
+ refresh_token: refreshToken,
36
+ login_customer_id: this.config.loginCustomerId,
37
+ });
38
+ return customer;
39
+ }
40
+ catch (error) {
41
+ throw new GoogleAdsError(`无法连接到客户账号 ${customerId}: ${error.message}`, error);
42
+ }
43
+ }
44
+ /**
45
+ * 执行 GAQL 查询
46
+ */
47
+ async query(customerId, gaqlQuery) {
48
+ try {
49
+ const customer = await this.getCustomer(customerId);
50
+ const results = await customer.query(gaqlQuery);
51
+ return results;
52
+ }
53
+ catch (error) {
54
+ const errorMessage = error.message
55
+ || error.details
56
+ || (error.errors && JSON.stringify(error.errors))
57
+ || JSON.stringify(error);
58
+ throw new GoogleAdsError(`查询失败: ${errorMessage}`, error);
59
+ }
60
+ }
61
+ /**
62
+ * 获取客户账号信息
63
+ */
64
+ async getCustomerInfo(customerId) {
65
+ const query = `
66
+ SELECT
67
+ customer.id,
68
+ customer.descriptive_name,
69
+ customer.currency_code,
70
+ customer.time_zone,
71
+ customer.manager,
72
+ customer.test_account,
73
+ customer.auto_tagging_enabled,
74
+ customer.tracking_url_template
75
+ FROM customer
76
+ WHERE customer.id = ${customerId}
77
+ `;
78
+ const results = await this.query(customerId, query);
79
+ return results[0]?.customer || null;
80
+ }
81
+ /**
82
+ * 列出可访问的客户账号
83
+ */
84
+ async listAccessibleCustomers() {
85
+ try {
86
+ const refreshToken = await this.oauth.getRefreshToken();
87
+ // 使用 client.listAccessibleCustomers() 方法
88
+ const response = await this.api.listAccessibleCustomers(refreshToken);
89
+ // response 是 ListAccessibleCustomersResponse 对象,包含 resource_names 数组
90
+ const resourceNames = response.resource_names || [];
91
+ // 提取 customer ID
92
+ const customerIds = resourceNames
93
+ .map((name) => {
94
+ const match = name.match(/customers\/(\d+)/);
95
+ return match ? match[1] : null;
96
+ })
97
+ .filter((id) => id !== null);
98
+ return customerIds;
99
+ }
100
+ catch (error) {
101
+ throw new GoogleAdsError(`获取可访问账号列表失败: ${error.message}`, error);
102
+ }
103
+ }
104
+ /**
105
+ * 列出广告系列
106
+ */
107
+ async listCampaigns(customerId, options) {
108
+ let query = `
109
+ SELECT
110
+ campaign.id,
111
+ campaign.name,
112
+ campaign.status,
113
+ campaign.advertising_channel_type,
114
+ campaign.bidding_strategy_type,
115
+ campaign.campaign_budget,
116
+ campaign.start_date,
117
+ campaign.end_date,
118
+ metrics.impressions,
119
+ metrics.clicks,
120
+ metrics.cost_micros,
121
+ metrics.conversions
122
+ FROM campaign
123
+ `;
124
+ const conditions = [];
125
+ if (options?.status) {
126
+ conditions.push(`campaign.status = '${options.status}'`);
127
+ }
128
+ if (conditions.length > 0) {
129
+ query += ' WHERE ' + conditions.join(' AND ');
130
+ }
131
+ query += ' ORDER BY campaign.id';
132
+ if (options?.limit) {
133
+ query += ` LIMIT ${options.limit}`;
134
+ }
135
+ return await this.query(customerId, query);
136
+ }
137
+ /**
138
+ * 获取广告系列详情
139
+ */
140
+ async getCampaign(customerId, campaignId) {
141
+ const query = `
142
+ SELECT
143
+ campaign.id,
144
+ campaign.name,
145
+ campaign.status,
146
+ campaign.advertising_channel_type,
147
+ campaign.bidding_strategy_type,
148
+ campaign.campaign_budget,
149
+ campaign.start_date,
150
+ campaign.end_date,
151
+ campaign.target_cpa.target_cpa_micros,
152
+ campaign.target_roas.target_roas,
153
+ campaign.optimization_score,
154
+ metrics.impressions,
155
+ metrics.clicks,
156
+ metrics.cost_micros,
157
+ metrics.conversions,
158
+ metrics.conversions_value,
159
+ metrics.average_cpc,
160
+ metrics.average_cpm,
161
+ metrics.ctr
162
+ FROM campaign
163
+ WHERE campaign.id = ${campaignId}
164
+ `;
165
+ const results = await this.query(customerId, query);
166
+ return results[0] || null;
167
+ }
168
+ /**
169
+ * 列出广告组
170
+ */
171
+ async listAdGroups(customerId, campaignId, options) {
172
+ let query = `
173
+ SELECT
174
+ ad_group.id,
175
+ ad_group.name,
176
+ ad_group.status,
177
+ ad_group.type,
178
+ ad_group.cpc_bid_micros,
179
+ campaign.id,
180
+ campaign.name,
181
+ metrics.impressions,
182
+ metrics.clicks,
183
+ metrics.cost_micros,
184
+ metrics.conversions,
185
+ metrics.ctr
186
+ FROM ad_group
187
+ `;
188
+ const conditions = [];
189
+ if (campaignId) {
190
+ conditions.push(`campaign.id = ${campaignId}`);
191
+ }
192
+ if (options?.status) {
193
+ conditions.push(`ad_group.status = '${options.status}'`);
194
+ }
195
+ else {
196
+ // 默认不显示已删除的广告组
197
+ conditions.push(`ad_group.status != 'REMOVED'`);
198
+ }
199
+ if (conditions.length > 0) {
200
+ query += ' WHERE ' + conditions.join(' AND ');
201
+ }
202
+ query += ` LIMIT ${options?.limit || 100}`;
203
+ return this.query(customerId, query);
204
+ }
205
+ /**
206
+ * 获取广告组详情
207
+ */
208
+ async getAdGroup(customerId, adGroupId) {
209
+ const query = `
210
+ SELECT
211
+ ad_group.id,
212
+ ad_group.name,
213
+ ad_group.status,
214
+ ad_group.type,
215
+ ad_group.cpc_bid_micros,
216
+ campaign.id,
217
+ campaign.name,
218
+ metrics.impressions,
219
+ metrics.clicks,
220
+ metrics.cost_micros,
221
+ metrics.conversions
222
+ FROM ad_group
223
+ WHERE ad_group.id = ${adGroupId}
224
+ `;
225
+ const results = await this.query(customerId, query);
226
+ return results[0] || null;
227
+ }
228
+ /**
229
+ * 更新广告组
230
+ */
231
+ async updateAdGroup(customerId, adGroupId, updateData) {
232
+ try {
233
+ const customer = await this.getCustomer(customerId);
234
+ const resourceName = `customers/${customerId}/adGroups/${adGroupId}`;
235
+ const resource = { resource_name: resourceName };
236
+ if (updateData.status) {
237
+ resource.status = updateData.status;
238
+ }
239
+ if (updateData.name) {
240
+ resource.name = updateData.name;
241
+ }
242
+ if (updateData.cpcBidMicros) {
243
+ resource.cpc_bid_micros = updateData.cpcBidMicros;
244
+ }
245
+ const operations = [
246
+ {
247
+ entity: 'ad_group',
248
+ operation: 'update',
249
+ resource: resource,
250
+ },
251
+ ];
252
+ const result = await customer.mutateResources(operations);
253
+ return result;
254
+ }
255
+ catch (error) {
256
+ const errorMessage = error.message || error.details || (error.errors && JSON.stringify(error.errors)) || JSON.stringify(error);
257
+ throw new GoogleAdsError(`更新广告组失败: ${errorMessage}`, error);
258
+ }
259
+ }
260
+ /**
261
+ * 列出关键词
262
+ */
263
+ async listKeywords(customerId, campaignId, options) {
264
+ let query = `
265
+ SELECT
266
+ ad_group_criterion.criterion_id,
267
+ ad_group_criterion.keyword.text,
268
+ ad_group_criterion.keyword.match_type,
269
+ ad_group_criterion.status,
270
+ ad_group_criterion.negative,
271
+ ad_group_criterion.quality_info.quality_score,
272
+ ad_group.id,
273
+ ad_group.name,
274
+ campaign.id,
275
+ campaign.name,
276
+ metrics.impressions,
277
+ metrics.clicks,
278
+ metrics.cost_micros,
279
+ metrics.conversions,
280
+ metrics.ctr,
281
+ metrics.average_cpc
282
+ FROM keyword_view
283
+ `;
284
+ const conditions = [];
285
+ if (campaignId) {
286
+ conditions.push(`campaign.id = ${campaignId}`);
287
+ }
288
+ if (options?.status) {
289
+ conditions.push(`ad_group_criterion.status = '${options.status}'`);
290
+ }
291
+ if (conditions.length > 0) {
292
+ query += ' WHERE ' + conditions.join(' AND ');
293
+ }
294
+ query += ' ORDER BY metrics.impressions DESC';
295
+ if (options?.limit) {
296
+ query += ` LIMIT ${options.limit}`;
297
+ }
298
+ return await this.query(customerId, query);
299
+ }
300
+ /**
301
+ * 获取效果数据
302
+ */
303
+ async getPerformanceMetrics(customerId, options) {
304
+ const resourceMap = {
305
+ campaign: 'campaign',
306
+ ad_group: 'ad_group',
307
+ keyword: 'keyword_view',
308
+ ad: 'ad_group_ad',
309
+ };
310
+ const resource = resourceMap[options.level];
311
+ let query = `SELECT `;
312
+ // 添加基础字段
313
+ const fields = [];
314
+ if (options.level === 'campaign') {
315
+ fields.push('campaign.id', 'campaign.name', 'campaign.status');
316
+ }
317
+ else if (options.level === 'ad_group') {
318
+ fields.push('campaign.id', 'campaign.name', 'ad_group.id', 'ad_group.name', 'ad_group.status');
319
+ }
320
+ else if (options.level === 'keyword') {
321
+ fields.push('campaign.id', 'campaign.name', 'ad_group.id', 'ad_group.name', 'ad_group_criterion.criterion_id', 'ad_group_criterion.keyword.text', 'ad_group_criterion.keyword.match_type');
322
+ }
323
+ else if (options.level === 'ad') {
324
+ fields.push('campaign.id', 'campaign.name', 'ad_group.id', 'ad_group.name', 'ad_group_ad.ad.id', 'ad_group_ad.ad.type');
325
+ }
326
+ // 添加指标
327
+ fields.push('metrics.impressions', 'metrics.clicks', 'metrics.cost_micros', 'metrics.conversions', 'metrics.conversions_value', 'metrics.ctr', 'metrics.average_cpc', 'metrics.average_cpm', 'metrics.cost_per_conversion');
328
+ // 如果有日期范围,添加日期字段
329
+ if (options.dateRange) {
330
+ fields.push('segments.date');
331
+ }
332
+ query += fields.join(', ') + ` FROM ${resource}`;
333
+ // 添加条件
334
+ const conditions = [];
335
+ if (options.campaignId) {
336
+ conditions.push(`campaign.id = ${options.campaignId}`);
337
+ }
338
+ if (options.dateRange) {
339
+ conditions.push(`segments.date DURING ${options.dateRange}`);
340
+ }
341
+ if (conditions.length > 0) {
342
+ query += ' WHERE ' + conditions.join(' AND ');
343
+ }
344
+ query += ' ORDER BY metrics.impressions DESC';
345
+ if (options.limit) {
346
+ query += ` LIMIT ${options.limit}`;
347
+ }
348
+ return await this.query(customerId, query);
349
+ }
350
+ /**
351
+ * 创建广告系列
352
+ *
353
+ * 使用 mutateResources API 批量创建预算和广告系列
354
+ */
355
+ async createCampaign(customerId, campaignData) {
356
+ try {
357
+ const customer = await this.getCustomer(customerId);
358
+ // 创建临时资源名称
359
+ const budgetResourceName = `customers/${customerId}/campaignBudgets/-1`;
360
+ const operations = [
361
+ // 创建预算
362
+ {
363
+ entity: 'campaign_budget',
364
+ operation: 'create',
365
+ resource: {
366
+ resource_name: budgetResourceName,
367
+ name: `${campaignData.name} Budget`,
368
+ delivery_method: enums.BudgetDeliveryMethod.STANDARD,
369
+ amount_micros: campaignData.budget_amount_micros,
370
+ },
371
+ },
372
+ // 创建广告系列
373
+ {
374
+ entity: 'campaign',
375
+ operation: 'create',
376
+ resource: {
377
+ name: campaignData.name,
378
+ advertising_channel_type: campaignData.advertising_channel_type || enums.AdvertisingChannelType.SEARCH,
379
+ status: campaignData.status || enums.CampaignStatus.PAUSED,
380
+ campaign_budget: budgetResourceName,
381
+ manual_cpc: {
382
+ enhanced_cpc_enabled: false,
383
+ },
384
+ // EU 政治广告合规声明(必需字段,从 2025 年 9 月开始强制要求)
385
+ contains_eu_political_advertising: enums.EuPoliticalAdvertisingStatus.DOES_NOT_CONTAIN_EU_POLITICAL_ADVERTISING,
386
+ network_settings: {
387
+ target_google_search: true,
388
+ },
389
+ },
390
+ },
391
+ ];
392
+ const result = await customer.mutateResources(operations);
393
+ return result;
394
+ }
395
+ catch (error) {
396
+ // 提取 Google Ads API 错误详情
397
+ let errorMessage = error.message || 'Unknown error';
398
+ if (error.errors && error.errors.length > 0) {
399
+ const firstError = error.errors[0];
400
+ console.error('Google Ads API Error Details:', JSON.stringify(firstError, null, 2));
401
+ if (firstError.message) {
402
+ errorMessage = firstError.message;
403
+ }
404
+ else if (firstError.error_code) {
405
+ errorMessage = `Error code: ${JSON.stringify(firstError.error_code)}`;
406
+ }
407
+ }
408
+ throw new GoogleAdsError(`创建广告系列失败: ${errorMessage}`, error);
409
+ }
410
+ }
411
+ /**
412
+ * 创建广告组
413
+ */
414
+ async createAdGroup(customerId, adGroupData) {
415
+ try {
416
+ const customer = await this.getCustomer(customerId);
417
+ const campaignResourceName = `customers/${customerId}/campaigns/${adGroupData.campaign_id}`;
418
+ const operations = [
419
+ {
420
+ entity: 'ad_group',
421
+ operation: 'create',
422
+ resource: {
423
+ name: adGroupData.name,
424
+ campaign: campaignResourceName,
425
+ status: adGroupData.status || enums.AdGroupStatus.ENABLED,
426
+ cpc_bid_micros: adGroupData.cpc_bid_micros || toMicros(1), // 默认 $1
427
+ },
428
+ },
429
+ ];
430
+ const result = await customer.mutateResources(operations);
431
+ return result;
432
+ }
433
+ catch (error) {
434
+ throw new GoogleAdsError(`创建广告组失败: ${error.message}`, error);
435
+ }
436
+ }
437
+ /**
438
+ * 添加关键词
439
+ */
440
+ async addKeywords(customerId, adGroupId, keywords) {
441
+ try {
442
+ const customer = await this.getCustomer(customerId);
443
+ const adGroupResourceName = `customers/${customerId}/adGroups/${adGroupId}`;
444
+ const operations = keywords.map((keyword) => ({
445
+ entity: 'ad_group_criterion',
446
+ operation: 'create',
447
+ resource: {
448
+ ad_group: adGroupResourceName,
449
+ keyword: {
450
+ text: keyword.text,
451
+ match_type: keyword.match_type || enums.KeywordMatchType.BROAD,
452
+ },
453
+ status: enums.AdGroupCriterionStatus.ENABLED,
454
+ },
455
+ }));
456
+ const result = await customer.mutateResources(operations);
457
+ return result;
458
+ }
459
+ catch (error) {
460
+ throw new GoogleAdsError(`添加关键词失败: ${error.message}`, error);
461
+ }
462
+ }
463
+ /**
464
+ * 删除广告系列
465
+ */
466
+ async deleteCampaign(customerId, campaignId) {
467
+ try {
468
+ const customer = await this.getCustomer(customerId);
469
+ const resourceName = `customers/${customerId}/campaigns/${campaignId}`;
470
+ const result = await customer.campaigns.remove([resourceName]);
471
+ return result;
472
+ }
473
+ catch (error) {
474
+ const errorMessage = error.message || error.details || (error.errors && JSON.stringify(error.errors)) || JSON.stringify(error);
475
+ throw new GoogleAdsError(`删除广告系列失败: ${errorMessage}`, error);
476
+ }
477
+ }
478
+ /**
479
+ * 删除广告组
480
+ */
481
+ async deleteAdGroup(customerId, adGroupId) {
482
+ try {
483
+ const customer = await this.getCustomer(customerId);
484
+ const resourceName = `customers/${customerId}/adGroups/${adGroupId}`;
485
+ const result = await customer.adGroups.remove([resourceName]);
486
+ return result;
487
+ }
488
+ catch (error) {
489
+ const errorMessage = error.message || error.details || (error.errors && JSON.stringify(error.errors)) || JSON.stringify(error);
490
+ throw new GoogleAdsError(`删除广告组失败: ${errorMessage}`, error);
491
+ }
492
+ }
493
+ /**
494
+ * 删除关键词
495
+ */
496
+ async deleteKeyword(customerId, adGroupId, criterionId) {
497
+ try {
498
+ const customer = await this.getCustomer(customerId);
499
+ const resourceName = `customers/${customerId}/adGroupCriteria/${adGroupId}~${criterionId}`;
500
+ const result = await customer.adGroupCriteria.remove([resourceName]);
501
+ return result;
502
+ }
503
+ catch (error) {
504
+ const errorMessage = error.message || error.details || (error.errors && JSON.stringify(error.errors)) || JSON.stringify(error);
505
+ throw new GoogleAdsError(`删除关键词失败: ${errorMessage}`, error);
506
+ }
507
+ }
508
+ /**
509
+ * 更新关键词(状态、出价)
510
+ */
511
+ async updateKeyword(customerId, adGroupId, criterionId, updateData) {
512
+ try {
513
+ const customer = await this.getCustomer(customerId);
514
+ const resourceName = `customers/${customerId}/adGroupCriteria/${adGroupId}~${criterionId}`;
515
+ const resource = { resource_name: resourceName };
516
+ if (updateData.status) {
517
+ resource.status = updateData.status;
518
+ }
519
+ if (updateData.cpcBidMicros) {
520
+ resource.cpc_bid_micros = updateData.cpcBidMicros;
521
+ }
522
+ const operations = [
523
+ {
524
+ entity: 'ad_group_criterion',
525
+ operation: 'update',
526
+ resource: resource,
527
+ },
528
+ ];
529
+ const result = await customer.mutateResources(operations);
530
+ return result;
531
+ }
532
+ catch (error) {
533
+ const errorMessage = error.message || error.details || (error.errors && JSON.stringify(error.errors)) || JSON.stringify(error);
534
+ throw new GoogleAdsError(`更新关键词失败: ${errorMessage}`, error);
535
+ }
536
+ }
537
+ /**
538
+ * 更新广告系列状态
539
+ */
540
+ async updateCampaignStatus(customerId, campaignId, status) {
541
+ return this.updateCampaign(customerId, campaignId, { status });
542
+ }
543
+ /**
544
+ * 更新广告系列(支持状态、名称、预算)
545
+ */
546
+ async updateCampaign(customerId, campaignId, updateData) {
547
+ try {
548
+ const customer = await this.getCustomer(customerId);
549
+ const resourceName = `customers/${customerId}/campaigns/${campaignId}`;
550
+ const resource = {};
551
+ if (updateData.status) {
552
+ resource.status = updateData.status;
553
+ }
554
+ if (updateData.name) {
555
+ resource.name = updateData.name;
556
+ }
557
+ // 如果需要更新预算,需要先获取当前广告系列的预算资源
558
+ if (updateData.budgetAmountMicros) {
559
+ // 首先获取广告系列的预算信息
560
+ const campaignQuery = `
561
+ SELECT campaign.campaign_budget
562
+ FROM campaign
563
+ WHERE campaign.id = ${campaignId}
564
+ `;
565
+ const campaignResult = await customer.query(campaignQuery);
566
+ if (campaignResult.length === 0) {
567
+ throw new GoogleAdsError('找不到广告系列');
568
+ }
569
+ const budgetResourceName = campaignResult[0].campaign?.campaign_budget;
570
+ if (budgetResourceName) {
571
+ // 更新预算
572
+ const budgetOperations = [
573
+ {
574
+ entity: 'campaign_budget',
575
+ operation: 'update',
576
+ resource: {
577
+ resource_name: budgetResourceName,
578
+ amount_micros: updateData.budgetAmountMicros,
579
+ },
580
+ },
581
+ ];
582
+ await customer.mutateResources(budgetOperations);
583
+ }
584
+ }
585
+ // 如果有状态或名称更新
586
+ if (Object.keys(resource).length > 0) {
587
+ resource.resource_name = resourceName;
588
+ const operations = [
589
+ {
590
+ entity: 'campaign',
591
+ operation: 'update',
592
+ resource: resource,
593
+ },
594
+ ];
595
+ const result = await customer.mutateResources(operations);
596
+ return result;
597
+ }
598
+ return { success: true };
599
+ }
600
+ catch (error) {
601
+ const errorMessage = error.message || error.details || (error.errors && JSON.stringify(error.errors)) || JSON.stringify(error);
602
+ throw new GoogleAdsError(`更新广告系列失败: ${errorMessage}`, error);
603
+ }
604
+ }
605
+ /**
606
+ * 列出广告
607
+ */
608
+ async listAds(customerId, options) {
609
+ let query = `
610
+ SELECT
611
+ ad_group_ad.ad.id,
612
+ ad_group_ad.ad.type,
613
+ ad_group_ad.ad.final_urls,
614
+ ad_group_ad.ad.responsive_search_ad.headlines,
615
+ ad_group_ad.ad.responsive_search_ad.descriptions,
616
+ ad_group_ad.ad.responsive_search_ad.path1,
617
+ ad_group_ad.ad.responsive_search_ad.path2,
618
+ ad_group_ad.status,
619
+ ad_group_ad.policy_summary.approval_status,
620
+ ad_group.id,
621
+ ad_group.name,
622
+ campaign.id,
623
+ campaign.name,
624
+ metrics.impressions,
625
+ metrics.clicks,
626
+ metrics.cost_micros,
627
+ metrics.conversions
628
+ FROM ad_group_ad
629
+ `;
630
+ const conditions = [];
631
+ if (options?.campaignId) {
632
+ conditions.push(`campaign.id = ${options.campaignId}`);
633
+ }
634
+ if (options?.adGroupId) {
635
+ conditions.push(`ad_group.id = ${options.adGroupId}`);
636
+ }
637
+ if (options?.status) {
638
+ conditions.push(`ad_group_ad.status = '${options.status}'`);
639
+ }
640
+ else {
641
+ conditions.push(`ad_group_ad.status != 'REMOVED'`);
642
+ }
643
+ if (conditions.length > 0) {
644
+ query += ' WHERE ' + conditions.join(' AND ');
645
+ }
646
+ query += ` LIMIT ${options?.limit || 100}`;
647
+ return this.query(customerId, query);
648
+ }
649
+ /**
650
+ * 获取广告详情
651
+ */
652
+ async getAd(customerId, adGroupId, adId) {
653
+ const query = `
654
+ SELECT
655
+ ad_group_ad.ad.id,
656
+ ad_group_ad.ad.type,
657
+ ad_group_ad.ad.final_urls,
658
+ ad_group_ad.ad.responsive_search_ad.headlines,
659
+ ad_group_ad.ad.responsive_search_ad.descriptions,
660
+ ad_group_ad.ad.responsive_search_ad.path1,
661
+ ad_group_ad.ad.responsive_search_ad.path2,
662
+ ad_group_ad.status,
663
+ ad_group_ad.policy_summary.approval_status,
664
+ ad_group.id,
665
+ ad_group.name,
666
+ campaign.id,
667
+ campaign.name,
668
+ metrics.impressions,
669
+ metrics.clicks,
670
+ metrics.cost_micros,
671
+ metrics.conversions
672
+ FROM ad_group_ad
673
+ WHERE ad_group.id = ${adGroupId} AND ad_group_ad.ad.id = ${adId}
674
+ `;
675
+ const results = await this.query(customerId, query);
676
+ return results[0] || null;
677
+ }
678
+ /**
679
+ * 更新广告状态
680
+ */
681
+ async updateAd(customerId, adGroupId, adId, updateData) {
682
+ try {
683
+ const customer = await this.getCustomer(customerId);
684
+ const resourceName = `customers/${customerId}/adGroupAds/${adGroupId}~${adId}`;
685
+ const resource = { resource_name: resourceName };
686
+ if (updateData.status) {
687
+ resource.status = updateData.status;
688
+ }
689
+ const operations = [
690
+ {
691
+ entity: 'ad_group_ad',
692
+ operation: 'update',
693
+ resource: resource,
694
+ },
695
+ ];
696
+ const result = await customer.mutateResources(operations);
697
+ return result;
698
+ }
699
+ catch (error) {
700
+ const errorMessage = error.message || error.details || (error.errors && JSON.stringify(error.errors)) || JSON.stringify(error);
701
+ throw new GoogleAdsError(`更新广告失败: ${errorMessage}`, error);
702
+ }
703
+ }
704
+ /**
705
+ * 创建响应式搜索广告 (RSA)
706
+ */
707
+ async createAd(customerId, adData) {
708
+ try {
709
+ const customer = await this.getCustomer(customerId);
710
+ const operations = [
711
+ {
712
+ entity: 'ad_group_ad',
713
+ operation: 'create',
714
+ resource: {
715
+ ad_group: `customers/${customerId}/adGroups/${adData.adGroupId}`,
716
+ status: adData.status === 'ENABLED' ? enums.AdGroupAdStatus.ENABLED : enums.AdGroupAdStatus.PAUSED,
717
+ ad: {
718
+ responsive_search_ad: {
719
+ headlines: adData.headlines.map(text => ({ text })),
720
+ descriptions: adData.descriptions.map(text => ({ text })),
721
+ path1: adData.path1,
722
+ path2: adData.path2,
723
+ },
724
+ final_urls: [adData.finalUrl],
725
+ },
726
+ },
727
+ },
728
+ ];
729
+ const result = await customer.mutateResources(operations);
730
+ return result;
731
+ }
732
+ catch (error) {
733
+ const errorMessage = error.message || error.details || (error.errors && JSON.stringify(error.errors)) || JSON.stringify(error);
734
+ throw new GoogleAdsError(`创建广告失败: ${errorMessage}`, error);
735
+ }
736
+ }
737
+ /**
738
+ * 删除广告
739
+ */
740
+ async deleteAd(customerId, adGroupId, adId) {
741
+ try {
742
+ const customer = await this.getCustomer(customerId);
743
+ const resourceName = `customers/${customerId}/adGroupAds/${adGroupId}~${adId}`;
744
+ const result = await customer.adGroupAds.remove([resourceName]);
745
+ return result;
746
+ }
747
+ catch (error) {
748
+ const errorMessage = error.message || error.details || (error.errors && JSON.stringify(error.errors)) || JSON.stringify(error);
749
+ throw new GoogleAdsError(`删除广告失败: ${errorMessage}`, error);
750
+ }
751
+ }
752
+ /**
753
+ * 发送 MCC 关联邀请
754
+ * 从 Manager Account (MCC) 向客户账号发送关联邀请
755
+ *
756
+ * @param clientCustomerId 客户账号的 Customer ID
757
+ * @returns manager_link_id 用于客户接受邀请
758
+ */
759
+ async sendLinkInvitation(clientCustomerId) {
760
+ try {
761
+ // 使用 MCC 账号发送邀请
762
+ const managerCustomerId = this.config.loginCustomerId;
763
+ if (!managerCustomerId) {
764
+ throw new GoogleAdsError('未配置 MCC 管理账号 ID\n' +
765
+ '请在 .env 文件中设置 GOOGLE_ADS_MANAGER_ACCOUNT_ID');
766
+ }
767
+ const manager = await this.getCustomer(managerCustomerId);
768
+ // 先检查是否已存在关联
769
+ const existingQuery = `
770
+ SELECT
771
+ customer_client_link.client_customer,
772
+ customer_client_link.manager_link_id,
773
+ customer_client_link.status
774
+ FROM customer_client_link
775
+ WHERE customer_client_link.client_customer = 'customers/${clientCustomerId}'
776
+ `;
777
+ const existingLinks = await manager.query(existingQuery);
778
+ if (existingLinks.length > 0) {
779
+ const existingLink = existingLinks[0]?.customer_client_link;
780
+ if (existingLink?.manager_link_id) {
781
+ // 已存在关联,返回现有的 manager_link_id
782
+ return `existing:${existingLink.manager_link_id}`;
783
+ }
784
+ }
785
+ // 直接调用底层的 CustomerClientLinkService
786
+ // 注意:封装的 customerClientLinks.create 方法有 bug,需要直接调用 gRPC 服务
787
+ const service = manager.loadService('CustomerClientLinkServiceClient');
788
+ const operation = {
789
+ create: {
790
+ client_customer: `customers/${clientCustomerId}`,
791
+ status: enums.ManagerLinkStatus.PENDING,
792
+ },
793
+ };
794
+ const request = {
795
+ customer_id: managerCustomerId,
796
+ operation: operation,
797
+ };
798
+ const [response] = await service.mutateCustomerClientLink(request, {
799
+ otherArgs: {
800
+ headers: {
801
+ 'developer-token': this.config.developerToken,
802
+ 'login-customer-id': managerCustomerId,
803
+ },
804
+ },
805
+ });
806
+ // 从 resource_name 提取 manager_link_id
807
+ // 格式: customers/{manager_id}/customerClientLinks/{client_id}~{manager_link_id}
808
+ const resourceName = response?.result?.resource_name;
809
+ if (!resourceName) {
810
+ throw new GoogleAdsError('创建关联成功但未返回 resource_name');
811
+ }
812
+ const match = resourceName.match(/~(\d+)$/);
813
+ const managerLinkId = match ? match[1] : 'unknown';
814
+ return managerLinkId;
815
+ }
816
+ catch (error) {
817
+ // 提取更详细的错误信息
818
+ const errorMessage = error.message
819
+ || error.details
820
+ || (error.errors && JSON.stringify(error.errors))
821
+ || JSON.stringify(error);
822
+ throw new GoogleAdsError(`发送关联邀请失败: ${errorMessage}`, error);
823
+ }
824
+ }
825
+ }
826
+ //# sourceMappingURL=google-ads-client.js.map