@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.
- package/.claude/CLAUDE.md +279 -0
- package/dist/commands/account/check.d.ts +6 -0
- package/dist/commands/account/check.d.ts.map +1 -0
- package/dist/commands/account/check.js +179 -0
- package/dist/commands/account/check.js.map +1 -0
- package/dist/commands/account/create.d.ts +6 -0
- package/dist/commands/account/create.d.ts.map +1 -0
- package/dist/commands/account/create.js +172 -0
- package/dist/commands/account/create.js.map +1 -0
- package/dist/commands/account/index.d.ts +6 -0
- package/dist/commands/account/index.d.ts.map +1 -0
- package/dist/commands/account/index.js +11 -0
- package/dist/commands/account/index.js.map +1 -0
- package/dist/commands/ad/create.d.ts +6 -0
- package/dist/commands/ad/create.d.ts.map +1 -0
- package/dist/commands/ad/create.js +77 -0
- package/dist/commands/ad/create.js.map +1 -0
- package/dist/commands/ad/delete.d.ts +6 -0
- package/dist/commands/ad/delete.d.ts.map +1 -0
- package/dist/commands/ad/delete.js +50 -0
- package/dist/commands/ad/delete.js.map +1 -0
- package/dist/commands/ad/index.d.ts +6 -0
- package/dist/commands/ad/index.d.ts.map +1 -0
- package/dist/commands/ad/index.js +17 -0
- package/dist/commands/ad/index.js.map +1 -0
- package/dist/commands/ad/info.d.ts +6 -0
- package/dist/commands/ad/info.d.ts.map +1 -0
- package/dist/commands/ad/info.js +90 -0
- package/dist/commands/ad/info.js.map +1 -0
- package/dist/commands/ad/list.d.ts +6 -0
- package/dist/commands/ad/list.d.ts.map +1 -0
- package/dist/commands/ad/list.js +94 -0
- package/dist/commands/ad/list.js.map +1 -0
- package/dist/commands/ad/update.d.ts +6 -0
- package/dist/commands/ad/update.d.ts.map +1 -0
- package/dist/commands/ad/update.js +42 -0
- package/dist/commands/ad/update.js.map +1 -0
- package/dist/commands/ad-group/create.d.ts +6 -0
- package/dist/commands/ad-group/create.d.ts.map +1 -0
- package/dist/commands/ad-group/create.js +59 -0
- package/dist/commands/ad-group/create.js.map +1 -0
- package/dist/commands/ad-group/delete.d.ts +6 -0
- package/dist/commands/ad-group/delete.d.ts.map +1 -0
- package/dist/commands/ad-group/delete.js +49 -0
- package/dist/commands/ad-group/delete.js.map +1 -0
- package/dist/commands/ad-group/index.d.ts +6 -0
- package/dist/commands/ad-group/index.d.ts.map +1 -0
- package/dist/commands/ad-group/index.js +17 -0
- package/dist/commands/ad-group/index.js.map +1 -0
- package/dist/commands/ad-group/info.d.ts +6 -0
- package/dist/commands/ad-group/info.d.ts.map +1 -0
- package/dist/commands/ad-group/info.js +59 -0
- package/dist/commands/ad-group/info.js.map +1 -0
- package/dist/commands/ad-group/list.d.ts +6 -0
- package/dist/commands/ad-group/list.d.ts.map +1 -0
- package/dist/commands/ad-group/list.js +83 -0
- package/dist/commands/ad-group/list.js.map +1 -0
- package/dist/commands/ad-group/update.d.ts +6 -0
- package/dist/commands/ad-group/update.d.ts.map +1 -0
- package/dist/commands/ad-group/update.js +56 -0
- package/dist/commands/ad-group/update.js.map +1 -0
- package/dist/commands/auth/index.d.ts +6 -0
- package/dist/commands/auth/index.d.ts.map +1 -0
- package/dist/commands/auth/index.js +13 -0
- package/dist/commands/auth/index.js.map +1 -0
- package/dist/commands/auth/login.d.ts +10 -0
- package/dist/commands/auth/login.d.ts.map +1 -0
- package/dist/commands/auth/login.js +154 -0
- package/dist/commands/auth/login.js.map +1 -0
- package/dist/commands/auth/logout.d.ts +6 -0
- package/dist/commands/auth/logout.d.ts.map +1 -0
- package/dist/commands/auth/logout.js +28 -0
- package/dist/commands/auth/logout.js.map +1 -0
- package/dist/commands/auth/status.d.ts +6 -0
- package/dist/commands/auth/status.d.ts.map +1 -0
- package/dist/commands/auth/status.js +47 -0
- package/dist/commands/auth/status.js.map +1 -0
- package/dist/commands/campaign/create.d.ts +6 -0
- package/dist/commands/campaign/create.d.ts.map +1 -0
- package/dist/commands/campaign/create.js +58 -0
- package/dist/commands/campaign/create.js.map +1 -0
- package/dist/commands/campaign/delete.d.ts +6 -0
- package/dist/commands/campaign/delete.d.ts.map +1 -0
- package/dist/commands/campaign/delete.js +49 -0
- package/dist/commands/campaign/delete.js.map +1 -0
- package/dist/commands/campaign/index.d.ts +6 -0
- package/dist/commands/campaign/index.d.ts.map +1 -0
- package/dist/commands/campaign/index.js +17 -0
- package/dist/commands/campaign/index.js.map +1 -0
- package/dist/commands/campaign/info.d.ts +6 -0
- package/dist/commands/campaign/info.d.ts.map +1 -0
- package/dist/commands/campaign/info.js +57 -0
- package/dist/commands/campaign/info.js.map +1 -0
- package/dist/commands/campaign/list.d.ts +6 -0
- package/dist/commands/campaign/list.d.ts.map +1 -0
- package/dist/commands/campaign/list.js +71 -0
- package/dist/commands/campaign/list.js.map +1 -0
- package/dist/commands/campaign/update.d.ts +6 -0
- package/dist/commands/campaign/update.d.ts.map +1 -0
- package/dist/commands/campaign/update.js +56 -0
- package/dist/commands/campaign/update.js.map +1 -0
- package/dist/commands/config.d.ts +6 -0
- package/dist/commands/config.d.ts.map +1 -0
- package/dist/commands/config.js +104 -0
- package/dist/commands/config.js.map +1 -0
- package/dist/commands/keyword/add.d.ts +6 -0
- package/dist/commands/keyword/add.d.ts.map +1 -0
- package/dist/commands/keyword/add.js +47 -0
- package/dist/commands/keyword/add.js.map +1 -0
- package/dist/commands/keyword/delete.d.ts +6 -0
- package/dist/commands/keyword/delete.d.ts.map +1 -0
- package/dist/commands/keyword/delete.js +50 -0
- package/dist/commands/keyword/delete.js.map +1 -0
- package/dist/commands/keyword/index.d.ts +6 -0
- package/dist/commands/keyword/index.d.ts.map +1 -0
- package/dist/commands/keyword/index.js +15 -0
- package/dist/commands/keyword/index.js.map +1 -0
- package/dist/commands/keyword/list.d.ts +6 -0
- package/dist/commands/keyword/list.d.ts.map +1 -0
- package/dist/commands/keyword/list.js +77 -0
- package/dist/commands/keyword/list.js.map +1 -0
- package/dist/commands/keyword/update.d.ts +6 -0
- package/dist/commands/keyword/update.d.ts.map +1 -0
- package/dist/commands/keyword/update.js +51 -0
- package/dist/commands/keyword/update.js.map +1 -0
- package/dist/commands/query.d.ts +6 -0
- package/dist/commands/query.d.ts.map +1 -0
- package/dist/commands/query.js +71 -0
- package/dist/commands/query.js.map +1 -0
- package/dist/config.d.ts +32 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +10 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +75 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/api-client.d.ts +100 -0
- package/dist/lib/api-client.d.ts.map +1 -0
- package/dist/lib/api-client.js +186 -0
- package/dist/lib/api-client.js.map +1 -0
- package/dist/lib/callback-server.d.ts +32 -0
- package/dist/lib/callback-server.d.ts.map +1 -0
- package/dist/lib/callback-server.js +132 -0
- package/dist/lib/callback-server.js.map +1 -0
- package/dist/lib/google-ads-client.d.ts +177 -0
- package/dist/lib/google-ads-client.d.ts.map +1 -0
- package/dist/lib/google-ads-client.js +826 -0
- package/dist/lib/google-ads-client.js.map +1 -0
- package/dist/lib/oauth2-manager.d.ts +27 -0
- package/dist/lib/oauth2-manager.d.ts.map +1 -0
- package/dist/lib/oauth2-manager.js +123 -0
- package/dist/lib/oauth2-manager.js.map +1 -0
- package/dist/lib/token-store.d.ts +43 -0
- package/dist/lib/token-store.d.ts.map +1 -0
- package/dist/lib/token-store.js +103 -0
- package/dist/lib/token-store.js.map +1 -0
- package/dist/utils/config.d.ts +9 -0
- package/dist/utils/config.d.ts.map +1 -0
- package/dist/utils/config.js +34 -0
- package/dist/utils/config.js.map +1 -0
- package/dist/utils/customer-id.d.ts +10 -0
- package/dist/utils/customer-id.d.ts.map +1 -0
- package/dist/utils/customer-id.js +29 -0
- package/dist/utils/customer-id.js.map +1 -0
- package/dist/utils/env-updater.d.ts +8 -0
- package/dist/utils/env-updater.d.ts.map +1 -0
- package/dist/utils/env-updater.js +45 -0
- package/dist/utils/env-updater.js.map +1 -0
- package/dist/utils/errors.d.ts +32 -0
- package/dist/utils/errors.d.ts.map +1 -0
- package/dist/utils/errors.js +68 -0
- package/dist/utils/errors.js.map +1 -0
- package/dist/utils/logger.d.ts +10 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +25 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/optima-config.d.ts +9 -0
- package/dist/utils/optima-config.d.ts.map +1 -0
- package/dist/utils/optima-config.js +83 -0
- package/dist/utils/optima-config.js.map +1 -0
- 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
|