@fwgi/openclaw-x-marketing 1.0.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/README.md +69 -0
- package/SKILL.md +140 -0
- package/docs/ref-tools.md +369 -0
- package/docs/ref-workflow.md +193 -0
- package/index.ts +882 -0
- package/openclaw.plugin.json +43 -0
- package/package.json +26 -0
package/index.ts
ADDED
|
@@ -0,0 +1,882 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* x-marketing OpenClaw Plugin
|
|
3
|
+
* X(Twitter) 营销管理助手 — 账号信息查看、受众管理(导入/筛选/分组/标签/营销活动)
|
|
4
|
+
*
|
|
5
|
+
* 认证方式:通过 x-api-key 请求头调用后端 API(api-backend.fwgi.ai),
|
|
6
|
+
* 启动时调用 /v1/api/checkLogin 验证 API Key 有效性并获取用户信息。
|
|
7
|
+
* 验证通过后,所有后续请求自动携带 x-api-key 头。
|
|
8
|
+
*
|
|
9
|
+
* v4.0.0
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import * as https from "node:https";
|
|
13
|
+
import * as http from "node:http";
|
|
14
|
+
|
|
15
|
+
// ─── 常量 ───
|
|
16
|
+
const DEFAULT_API_BASE_URL = "https://api-backend.fwgi.ai";
|
|
17
|
+
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
18
|
+
const SLOW_TIMEOUT_MS = 60_000;
|
|
19
|
+
const PLUGIN_ID = "x-marketing";
|
|
20
|
+
|
|
21
|
+
// ─── 类型 ───
|
|
22
|
+
type RuntimeConfig = {
|
|
23
|
+
apiBaseUrl: string;
|
|
24
|
+
apiKey: string;
|
|
25
|
+
userId: number | null;
|
|
26
|
+
userEmail: string | null;
|
|
27
|
+
timeoutMs: number;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
type ApiResponse = {
|
|
31
|
+
status: number;
|
|
32
|
+
body: string;
|
|
33
|
+
data: any;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// ─── 内存状态 ───
|
|
37
|
+
let apiKeyVerified = false;
|
|
38
|
+
let apiKeyFingerprint = "";
|
|
39
|
+
let verifiedUserId: number | null = null;
|
|
40
|
+
let verifiedUserEmail: string | null = null;
|
|
41
|
+
|
|
42
|
+
// ─── 配置解析 ───
|
|
43
|
+
|
|
44
|
+
function getRawPluginConfig(api: any): Record<string, any> {
|
|
45
|
+
const rootCfg = api?.config ?? {};
|
|
46
|
+
const nestedCfg = rootCfg?.plugins?.entries?.[PLUGIN_ID]?.config;
|
|
47
|
+
if (nestedCfg && typeof nestedCfg === "object") {
|
|
48
|
+
return { ...rootCfg, ...nestedCfg };
|
|
49
|
+
}
|
|
50
|
+
return rootCfg;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function toPositiveInt(value: unknown, fallback: number): number {
|
|
54
|
+
const parsed = Number(value);
|
|
55
|
+
return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : fallback;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function resolveRuntimeConfig(api: any): RuntimeConfig {
|
|
59
|
+
const cfg = getRawPluginConfig(api);
|
|
60
|
+
|
|
61
|
+
const apiBaseUrl = String(
|
|
62
|
+
cfg.apiBaseUrl ||
|
|
63
|
+
process.env.X_MARKETING_API_BASE_URL ||
|
|
64
|
+
DEFAULT_API_BASE_URL,
|
|
65
|
+
).replace(/\/+$/, "");
|
|
66
|
+
|
|
67
|
+
const apiKey = String(
|
|
68
|
+
cfg.apiKey ||
|
|
69
|
+
cfg.xApiKey ||
|
|
70
|
+
process.env.X_MARKETING_API_KEY ||
|
|
71
|
+
"",
|
|
72
|
+
).trim();
|
|
73
|
+
|
|
74
|
+
const userId = verifiedUserId ?? (cfg.userId ? Number(cfg.userId) : null);
|
|
75
|
+
const userEmail = verifiedUserEmail ?? null;
|
|
76
|
+
|
|
77
|
+
const timeoutMs = toPositiveInt(
|
|
78
|
+
cfg.requestTimeoutMs ||
|
|
79
|
+
process.env.X_MARKETING_TIMEOUT_MS,
|
|
80
|
+
DEFAULT_TIMEOUT_MS,
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
return { apiBaseUrl, apiKey, userId, userEmail, timeoutMs };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ─── HTTP 请求封装 ───
|
|
87
|
+
|
|
88
|
+
function apiRequest(
|
|
89
|
+
config: RuntimeConfig,
|
|
90
|
+
method: string,
|
|
91
|
+
reqPath: string,
|
|
92
|
+
options: {
|
|
93
|
+
body?: any;
|
|
94
|
+
query?: Record<string, string>;
|
|
95
|
+
timeoutMs?: number;
|
|
96
|
+
} = {},
|
|
97
|
+
): Promise<ApiResponse> {
|
|
98
|
+
const url = new URL(reqPath, config.apiBaseUrl + "/");
|
|
99
|
+
|
|
100
|
+
if (options.query) {
|
|
101
|
+
for (const [key, value] of Object.entries(options.query)) {
|
|
102
|
+
if (value !== undefined && value !== null && value !== "") {
|
|
103
|
+
url.searchParams.set(key, value);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const headers: Record<string, string> = {
|
|
109
|
+
"Content-Type": "application/json",
|
|
110
|
+
Accept: "application/json",
|
|
111
|
+
"x-api-key": config.apiKey,
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const bodyStr = options.body ? JSON.stringify(options.body) : undefined;
|
|
115
|
+
if (bodyStr) {
|
|
116
|
+
headers["Content-Length"] = String(Buffer.byteLength(bodyStr));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const client = url.protocol === "https:" ? https : http;
|
|
120
|
+
const effectiveTimeout = options.timeoutMs || config.timeoutMs;
|
|
121
|
+
|
|
122
|
+
return new Promise((resolve, reject) => {
|
|
123
|
+
const req = client.request(
|
|
124
|
+
{
|
|
125
|
+
protocol: url.protocol,
|
|
126
|
+
hostname: url.hostname,
|
|
127
|
+
port: url.port || (url.protocol === "https:" ? 443 : 80),
|
|
128
|
+
path: `${url.pathname}${url.search}`,
|
|
129
|
+
method,
|
|
130
|
+
headers,
|
|
131
|
+
},
|
|
132
|
+
(res) => {
|
|
133
|
+
const chunks: Buffer[] = [];
|
|
134
|
+
res.on("data", (chunk: Buffer) => chunks.push(chunk));
|
|
135
|
+
res.on("end", () => {
|
|
136
|
+
clearTimeout(timer);
|
|
137
|
+
const body = Buffer.concat(chunks).toString("utf-8");
|
|
138
|
+
let data: any;
|
|
139
|
+
try {
|
|
140
|
+
data = JSON.parse(body);
|
|
141
|
+
} catch {
|
|
142
|
+
data = { raw: body };
|
|
143
|
+
}
|
|
144
|
+
resolve({ status: res.statusCode || 200, body, data });
|
|
145
|
+
});
|
|
146
|
+
},
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
const timer = setTimeout(() => {
|
|
150
|
+
req.destroy(new Error(`请求超时 (${Math.round(effectiveTimeout / 1000)}s)`));
|
|
151
|
+
}, effectiveTimeout);
|
|
152
|
+
|
|
153
|
+
req.on("error", (error) => {
|
|
154
|
+
clearTimeout(timer);
|
|
155
|
+
reject(error);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
if (bodyStr) {
|
|
159
|
+
req.write(bodyStr);
|
|
160
|
+
}
|
|
161
|
+
req.end();
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ─── API Key 验证 ───
|
|
166
|
+
|
|
167
|
+
async function verifyApiKey(config: RuntimeConfig): Promise<void> {
|
|
168
|
+
const fingerprint = `${config.apiBaseUrl}::${config.apiKey}`;
|
|
169
|
+
if (apiKeyVerified && fingerprint === apiKeyFingerprint) return;
|
|
170
|
+
|
|
171
|
+
if (!config.apiKey) {
|
|
172
|
+
throw new Error(
|
|
173
|
+
"未配置 API Key。请在 plugins.entries.x-marketing.config.apiKey 或环境变量 X_MARKETING_API_KEY 中设置。",
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const resp = await apiRequest(config, "GET", "/v1/api/checkLogin", {
|
|
178
|
+
timeoutMs: 15_000,
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
if (resp.status === 401 || resp.data?.code === 4001) {
|
|
182
|
+
throw new Error("API Key 无效或已过期。请检查后重试。");
|
|
183
|
+
}
|
|
184
|
+
if (resp.status >= 400) {
|
|
185
|
+
throw new Error(`API Key 验证失败 (HTTP ${resp.status}): ${resp.body}`);
|
|
186
|
+
}
|
|
187
|
+
if (resp.data?.code === 200 && resp.data?.data?.is_login) {
|
|
188
|
+
apiKeyVerified = true;
|
|
189
|
+
apiKeyFingerprint = fingerprint;
|
|
190
|
+
verifiedUserId = resp.data.data.user_id ?? null;
|
|
191
|
+
verifiedUserEmail = resp.data.data.account ?? null;
|
|
192
|
+
} else {
|
|
193
|
+
throw new Error(`API Key 验证返回异常: ${resp.data?.message || resp.body}`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ─── 响应处理 ───
|
|
198
|
+
|
|
199
|
+
function ok(text: string) {
|
|
200
|
+
return { content: [{ type: "text", text }] };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function err(message: string) {
|
|
204
|
+
return { content: [{ type: "text", text: `❌ ${message}` }] };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function formatResponse(data: any): { content: Array<{ type: string; text: string }> } {
|
|
208
|
+
if (data?.code === 200 || data?.status === "true") {
|
|
209
|
+
return ok(JSON.stringify(data.data ?? data, null, 2));
|
|
210
|
+
}
|
|
211
|
+
const msg = data?.message || JSON.stringify(data);
|
|
212
|
+
throw new Error(`API 返回错误 (code: ${data?.code}): ${msg}`);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function classifyError(message: string): string {
|
|
216
|
+
if (message.includes("超时")) {
|
|
217
|
+
return `请求超时: ${message}。请检查网络连接或增大 requestTimeoutMs。`;
|
|
218
|
+
}
|
|
219
|
+
if (message.includes("ECONNREFUSED")) {
|
|
220
|
+
return `无法连接到后端服务,请检查 apiBaseUrl 配置是否正确。`;
|
|
221
|
+
}
|
|
222
|
+
if (message.includes("ECONNRESET") || message.includes("socket hang up")) {
|
|
223
|
+
return `连接被重置,可能是网络不稳定。请稍后重试。`;
|
|
224
|
+
}
|
|
225
|
+
if (message.includes("5006") || message.includes("Frequent")) {
|
|
226
|
+
return `操作过于频繁,请等待 30 秒后重试。`;
|
|
227
|
+
}
|
|
228
|
+
return message;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// ─── 工具注册 ───
|
|
232
|
+
|
|
233
|
+
export default function register(api: any) {
|
|
234
|
+
|
|
235
|
+
// ═══════════════════════════════════════════
|
|
236
|
+
// A. 连接验证
|
|
237
|
+
// ═══════════════════════════════════════════
|
|
238
|
+
|
|
239
|
+
api.registerTool({
|
|
240
|
+
name: "x_verify_connection",
|
|
241
|
+
description: "验证 API Key 连接。通过 checkLogin 接口确认 API Key 有效性并获取当前用户信息(user_id、邮箱)。每次会话开始前建议先调用此工具。",
|
|
242
|
+
parameters: { type: "object", properties: {}, required: [] },
|
|
243
|
+
async execute(_id: string, _params: any) {
|
|
244
|
+
try {
|
|
245
|
+
const config = resolveRuntimeConfig(api);
|
|
246
|
+
await verifyApiKey(config);
|
|
247
|
+
return ok(
|
|
248
|
+
`✅ API Key 验证通过!\n` +
|
|
249
|
+
`账号: ${verifiedUserEmail}\n` +
|
|
250
|
+
`user_id: ${verifiedUserId}\n` +
|
|
251
|
+
`后端: ${config.apiBaseUrl}`,
|
|
252
|
+
);
|
|
253
|
+
} catch (e: any) {
|
|
254
|
+
return err(classifyError(e.message));
|
|
255
|
+
}
|
|
256
|
+
},
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
// ═══════════════════════════════════════════
|
|
260
|
+
// B. X 账号信息查看
|
|
261
|
+
// ═══════════════════════════════════════════
|
|
262
|
+
|
|
263
|
+
api.registerTool({
|
|
264
|
+
name: "x_account_list",
|
|
265
|
+
description: "获取当前用户的 X 账号列表。支持按 handle/显示名称/邮箱筛选。",
|
|
266
|
+
parameters: {
|
|
267
|
+
type: "object",
|
|
268
|
+
properties: {
|
|
269
|
+
handle: { type: "string", description: "X 用户名(@ 后的内容)筛选" },
|
|
270
|
+
display_name: { type: "string", description: "显示名称筛选" },
|
|
271
|
+
email: { type: "string", description: "关联邮箱筛选" },
|
|
272
|
+
},
|
|
273
|
+
required: [],
|
|
274
|
+
},
|
|
275
|
+
async execute(_id: string, params: any) {
|
|
276
|
+
try {
|
|
277
|
+
const config = resolveRuntimeConfig(api);
|
|
278
|
+
await verifyApiKey(config);
|
|
279
|
+
const query: Record<string, string> = {};
|
|
280
|
+
if (params?.handle) query.handle = params.handle;
|
|
281
|
+
if (params?.display_name) query.display_name = params.display_name;
|
|
282
|
+
if (params?.email) query.email = params.email;
|
|
283
|
+
|
|
284
|
+
const resp = await apiRequest(config, "GET", "/v1/api/twitter/auto/list", { query });
|
|
285
|
+
return formatResponse(resp.data);
|
|
286
|
+
} catch (e: any) {
|
|
287
|
+
return err(classifyError(e.message));
|
|
288
|
+
}
|
|
289
|
+
},
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
api.registerTool({
|
|
293
|
+
name: "x_account_detail",
|
|
294
|
+
description: "获取 X 账号详情,包含 audiences、pillars、kols、templates、tweetStyle、postingSchedule、account_actions 等完整配置。",
|
|
295
|
+
parameters: {
|
|
296
|
+
type: "object",
|
|
297
|
+
properties: {
|
|
298
|
+
account_id: { type: "number", description: "账号 ID" },
|
|
299
|
+
},
|
|
300
|
+
required: ["account_id"],
|
|
301
|
+
},
|
|
302
|
+
async execute(_id: string, params: any) {
|
|
303
|
+
try {
|
|
304
|
+
const config = resolveRuntimeConfig(api);
|
|
305
|
+
await verifyApiKey(config);
|
|
306
|
+
const resp = await apiRequest(config, "GET", "/v1/api/twitter/auto/detail", {
|
|
307
|
+
query: { account_id: String(params.account_id) },
|
|
308
|
+
});
|
|
309
|
+
return formatResponse(resp.data);
|
|
310
|
+
} catch (e: any) {
|
|
311
|
+
return err(classifyError(e.message));
|
|
312
|
+
}
|
|
313
|
+
},
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
api.registerTool({
|
|
317
|
+
name: "x_get_field_info",
|
|
318
|
+
description: "获取上号表单的字段说明(handle、display_name 等字段的中英文说明及示例)。",
|
|
319
|
+
parameters: { type: "object", properties: {}, required: [] },
|
|
320
|
+
async execute(_id: string, _params: any) {
|
|
321
|
+
try {
|
|
322
|
+
const config = resolveRuntimeConfig(api);
|
|
323
|
+
await verifyApiKey(config);
|
|
324
|
+
const resp = await apiRequest(config, "GET", "/v1/api/twitter/auto/fieldInfo");
|
|
325
|
+
return formatResponse(resp.data);
|
|
326
|
+
} catch (e: any) {
|
|
327
|
+
return err(classifyError(e.message));
|
|
328
|
+
}
|
|
329
|
+
},
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
// ═══════════════════════════════════════════
|
|
333
|
+
// C. 受众管理模块
|
|
334
|
+
// ═══════════════════════════════════════════
|
|
335
|
+
|
|
336
|
+
// ─── C.1 受众列表与导入 ───
|
|
337
|
+
|
|
338
|
+
api.registerTool({
|
|
339
|
+
name: "x_audience_list",
|
|
340
|
+
description: "获取受众列表。支持按关键词、是否可私信、机器人评分、兴趣标签、活跃度等多维度筛选。",
|
|
341
|
+
parameters: {
|
|
342
|
+
type: "object",
|
|
343
|
+
properties: {
|
|
344
|
+
account_id: { type: "number", description: "账号 ID" },
|
|
345
|
+
keyword: { type: "string", description: "关键词(匹配用户名、名称、简介)" },
|
|
346
|
+
can_dm: { type: "string", description: "是否可私信(true/false)" },
|
|
347
|
+
bot_score: { type: "string", description: "机器人评分:safe / medium / risk" },
|
|
348
|
+
interest_tags: { type: "string", description: "兴趣标签,多个用逗号分隔" },
|
|
349
|
+
activity_min: { type: "number", description: "最小活跃度(0-100)" },
|
|
350
|
+
activity_max: { type: "number", description: "最大活跃度(0-100)" },
|
|
351
|
+
activity_days: { type: "number", description: "活跃度计算天数,默认 30" },
|
|
352
|
+
group_id: { type: "number", description: "分组 ID" },
|
|
353
|
+
source_username: { type: "string", description: "来源用户名" },
|
|
354
|
+
is_send: { type: "string", description: "发送状态筛选" },
|
|
355
|
+
page: { type: "number", description: "页码,默认 1" },
|
|
356
|
+
page_size: { type: "number", description: "每页数量,默认 20" },
|
|
357
|
+
},
|
|
358
|
+
required: ["account_id"],
|
|
359
|
+
},
|
|
360
|
+
async execute(_id: string, params: any) {
|
|
361
|
+
try {
|
|
362
|
+
const config = resolveRuntimeConfig(api);
|
|
363
|
+
await verifyApiKey(config);
|
|
364
|
+
const query: Record<string, string> = {
|
|
365
|
+
account_id: String(params.account_id),
|
|
366
|
+
};
|
|
367
|
+
if (params.keyword) query.keyword = params.keyword;
|
|
368
|
+
if (params.can_dm !== undefined) query.can_dm = String(params.can_dm);
|
|
369
|
+
if (params.bot_score) query.bot_score = params.bot_score;
|
|
370
|
+
if (params.interest_tags) query.interest_tags = params.interest_tags;
|
|
371
|
+
if (params.activity_min !== undefined) query.activity_min = String(params.activity_min);
|
|
372
|
+
if (params.activity_max !== undefined) query.activity_max = String(params.activity_max);
|
|
373
|
+
if (params.activity_days) query.activity_days = String(params.activity_days);
|
|
374
|
+
if (params.group_id) query.group_id = String(params.group_id);
|
|
375
|
+
if (params.source_username) query.source_username = params.source_username;
|
|
376
|
+
if (params.is_send) query.is_send = params.is_send;
|
|
377
|
+
if (params.page) query.page = String(params.page);
|
|
378
|
+
if (params.page_size) query.page_size = String(params.page_size);
|
|
379
|
+
|
|
380
|
+
const resp = await apiRequest(config, "GET", "/v1/api/twitter/following/list", { query });
|
|
381
|
+
return formatResponse(resp.data);
|
|
382
|
+
} catch (e: any) {
|
|
383
|
+
return err(classifyError(e.message));
|
|
384
|
+
}
|
|
385
|
+
},
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
api.registerTool({
|
|
389
|
+
name: "x_audience_import_url",
|
|
390
|
+
description: "通过 X 用户主页 URL 导入其关注列表作为受众。支持 cursor 分页导入。注意:此接口耗时较长,且 count 参数为建议值(实际可能返回更多)。",
|
|
391
|
+
parameters: {
|
|
392
|
+
type: "object",
|
|
393
|
+
properties: {
|
|
394
|
+
account_id: { type: "number", description: "账号 ID" },
|
|
395
|
+
x_url: { type: "string", description: "X 用户主页 URL,如 https://x.com/username" },
|
|
396
|
+
cursor: { type: "string", description: "分页游标,上一页返回的 next_cursor" },
|
|
397
|
+
count: { type: "number", description: "期望每页数量,默认 20" },
|
|
398
|
+
},
|
|
399
|
+
required: ["account_id", "x_url"],
|
|
400
|
+
},
|
|
401
|
+
async execute(_id: string, params: any) {
|
|
402
|
+
try {
|
|
403
|
+
const config = resolveRuntimeConfig(api);
|
|
404
|
+
await verifyApiKey(config);
|
|
405
|
+
const body: any = {
|
|
406
|
+
account_id: params.account_id,
|
|
407
|
+
x_url: params.x_url,
|
|
408
|
+
};
|
|
409
|
+
if (params.cursor) body.cursor = params.cursor;
|
|
410
|
+
if (params.count) body.count = params.count;
|
|
411
|
+
|
|
412
|
+
const resp = await apiRequest(config, "POST", "/v1/api/twitter/following/add", {
|
|
413
|
+
body,
|
|
414
|
+
timeoutMs: SLOW_TIMEOUT_MS,
|
|
415
|
+
});
|
|
416
|
+
if (resp.data?.code === 200) {
|
|
417
|
+
const d = resp.data.data;
|
|
418
|
+
const sr = d?.save_result || {};
|
|
419
|
+
return ok(
|
|
420
|
+
`✅ 受众导入完成\n` +
|
|
421
|
+
`来源: ${params.x_url}\n` +
|
|
422
|
+
`返回用户数: ${d?.total_returned ?? d?.users?.length ?? 0}\n` +
|
|
423
|
+
`保存成功: ${sr.success ?? 0},失败: ${sr.failed ?? 0}\n` +
|
|
424
|
+
`还有更多: ${d?.has_more ? "是(使用 next_cursor 继续)" : "否"}\n` +
|
|
425
|
+
(d?.next_cursor ? `next_cursor: ${d.next_cursor}` : ""),
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
return err(`导入失败: ${resp.data?.message || resp.body}`);
|
|
429
|
+
} catch (e: any) {
|
|
430
|
+
return err(classifyError(e.message));
|
|
431
|
+
}
|
|
432
|
+
},
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
api.registerTool({
|
|
436
|
+
name: "x_audience_interactive_users",
|
|
437
|
+
description: "根据推文 URL 或 ID 获取转发/点赞该推文的用户并导入到受众库。依赖 RapidAPI 外部服务,可能受频率限制。",
|
|
438
|
+
parameters: {
|
|
439
|
+
type: "object",
|
|
440
|
+
properties: {
|
|
441
|
+
account_id: { type: "number", description: "账号 ID" },
|
|
442
|
+
tweet_url_or_id: { type: "string", description: "推文 URL 或推文 ID" },
|
|
443
|
+
count: { type: "number", description: "返回数量,默认 20" },
|
|
444
|
+
},
|
|
445
|
+
required: ["account_id", "tweet_url_or_id"],
|
|
446
|
+
},
|
|
447
|
+
async execute(_id: string, params: any) {
|
|
448
|
+
try {
|
|
449
|
+
const config = resolveRuntimeConfig(api);
|
|
450
|
+
await verifyApiKey(config);
|
|
451
|
+
const body: any = {
|
|
452
|
+
account_id: params.account_id,
|
|
453
|
+
tweet_url_or_id: params.tweet_url_or_id,
|
|
454
|
+
};
|
|
455
|
+
if (params.count) body.count = params.count;
|
|
456
|
+
|
|
457
|
+
const resp = await apiRequest(config, "POST", "/v1/api/twitter/following/interactive-users", {
|
|
458
|
+
body,
|
|
459
|
+
timeoutMs: SLOW_TIMEOUT_MS,
|
|
460
|
+
});
|
|
461
|
+
if (resp.data?.code === 200) {
|
|
462
|
+
const d = resp.data.data;
|
|
463
|
+
return ok(
|
|
464
|
+
`✅ 交互用户获取完成\n` +
|
|
465
|
+
`总计: ${d?.total ?? 0}\n` +
|
|
466
|
+
`新增保存: ${d?.saved ?? 0}\n` +
|
|
467
|
+
`更新: ${d?.updated ?? 0}\n` +
|
|
468
|
+
`失败: ${d?.failed ?? 0}`,
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
return err(`获取交互用户失败: ${resp.data?.message || resp.body}`);
|
|
472
|
+
} catch (e: any) {
|
|
473
|
+
return err(classifyError(e.message));
|
|
474
|
+
}
|
|
475
|
+
},
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
api.registerTool({
|
|
479
|
+
name: "x_audience_search_keyword",
|
|
480
|
+
description: "根据关键词搜索推文,提取推文作者作为潜在受众并导入。依赖 RapidAPI,有频率限制(建议调用间隔大于30秒)。",
|
|
481
|
+
parameters: {
|
|
482
|
+
type: "object",
|
|
483
|
+
properties: {
|
|
484
|
+
account_id: { type: "number", description: "账号 ID" },
|
|
485
|
+
keyword: { type: "string", description: "搜索关键词" },
|
|
486
|
+
count: { type: "number", description: "返回数量,默认 20" },
|
|
487
|
+
},
|
|
488
|
+
required: ["account_id", "keyword"],
|
|
489
|
+
},
|
|
490
|
+
async execute(_id: string, params: any) {
|
|
491
|
+
try {
|
|
492
|
+
const config = resolveRuntimeConfig(api);
|
|
493
|
+
await verifyApiKey(config);
|
|
494
|
+
const body: any = {
|
|
495
|
+
account_id: params.account_id,
|
|
496
|
+
keyword: params.keyword,
|
|
497
|
+
};
|
|
498
|
+
if (params.count) body.count = params.count;
|
|
499
|
+
|
|
500
|
+
const resp = await apiRequest(config, "POST", "/v1/api/twitter/following/search-by-keyword", {
|
|
501
|
+
body,
|
|
502
|
+
timeoutMs: SLOW_TIMEOUT_MS,
|
|
503
|
+
});
|
|
504
|
+
if (resp.data?.code === 200) {
|
|
505
|
+
const d = resp.data.data;
|
|
506
|
+
return ok(
|
|
507
|
+
`✅ 关键词搜索完成\n` +
|
|
508
|
+
`关键词: ${params.keyword}\n` +
|
|
509
|
+
`总计: ${d?.total ?? 0}\n` +
|
|
510
|
+
`新增保存: ${d?.saved ?? 0}\n` +
|
|
511
|
+
`更新: ${d?.updated ?? 0}\n` +
|
|
512
|
+
`失败: ${d?.failed ?? 0}`,
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
return err(`关键词搜索失败: ${resp.data?.message || resp.body}`);
|
|
516
|
+
} catch (e: any) {
|
|
517
|
+
return err(classifyError(e.message));
|
|
518
|
+
}
|
|
519
|
+
},
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
// ─── C.2 分组管理 ───
|
|
523
|
+
|
|
524
|
+
api.registerTool({
|
|
525
|
+
name: "x_audience_group_create",
|
|
526
|
+
description: "创建受众分组。可创建动态分组(基于筛选条件自动匹配)或静态分组(指定用户ID列表)。",
|
|
527
|
+
parameters: {
|
|
528
|
+
type: "object",
|
|
529
|
+
properties: {
|
|
530
|
+
name: { type: "string", description: "分组名称" },
|
|
531
|
+
description: { type: "string", description: "分组描述" },
|
|
532
|
+
filter_conditions: {
|
|
533
|
+
type: "object",
|
|
534
|
+
description: "动态分组筛选条件(account_id, keyword, can_dm, bot_score, interest_tags, activity_min, activity_max, activity_days)",
|
|
535
|
+
},
|
|
536
|
+
user_ids: {
|
|
537
|
+
type: "array",
|
|
538
|
+
items: { type: "number" },
|
|
539
|
+
description: "静态分组的受众 ID 列表(following 表的 id)",
|
|
540
|
+
},
|
|
541
|
+
},
|
|
542
|
+
required: ["name"],
|
|
543
|
+
},
|
|
544
|
+
async execute(_id: string, params: any) {
|
|
545
|
+
try {
|
|
546
|
+
const config = resolveRuntimeConfig(api);
|
|
547
|
+
await verifyApiKey(config);
|
|
548
|
+
const body: any = { name: params.name };
|
|
549
|
+
if (params.description) body.description = params.description;
|
|
550
|
+
if (params.filter_conditions) body.filter_conditions = params.filter_conditions;
|
|
551
|
+
if (params.user_ids) body.user_ids = params.user_ids;
|
|
552
|
+
|
|
553
|
+
const resp = await apiRequest(config, "POST", "/v1/api/twitter/following/group/create", { body });
|
|
554
|
+
if (resp.data?.code === 200) {
|
|
555
|
+
const d = resp.data.data;
|
|
556
|
+
return ok(
|
|
557
|
+
`✅ 分组创建成功\n` +
|
|
558
|
+
`group_id: ${d?.group_id}\n` +
|
|
559
|
+
`名称: ${d?.name}\n` +
|
|
560
|
+
`匹配用户数: ${d?.user_count ?? 0}`,
|
|
561
|
+
);
|
|
562
|
+
}
|
|
563
|
+
return err(`创建分组失败: ${resp.data?.message || resp.body}`);
|
|
564
|
+
} catch (e: any) {
|
|
565
|
+
return err(classifyError(e.message));
|
|
566
|
+
}
|
|
567
|
+
},
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
api.registerTool({
|
|
571
|
+
name: "x_audience_group_list",
|
|
572
|
+
description: "获取受众分组列表。",
|
|
573
|
+
parameters: {
|
|
574
|
+
type: "object",
|
|
575
|
+
properties: {
|
|
576
|
+
page: { type: "number", description: "页码,默认 1" },
|
|
577
|
+
page_size: { type: "number", description: "每页数量,默认 20" },
|
|
578
|
+
},
|
|
579
|
+
required: [],
|
|
580
|
+
},
|
|
581
|
+
async execute(_id: string, params: any) {
|
|
582
|
+
try {
|
|
583
|
+
const config = resolveRuntimeConfig(api);
|
|
584
|
+
await verifyApiKey(config);
|
|
585
|
+
const query: Record<string, string> = {};
|
|
586
|
+
if (params?.page) query.page = String(params.page);
|
|
587
|
+
if (params?.page_size) query.page_size = String(params.page_size);
|
|
588
|
+
|
|
589
|
+
const resp = await apiRequest(config, "GET", "/v1/api/twitter/following/group/list", { query });
|
|
590
|
+
return formatResponse(resp.data);
|
|
591
|
+
} catch (e: any) {
|
|
592
|
+
return err(classifyError(e.message));
|
|
593
|
+
}
|
|
594
|
+
},
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
api.registerTool({
|
|
598
|
+
name: "x_audience_group_detail",
|
|
599
|
+
description: "获取受众分组详情,包含分组信息及筛选条件。",
|
|
600
|
+
parameters: {
|
|
601
|
+
type: "object",
|
|
602
|
+
properties: {
|
|
603
|
+
group_id: { type: "number", description: "分组 ID" },
|
|
604
|
+
},
|
|
605
|
+
required: ["group_id"],
|
|
606
|
+
},
|
|
607
|
+
async execute(_id: string, params: any) {
|
|
608
|
+
try {
|
|
609
|
+
const config = resolveRuntimeConfig(api);
|
|
610
|
+
await verifyApiKey(config);
|
|
611
|
+
const resp = await apiRequest(config, "GET", "/v1/api/twitter/following/group/detail", {
|
|
612
|
+
query: { group_id: String(params.group_id) },
|
|
613
|
+
});
|
|
614
|
+
return formatResponse(resp.data);
|
|
615
|
+
} catch (e: any) {
|
|
616
|
+
return err(classifyError(e.message));
|
|
617
|
+
}
|
|
618
|
+
},
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
api.registerTool({
|
|
622
|
+
name: "x_audience_group_update",
|
|
623
|
+
description: "更新受众分组的名称、描述或筛选条件。",
|
|
624
|
+
parameters: {
|
|
625
|
+
type: "object",
|
|
626
|
+
properties: {
|
|
627
|
+
group_id: { type: "number", description: "分组 ID" },
|
|
628
|
+
name: { type: "string", description: "新名称" },
|
|
629
|
+
description: { type: "string", description: "新描述" },
|
|
630
|
+
filter_conditions: { type: "object", description: "新的筛选条件" },
|
|
631
|
+
},
|
|
632
|
+
required: ["group_id"],
|
|
633
|
+
},
|
|
634
|
+
async execute(_id: string, params: any) {
|
|
635
|
+
try {
|
|
636
|
+
const config = resolveRuntimeConfig(api);
|
|
637
|
+
await verifyApiKey(config);
|
|
638
|
+
const body: any = { group_id: params.group_id };
|
|
639
|
+
if (params.name) body.name = params.name;
|
|
640
|
+
if (params.description) body.description = params.description;
|
|
641
|
+
if (params.filter_conditions) body.filter_conditions = params.filter_conditions;
|
|
642
|
+
|
|
643
|
+
const resp = await apiRequest(config, "POST", "/v1/api/twitter/following/group/update", { body });
|
|
644
|
+
if (resp.data?.code === 200) {
|
|
645
|
+
return ok(`✅ 分组 ${params.group_id} 已更新`);
|
|
646
|
+
}
|
|
647
|
+
return err(`更新失败: ${resp.data?.message || resp.body}`);
|
|
648
|
+
} catch (e: any) {
|
|
649
|
+
return err(classifyError(e.message));
|
|
650
|
+
}
|
|
651
|
+
},
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
api.registerTool({
|
|
655
|
+
name: "x_audience_group_delete",
|
|
656
|
+
description: "删除受众分组。⚠️ 此操作不可逆。",
|
|
657
|
+
parameters: {
|
|
658
|
+
type: "object",
|
|
659
|
+
properties: {
|
|
660
|
+
group_id: { type: "number", description: "分组 ID" },
|
|
661
|
+
},
|
|
662
|
+
required: ["group_id"],
|
|
663
|
+
},
|
|
664
|
+
async execute(_id: string, params: any) {
|
|
665
|
+
try {
|
|
666
|
+
const config = resolveRuntimeConfig(api);
|
|
667
|
+
await verifyApiKey(config);
|
|
668
|
+
const resp = await apiRequest(config, "POST", "/v1/api/twitter/following/group/delete", {
|
|
669
|
+
body: { group_id: params.group_id },
|
|
670
|
+
});
|
|
671
|
+
if (resp.data?.code === 200) {
|
|
672
|
+
return ok(`✅ 分组 ${params.group_id} 已删除`);
|
|
673
|
+
}
|
|
674
|
+
return err(`删除失败: ${resp.data?.message || resp.body}`);
|
|
675
|
+
} catch (e: any) {
|
|
676
|
+
return err(classifyError(e.message));
|
|
677
|
+
}
|
|
678
|
+
},
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
// ─── C.3 兴趣标签 ───
|
|
682
|
+
|
|
683
|
+
api.registerTool({
|
|
684
|
+
name: "x_audience_interest_tags",
|
|
685
|
+
description: "获取某账号下受众的兴趣标签统计列表。",
|
|
686
|
+
parameters: {
|
|
687
|
+
type: "object",
|
|
688
|
+
properties: {
|
|
689
|
+
account_id: { type: "number", description: "账号 ID" },
|
|
690
|
+
},
|
|
691
|
+
required: ["account_id"],
|
|
692
|
+
},
|
|
693
|
+
async execute(_id: string, params: any) {
|
|
694
|
+
try {
|
|
695
|
+
const config = resolveRuntimeConfig(api);
|
|
696
|
+
await verifyApiKey(config);
|
|
697
|
+
const resp = await apiRequest(config, "GET", "/v1/api/twitter/following/interest-tags", {
|
|
698
|
+
query: { account_id: String(params.account_id) },
|
|
699
|
+
});
|
|
700
|
+
return formatResponse(resp.data);
|
|
701
|
+
} catch (e: any) {
|
|
702
|
+
return err(classifyError(e.message));
|
|
703
|
+
}
|
|
704
|
+
},
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
api.registerTool({
|
|
708
|
+
name: "x_audience_interest_tags_add",
|
|
709
|
+
description: "为指定受众添加兴趣标签。",
|
|
710
|
+
parameters: {
|
|
711
|
+
type: "object",
|
|
712
|
+
properties: {
|
|
713
|
+
account_id: { type: "number", description: "账号 ID" },
|
|
714
|
+
following_id: { type: "number", description: "受众 ID(受众列表中的 id 字段)" },
|
|
715
|
+
tags: {
|
|
716
|
+
type: "array",
|
|
717
|
+
items: { type: "string" },
|
|
718
|
+
description: "标签列表,如 [\"AI\", \"Creator\"]",
|
|
719
|
+
},
|
|
720
|
+
source: { type: "string", description: "来源,默认 manual" },
|
|
721
|
+
},
|
|
722
|
+
required: ["account_id", "following_id", "tags"],
|
|
723
|
+
},
|
|
724
|
+
async execute(_id: string, params: any) {
|
|
725
|
+
try {
|
|
726
|
+
const config = resolveRuntimeConfig(api);
|
|
727
|
+
await verifyApiKey(config);
|
|
728
|
+
const body: any = {
|
|
729
|
+
account_id: params.account_id,
|
|
730
|
+
following_id: params.following_id,
|
|
731
|
+
tags: params.tags,
|
|
732
|
+
};
|
|
733
|
+
if (params.source) body.source = params.source;
|
|
734
|
+
|
|
735
|
+
const resp = await apiRequest(config, "POST", "/v1/api/twitter/following/interest-tags/add", { body });
|
|
736
|
+
if (resp.data?.code === 200) {
|
|
737
|
+
return ok(`✅ 已为受众 ${params.following_id} 添加标签: ${params.tags.join(", ")}`);
|
|
738
|
+
}
|
|
739
|
+
return err(`添加标签失败: ${resp.data?.message || resp.body}`);
|
|
740
|
+
} catch (e: any) {
|
|
741
|
+
return err(classifyError(e.message));
|
|
742
|
+
}
|
|
743
|
+
},
|
|
744
|
+
});
|
|
745
|
+
|
|
746
|
+
api.registerTool({
|
|
747
|
+
name: "x_audience_interest_tags_remove",
|
|
748
|
+
description: "移除指定受众的某个兴趣标签。",
|
|
749
|
+
parameters: {
|
|
750
|
+
type: "object",
|
|
751
|
+
properties: {
|
|
752
|
+
account_id: { type: "number", description: "账号 ID" },
|
|
753
|
+
following_id: { type: "number", description: "受众 ID" },
|
|
754
|
+
tag_name: { type: "string", description: "要移除的标签名称" },
|
|
755
|
+
},
|
|
756
|
+
required: ["account_id", "following_id", "tag_name"],
|
|
757
|
+
},
|
|
758
|
+
async execute(_id: string, params: any) {
|
|
759
|
+
try {
|
|
760
|
+
const config = resolveRuntimeConfig(api);
|
|
761
|
+
await verifyApiKey(config);
|
|
762
|
+
const resp = await apiRequest(config, "POST", "/v1/api/twitter/following/interest-tags/remove", {
|
|
763
|
+
body: {
|
|
764
|
+
account_id: params.account_id,
|
|
765
|
+
following_id: params.following_id,
|
|
766
|
+
tag_name: params.tag_name,
|
|
767
|
+
},
|
|
768
|
+
});
|
|
769
|
+
if (resp.data?.code === 200) {
|
|
770
|
+
return ok(`✅ 已从受众 ${params.following_id} 移除标签: ${params.tag_name}`);
|
|
771
|
+
}
|
|
772
|
+
return err(`移除标签失败: ${resp.data?.message || resp.body}`);
|
|
773
|
+
} catch (e: any) {
|
|
774
|
+
return err(classifyError(e.message));
|
|
775
|
+
}
|
|
776
|
+
},
|
|
777
|
+
});
|
|
778
|
+
|
|
779
|
+
// ─── C.4 营销活动 ───
|
|
780
|
+
|
|
781
|
+
api.registerTool({
|
|
782
|
+
name: "x_audience_add_to_activity",
|
|
783
|
+
description: "将受众添加到营销活动。可通过指定用户ID列表或筛选条件批量添加。此操作为异步,可用 x_audience_activity_progress 查询进度。",
|
|
784
|
+
parameters: {
|
|
785
|
+
type: "object",
|
|
786
|
+
properties: {
|
|
787
|
+
activity_id: { type: "number", description: "活动 ID(task_id)" },
|
|
788
|
+
account_id: { type: "number", description: "账号 ID" },
|
|
789
|
+
user_ids: {
|
|
790
|
+
type: "array",
|
|
791
|
+
items: { type: "number" },
|
|
792
|
+
description: "受众 ID 列表(与 filter_conditions 二选一)",
|
|
793
|
+
},
|
|
794
|
+
filter_conditions: {
|
|
795
|
+
type: "object",
|
|
796
|
+
description: "筛选条件(与 user_ids 二选一),字段同受众列表筛选",
|
|
797
|
+
},
|
|
798
|
+
send_type: { type: "string", description: "manual=仅生成内容,immediate=生成后立即发送,默认 manual" },
|
|
799
|
+
},
|
|
800
|
+
required: ["activity_id", "account_id"],
|
|
801
|
+
},
|
|
802
|
+
async execute(_id: string, params: any) {
|
|
803
|
+
try {
|
|
804
|
+
const config = resolveRuntimeConfig(api);
|
|
805
|
+
await verifyApiKey(config);
|
|
806
|
+
const body: any = {
|
|
807
|
+
activity_id: params.activity_id,
|
|
808
|
+
account_id: params.account_id,
|
|
809
|
+
};
|
|
810
|
+
if (params.user_ids) body.user_ids = params.user_ids;
|
|
811
|
+
if (params.filter_conditions) body.filter_conditions = params.filter_conditions;
|
|
812
|
+
if (params.send_type) body.send_type = params.send_type;
|
|
813
|
+
|
|
814
|
+
if (!body.user_ids && !body.filter_conditions) {
|
|
815
|
+
return err("请提供 user_ids 或 filter_conditions 之一。");
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
const resp = await apiRequest(config, "POST", "/v1/api/twitter/following/add-to-activity", {
|
|
819
|
+
body,
|
|
820
|
+
timeoutMs: SLOW_TIMEOUT_MS,
|
|
821
|
+
});
|
|
822
|
+
if (resp.data?.code === 200) {
|
|
823
|
+
const d = resp.data.data;
|
|
824
|
+
return ok(
|
|
825
|
+
`✅ 受众已添加到活动\n` +
|
|
826
|
+
`活动ID: ${d?.activity_id ?? params.activity_id}\n` +
|
|
827
|
+
`添加数量: ${d?.total ?? 0}\n` +
|
|
828
|
+
`${d?.message || "操作成功"}`,
|
|
829
|
+
);
|
|
830
|
+
}
|
|
831
|
+
if (resp.data?.code === 4007) {
|
|
832
|
+
return err("活动不存在,请检查 activity_id 是否正确。");
|
|
833
|
+
}
|
|
834
|
+
return err(`添加到活动失败: ${resp.data?.message || resp.body}`);
|
|
835
|
+
} catch (e: any) {
|
|
836
|
+
return err(classifyError(e.message));
|
|
837
|
+
}
|
|
838
|
+
},
|
|
839
|
+
});
|
|
840
|
+
|
|
841
|
+
api.registerTool({
|
|
842
|
+
name: "x_audience_activity_progress",
|
|
843
|
+
description: "查询受众添加到活动的进度(异步操作的进度轮询)。",
|
|
844
|
+
parameters: {
|
|
845
|
+
type: "object",
|
|
846
|
+
properties: {
|
|
847
|
+
activity_id: { type: "number", description: "活动 ID" },
|
|
848
|
+
account_id: { type: "number", description: "账号 ID" },
|
|
849
|
+
},
|
|
850
|
+
required: ["activity_id", "account_id"],
|
|
851
|
+
},
|
|
852
|
+
async execute(_id: string, params: any) {
|
|
853
|
+
try {
|
|
854
|
+
const config = resolveRuntimeConfig(api);
|
|
855
|
+
await verifyApiKey(config);
|
|
856
|
+
const resp = await apiRequest(config, "GET", "/v1/api/twitter/following/add-to-activity/progress", {
|
|
857
|
+
query: {
|
|
858
|
+
activity_id: String(params.activity_id),
|
|
859
|
+
account_id: String(params.account_id),
|
|
860
|
+
},
|
|
861
|
+
});
|
|
862
|
+
if (resp.data?.code === 200) {
|
|
863
|
+
const d = resp.data.data;
|
|
864
|
+
return ok(
|
|
865
|
+
`📊 活动进度\n` +
|
|
866
|
+
`活动ID: ${d?.activity_id ?? params.activity_id}\n` +
|
|
867
|
+
`总计: ${d?.total ?? 0}\n` +
|
|
868
|
+
`已完成: ${d?.completed ?? 0}\n` +
|
|
869
|
+
`失败: ${d?.failed ?? 0}\n` +
|
|
870
|
+
`进行中: ${d?.in_progress ?? 0}`,
|
|
871
|
+
);
|
|
872
|
+
}
|
|
873
|
+
if (resp.data?.code === 4007) {
|
|
874
|
+
return err("活动不存在或无进度数据。");
|
|
875
|
+
}
|
|
876
|
+
return formatResponse(resp.data);
|
|
877
|
+
} catch (e: any) {
|
|
878
|
+
return err(classifyError(e.message));
|
|
879
|
+
}
|
|
880
|
+
},
|
|
881
|
+
});
|
|
882
|
+
}
|