ppxc-leads-mcp 0.1.10 → 0.1.12
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 +1 -1
- package/dist/browser/platform-runner.js +90 -4
- package/dist/mcp/server.js +261 -30
- package/package.json +1 -1
- package/skills/ppxc-find-customers/SKILL.md +45 -25
package/README.md
CHANGED
|
@@ -63,7 +63,7 @@ Install the whole `skills/ppxc-find-customers/` folder into your assistant's ski
|
|
|
63
63
|
~/.codex/skills/ppxc-find-customers/
|
|
64
64
|
```
|
|
65
65
|
|
|
66
|
-
MCP installation exposes the tools. Skill installation teaches the assistant the safe workflow:
|
|
66
|
+
MCP installation exposes the tools. Skill installation teaches the assistant the safe workflow: read the workflow manifest, ask for the product/service and target platform, run a trial analysis first, show the first two customer signals, then ask the user to log in only when they want to save, unlock, or query the full customer pool. OPC login is not required before a trial scan.
|
|
67
67
|
|
|
68
68
|
## Tools
|
|
69
69
|
|
|
@@ -63,6 +63,19 @@ function randomDelay(adapter) {
|
|
|
63
63
|
const { perItemMinDelayMs, perItemMaxDelayMs } = adapter.risk;
|
|
64
64
|
return perItemMinDelayMs + Math.floor(Math.random() * (perItemMaxDelayMs - perItemMinDelayMs));
|
|
65
65
|
}
|
|
66
|
+
async function emitSearchProgress(handler, event) {
|
|
67
|
+
if (!handler)
|
|
68
|
+
return;
|
|
69
|
+
try {
|
|
70
|
+
await handler(event);
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
log.warn("search progress handler failed", {
|
|
74
|
+
stage: event.stage,
|
|
75
|
+
msg: err instanceof Error ? err.message : String(err),
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
66
79
|
async function ensureOnContentPage(session, contentId, contentUrl) {
|
|
67
80
|
const current = session.currentUrl();
|
|
68
81
|
if (current.includes(contentId)) {
|
|
@@ -142,7 +155,7 @@ async function fetchContentCommentsInner(adapter, contentUrlOrId, maxComments) {
|
|
|
142
155
|
hasMore: fetched.hasMore,
|
|
143
156
|
};
|
|
144
157
|
}
|
|
145
|
-
async function searchKeywordForLeads(platform, keyword, maxItems = 5, commentsPerItem = 30, slotId = SLOT_ID, shouldAbort) {
|
|
158
|
+
async function searchKeywordForLeads(platform, keyword, maxItems = 5, commentsPerItem = 30, slotId = SLOT_ID, shouldAbort, onProgress) {
|
|
146
159
|
const adapter = (0, registry_1.getPlatformAdapter)(platform);
|
|
147
160
|
const kw = String(keyword ?? "").trim();
|
|
148
161
|
if (!kw)
|
|
@@ -185,6 +198,7 @@ async function searchKeywordForLeads(platform, keyword, maxItems = 5, commentsPe
|
|
|
185
198
|
throw new RunnerError("NO_SEARCH_RESULT", `「${kw}」搜到的内容评论都太少,换个词试试`);
|
|
186
199
|
}
|
|
187
200
|
const items = [];
|
|
201
|
+
const readAttempts = [];
|
|
188
202
|
let totalComments = 0;
|
|
189
203
|
for (const it of selected) {
|
|
190
204
|
if (shouldAbort?.()) {
|
|
@@ -193,11 +207,33 @@ async function searchKeywordForLeads(platform, keyword, maxItems = 5, commentsPe
|
|
|
193
207
|
}
|
|
194
208
|
const contentId = String(it.contentId);
|
|
195
209
|
const contentUrl = it.contentUrl || adapter.buildContentUrl(contentId);
|
|
210
|
+
const attempt = {
|
|
211
|
+
contentId,
|
|
212
|
+
contentUrl,
|
|
213
|
+
title: it.title ?? "",
|
|
214
|
+
authorName: it.authorName ?? "",
|
|
215
|
+
estimatedCommentCount: it.commentCount ?? 0,
|
|
216
|
+
status: "failed",
|
|
217
|
+
commentsFetched: 0,
|
|
218
|
+
};
|
|
219
|
+
readAttempts.push(attempt);
|
|
220
|
+
await emitSearchProgress(onProgress, {
|
|
221
|
+
stage: "content_selected",
|
|
222
|
+
platform,
|
|
223
|
+
keyword: kw,
|
|
224
|
+
contentId,
|
|
225
|
+
contentUrl,
|
|
226
|
+
title: attempt.title,
|
|
227
|
+
authorName: attempt.authorName,
|
|
228
|
+
estimatedCommentCount: attempt.estimatedCommentCount,
|
|
229
|
+
});
|
|
196
230
|
try {
|
|
197
231
|
await ensureOnContentPage(session, contentId, contentUrl);
|
|
198
232
|
const fetched = await adapter.fetchComments(session, contentId, contentUrl, commentCount, DEFAULT_FETCH_WAIT_MS);
|
|
199
233
|
if (fetched.json) {
|
|
200
234
|
const comments = adapter.parseComments(contentId, fetched.json);
|
|
235
|
+
attempt.status = "ok";
|
|
236
|
+
attempt.commentsFetched = comments.length;
|
|
201
237
|
totalComments += comments.length;
|
|
202
238
|
items.push({
|
|
203
239
|
contentId,
|
|
@@ -210,13 +246,28 @@ async function searchKeywordForLeads(platform, keyword, maxItems = 5, commentsPe
|
|
|
210
246
|
comments,
|
|
211
247
|
});
|
|
212
248
|
}
|
|
249
|
+
else {
|
|
250
|
+
attempt.error = "评论接口没有返回数据";
|
|
251
|
+
}
|
|
213
252
|
}
|
|
214
253
|
catch (err) {
|
|
254
|
+
attempt.error = err instanceof Error ? err.message : String(err);
|
|
215
255
|
log.warn("search item comment fetch failed", {
|
|
216
256
|
contentId,
|
|
217
|
-
msg:
|
|
257
|
+
msg: attempt.error,
|
|
218
258
|
});
|
|
219
259
|
}
|
|
260
|
+
await emitSearchProgress(onProgress, {
|
|
261
|
+
stage: "content_read",
|
|
262
|
+
platform,
|
|
263
|
+
keyword: kw,
|
|
264
|
+
contentId,
|
|
265
|
+
contentUrl,
|
|
266
|
+
title: attempt.title,
|
|
267
|
+
status: attempt.status === "ok" ? "ok" : "failed",
|
|
268
|
+
commentsFetched: attempt.commentsFetched,
|
|
269
|
+
...(attempt.error ? { error: attempt.error } : {}),
|
|
270
|
+
});
|
|
220
271
|
await sleep(randomDelay(adapter));
|
|
221
272
|
}
|
|
222
273
|
return {
|
|
@@ -224,11 +275,12 @@ async function searchKeywordForLeads(platform, keyword, maxItems = 5, commentsPe
|
|
|
224
275
|
itemsFound: ranked.length,
|
|
225
276
|
itemsRead: items.length,
|
|
226
277
|
items,
|
|
278
|
+
readAttempts,
|
|
227
279
|
videos: items,
|
|
228
280
|
totalComments,
|
|
229
281
|
};
|
|
230
282
|
}
|
|
231
|
-
async function searchKeywordsBatch(platform, keywords, maxItemsPerKeyword = 5, commentsPerItem = 30) {
|
|
283
|
+
async function searchKeywordsBatch(platform, keywords, maxItemsPerKeyword = 5, commentsPerItem = 30, onProgress) {
|
|
232
284
|
const adapter = (0, registry_1.getPlatformAdapter)(platform);
|
|
233
285
|
const cleaned = [];
|
|
234
286
|
const seen = new Set();
|
|
@@ -250,6 +302,13 @@ async function searchKeywordsBatch(platform, keywords, maxItemsPerKeyword = 5, c
|
|
|
250
302
|
}
|
|
251
303
|
assertCrawlQuota(platform, cleaned.length);
|
|
252
304
|
const perKeywordCap = Math.max(1, Math.min(Math.max(1, Math.min(adapter.risk.maxVideosPerKeyword, Math.floor(maxItemsPerKeyword) || 5)), Math.floor(adapter.risk.totalContentBudget / cleaned.length)));
|
|
305
|
+
await emitSearchProgress(onProgress, {
|
|
306
|
+
stage: "batch_start",
|
|
307
|
+
platform,
|
|
308
|
+
keywords: cleaned,
|
|
309
|
+
maxItemsPerKeyword: perKeywordCap,
|
|
310
|
+
commentsPerItem,
|
|
311
|
+
});
|
|
253
312
|
const slotCount = Math.min(MAX_SLOTS, cleaned.length);
|
|
254
313
|
const outcomes = [];
|
|
255
314
|
let verificationHit = false;
|
|
@@ -263,8 +322,22 @@ async function searchKeywordsBatch(platform, keywords, maxItemsPerKeyword = 5, c
|
|
|
263
322
|
continue;
|
|
264
323
|
}
|
|
265
324
|
try {
|
|
266
|
-
|
|
325
|
+
await emitSearchProgress(onProgress, {
|
|
326
|
+
stage: "keyword_start",
|
|
327
|
+
platform,
|
|
328
|
+
keyword: kw,
|
|
329
|
+
});
|
|
330
|
+
const result = await searchKeywordForLeads(platform, kw, perKeywordCap, commentsPerItem, slotId, () => verificationHit, onProgress);
|
|
267
331
|
outcomes.push({ keyword: kw, ok: true, result });
|
|
332
|
+
await emitSearchProgress(onProgress, {
|
|
333
|
+
stage: "keyword_done",
|
|
334
|
+
platform,
|
|
335
|
+
keyword: kw,
|
|
336
|
+
itemsFound: result.itemsFound,
|
|
337
|
+
itemsSelected: result.readAttempts?.length ?? result.items.length,
|
|
338
|
+
itemsRead: result.itemsRead,
|
|
339
|
+
commentsFetched: result.totalComments,
|
|
340
|
+
});
|
|
268
341
|
}
|
|
269
342
|
catch (err) {
|
|
270
343
|
const code = err instanceof RunnerError ? err.code : "INTERNAL";
|
|
@@ -272,10 +345,23 @@ async function searchKeywordsBatch(platform, keywords, maxItemsPerKeyword = 5, c
|
|
|
272
345
|
outcomes.push({ keyword: kw, ok: false, errorCode: code });
|
|
273
346
|
if (code === "VERIFICATION_REQUIRED")
|
|
274
347
|
verificationHit = true;
|
|
348
|
+
await emitSearchProgress(onProgress, {
|
|
349
|
+
stage: "keyword_failed",
|
|
350
|
+
platform,
|
|
351
|
+
keyword: kw,
|
|
352
|
+
errorCode: code,
|
|
353
|
+
});
|
|
275
354
|
}
|
|
276
355
|
}
|
|
277
356
|
};
|
|
278
357
|
await Promise.all(Array.from({ length: slotCount }, (_v, slotId) => runSlot(slotId)));
|
|
358
|
+
await emitSearchProgress(onProgress, {
|
|
359
|
+
stage: "batch_done",
|
|
360
|
+
platform,
|
|
361
|
+
keywords: cleaned,
|
|
362
|
+
completed: outcomes.filter((o) => o.ok).length,
|
|
363
|
+
failed: outcomes.filter((o) => !o.ok).length,
|
|
364
|
+
});
|
|
279
365
|
return outcomes;
|
|
280
366
|
});
|
|
281
367
|
}
|
package/dist/mcp/server.js
CHANGED
|
@@ -97,6 +97,35 @@ function hasProductContext(ctx) {
|
|
|
97
97
|
function productContextHint() {
|
|
98
98
|
return "先告诉我你卖的产品/服务名称,最好再补一句适合谁、解决什么问题;我可以先试跑一轮,给你看前 2 条线索。";
|
|
99
99
|
}
|
|
100
|
+
function traceFromAttempt(attempt) {
|
|
101
|
+
return {
|
|
102
|
+
contentId: attempt.contentId,
|
|
103
|
+
url: attempt.contentUrl,
|
|
104
|
+
...(attempt.title ? { title: attempt.title } : {}),
|
|
105
|
+
...(attempt.authorName ? { authorName: attempt.authorName } : {}),
|
|
106
|
+
estimatedCommentCount: attempt.estimatedCommentCount,
|
|
107
|
+
commentsFetched: attempt.commentsFetched,
|
|
108
|
+
status: attempt.status === "ok" ? "read" : "failed",
|
|
109
|
+
...(attempt.error ? { error: attempt.error } : {}),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
function workflowTraceNarrative(input) {
|
|
113
|
+
const lines = [];
|
|
114
|
+
const kw = input.keywords?.length ? `,关键词:${input.keywords.join("、")}` : "";
|
|
115
|
+
lines.push(`本轮在${input.platformName}执行${input.keywords?.length ? "关键词搜索" : "单链接分析"}${kw}。`);
|
|
116
|
+
lines.push(`实际打开并读取了 ${input.itemsRead} 条内容,送 AI 分析 ${input.commentsAnalyzed} 条评论,识别出 ${input.demandsFound} 个潜在客户。`);
|
|
117
|
+
const visible = input.contents.slice(0, 8);
|
|
118
|
+
for (const item of visible) {
|
|
119
|
+
const title = item.title ? `《${item.title.slice(0, 40)}》` : item.contentId;
|
|
120
|
+
const demandText = typeof item.demandsFound === "number" ? `,识别 ${item.demandsFound} 个线索` : "";
|
|
121
|
+
const statusText = item.status === "read" ? `读了 ${item.commentsAnalyzed ?? item.commentsFetched} 条评论${demandText}` : `读取失败:${item.error ?? "未知原因"}`;
|
|
122
|
+
lines.push(`看过:${title},${statusText},链接:${item.url}`);
|
|
123
|
+
}
|
|
124
|
+
if (input.contents.length > visible.length) {
|
|
125
|
+
lines.push(`还有 ${input.contents.length - visible.length} 条内容已记录在 workflowTrace.contents 里。`);
|
|
126
|
+
}
|
|
127
|
+
return lines;
|
|
128
|
+
}
|
|
100
129
|
function topPickOf(reportLeads) {
|
|
101
130
|
const sorted = [...reportLeads].sort((a, b) => (b.saleScore ?? 0) - (a.saleScore ?? 0));
|
|
102
131
|
const top = sorted[0];
|
|
@@ -117,8 +146,65 @@ function formatCommitteeKeywords(keywords, cap = 40) {
|
|
|
117
146
|
...(k.reason ? { reason: k.reason } : {}),
|
|
118
147
|
}));
|
|
119
148
|
}
|
|
149
|
+
function progressMessage(event, platformName) {
|
|
150
|
+
switch (event.stage) {
|
|
151
|
+
case "batch_start":
|
|
152
|
+
return `开始在${platformName}搜 ${event.keywords.length} 个关键词:${event.keywords.join("、")}`;
|
|
153
|
+
case "keyword_start":
|
|
154
|
+
return `开始搜「${event.keyword}」。`;
|
|
155
|
+
case "content_selected": {
|
|
156
|
+
const title = event.title ? `《${event.title.slice(0, 48)}》` : event.contentId;
|
|
157
|
+
return `打开候选内容 ${title},预估评论 ${event.estimatedCommentCount} 条,链接:${event.contentUrl}`;
|
|
158
|
+
}
|
|
159
|
+
case "content_read": {
|
|
160
|
+
const title = event.title ? `《${event.title.slice(0, 48)}》` : event.contentId;
|
|
161
|
+
if (event.status === "ok") {
|
|
162
|
+
return `读完 ${title},抓到 ${event.commentsFetched} 条评论。`;
|
|
163
|
+
}
|
|
164
|
+
return `这条内容读取失败:${title},原因:${event.error ?? "未知原因"}`;
|
|
165
|
+
}
|
|
166
|
+
case "keyword_done":
|
|
167
|
+
return `「${event.keyword}」完成:候选 ${event.itemsFound} 条,打开 ${event.itemsSelected} 条,读到 ${event.commentsFetched} 条评论。`;
|
|
168
|
+
case "keyword_failed":
|
|
169
|
+
return `「${event.keyword}」没有完成,错误码:${event.errorCode}`;
|
|
170
|
+
case "batch_done":
|
|
171
|
+
return `本轮搜索结束:完成 ${event.completed} 个关键词,失败 ${event.failed} 个。`;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
function createSearchProgressReporter(extra, platformName) {
|
|
175
|
+
let progress = 0;
|
|
176
|
+
return async (event) => {
|
|
177
|
+
progress += 1;
|
|
178
|
+
const message = progressMessage(event, platformName);
|
|
179
|
+
try {
|
|
180
|
+
if (extra._meta?.progressToken !== undefined) {
|
|
181
|
+
await extra.sendNotification({
|
|
182
|
+
method: "notifications/progress",
|
|
183
|
+
params: {
|
|
184
|
+
progressToken: extra._meta.progressToken,
|
|
185
|
+
progress,
|
|
186
|
+
message,
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
await extra.sendNotification({
|
|
191
|
+
method: "notifications/message",
|
|
192
|
+
params: {
|
|
193
|
+
level: event.stage.endsWith("failed") ? "warning" : "info",
|
|
194
|
+
logger: "opc-comment-lead-radar",
|
|
195
|
+
data: {
|
|
196
|
+
message,
|
|
197
|
+
event,
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
}
|
|
120
206
|
const RUNNER_HINTS = {
|
|
121
|
-
LOGIN_REQUIRED: "
|
|
207
|
+
LOGIN_REQUIRED: "平台还没登录。请用 check_status_and_login 对应平台的登录动作扫码,再重试。OPC 账号可等用户要解锁或保存完整名单时再登录。",
|
|
122
208
|
VERIFICATION_REQUIRED: "平台要求验证,已弹出窗口。请完成验证后再重试,不要换词硬刷。",
|
|
123
209
|
FETCH_TIMEOUT: "评论没拉回来,可能是网络或风控。稍等一下再试。",
|
|
124
210
|
BAD_VIDEO_LINK: "链接无法识别,请确认是有效的内容链接。",
|
|
@@ -133,7 +219,7 @@ function runnerErrorText(err, platform) {
|
|
|
133
219
|
const name = (0, registry_1.getPlatformAdapter)(platform).displayName;
|
|
134
220
|
const hints = {
|
|
135
221
|
...RUNNER_HINTS,
|
|
136
|
-
LOGIN_REQUIRED: `${name}还没登录。请先用 check_status_and_login 登录 ${name}
|
|
222
|
+
LOGIN_REQUIRED: `${name}还没登录。请先用 check_status_and_login 登录 ${name},再重试。OPC 账号可等用户要解锁或保存完整名单时再登录。`,
|
|
137
223
|
VERIFICATION_REQUIRED: `${name}要求验证,已弹出窗口。请完成验证后再重试。`,
|
|
138
224
|
BAD_VIDEO_LINK: `这个链接里没认出${name}内容,请确认链接正确。`,
|
|
139
225
|
DAILY_LIMITED: `今天 ${name} 的安全抓取额度已用完,请明天再继续。`,
|
|
@@ -191,7 +277,7 @@ function createMcpServer() {
|
|
|
191
277
|
});
|
|
192
278
|
const registerTool = server.registerTool.bind(server);
|
|
193
279
|
registerTool("check_status_and_login", {
|
|
194
|
-
description: "
|
|
280
|
+
description: "只做状态检查或显式登录。默认 action=status 只返回 OPC 与平台登录态,不弹窗;不要在试用扫描前调用 login_ppxc。用户要解锁、保存完整名单或查询客户池时,才用 action=login_ppxc 弹 OPC 登录窗;平台未登录导致无法抓评论时,才用 login_douyin|login_xiaohongshu|login_kuaishou 弹对应平台扫码窗。",
|
|
195
281
|
inputSchema: {
|
|
196
282
|
action: zod_1.z
|
|
197
283
|
.enum([
|
|
@@ -202,7 +288,7 @@ function createMcpServer() {
|
|
|
202
288
|
"login_ppxc",
|
|
203
289
|
])
|
|
204
290
|
.optional()
|
|
205
|
-
.describe("status
|
|
291
|
+
.describe("status=只查不弹窗;login_douyin/login_xiaohongshu/login_kuaishou=平台扫码;login_ppxc=用户要解锁/保存/查客户池时才登录 OPC。"),
|
|
206
292
|
},
|
|
207
293
|
}, async (args) => {
|
|
208
294
|
const action = args.action ?? "status";
|
|
@@ -247,21 +333,24 @@ function createMcpServer() {
|
|
|
247
333
|
}
|
|
248
334
|
const platforms = await allPlatformLoginStatus();
|
|
249
335
|
const ppxc = (0, ppxc_login_window_1.isPpxcLoggedIn)();
|
|
250
|
-
const
|
|
336
|
+
const missingPlatforms = [];
|
|
251
337
|
for (const adapter of (0, registry_1.listPlatformAdapters)()) {
|
|
252
338
|
if (!platforms[adapter.id]) {
|
|
253
|
-
|
|
339
|
+
missingPlatforms.push(`${adapter.displayName}(action=login_${adapter.id === "xiaohongshu" ? "xiaohongshu" : adapter.id})`);
|
|
254
340
|
}
|
|
255
341
|
}
|
|
256
|
-
if (!ppxc)
|
|
257
|
-
missing.push("OPC 账号(action=login_ppxc)");
|
|
258
342
|
return jsonText({
|
|
259
343
|
ok: true,
|
|
260
344
|
platforms,
|
|
261
345
|
ppxcLoggedIn: ppxc,
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
346
|
+
trialScanAvailable: true,
|
|
347
|
+
fullPoolRequiresPpxcLogin: !ppxc,
|
|
348
|
+
missingPlatforms,
|
|
349
|
+
hint: missingPlatforms.length === 0
|
|
350
|
+
? ppxc
|
|
351
|
+
? "平台与 OPC 账号都已登录,可以完整找客户并保存到客户池。"
|
|
352
|
+
: "平台已登录,OPC 账号未登录也可以先试用扫描并展示前 2 条线索;用户要解锁、保存完整名单或查客户池时,再调用 action=login_ppxc。"
|
|
353
|
+
: `试用扫描不要求先登录 OPC,但抓评论需要对应平台登录:${missingPlatforms.join("、")}。请只弹平台扫码窗;不要先弹 OPC 登录窗。`,
|
|
265
354
|
workflowManifest,
|
|
266
355
|
logFile: logger_1.LOG_FILE_PATH,
|
|
267
356
|
});
|
|
@@ -284,7 +373,7 @@ function createMcpServer() {
|
|
|
284
373
|
}
|
|
285
374
|
});
|
|
286
375
|
registerTool("list_products", {
|
|
287
|
-
description: "列出当前 OPC 账号下的产品,返回 id
|
|
376
|
+
description: "列出当前 OPC 账号下的产品,返回 id 和名称。仅当用户已登录且明确要用已保存产品/客户池时调用;未登录试用扫描不要调用本工具,直接把 productName/productDescription 传给 analyze_video_comments 或 search_keyword_for_leads。",
|
|
288
377
|
inputSchema: {},
|
|
289
378
|
}, async () => {
|
|
290
379
|
try {
|
|
@@ -293,8 +382,8 @@ function createMcpServer() {
|
|
|
293
382
|
ok: true,
|
|
294
383
|
products,
|
|
295
384
|
hint: products.length === 0
|
|
296
|
-
? "
|
|
297
|
-
: "
|
|
385
|
+
? "这个账号下还没有已保存产品。不要因此中断试用扫描;请让用户给产品/服务名称和搜索词,直接调用 search_keyword_for_leads 或 analyze_video_comments。只有用户要保存完整名单、查客户池或长期复盘时,再引导他到 OPC 创建产品。"
|
|
386
|
+
: "只有用户明确要使用已保存产品时,才把其中一个产品的 id 作为 productId;普通试用扫描仍可直接传 productName/productDescription。",
|
|
298
387
|
});
|
|
299
388
|
}
|
|
300
389
|
catch (err) {
|
|
@@ -302,7 +391,7 @@ function createMcpServer() {
|
|
|
302
391
|
return jsonText({
|
|
303
392
|
ok: false,
|
|
304
393
|
code: "PPXC_LOGIN_REQUIRED",
|
|
305
|
-
userHint: "OPC
|
|
394
|
+
userHint: "产品列表只用于已登录完整模式。若用户只是试用扫描,不要先登录 OPC;请收集 productName/productDescription 和搜索词,直接调用 search_keyword_for_leads。",
|
|
306
395
|
});
|
|
307
396
|
}
|
|
308
397
|
return jsonText({
|
|
@@ -315,7 +404,7 @@ function createMcpServer() {
|
|
|
315
404
|
});
|
|
316
405
|
registerTool("analyze_video_comments", {
|
|
317
406
|
description: "给一条抖音/小红书/快手内容链接 + productId 或 productName:读评论 → OPC AI 分析 → 客户战报。未登录用户可只传 productName 先试跑,返回前 2 条线索;登录后传 productId 才会落客户池。platform 可省略,会从链接自动识别。" +
|
|
318
|
-
"
|
|
407
|
+
"工具会通过 MCP 通知实时汇报进度,并在结果里返回 workflowTrace/processNarrative。reportFile 只是本机临时战报备份,不是解锁入口;用户要看剩余线索或保存完整名单时,应引导登录 OPC 网页端客户池。" +
|
|
319
408
|
"结果里的 contentAngles 是从这批评论提炼的内容选题方向(拍什么/为什么/客户原话),汇报后可主动提议「要不要根据这些评论写下一条视频脚本」。",
|
|
320
409
|
inputSchema: {
|
|
321
410
|
videoUrl: zod_1.z.string().describe("内容链接(抖音视频/小红书笔记/快手视频)或 ID。"),
|
|
@@ -331,12 +420,14 @@ function createMcpServer() {
|
|
|
331
420
|
maxComments: zod_1.z.number().int().min(1).max(50).optional().describe("最多读多少条评论,默认 30。"),
|
|
332
421
|
save: zod_1.z.boolean().optional().describe("是否落客户池,默认 true。"),
|
|
333
422
|
},
|
|
334
|
-
}, async (args) => {
|
|
423
|
+
}, async (args, extra) => {
|
|
335
424
|
const videoUrl = args.videoUrl;
|
|
336
425
|
const productContext = productContextFromArgs(args);
|
|
337
426
|
const maxComments = args.maxComments;
|
|
338
427
|
const save = args.save;
|
|
339
428
|
const platform = (0, detect_platform_1.normalizePlatformId)(args.platform, videoUrl);
|
|
429
|
+
const adapter = (0, registry_1.getPlatformAdapter)(platform);
|
|
430
|
+
const reportProgress = createSearchProgressReporter(extra, adapter.displayName);
|
|
340
431
|
if (!hasProductContext(productContext)) {
|
|
341
432
|
return jsonText({
|
|
342
433
|
ok: false,
|
|
@@ -346,9 +437,40 @@ function createMcpServer() {
|
|
|
346
437
|
}
|
|
347
438
|
let fetched;
|
|
348
439
|
try {
|
|
440
|
+
await reportProgress({
|
|
441
|
+
stage: "content_selected",
|
|
442
|
+
platform,
|
|
443
|
+
keyword: "单链接分析",
|
|
444
|
+
contentId: videoUrl,
|
|
445
|
+
contentUrl: videoUrl,
|
|
446
|
+
title: "",
|
|
447
|
+
authorName: "",
|
|
448
|
+
estimatedCommentCount: maxComments ?? 30,
|
|
449
|
+
});
|
|
349
450
|
fetched = await (0, platform_runner_1.fetchContentComments)(platform, videoUrl, maxComments ?? 30);
|
|
451
|
+
await reportProgress({
|
|
452
|
+
stage: "content_read",
|
|
453
|
+
platform,
|
|
454
|
+
keyword: "单链接分析",
|
|
455
|
+
contentId: fetched.contentId,
|
|
456
|
+
contentUrl: fetched.contentUrl,
|
|
457
|
+
title: "",
|
|
458
|
+
status: "ok",
|
|
459
|
+
commentsFetched: fetched.comments.length,
|
|
460
|
+
});
|
|
350
461
|
}
|
|
351
462
|
catch (err) {
|
|
463
|
+
await reportProgress({
|
|
464
|
+
stage: "content_read",
|
|
465
|
+
platform,
|
|
466
|
+
keyword: "单链接分析",
|
|
467
|
+
contentId: videoUrl,
|
|
468
|
+
contentUrl: videoUrl,
|
|
469
|
+
title: "",
|
|
470
|
+
status: "failed",
|
|
471
|
+
commentsFetched: 0,
|
|
472
|
+
error: err instanceof Error ? err.message : String(err),
|
|
473
|
+
});
|
|
352
474
|
if (err instanceof platform_runner_1.RunnerError) {
|
|
353
475
|
return jsonText(runnerErrorText(err, platform));
|
|
354
476
|
}
|
|
@@ -382,7 +504,7 @@ function createMcpServer() {
|
|
|
382
504
|
if (reportLeads.length > 0) {
|
|
383
505
|
const exported = (0, battle_report_1.exportBattleReport)({
|
|
384
506
|
kind: "link",
|
|
385
|
-
platformName:
|
|
507
|
+
platformName: adapter.displayName,
|
|
386
508
|
productName: productContext.productName || (productContext.productId ? await productNameOf(productContext.productId) : undefined),
|
|
387
509
|
contentUrl: fetched.contentUrl,
|
|
388
510
|
summary: analysis.summary,
|
|
@@ -406,10 +528,39 @@ function createMcpServer() {
|
|
|
406
528
|
const lockedTail = paywallLocked
|
|
407
529
|
? `你当前是体验版,只解锁了前 ${reportLeads.length} 个完整客户;${paywall.unlockHint}`
|
|
408
530
|
: "";
|
|
531
|
+
const contentTrace = {
|
|
532
|
+
contentId: fetched.contentId,
|
|
533
|
+
url: fetched.contentUrl,
|
|
534
|
+
commentsFetched: fetched.comments.length,
|
|
535
|
+
commentsAnalyzed: analysis.summary.commentsAnalyzed,
|
|
536
|
+
demandsFound: analysis.summary.demandsFound,
|
|
537
|
+
highIntentCount: analysis.summary.highIntentCount,
|
|
538
|
+
status: "read",
|
|
539
|
+
};
|
|
540
|
+
const processNarrative = workflowTraceNarrative({
|
|
541
|
+
platformName: adapter.displayName,
|
|
542
|
+
itemsRead: 1,
|
|
543
|
+
commentsAnalyzed: analysis.summary.commentsAnalyzed,
|
|
544
|
+
demandsFound: analysis.summary.demandsFound,
|
|
545
|
+
contents: [contentTrace],
|
|
546
|
+
});
|
|
409
547
|
return jsonText({
|
|
410
548
|
ok: true,
|
|
411
549
|
platform,
|
|
412
550
|
content: { id: fetched.contentId, url: fetched.contentUrl },
|
|
551
|
+
workflowTrace: {
|
|
552
|
+
mode: "single_content",
|
|
553
|
+
platform,
|
|
554
|
+
contents: [contentTrace],
|
|
555
|
+
totals: {
|
|
556
|
+
itemsRead: 1,
|
|
557
|
+
commentsFetched: fetched.comments.length,
|
|
558
|
+
commentsAnalyzed: analysis.summary.commentsAnalyzed,
|
|
559
|
+
demandsFound: analysis.summary.demandsFound,
|
|
560
|
+
highIntentCount: analysis.summary.highIntentCount,
|
|
561
|
+
},
|
|
562
|
+
},
|
|
563
|
+
processNarrative,
|
|
413
564
|
summary: {
|
|
414
565
|
...analysis.summary,
|
|
415
566
|
commentsFetched: fetched.comments.length,
|
|
@@ -427,8 +578,8 @@ function createMcpServer() {
|
|
|
427
578
|
? {
|
|
428
579
|
reportFile: report.file,
|
|
429
580
|
reportHint: paywallLocked
|
|
430
|
-
?
|
|
431
|
-
:
|
|
581
|
+
? `已生成本机临时战报备份,只展示已解锁的前 ${reportLeads.length} 个客户;它不是解锁入口。要看剩余线索,请登录 OPC 网页端客户池并开通/解锁完整名单。备份文件在:${report.file}`
|
|
582
|
+
: `已生成本机临时战报备份(含话术,可转发同事照着跟进),放在:${report.file}`,
|
|
432
583
|
}
|
|
433
584
|
: {}),
|
|
434
585
|
});
|
|
@@ -477,7 +628,7 @@ function createMcpServer() {
|
|
|
477
628
|
});
|
|
478
629
|
registerTool("search_keyword_for_leads", {
|
|
479
630
|
description: "关键词 + productId 或 productName + platform:搜内容 → 读评论 → AI 分析 → 汇总战报。未登录用户可只传 productName 先试跑,返回前 2 条线索;登录后传 productId 才会落客户池。支持 douyin/xiaohongshu/kuaishou。多关键词 2 窗口并行(抖音);小红书/快手更保守。" +
|
|
480
|
-
"
|
|
631
|
+
"工具会通过 MCP 通知实时汇报搜了哪些词、打开哪些内容、每条读了多少评论;最终结果也会返回 workflowTrace/processNarrative。reportFile 只是本机临时战报备份,不是解锁入口;用户要看剩余线索或保存完整名单时,应引导登录 OPC 网页端客户池。" +
|
|
481
632
|
"结果里的 contentAngles 是从这批评论提炼的内容选题方向(拍什么/为什么/客户原话),汇报后可主动提议「要不要根据这些评论写下一条视频脚本」。",
|
|
482
633
|
inputSchema: {
|
|
483
634
|
platform: PLATFORM_ZOD.describe("在哪个平台搜:douyin | xiaohongshu | kuaishou。"),
|
|
@@ -501,7 +652,7 @@ function createMcpServer() {
|
|
|
501
652
|
.describe("每个关键词最多读几条内容;合计有平台上限(抖音 12、小红书/快手 8)。"),
|
|
502
653
|
save: zod_1.z.boolean().optional().describe("是否落客户池,默认 true。"),
|
|
503
654
|
},
|
|
504
|
-
}, async (args) => {
|
|
655
|
+
}, async (args, extra) => {
|
|
505
656
|
const platform = (0, detect_platform_1.normalizePlatformId)(args.platform);
|
|
506
657
|
const keywords = args.keywords ?? [];
|
|
507
658
|
const productContext = productContextFromArgs(args);
|
|
@@ -516,7 +667,7 @@ function createMcpServer() {
|
|
|
516
667
|
}
|
|
517
668
|
let outcomes;
|
|
518
669
|
try {
|
|
519
|
-
outcomes = await (0, platform_runner_1.searchKeywordsBatch)(platform, keywords, maxVideosPerKeyword ?? 5);
|
|
670
|
+
outcomes = await (0, platform_runner_1.searchKeywordsBatch)(platform, keywords, maxVideosPerKeyword ?? 5, 30, createSearchProgressReporter(extra, adapter.displayName));
|
|
520
671
|
}
|
|
521
672
|
catch (err) {
|
|
522
673
|
if (err instanceof platform_runner_1.RunnerError) {
|
|
@@ -537,19 +688,48 @@ function createMcpServer() {
|
|
|
537
688
|
let skippedDuplicates = 0;
|
|
538
689
|
let saveFailedCount = 0;
|
|
539
690
|
let itemsRead = 0;
|
|
691
|
+
let commentsFetched = 0;
|
|
540
692
|
let lockedFromBackend = 0;
|
|
541
693
|
let anyLocked = false;
|
|
542
694
|
const perKeyword = [];
|
|
695
|
+
const workflowKeywords = [];
|
|
543
696
|
for (const oc of outcomes) {
|
|
544
697
|
if (!oc.ok || !oc.result) {
|
|
545
698
|
perKeyword.push({ keyword: oc.keyword, ok: false, code: oc.errorCode });
|
|
699
|
+
workflowKeywords.push({
|
|
700
|
+
keyword: oc.keyword,
|
|
701
|
+
ok: false,
|
|
702
|
+
errorCode: oc.errorCode,
|
|
703
|
+
contents: [],
|
|
704
|
+
});
|
|
546
705
|
continue;
|
|
547
706
|
}
|
|
548
707
|
let kwDemands = 0;
|
|
708
|
+
let kwCommentsAnalyzed = 0;
|
|
709
|
+
let kwHighIntentCount = 0;
|
|
549
710
|
const contentItems = oc.result.items ?? oc.result.videos ?? [];
|
|
711
|
+
const contentTraceById = new Map();
|
|
712
|
+
for (const attempt of oc.result.readAttempts ?? []) {
|
|
713
|
+
contentTraceById.set(attempt.contentId, traceFromAttempt(attempt));
|
|
714
|
+
}
|
|
550
715
|
for (const v of contentItems) {
|
|
551
716
|
itemsRead += 1;
|
|
552
717
|
const itemUrl = v.contentUrl ?? v.videoUrl ?? "";
|
|
718
|
+
commentsFetched += v.comments.length;
|
|
719
|
+
const contentTrace = contentTraceById.get(v.contentId) ??
|
|
720
|
+
{
|
|
721
|
+
contentId: v.contentId,
|
|
722
|
+
url: itemUrl,
|
|
723
|
+
title: v.title,
|
|
724
|
+
authorName: v.authorName,
|
|
725
|
+
estimatedCommentCount: v.commentCount,
|
|
726
|
+
commentsFetched: v.comments.length,
|
|
727
|
+
status: "read",
|
|
728
|
+
};
|
|
729
|
+
contentTrace.status = "read";
|
|
730
|
+
contentTrace.commentsFetched = v.comments.length;
|
|
731
|
+
contentTrace.url = itemUrl || contentTrace.url;
|
|
732
|
+
contentTraceById.set(v.contentId, contentTrace);
|
|
553
733
|
try {
|
|
554
734
|
const analysis = await (0, ppxc_client_1.analyzeComments)({
|
|
555
735
|
...productContext,
|
|
@@ -560,9 +740,14 @@ function createMcpServer() {
|
|
|
560
740
|
save: save !== false,
|
|
561
741
|
});
|
|
562
742
|
commentsAnalyzed += analysis.summary.commentsAnalyzed;
|
|
743
|
+
kwCommentsAnalyzed += analysis.summary.commentsAnalyzed;
|
|
563
744
|
demandsFound += analysis.summary.demandsFound;
|
|
564
745
|
kwDemands += analysis.summary.demandsFound;
|
|
565
746
|
highIntentCount += analysis.summary.highIntentCount;
|
|
747
|
+
kwHighIntentCount += analysis.summary.highIntentCount;
|
|
748
|
+
contentTrace.commentsAnalyzed = analysis.summary.commentsAnalyzed;
|
|
749
|
+
contentTrace.demandsFound = analysis.summary.demandsFound;
|
|
750
|
+
contentTrace.highIntentCount = analysis.summary.highIntentCount;
|
|
566
751
|
savedToPool += analysis.saved;
|
|
567
752
|
skippedDuplicates += analysis.skippedDuplicates ?? 0;
|
|
568
753
|
if (analysis.saveFailed === true)
|
|
@@ -606,9 +791,24 @@ function createMcpServer() {
|
|
|
606
791
|
perKeyword.push({
|
|
607
792
|
keyword: oc.keyword,
|
|
608
793
|
ok: true,
|
|
794
|
+
itemsFound: oc.result.itemsFound,
|
|
609
795
|
itemsRead: contentItems.length,
|
|
796
|
+
commentsFetched: oc.result.totalComments,
|
|
797
|
+
commentsAnalyzed: kwCommentsAnalyzed,
|
|
610
798
|
demands: kwDemands,
|
|
611
799
|
});
|
|
800
|
+
workflowKeywords.push({
|
|
801
|
+
keyword: oc.keyword,
|
|
802
|
+
ok: true,
|
|
803
|
+
itemsFound: oc.result.itemsFound,
|
|
804
|
+
itemsSelected: oc.result.readAttempts?.length ?? contentItems.length,
|
|
805
|
+
itemsRead: contentItems.length,
|
|
806
|
+
commentsFetched: oc.result.totalComments,
|
|
807
|
+
commentsAnalyzed: kwCommentsAnalyzed,
|
|
808
|
+
demandsFound: kwDemands,
|
|
809
|
+
highIntentCount: kwHighIntentCount,
|
|
810
|
+
contents: [...contentTraceById.values()],
|
|
811
|
+
});
|
|
612
812
|
}
|
|
613
813
|
const hitVerification = outcomes.some((o) => !o.ok && o.errorCode === "VERIFICATION_REQUIRED");
|
|
614
814
|
const hitLoginRequired = outcomes.some((o) => !o.ok && o.errorCode === "LOGIN_REQUIRED");
|
|
@@ -619,6 +819,7 @@ function createMcpServer() {
|
|
|
619
819
|
code: "VERIFICATION_REQUIRED",
|
|
620
820
|
userHint: `${adapter.displayName}要求验证,已弹出窗口,本次搜索已停止。请先完成验证,不要换词重试。`,
|
|
621
821
|
perKeyword,
|
|
822
|
+
workflowTrace: { mode: "keyword_search", platform, keywords: workflowKeywords },
|
|
622
823
|
});
|
|
623
824
|
}
|
|
624
825
|
if (hitLoginRequired) {
|
|
@@ -627,6 +828,7 @@ function createMcpServer() {
|
|
|
627
828
|
code: "LOGIN_REQUIRED",
|
|
628
829
|
userHint: `${adapter.displayName}登录已失效,请先登录后再重试。`,
|
|
629
830
|
perKeyword,
|
|
831
|
+
workflowTrace: { mode: "keyword_search", platform, keywords: workflowKeywords },
|
|
630
832
|
});
|
|
631
833
|
}
|
|
632
834
|
return jsonText({
|
|
@@ -634,6 +836,7 @@ function createMcpServer() {
|
|
|
634
836
|
code: "EMPTY_COMMENTS",
|
|
635
837
|
userHint: "搜到的内容没读到可分析的评论,换个词试试。",
|
|
636
838
|
perKeyword,
|
|
839
|
+
workflowTrace: { mode: "keyword_search", platform, keywords: workflowKeywords },
|
|
637
840
|
});
|
|
638
841
|
}
|
|
639
842
|
allLeads.sort((a, b) => (b.saleScore ?? 0) - (a.saleScore ?? 0));
|
|
@@ -675,14 +878,38 @@ function createMcpServer() {
|
|
|
675
878
|
const lockedTail = paywallLocked
|
|
676
879
|
? `你当前是体验版,只解锁了前 ${visibleAllLeads.length} 个完整客户;${unlockHint}`
|
|
677
880
|
: "";
|
|
881
|
+
const allContentTrace = workflowKeywords.flatMap((kw) => kw.contents);
|
|
882
|
+
const processNarrative = workflowTraceNarrative({
|
|
883
|
+
platformName: adapter.displayName,
|
|
884
|
+
keywords: outcomes.map((o) => o.keyword),
|
|
885
|
+
itemsRead,
|
|
886
|
+
commentsAnalyzed,
|
|
887
|
+
demandsFound,
|
|
888
|
+
contents: allContentTrace,
|
|
889
|
+
});
|
|
678
890
|
return jsonText({
|
|
679
891
|
ok: true,
|
|
680
892
|
platform,
|
|
681
893
|
keywords: outcomes.map((o) => o.keyword),
|
|
682
894
|
warning,
|
|
895
|
+
workflowTrace: {
|
|
896
|
+
mode: "keyword_search",
|
|
897
|
+
platform,
|
|
898
|
+
keywords: workflowKeywords,
|
|
899
|
+
totals: {
|
|
900
|
+
keywordsSearched: outcomes.length,
|
|
901
|
+
itemsRead,
|
|
902
|
+
commentsFetched,
|
|
903
|
+
commentsAnalyzed,
|
|
904
|
+
demandsFound,
|
|
905
|
+
highIntentCount,
|
|
906
|
+
},
|
|
907
|
+
},
|
|
908
|
+
processNarrative,
|
|
683
909
|
summary: {
|
|
684
910
|
keywordsSearched: outcomes.length,
|
|
685
911
|
itemsRead,
|
|
912
|
+
commentsFetched,
|
|
686
913
|
commentsAnalyzed,
|
|
687
914
|
demandsFound,
|
|
688
915
|
highIntentCount,
|
|
@@ -701,19 +928,19 @@ function createMcpServer() {
|
|
|
701
928
|
? {
|
|
702
929
|
reportFile: report.file,
|
|
703
930
|
reportHint: paywallLocked
|
|
704
|
-
?
|
|
705
|
-
:
|
|
931
|
+
? `已生成本机临时战报备份,只展示已解锁的前 ${visibleAllLeads.length} 个客户;它不是解锁入口。要看剩余线索,请登录 OPC 网页端客户池并开通/解锁完整名单。备份文件在:${report.file}`
|
|
932
|
+
: `已生成本机临时战报备份(含每个词的成绩和话术,可转发同事照着跟进),放在:${report.file}`,
|
|
706
933
|
}
|
|
707
934
|
: {}),
|
|
708
935
|
});
|
|
709
936
|
});
|
|
710
937
|
registerTool("suggest_search_keywords", {
|
|
711
|
-
description: "
|
|
938
|
+
description: "完整模式专用:给已保存产品的 productId,返回 OPC 想词委员会为该产品生成的精选搜索词(带词型分类和推荐理由)。新用户/未登录试用/没有产品列表时不要调用本工具;应先向用户要产品/服务名称和 1-3 个朴素搜索词,或根据产品描述自己建议词,然后直接调用 search_keyword_for_leads 并传 productName/productDescription。" +
|
|
712
939
|
"词单通常在产品创建/编辑时已自动生成,本工具默认直接读现成结果;没有现成词单时会触发一轮生成并等待(约 1 分钟)。" +
|
|
713
|
-
"
|
|
940
|
+
"只有用户已登录且明确选择了已保存产品时,才在开搜前调它,从主力词里挑 3~6 个传给 search_keyword_for_leads。" +
|
|
714
941
|
"regenerate=true 强制重新生成(消耗用户电力,仅当用户明确要求换一批词时使用)。",
|
|
715
942
|
inputSchema: {
|
|
716
|
-
productId: zod_1.z.string().describe("
|
|
943
|
+
productId: zod_1.z.string().describe("已登录完整模式的产品 id,用 list_products 获取;试用扫描不要为了拿 productId 要求用户登录或创建产品。"),
|
|
717
944
|
regenerate: zod_1.z
|
|
718
945
|
.boolean()
|
|
719
946
|
.optional()
|
|
@@ -723,7 +950,11 @@ function createMcpServer() {
|
|
|
723
950
|
const productId = args.productId;
|
|
724
951
|
const regenerate = args.regenerate === true;
|
|
725
952
|
if (!productId || typeof productId !== "string") {
|
|
726
|
-
return jsonText({
|
|
953
|
+
return jsonText({
|
|
954
|
+
ok: false,
|
|
955
|
+
code: "FULL_MODE_ONLY",
|
|
956
|
+
userHint: "想词委员会只用于已登录且已有产品的完整模式。新用户或试用扫描不要先建产品;请让用户给产品/服务名称和 1-3 个搜索词,然后直接调用 search_keyword_for_leads,传 productName/productDescription。",
|
|
957
|
+
});
|
|
727
958
|
}
|
|
728
959
|
try {
|
|
729
960
|
let current = await (0, ppxc_client_1.getCommitteeKeywords)(productId);
|
|
@@ -774,7 +1005,7 @@ function createMcpServer() {
|
|
|
774
1005
|
return jsonText({
|
|
775
1006
|
ok: false,
|
|
776
1007
|
code: "PPXC_LOGIN_REQUIRED",
|
|
777
|
-
userHint: "OPC
|
|
1008
|
+
userHint: "想词委员会需要 OPC 登录和已保存产品。若用户只是试用找客户,不要在这里卡住;请改为收集 productName/productDescription 和搜索词,直接调用 search_keyword_for_leads。只有用户明确要用已保存产品时,再登录 OPC。",
|
|
778
1009
|
});
|
|
779
1010
|
}
|
|
780
1011
|
if (err instanceof ppxc_client_1.PpxcApiError && err.status === 403) {
|
package/package.json
CHANGED
|
@@ -15,6 +15,7 @@ description: OPC 评论线索雷达:找客户、销售线索、评论分析、
|
|
|
15
15
|
- 不要把它叫成「PPXC 后台」「PPXC 后端」「本机后台」。用户不需要、也不能自己启动一个 PPXC 后台。
|
|
16
16
|
- MCP 工具不可用时,判断为「连接器没有启用 / MCP 配置没有生效 / 宿主还没重启」,不要说「后台没起来」。
|
|
17
17
|
- 不要一上来要求用户登录 OPC。先让用户看到试用结果:用产品/服务描述 + 平台链接或关键词跑一次,展示前 2 条客户线索;用户要保存、看完整名单或解锁更多时,再引导登录 OPC。
|
|
18
|
+
- **已登录不等于要走产品列表。** 即使 `check_status_and_login` 显示 OPC 已登录、`list_products` 能列出旧产品,也不要自动使用旧产品;用户只是“测试一下 / 找客户 / 扫描评论”时,仍然先按试用路径收产品/服务描述并直接搜索。只有用户明确说「用我保存的产品」「查客户池」「保存完整名单」「继续跟进历史客户」时,才进入产品列表/客户池路径。
|
|
18
19
|
|
|
19
20
|
## 第 0 步:自检与自动接线(工具不可用时才走)
|
|
20
21
|
|
|
@@ -40,15 +41,15 @@ description: OPC 评论线索雷达:找客户、销售线索、评论分析、
|
|
|
40
41
|
|
|
41
42
|
3. **告诉用户**:「OPC 评论线索雷达的 MCP 配置已经加好了。首次启动时,智能体会按这条配置拉起 MCP 运行包(约一两分钟,取决于网络)。」
|
|
42
43
|
4. **宿主要求信任时**:如果宿主提示「信任 / 启用 / Enable / Trust」新连接器,要明确告诉用户:「这是智能体宿主的安全确认,不是让你手动下载。请在连接器管理里信任/启用 `ppxc-leads` 或 `ppxc-find-customers`,点完回来告诉我,我继续试跑找客户。」不要把用户甩去自己研究配置。
|
|
43
|
-
5.
|
|
44
|
+
5. **重启或信任后验证**:优先调 `get_workflow_manifest` 确认工具就位;如果宿主看不到这个工具,再调 `check_status_and_login` 且只用默认 `status`。确认后从第 1 步继续,**不要**因此弹 OPC 登录窗。
|
|
44
45
|
6. **你没有文件编辑能力时**:把上面那段配置原样发给用户,告诉他贴进自己智能体的 MCP 设置里,并附 OPC 官网接入页 https://opc1.me/download/mcp(有逐家图文步骤)。注意:这个页面只是接入说明,不是登录窗口。
|
|
45
46
|
|
|
46
|
-
##
|
|
47
|
+
## 动态工作流优先(每次开始都先做,但不要弹登录窗)
|
|
47
48
|
|
|
48
49
|
本 Skill 不是完整业务逻辑的唯一来源。OPC 后端会持续进化找客户流程,所以每次开始找客户、复盘客户池或处理用户反馈前,必须先读一次当前动态工作流:
|
|
49
50
|
|
|
50
|
-
1. 优先调 `
|
|
51
|
-
2.
|
|
51
|
+
1. 优先调 `get_workflow_manifest` 读取最新作战手册。
|
|
52
|
+
2. 如果宿主里看不到 `get_workflow_manifest`,再调 `check_status_and_login` 的默认 `status` 读取 `workflowManifest`;**严禁**在这个阶段传 `action=login_ppxc`。
|
|
52
53
|
3. 如果动态工作流读取失败,不要中断找客户;继续按本文内置流程执行,并告诉用户“后端动态工作流暂时不可用,先用本地流程继续”。
|
|
53
54
|
4. 如果返回里有 `skill.updateHint` 或 `skill.updateCommand`,在合适时机提醒用户:“OPC 评论线索雷达 Skill 有新版流程,可按官网或这条命令更新。”
|
|
54
55
|
|
|
@@ -56,22 +57,40 @@ description: OPC 评论线索雷达:找客户、销售线索、评论分析、
|
|
|
56
57
|
|
|
57
58
|
## 标准流程(按顺序)
|
|
58
59
|
|
|
59
|
-
### 第 1
|
|
60
|
+
### 第 1 步:先收产品上下文,不先登录 OPC
|
|
60
61
|
|
|
61
|
-
|
|
62
|
+
用户说“测试一下 / 帮我找客户 / 扫描评论 / 分析评论区”时,先拿试用扫描所需的最少信息:
|
|
62
63
|
|
|
63
|
-
-
|
|
64
|
-
-
|
|
65
|
-
-
|
|
64
|
+
- 产品/服务:至少要有 `productName`,能补 `productDescription / sellingPoints / targetPersona` 更好。
|
|
65
|
+
- 平台:抖音 / 小红书 / 快手,用户没说就问一句。
|
|
66
|
+
- 入口:用户给了视频/笔记链接就直接分析链接;没给链接就要 1~3 个关键词,或根据产品描述先建议 1~3 个朴素搜索词给用户确认。
|
|
66
67
|
|
|
67
|
-
|
|
68
|
+
不要一上来调 `check_status_and_login(action=login_ppxc)`,也不要先调 `list_products`。OPC 登录只在用户要看剩余线索、保存完整名单或查询客户池时发生。
|
|
69
|
+
|
|
70
|
+
禁止路径:
|
|
71
|
+
|
|
72
|
+
- 不要因为用户说“测试一下”就先登录 OPC。
|
|
73
|
+
- 不要因为用户已经登录就自动调用 `list_products`。
|
|
74
|
+
- 不要因为没有产品列表就要求用户注册、建产品或补后台资料。
|
|
75
|
+
- 不要为了调用 `suggest_search_keywords` 去要求 productId;试用阶段先用朴素搜索词跑出结果。
|
|
76
|
+
|
|
77
|
+
### 第 2 步:平台登录只在抓评论需要时处理
|
|
78
|
+
|
|
79
|
+
试用扫描也需要借用户自己的平台登录态抓公开评论,但这不是 OPC 登录:
|
|
80
|
+
|
|
81
|
+
- 先直接调 `search_keyword_for_leads` 或 `analyze_video_comments`,传 `productName/productDescription` 走未登录试用模式。
|
|
82
|
+
- 如果工具返回 `LOGIN_REQUIRED`,只针对对应平台调 `check_status_and_login`:`action=login_douyin / login_xiaohongshu / login_kuaishou`,请用户用对应 App 扫码。
|
|
83
|
+
- OPC 账号未登录 → 继续试用扫描,不弹 OPC 登录窗。
|
|
84
|
+
- 如果弹出的窗口是“接入说明页”而不是登录表单,告诉用户这是配置地址误填或旧包问题:先关闭窗口,更新到新版 `ppxc-leads-mcp`,再重新调用对应动作。
|
|
85
|
+
|
|
86
|
+
### 第 3 步:已登录完整模式才拿产品
|
|
68
87
|
|
|
69
88
|
优先让用户先看到结果:
|
|
70
89
|
|
|
71
90
|
- 未登录或用户只是试试看 → 不调 `list_products`。请用户给一句产品/服务描述,至少要有 `productName`,能补 `productDescription / sellingPoints / targetPersona` 更好。
|
|
72
|
-
-
|
|
91
|
+
- 已登录且用户明确要用已保存产品/客户池 → 调 `list_products`。只有一个产品直接用;有多个时把名字列给用户选,**不要替用户猜**。
|
|
73
92
|
|
|
74
|
-
### 第
|
|
93
|
+
### 第 4 步:先要词,再开搜
|
|
75
94
|
|
|
76
95
|
不要在未登录试用阶段卡住用户:
|
|
77
96
|
|
|
@@ -80,32 +99,33 @@ description: OPC 评论线索雷达:找客户、销售线索、评论分析、
|
|
|
80
99
|
|
|
81
100
|
`regenerate=true` 会重新生成并消耗用户电力——只有用户明确说「换一批词」才用。
|
|
82
101
|
|
|
83
|
-
### 第
|
|
102
|
+
### 第 5 步:开搜
|
|
84
103
|
|
|
85
104
|
调 `search_keyword_for_leads`。平台听用户的;用户没说就问一句,不要默认猜。
|
|
86
105
|
|
|
87
106
|
- 已登录完整模式:传 `keywords + productId + platform`,结果会落客户池。
|
|
88
107
|
- 未登录试用模式:传 `keywords + productName/productDescription + platform`,结果只展示前 2 条线索,不落客户池。
|
|
89
108
|
|
|
90
|
-
开搜前告诉用户:这一步要 2~3
|
|
109
|
+
开搜前告诉用户:这一步要 2~3 分钟,会在后台用隐藏窗口干活。新版 MCP 会持续把进度事件发给智能体;如果宿主展示这些通知,要把“正在搜哪个词、打开了哪个链接、读到多少评论、哪条失败了”按事实转述给用户,不要只说“还在跑”。
|
|
91
110
|
|
|
92
111
|
如果用户给的是具体的视频/笔记链接,跳过想词和搜索,直接调 `analyze_video_comments`:
|
|
93
112
|
|
|
94
113
|
- 已登录完整模式:传 `videoUrl + productId`。
|
|
95
114
|
- 未登录试用模式:传 `videoUrl + productName/productDescription`。
|
|
96
115
|
|
|
97
|
-
### 第
|
|
116
|
+
### 第 6 步:汇报成果(固定格式)
|
|
98
117
|
|
|
99
118
|
按这个顺序说,不要把原始 JSON 念出来:
|
|
100
119
|
|
|
101
120
|
1. **一句总结**:直接用返回里的 `summary.verdict`(已含首推客户及理由)。
|
|
102
|
-
2.
|
|
103
|
-
3.
|
|
104
|
-
4.
|
|
105
|
-
5.
|
|
106
|
-
6.
|
|
121
|
+
2. **过程透明**:如果返回里有 `processNarrative`,必须先用 2~5 行转述:搜了哪些词、实际看了哪些链接、每条读了多少评论、总共分析多少评论。不要用“应该/可能/还在跑”这种猜测词;只说工具返回的事实。需要更细时读取 `workflowTrace.keywords[].contents[]`。
|
|
122
|
+
3. **前 5 名**:每人一行——昵称、意向、需求类型、一句评论原话。
|
|
123
|
+
4. **战报文件**:如果返回里有 `reportFile`,只能把它说成「本机临时战报备份」,可用于转发同事照着跟进;不要把它说成解锁入口,也不要让用户误以为本地 HTML 能解锁全部客户。
|
|
124
|
+
5. **下一步提示**:提醒完整名单、历史记录、解锁和保存动作在 OPC 网页端客户池完成。
|
|
125
|
+
6. **转化动作(重要)**:如果返回里 `paywall.locked` 为真,说明已经先给用户看到了前几个完整客户。要**如实、不啰嗦**地转达:「这次挖到 N 个,先给你看前 2 个完整线索(含话术和主页入口),其余 X 个已锁。登录/开通后可以保存到客户池并解锁完整名单」。用 `paywall.unlockHint` 的话术,别夸大、别假装全给了。
|
|
126
|
+
7. **登录时机**:用户说「看剩下的」「保存」「查客户池」「继续跟进」时,再调 `check_status_and_login`,参数 `action=login_ppxc`,让他登录 OPC。不要在试用扫描前做这一步。
|
|
107
127
|
|
|
108
|
-
### 第
|
|
128
|
+
### 第 6.1 步:收集用户判断(持续学习的关键)
|
|
109
129
|
|
|
110
130
|
准不准不是系统说了算,是用户说了算。汇报完客户名单后,主动问一句:
|
|
111
131
|
|
|
@@ -121,7 +141,7 @@ description: OPC 评论线索雷达:找客户、销售线索、评论分析、
|
|
|
121
141
|
|
|
122
142
|
用户反馈可以只针对 1 条,不要强迫他给整批打分。每次反馈都要带 `leadId`;如果当前汇报里没显示 id,就先用 `query_leads` 查出对应线索再标记。
|
|
123
143
|
|
|
124
|
-
### 第
|
|
144
|
+
### 第 6.2 步:记录跟进结果(成交闭环的关键)
|
|
125
145
|
|
|
126
146
|
系统不能保证成交,只能保证把可跟进机会识别、排序、提醒和复盘。真正是否成交,要靠用户跟进后回填。
|
|
127
147
|
|
|
@@ -134,7 +154,7 @@ description: OPC 评论线索雷达:找客户、销售线索、评论分析、
|
|
|
134
154
|
|
|
135
155
|
更新后提醒用户:这些状态会进入后端学习和复盘,下一轮会更贴近他的真实客户。
|
|
136
156
|
|
|
137
|
-
### 第
|
|
157
|
+
### 第 6.5 步:内容彩蛋(挖到客户后主动提议)
|
|
138
158
|
|
|
139
159
|
每次找客户的返回里都带 `contentAngles`——从这批评论提炼的内容选题方向(每条含:拍什么角度、为什么、客户原话)。汇报完客户名单后,**主动加一句**:
|
|
140
160
|
|
|
@@ -149,7 +169,7 @@ description: OPC 评论线索雷达:找客户、销售线索、评论分析、
|
|
|
149
169
|
|
|
150
170
|
这一步是「客户信号」到「内容获客」的飞轮:评论既识别了这批销售线索,又指明了下一条吸引同类客户的内容。**别强推**——用户不接就跳过。
|
|
151
171
|
|
|
152
|
-
### 第
|
|
172
|
+
### 第 7 步(隔天/复盘场景):查战果、换词
|
|
153
173
|
|
|
154
174
|
用户问「之前挖到的客户怎么样了」「昨天那批有跟进吗」「哪些还没跟」时优先调 `review_followup_queue`,再按需要调 `query_leads` 看明细。
|
|
155
175
|
|
|
@@ -179,4 +199,4 @@ description: OPC 评论线索雷达:找客户、销售线索、评论分析、
|
|
|
179
199
|
> 2. Momo(高意向 · 竞品不满):“用了某大牌的防晒整张脸闷痘……”
|
|
180
200
|
> 3. ……
|
|
181
201
|
>
|
|
182
|
-
>
|
|
202
|
+
> 本机临时战报备份已放到你桌面(含可复制话术):OPC客户战报-xxxx.html,可以转给同事照着跟进。完整名单、解锁和历史记录在 OPC 网页端客户池里看。
|