ppxc-leads-mcp 0.1.8 → 0.1.11
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/LICENSE.md +6 -6
- package/README.md +28 -10
- package/dist/backend/config.js +11 -2
- package/dist/backend/ppxc-client.js +83 -7
- package/dist/backend/ppxc-login-window.js +3 -3
- package/dist/mcp/battle-report.js +5 -3
- package/dist/mcp/server.js +282 -51
- package/package.json +19 -3
- package/skills/ppxc-find-customers/SKILL.md +110 -27
package/LICENSE.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
#
|
|
1
|
+
# OPC 评论线索雷达 MCP Proprietary License
|
|
2
2
|
|
|
3
|
-
Copyright (c)
|
|
3
|
+
Copyright (c) OPC. All rights reserved.
|
|
4
4
|
|
|
5
|
-
This package is provided only for use with the
|
|
5
|
+
This package is provided only for use with the OPC service.
|
|
6
6
|
|
|
7
|
-
You may install and run this package to connect an MCP-capable assistant to your own
|
|
7
|
+
You may install and run this package to connect an MCP-capable assistant to your own OPC account.
|
|
8
8
|
|
|
9
|
-
You may not copy, modify, decompile, reverse engineer, republish, redistribute, sublicense, sell, rent, host, or use this package or any substantial part of it to build a competing or derivative service without prior written permission from
|
|
9
|
+
You may not copy, modify, decompile, reverse engineer, republish, redistribute, sublicense, sell, rent, host, or use this package or any substantial part of it to build a competing or derivative service without prior written permission from OPC.
|
|
10
10
|
|
|
11
|
-
This package is provided "as is" without warranties of any kind.
|
|
11
|
+
This package is provided "as is" without warranties of any kind. OPC may update, limit, suspend, or discontinue access to service-backed functionality according to OPC account, billing, safety, and abuse-control rules.
|
package/README.md
CHANGED
|
@@ -1,10 +1,24 @@
|
|
|
1
|
-
#
|
|
1
|
+
# OPC 评论线索雷达 MCP
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
OPC 评论线索雷达是一款找客户 Agent Skill / MCP 工具,帮助商家从抖音、小红书、快手公开评论中识别购买意向、销售线索和可跟进客户名单。
|
|
4
|
+
|
|
5
|
+
OPC Comment Lead Radar is an Agent Skill and MCP tool for lead generation from short-video comments, helping teams discover customer intent and sales leads from Douyin, Xiaohongshu, and Kuaishou.
|
|
6
|
+
|
|
7
|
+
OPC 评论线索雷达 MCP lets an MCP-capable assistant turn public comments on supported social platforms into customer signals, ranked leads, follow-up scripts, and reusable customer-pool records.
|
|
4
8
|
|
|
5
9
|
Package: <https://www.npmjs.com/package/ppxc-leads-mcp>
|
|
6
10
|
Setup guide: <https://opc1.me/download/mcp>
|
|
7
11
|
|
|
12
|
+
## What It Does
|
|
13
|
+
|
|
14
|
+
- Finds high-intent sales leads from public comments.
|
|
15
|
+
- Detects purchase intent, comparison questions, complaints, and recommendation requests.
|
|
16
|
+
- Generates follow-up scripts that a sales or private-traffic team can use.
|
|
17
|
+
- Saves the results into the user's customer pool for later review.
|
|
18
|
+
- Helps content teams turn real customer questions into the next content angles.
|
|
19
|
+
|
|
20
|
+
Best-fit searches: lead generation, sales leads, social media leads, comment analysis, intent detection, customer discovery, CRM, social listening, local business leads, Xiaohongshu leads, Douyin leads, Kuaishou leads.
|
|
21
|
+
|
|
8
22
|
## Install MCP
|
|
9
23
|
|
|
10
24
|
Add this server to an MCP-capable host:
|
|
@@ -49,26 +63,30 @@ Install the whole `skills/ppxc-find-customers/` folder into your assistant's ski
|
|
|
49
63
|
~/.codex/skills/ppxc-find-customers/
|
|
50
64
|
```
|
|
51
65
|
|
|
52
|
-
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.
|
|
53
67
|
|
|
54
68
|
## Tools
|
|
55
69
|
|
|
56
|
-
The package exposes
|
|
70
|
+
The package exposes eleven MCP tools:
|
|
57
71
|
|
|
58
72
|
- `check_status_and_login`
|
|
73
|
+
- `get_workflow_manifest`
|
|
59
74
|
- `list_products`
|
|
60
75
|
- `suggest_search_keywords`
|
|
61
76
|
- `search_keyword_for_leads`
|
|
62
77
|
- `analyze_video_comments`
|
|
63
78
|
- `query_leads`
|
|
79
|
+
- `mark_lead_feedback`
|
|
80
|
+
- `update_lead_status`
|
|
81
|
+
- `review_followup_queue`
|
|
64
82
|
- `export_diagnostics`
|
|
65
83
|
|
|
66
84
|
## Privacy And Safety
|
|
67
85
|
|
|
68
|
-
- Platform login sessions and
|
|
69
|
-
- Platform tokens and cookies are not uploaded to
|
|
70
|
-
- Public comment text is sent to
|
|
71
|
-
- Results are saved only to the user's own
|
|
86
|
+
- Platform login sessions and OPC login credentials stay on the user's machine.
|
|
87
|
+
- Platform tokens and cookies are not uploaded to OPC and are not written to diagnostics.
|
|
88
|
+
- Public comment text is sent to OPC over HTTPS for customer-intent analysis, follow-up suggestions, and customer-pool storage.
|
|
89
|
+
- Results are saved only to the user's own OPC customer pool.
|
|
72
90
|
- Captchas, QR-code login, and security checks are handled by the user in a local window. This package does not bypass verification.
|
|
73
91
|
- The tool does not send private messages or automate outreach.
|
|
74
92
|
|
|
@@ -80,8 +98,8 @@ If startup is slow, wait for the browser runtime download to finish. In mainland
|
|
|
80
98
|
|
|
81
99
|
If a platform asks for QR-code login or verification, complete it manually in the local window, then retry the assistant request.
|
|
82
100
|
|
|
83
|
-
If something fails, ask the assistant to run `export_diagnostics` and send the generated diagnostic file to
|
|
101
|
+
If something fails, ask the assistant to run `export_diagnostics` and send the generated diagnostic file to OPC support.
|
|
84
102
|
|
|
85
103
|
## License
|
|
86
104
|
|
|
87
|
-
This package is proprietary. It may be installed and used as part of the
|
|
105
|
+
This package is proprietary. It may be installed and used as part of the OPC service, but copying, modifying, reverse engineering, republishing, or redistributing it is not allowed except with written permission from OPC.
|
package/dist/backend/config.js
CHANGED
|
@@ -4,8 +4,17 @@ exports.getApiBase = getApiBase;
|
|
|
4
4
|
exports.isHttpUrl = isHttpUrl;
|
|
5
5
|
const PRODUCTION_API_BASE = "https://opc1.me";
|
|
6
6
|
function getApiBase() {
|
|
7
|
-
const raw = (process.env.PPXC_API_BASE || PRODUCTION_API_BASE).trim()
|
|
8
|
-
|
|
7
|
+
const raw = (process.env.PPXC_API_BASE || PRODUCTION_API_BASE).trim();
|
|
8
|
+
try {
|
|
9
|
+
const url = new URL(raw);
|
|
10
|
+
url.pathname = "";
|
|
11
|
+
url.search = "";
|
|
12
|
+
url.hash = "";
|
|
13
|
+
return url.toString().replace(/\/+$/, "");
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return raw.replace(/\/+$/, "");
|
|
17
|
+
}
|
|
9
18
|
}
|
|
10
19
|
function isHttpUrl(value) {
|
|
11
20
|
return /^https?:\/\//i.test(value);
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.PpxcApiError = void 0;
|
|
4
|
+
exports.getMcpCapabilities = getMcpCapabilities;
|
|
4
5
|
exports.listProducts = listProducts;
|
|
5
6
|
exports.analyzeComments = analyzeComments;
|
|
6
7
|
exports.getCommitteeKeywords = getCommitteeKeywords;
|
|
7
8
|
exports.triggerCommitteeRun = triggerCommitteeRun;
|
|
8
9
|
exports.queryLeads = queryLeads;
|
|
10
|
+
exports.markLeadFeedback = markLeadFeedback;
|
|
11
|
+
exports.updateLeadStatus = updateLeadStatus;
|
|
9
12
|
const logger_1 = require("../browser/kernel/logger");
|
|
10
13
|
const version_1 = require("../version");
|
|
11
14
|
const config_1 = require("./config");
|
|
@@ -13,6 +16,7 @@ const token_store_1 = require("./token-store");
|
|
|
13
16
|
const log = logger_1.logger.scope("ppxc-client");
|
|
14
17
|
const LIST_TIMEOUT_MS = 15000;
|
|
15
18
|
const ANALYZE_TIMEOUT_MS = 180000;
|
|
19
|
+
const CAPABILITIES_TIMEOUT_MS = 10000;
|
|
16
20
|
class PpxcApiError extends Error {
|
|
17
21
|
constructor(status, message) {
|
|
18
22
|
super(message);
|
|
@@ -24,16 +28,19 @@ exports.PpxcApiError = PpxcApiError;
|
|
|
24
28
|
function authHeaders() {
|
|
25
29
|
const token = (0, token_store_1.readToken)();
|
|
26
30
|
if (!token)
|
|
27
|
-
throw new PpxcApiError(401, "
|
|
31
|
+
throw new PpxcApiError(401, "OPC 账号未登录");
|
|
32
|
+
return optionalAuthHeaders(token);
|
|
33
|
+
}
|
|
34
|
+
function optionalAuthHeaders(token = (0, token_store_1.readToken)()) {
|
|
28
35
|
return {
|
|
29
|
-
Authorization: `Bearer ${token}`,
|
|
30
36
|
"Content-Type": "application/json",
|
|
31
37
|
"X-PPXC-MCP-Version": version_1.OWN_VERSION,
|
|
38
|
+
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
32
39
|
};
|
|
33
40
|
}
|
|
34
41
|
function handleUnauthorized() {
|
|
35
42
|
(0, token_store_1.clearToken)();
|
|
36
|
-
return new PpxcApiError(401, "
|
|
43
|
+
return new PpxcApiError(401, "OPC 登录已失效");
|
|
37
44
|
}
|
|
38
45
|
async function fetchWithTimeout(url, init, timeoutMs) {
|
|
39
46
|
try {
|
|
@@ -46,6 +53,12 @@ async function fetchWithTimeout(url, init, timeoutMs) {
|
|
|
46
53
|
throw err;
|
|
47
54
|
}
|
|
48
55
|
}
|
|
56
|
+
async function getMcpCapabilities() {
|
|
57
|
+
const res = await fetchWithTimeout(`${(0, config_1.getApiBase)()}/api/v2/mcp/capabilities`, { method: "GET", headers: optionalAuthHeaders() }, CAPABILITIES_TIMEOUT_MS);
|
|
58
|
+
if (!res.ok)
|
|
59
|
+
throw new PpxcApiError(res.status, `读取动态工作流失败 (${res.status})`);
|
|
60
|
+
return (await res.json());
|
|
61
|
+
}
|
|
49
62
|
async function listProducts() {
|
|
50
63
|
const res = await fetchWithTimeout(`${(0, config_1.getApiBase)()}/api/v2/products`, { method: "GET", headers: authHeaders() }, LIST_TIMEOUT_MS);
|
|
51
64
|
if (res.status === 401)
|
|
@@ -65,11 +78,16 @@ async function listProducts() {
|
|
|
65
78
|
}
|
|
66
79
|
async function analyzeComments(input) {
|
|
67
80
|
const body = {
|
|
68
|
-
productId: input.productId,
|
|
81
|
+
productId: input.productId ?? "",
|
|
82
|
+
productName: input.productName ?? "",
|
|
83
|
+
productDescription: input.productDescription ?? "",
|
|
84
|
+
sellingPoints: input.sellingPoints ?? [],
|
|
85
|
+
problemSolved: input.problemSolved ?? "",
|
|
86
|
+
targetPersona: input.targetPersona ?? "",
|
|
69
87
|
videoUrl: input.videoUrl ?? "",
|
|
70
88
|
videoDesc: input.videoDesc ?? "",
|
|
71
89
|
sourceKeyword: input.sourceKeyword ?? "",
|
|
72
|
-
save: input.save !== false,
|
|
90
|
+
save: input.productId ? input.save !== false : false,
|
|
73
91
|
comments: input.comments.map((c) => ({
|
|
74
92
|
text: c.text,
|
|
75
93
|
nickname: c.nickname,
|
|
@@ -80,7 +98,11 @@ async function analyzeComments(input) {
|
|
|
80
98
|
createTime: c.createTime,
|
|
81
99
|
})),
|
|
82
100
|
};
|
|
83
|
-
const res = await fetchWithTimeout(`${(0, config_1.getApiBase)()}/api/v2/comments/analyze`, {
|
|
101
|
+
const res = await fetchWithTimeout(`${(0, config_1.getApiBase)()}/api/v2/comments/analyze`, {
|
|
102
|
+
method: "POST",
|
|
103
|
+
headers: input.productId ? authHeaders() : optionalAuthHeaders(),
|
|
104
|
+
body: JSON.stringify(body),
|
|
105
|
+
}, ANALYZE_TIMEOUT_MS);
|
|
84
106
|
if (res.status === 401)
|
|
85
107
|
throw handleUnauthorized();
|
|
86
108
|
if (res.status === 403)
|
|
@@ -133,6 +155,13 @@ async function triggerCommitteeRun(productId) {
|
|
|
133
155
|
log.info("committee run triggered", { productId });
|
|
134
156
|
}
|
|
135
157
|
const LEADS_TIMEOUT_MS = 30000;
|
|
158
|
+
function readHeaderInt(headers, name) {
|
|
159
|
+
const raw = headers.get(name);
|
|
160
|
+
if (!raw)
|
|
161
|
+
return 0;
|
|
162
|
+
const value = Number(raw);
|
|
163
|
+
return Number.isFinite(value) && value > 0 ? Math.floor(value) : 0;
|
|
164
|
+
}
|
|
136
165
|
async function queryLeads(opts) {
|
|
137
166
|
const params = new URLSearchParams();
|
|
138
167
|
if (opts.productId)
|
|
@@ -152,5 +181,52 @@ async function queryLeads(opts) {
|
|
|
152
181
|
throw new PpxcApiError(res.status, `查询客户池失败 (${res.status})`);
|
|
153
182
|
const data = (await res.json());
|
|
154
183
|
const paywallLocked = res.headers.get("x-ppxc-paywall") === "locked";
|
|
155
|
-
|
|
184
|
+
const lockedCount = readHeaderInt(res.headers, "x-ppxc-paywall-locked-count");
|
|
185
|
+
return {
|
|
186
|
+
rows: Array.isArray(data) ? data : [],
|
|
187
|
+
paywall: paywallLocked
|
|
188
|
+
? {
|
|
189
|
+
locked: true,
|
|
190
|
+
lockedCount,
|
|
191
|
+
lockedIntents: {
|
|
192
|
+
high: readHeaderInt(res.headers, "x-ppxc-paywall-locked-high"),
|
|
193
|
+
medium: readHeaderInt(res.headers, "x-ppxc-paywall-locked-medium"),
|
|
194
|
+
low: readHeaderInt(res.headers, "x-ppxc-paywall-locked-low"),
|
|
195
|
+
},
|
|
196
|
+
unlockHint: lockedCount > 0
|
|
197
|
+
? `另有 ${lockedCount} 个潜在客户已锁定,开通套餐后重新查询即可解锁完整结果。`
|
|
198
|
+
: "名单里的更多信息已锁定,开通套餐后重新查询即可解锁完整结果。",
|
|
199
|
+
}
|
|
200
|
+
: { locked: false },
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
async function markLeadFeedback(input) {
|
|
204
|
+
const res = await fetchWithTimeout(`${(0, config_1.getApiBase)()}/api/v2/lead-feedback`, {
|
|
205
|
+
method: "POST",
|
|
206
|
+
headers: authHeaders(),
|
|
207
|
+
body: JSON.stringify({
|
|
208
|
+
leadId: input.leadId,
|
|
209
|
+
tag: input.tag,
|
|
210
|
+
...(input.reason ? { reason: input.reason } : {}),
|
|
211
|
+
}),
|
|
212
|
+
}, LIST_TIMEOUT_MS);
|
|
213
|
+
if (res.status === 401)
|
|
214
|
+
throw handleUnauthorized();
|
|
215
|
+
if (res.status === 403)
|
|
216
|
+
throw new PpxcApiError(403, "无权操作这条客户线索");
|
|
217
|
+
if (!res.ok)
|
|
218
|
+
throw new PpxcApiError(res.status, `提交线索反馈失败 (${res.status})`);
|
|
219
|
+
}
|
|
220
|
+
async function updateLeadStatus(input) {
|
|
221
|
+
const res = await fetchWithTimeout(`${(0, config_1.getApiBase)()}/api/v2/leads`, {
|
|
222
|
+
method: "PATCH",
|
|
223
|
+
headers: authHeaders(),
|
|
224
|
+
body: JSON.stringify({ id: input.leadId, status: input.status }),
|
|
225
|
+
}, LIST_TIMEOUT_MS);
|
|
226
|
+
if (res.status === 401)
|
|
227
|
+
throw handleUnauthorized();
|
|
228
|
+
if (res.status === 403)
|
|
229
|
+
throw new PpxcApiError(403, "无权操作这条客户线索");
|
|
230
|
+
if (!res.ok)
|
|
231
|
+
throw new PpxcApiError(res.status, `更新跟进状态失败 (${res.status})`);
|
|
156
232
|
}
|
|
@@ -37,14 +37,14 @@ function isPpxcLoggedIn() {
|
|
|
37
37
|
async function openPpxcLoginWindow() {
|
|
38
38
|
const apiBase = (0, config_1.getApiBase)();
|
|
39
39
|
if (!(0, config_1.isHttpUrl)(apiBase)) {
|
|
40
|
-
return { ok: false, code: "api_base_invalid", message: "
|
|
40
|
+
return { ok: false, code: "api_base_invalid", message: "OPC 服务地址无效,请检查 PPXC_API_BASE" };
|
|
41
41
|
}
|
|
42
42
|
if (activeLoginWindow && !activeLoginWindow.isDestroyed()) {
|
|
43
43
|
activeLoginWindow.focus();
|
|
44
44
|
return { ok: false, code: "user_cancelled", message: "登录窗口已在打开中" };
|
|
45
45
|
}
|
|
46
46
|
const win = new electron_1.BrowserWindow({
|
|
47
|
-
title: "登录
|
|
47
|
+
title: "登录 OPC 评论线索雷达",
|
|
48
48
|
width: 460,
|
|
49
49
|
height: 680,
|
|
50
50
|
show: false,
|
|
@@ -95,7 +95,7 @@ async function openPpxcLoginWindow() {
|
|
|
95
95
|
if (!win.isDestroyed())
|
|
96
96
|
win.destroy();
|
|
97
97
|
activeLoginWindow = null;
|
|
98
|
-
return { ok: false, code: "network_error", message: "
|
|
98
|
+
return { ok: false, code: "network_error", message: "无法打开 OPC 登录页,请检查网络是否能访问 opc1.me" };
|
|
99
99
|
}
|
|
100
100
|
win.show();
|
|
101
101
|
return new Promise((resolve) => {
|
|
@@ -119,11 +119,12 @@ const STYLE = `
|
|
|
119
119
|
text-align:center; }
|
|
120
120
|
.lock-icon { font-size:26px; }
|
|
121
121
|
.lock-title { font-size:18px; font-weight:800; color:#9a3412; margin:6px 0 4px; }
|
|
122
|
-
.lock-sub { font-size:14px; color:#7c5a3a; line-height:1.7; }
|
|
122
|
+
.lock-sub { font-size:14px; color:#7c5a3a; line-height:1.7; max-width:620px; margin:0 auto; }
|
|
123
123
|
.lock-rows { display:flex; justify-content:center; gap:12px; flex-wrap:wrap; margin:14px 0 4px; }
|
|
124
124
|
.lock-chip { background:#fff; border:1px solid #f0d9bf; border-radius:999px; padding:6px 16px; font-size:13px; color:#9a3412; }
|
|
125
125
|
.lock-cta { display:inline-block; margin-top:14px; background:#ea7a26; color:#fff; font-weight:700;
|
|
126
|
-
padding:10px 26px; border-radius:999px; font-size:14px; }
|
|
126
|
+
padding:10px 26px; border-radius:999px; font-size:14px; text-decoration:none; }
|
|
127
|
+
.lock-note { margin-top:10px; font-size:12px; color:#9a6a3a; }
|
|
127
128
|
.links { margin-top:12px; font-size:13px; display:flex; gap:18px; flex-wrap:wrap; }
|
|
128
129
|
.links a { color:var(--green-deep); text-decoration:none; border-bottom:1px dashed currentColor; padding-bottom:1px; }
|
|
129
130
|
.tablewrap { background:var(--card); border-radius:18px; box-shadow:var(--shadow); overflow:hidden; }
|
|
@@ -225,7 +226,8 @@ function renderPaywall(input) {
|
|
|
225
226
|
<div class="lock-title">还有 ${pw.lockedCount} 个潜在客户没解锁</div>
|
|
226
227
|
<div class="lock-sub">${esc(pw.unlockHint || "开通套餐后,这些客户的评论、跟进话术和主页链接全部解锁。")}</div>
|
|
227
228
|
${chips ? `<div class="lock-rows">${chips}</div>` : ""}
|
|
228
|
-
<
|
|
229
|
+
<a class="lock-cta" href="https://opc1.me/recharge" target="_blank">开通套餐 · 解锁全部客户</a>
|
|
230
|
+
<div class="lock-note">付款后让智能体重新查询客户池或重新生成战报,完整客户会自动放开。</div>
|
|
229
231
|
</div>`;
|
|
230
232
|
}
|
|
231
233
|
function renderBattleReport(input, generatedAt = new Date()) {
|
package/dist/mcp/server.js
CHANGED
|
@@ -70,6 +70,33 @@ async function productNameOf(productId) {
|
|
|
70
70
|
return undefined;
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
|
+
function stringArg(args, key) {
|
|
74
|
+
const value = args[key];
|
|
75
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
76
|
+
}
|
|
77
|
+
function stringArrayArg(args, key) {
|
|
78
|
+
const value = args[key];
|
|
79
|
+
if (!Array.isArray(value))
|
|
80
|
+
return undefined;
|
|
81
|
+
const arr = value.map((item) => String(item ?? "").trim()).filter(Boolean).slice(0, 8);
|
|
82
|
+
return arr.length > 0 ? arr : undefined;
|
|
83
|
+
}
|
|
84
|
+
function productContextFromArgs(args) {
|
|
85
|
+
return {
|
|
86
|
+
productId: stringArg(args, "productId"),
|
|
87
|
+
productName: stringArg(args, "productName"),
|
|
88
|
+
productDescription: stringArg(args, "productDescription"),
|
|
89
|
+
sellingPoints: stringArrayArg(args, "sellingPoints"),
|
|
90
|
+
problemSolved: stringArg(args, "problemSolved"),
|
|
91
|
+
targetPersona: stringArg(args, "targetPersona"),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function hasProductContext(ctx) {
|
|
95
|
+
return Boolean(ctx.productId || ctx.productName);
|
|
96
|
+
}
|
|
97
|
+
function productContextHint() {
|
|
98
|
+
return "先告诉我你卖的产品/服务名称,最好再补一句适合谁、解决什么问题;我可以先试跑一轮,给你看前 2 条线索。";
|
|
99
|
+
}
|
|
73
100
|
function topPickOf(reportLeads) {
|
|
74
101
|
const sorted = [...reportLeads].sort((a, b) => (b.saleScore ?? 0) - (a.saleScore ?? 0));
|
|
75
102
|
const top = sorted[0];
|
|
@@ -91,7 +118,7 @@ function formatCommitteeKeywords(keywords, cap = 40) {
|
|
|
91
118
|
}));
|
|
92
119
|
}
|
|
93
120
|
const RUNNER_HINTS = {
|
|
94
|
-
LOGIN_REQUIRED: "
|
|
121
|
+
LOGIN_REQUIRED: "平台还没登录。请用 check_status_and_login 对应平台的登录动作扫码,再重试。OPC 账号可等用户要解锁或保存完整名单时再登录。",
|
|
95
122
|
VERIFICATION_REQUIRED: "平台要求验证,已弹出窗口。请完成验证后再重试,不要换词硬刷。",
|
|
96
123
|
FETCH_TIMEOUT: "评论没拉回来,可能是网络或风控。稍等一下再试。",
|
|
97
124
|
BAD_VIDEO_LINK: "链接无法识别,请确认是有效的内容链接。",
|
|
@@ -106,7 +133,7 @@ function runnerErrorText(err, platform) {
|
|
|
106
133
|
const name = (0, registry_1.getPlatformAdapter)(platform).displayName;
|
|
107
134
|
const hints = {
|
|
108
135
|
...RUNNER_HINTS,
|
|
109
|
-
LOGIN_REQUIRED: `${name}还没登录。请先用 check_status_and_login 登录 ${name}
|
|
136
|
+
LOGIN_REQUIRED: `${name}还没登录。请先用 check_status_and_login 登录 ${name},再重试。OPC 账号可等用户要解锁或保存完整名单时再登录。`,
|
|
110
137
|
VERIFICATION_REQUIRED: `${name}要求验证,已弹出窗口。请完成验证后再重试。`,
|
|
111
138
|
BAD_VIDEO_LINK: `这个链接里没认出${name}内容,请确认链接正确。`,
|
|
112
139
|
DAILY_LIMITED: `今天 ${name} 的安全抓取额度已用完,请明天再继续。`,
|
|
@@ -131,6 +158,32 @@ async function allPlatformLoginStatus() {
|
|
|
131
158
|
}
|
|
132
159
|
return out;
|
|
133
160
|
}
|
|
161
|
+
async function workflowManifestPayload() {
|
|
162
|
+
try {
|
|
163
|
+
const manifest = await (0, ppxc_client_1.getMcpCapabilities)();
|
|
164
|
+
return {
|
|
165
|
+
ok: true,
|
|
166
|
+
workflowVersion: manifest.workflowVersion,
|
|
167
|
+
skill: manifest.skill,
|
|
168
|
+
mcpPackage: manifest.mcpPackage,
|
|
169
|
+
capabilities: manifest.capabilities,
|
|
170
|
+
workflow: manifest.workflow,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
catch (err) {
|
|
174
|
+
return {
|
|
175
|
+
ok: false,
|
|
176
|
+
workflowVersion: "local-fallback",
|
|
177
|
+
warning: "后端动态工作流暂时不可用,已使用 MCP 内置流程继续执行。",
|
|
178
|
+
detail: err instanceof Error ? err.message : String(err),
|
|
179
|
+
capabilities: {
|
|
180
|
+
leadFeedback: true,
|
|
181
|
+
leadStatusUpdate: true,
|
|
182
|
+
followupReview: true,
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
}
|
|
134
187
|
function createMcpServer() {
|
|
135
188
|
const server = new mcp_js_1.McpServer({
|
|
136
189
|
name: "ppxc-leads-mcp",
|
|
@@ -138,7 +191,7 @@ function createMcpServer() {
|
|
|
138
191
|
});
|
|
139
192
|
const registerTool = server.registerTool.bind(server);
|
|
140
193
|
registerTool("check_status_and_login", {
|
|
141
|
-
description: "
|
|
194
|
+
description: "只做状态检查或显式登录。默认 action=status 只返回 OPC 与平台登录态,不弹窗;不要在试用扫描前调用 login_ppxc。用户要解锁、保存完整名单或查询客户池时,才用 action=login_ppxc 弹 OPC 登录窗;平台未登录导致无法抓评论时,才用 login_douyin|login_xiaohongshu|login_kuaishou 弹对应平台扫码窗。",
|
|
142
195
|
inputSchema: {
|
|
143
196
|
action: zod_1.z
|
|
144
197
|
.enum([
|
|
@@ -149,7 +202,7 @@ function createMcpServer() {
|
|
|
149
202
|
"login_ppxc",
|
|
150
203
|
])
|
|
151
204
|
.optional()
|
|
152
|
-
.describe("status
|
|
205
|
+
.describe("status=只查不弹窗;login_douyin/login_xiaohongshu/login_kuaishou=平台扫码;login_ppxc=用户要解锁/保存/查客户池时才登录 OPC。"),
|
|
153
206
|
},
|
|
154
207
|
}, async (args) => {
|
|
155
208
|
const action = args.action ?? "status";
|
|
@@ -159,6 +212,7 @@ function createMcpServer() {
|
|
|
159
212
|
login_xiaohongshu: "xiaohongshu",
|
|
160
213
|
login_kuaishou: "kuaishou",
|
|
161
214
|
};
|
|
215
|
+
const workflowManifest = await workflowManifestPayload();
|
|
162
216
|
if (action in loginPlatformMap) {
|
|
163
217
|
const platform = loginPlatformMap[action];
|
|
164
218
|
const adapter = (0, registry_1.getPlatformAdapter)(platform);
|
|
@@ -173,6 +227,7 @@ function createMcpServer() {
|
|
|
173
227
|
hint: result.loggedIn
|
|
174
228
|
? `${adapter.displayName}已登录。`
|
|
175
229
|
: `还没检测到${adapter.displayName}登录成功。请在弹出窗口扫码,扫完再调用本工具确认。`,
|
|
230
|
+
workflowManifest,
|
|
176
231
|
logFile: logger_1.LOG_FILE_PATH,
|
|
177
232
|
});
|
|
178
233
|
}
|
|
@@ -184,28 +239,33 @@ function createMcpServer() {
|
|
|
184
239
|
platforms,
|
|
185
240
|
ppxcLoggedIn: (0, ppxc_login_window_1.isPpxcLoggedIn)(),
|
|
186
241
|
hint: result.ok
|
|
187
|
-
? "
|
|
188
|
-
: `
|
|
242
|
+
? "OPC 账号已登录。"
|
|
243
|
+
: `OPC 登录未完成:${result.message ?? "请重试"}`,
|
|
244
|
+
workflowManifest,
|
|
189
245
|
logFile: logger_1.LOG_FILE_PATH,
|
|
190
246
|
});
|
|
191
247
|
}
|
|
192
248
|
const platforms = await allPlatformLoginStatus();
|
|
193
249
|
const ppxc = (0, ppxc_login_window_1.isPpxcLoggedIn)();
|
|
194
|
-
const
|
|
250
|
+
const missingPlatforms = [];
|
|
195
251
|
for (const adapter of (0, registry_1.listPlatformAdapters)()) {
|
|
196
252
|
if (!platforms[adapter.id]) {
|
|
197
|
-
|
|
253
|
+
missingPlatforms.push(`${adapter.displayName}(action=login_${adapter.id === "xiaohongshu" ? "xiaohongshu" : adapter.id})`);
|
|
198
254
|
}
|
|
199
255
|
}
|
|
200
|
-
if (!ppxc)
|
|
201
|
-
missing.push("PPXC 账号(action=login_ppxc)");
|
|
202
256
|
return jsonText({
|
|
203
257
|
ok: true,
|
|
204
258
|
platforms,
|
|
205
259
|
ppxcLoggedIn: ppxc,
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
260
|
+
trialScanAvailable: true,
|
|
261
|
+
fullPoolRequiresPpxcLogin: !ppxc,
|
|
262
|
+
missingPlatforms,
|
|
263
|
+
hint: missingPlatforms.length === 0
|
|
264
|
+
? ppxc
|
|
265
|
+
? "平台与 OPC 账号都已登录,可以完整找客户并保存到客户池。"
|
|
266
|
+
: "平台已登录,OPC 账号未登录也可以先试用扫描并展示前 2 条线索;用户要解锁、保存完整名单或查客户池时,再调用 action=login_ppxc。"
|
|
267
|
+
: `试用扫描不要求先登录 OPC,但抓评论需要对应平台登录:${missingPlatforms.join("、")}。请只弹平台扫码窗;不要先弹 OPC 登录窗。`,
|
|
268
|
+
workflowManifest,
|
|
209
269
|
logFile: logger_1.LOG_FILE_PATH,
|
|
210
270
|
});
|
|
211
271
|
}
|
|
@@ -227,7 +287,7 @@ function createMcpServer() {
|
|
|
227
287
|
}
|
|
228
288
|
});
|
|
229
289
|
registerTool("list_products", {
|
|
230
|
-
description: "列出当前
|
|
290
|
+
description: "列出当前 OPC 账号下的产品,返回 id 和名称。仅当用户已登录且明确要用已保存产品/客户池时调用;未登录试用扫描不要调用本工具,直接把 productName/productDescription 传给 analyze_video_comments 或 search_keyword_for_leads。",
|
|
231
291
|
inputSchema: {},
|
|
232
292
|
}, async () => {
|
|
233
293
|
try {
|
|
@@ -236,7 +296,7 @@ function createMcpServer() {
|
|
|
236
296
|
ok: true,
|
|
237
297
|
products,
|
|
238
298
|
hint: products.length === 0
|
|
239
|
-
? "这个账号下还没有产品,请先到
|
|
299
|
+
? "这个账号下还没有产品,请先到 OPC 网页端创建一个产品。"
|
|
240
300
|
: "把其中一个产品的 id 作为 productId 传给 analyze_video_comments。",
|
|
241
301
|
});
|
|
242
302
|
}
|
|
@@ -245,7 +305,7 @@ function createMcpServer() {
|
|
|
245
305
|
return jsonText({
|
|
246
306
|
ok: false,
|
|
247
307
|
code: "PPXC_LOGIN_REQUIRED",
|
|
248
|
-
userHint: "
|
|
308
|
+
userHint: "OPC 账号未登录或登录失效,请先用 check_status_and_login(action=login_ppxc)登录。",
|
|
249
309
|
});
|
|
250
310
|
}
|
|
251
311
|
return jsonText({
|
|
@@ -257,12 +317,17 @@ function createMcpServer() {
|
|
|
257
317
|
}
|
|
258
318
|
});
|
|
259
319
|
registerTool("analyze_video_comments", {
|
|
260
|
-
description: "给一条抖音/小红书/快手内容链接 + productId:读评论 →
|
|
320
|
+
description: "给一条抖音/小红书/快手内容链接 + productId 或 productName:读评论 → OPC AI 分析 → 客户战报。未登录用户可只传 productName 先试跑,返回前 2 条线索;登录后传 productId 才会落客户池。platform 可省略,会从链接自动识别。" +
|
|
261
321
|
"挖到客户时会在用户桌面生成一份战报网页文件(结果里的 reportFile),汇报时务必告诉用户文件位置和首推客户及理由(topPick)。" +
|
|
262
322
|
"结果里的 contentAngles 是从这批评论提炼的内容选题方向(拍什么/为什么/客户原话),汇报后可主动提议「要不要根据这些评论写下一条视频脚本」。",
|
|
263
323
|
inputSchema: {
|
|
264
324
|
videoUrl: zod_1.z.string().describe("内容链接(抖音视频/小红书笔记/快手视频)或 ID。"),
|
|
265
|
-
productId: zod_1.z.string().describe("
|
|
325
|
+
productId: zod_1.z.string().optional().describe("已登录用户的产品 id,用 list_products 获取;未登录试用可不传。"),
|
|
326
|
+
productName: zod_1.z.string().optional().describe("未登录试用时填写:用户卖的产品/服务名称。"),
|
|
327
|
+
productDescription: zod_1.z.string().optional().describe("未登录试用时填写:适合谁、解决什么问题、主要卖点。"),
|
|
328
|
+
sellingPoints: zod_1.z.array(zod_1.z.string()).max(8).optional().describe("未登录试用时可填:卖点列表。"),
|
|
329
|
+
problemSolved: zod_1.z.string().optional().describe("未登录试用时可填:解决的痛点。"),
|
|
330
|
+
targetPersona: zod_1.z.string().optional().describe("未登录试用时可填:目标人群。"),
|
|
266
331
|
platform: PLATFORM_ZOD
|
|
267
332
|
.optional()
|
|
268
333
|
.describe("可选:douyin | xiaohongshu | kuaishou。不传则从链接自动识别。"),
|
|
@@ -271,15 +336,15 @@ function createMcpServer() {
|
|
|
271
336
|
},
|
|
272
337
|
}, async (args) => {
|
|
273
338
|
const videoUrl = args.videoUrl;
|
|
274
|
-
const
|
|
339
|
+
const productContext = productContextFromArgs(args);
|
|
275
340
|
const maxComments = args.maxComments;
|
|
276
341
|
const save = args.save;
|
|
277
342
|
const platform = (0, detect_platform_1.normalizePlatformId)(args.platform, videoUrl);
|
|
278
|
-
if (!
|
|
343
|
+
if (!hasProductContext(productContext)) {
|
|
279
344
|
return jsonText({
|
|
280
345
|
ok: false,
|
|
281
|
-
code: "
|
|
282
|
-
userHint:
|
|
346
|
+
code: "NO_PRODUCT_CONTEXT",
|
|
347
|
+
userHint: productContextHint(),
|
|
283
348
|
});
|
|
284
349
|
}
|
|
285
350
|
let fetched;
|
|
@@ -307,7 +372,7 @@ function createMcpServer() {
|
|
|
307
372
|
}
|
|
308
373
|
try {
|
|
309
374
|
const analysis = await (0, ppxc_client_1.analyzeComments)({
|
|
310
|
-
|
|
375
|
+
...productContext,
|
|
311
376
|
comments: fetched.comments,
|
|
312
377
|
videoUrl: fetched.contentUrl,
|
|
313
378
|
save: save !== false,
|
|
@@ -321,7 +386,7 @@ function createMcpServer() {
|
|
|
321
386
|
const exported = (0, battle_report_1.exportBattleReport)({
|
|
322
387
|
kind: "link",
|
|
323
388
|
platformName: (0, registry_1.getPlatformAdapter)(platform).displayName,
|
|
324
|
-
productName: await productNameOf(productId),
|
|
389
|
+
productName: productContext.productName || (productContext.productId ? await productNameOf(productContext.productId) : undefined),
|
|
325
390
|
contentUrl: fetched.contentUrl,
|
|
326
391
|
summary: analysis.summary,
|
|
327
392
|
leads: reportLeads,
|
|
@@ -365,7 +430,7 @@ function createMcpServer() {
|
|
|
365
430
|
? {
|
|
366
431
|
reportFile: report.file,
|
|
367
432
|
reportHint: paywallLocked
|
|
368
|
-
? `已生成战报(网页文件),里面只展示了解锁的前 ${reportLeads.length}
|
|
433
|
+
? `已生成战报(网页文件),里面只展示了解锁的前 ${reportLeads.length} 个客户;其余已锁。开通套餐后,重新查询客户池或重新生成战报即可拿到完整结果。文件在:${report.file}`
|
|
369
434
|
: `已生成一份客户战报(网页文件,含话术,可转发同事照着跟进),放在:${report.file}`,
|
|
370
435
|
}
|
|
371
436
|
: {}),
|
|
@@ -377,14 +442,16 @@ function createMcpServer() {
|
|
|
377
442
|
return jsonText({
|
|
378
443
|
ok: false,
|
|
379
444
|
code: "PPXC_LOGIN_REQUIRED",
|
|
380
|
-
userHint:
|
|
445
|
+
userHint: productContext.productId
|
|
446
|
+
? "OPC 账号未登录或登录失效,请用 check_status_and_login(action=login_ppxc)登录后重试。"
|
|
447
|
+
: "试用分析暂时没跑通。可以先登录 OPC,或补充产品名称后重试。",
|
|
381
448
|
});
|
|
382
449
|
}
|
|
383
450
|
if (err.status === 402) {
|
|
384
451
|
return jsonText({
|
|
385
452
|
ok: false,
|
|
386
453
|
code: "INSUFFICIENT_CREDITS",
|
|
387
|
-
userHint: `${err.message}。请到
|
|
454
|
+
userHint: `${err.message}。请到 OPC 网页端给 AI 充电后再试。`,
|
|
388
455
|
});
|
|
389
456
|
}
|
|
390
457
|
if (err.status === 403) {
|
|
@@ -398,21 +465,21 @@ function createMcpServer() {
|
|
|
398
465
|
return jsonText({
|
|
399
466
|
ok: false,
|
|
400
467
|
code: "BACKEND_RATE_LIMITED",
|
|
401
|
-
userHint:
|
|
468
|
+
userHint: `OPC 服务提示请求太频繁:${err.message}`,
|
|
402
469
|
});
|
|
403
470
|
}
|
|
404
|
-
return jsonText({ ok: false, code: "BACKEND_ERROR", userHint:
|
|
471
|
+
return jsonText({ ok: false, code: "BACKEND_ERROR", userHint: `OPC 分析服务失败:${err.message}` });
|
|
405
472
|
}
|
|
406
473
|
return jsonText({
|
|
407
474
|
ok: false,
|
|
408
475
|
code: "INTERNAL",
|
|
409
|
-
userHint: "
|
|
476
|
+
userHint: "送 OPC 分析服务时内部出错,请稍后重试。",
|
|
410
477
|
detail: err instanceof Error ? err.message : String(err),
|
|
411
478
|
});
|
|
412
479
|
}
|
|
413
480
|
});
|
|
414
481
|
registerTool("search_keyword_for_leads", {
|
|
415
|
-
description: "关键词 + productId + platform:搜内容 → 读评论 → AI 分析 →
|
|
482
|
+
description: "关键词 + productId 或 productName + platform:搜内容 → 读评论 → AI 分析 → 汇总战报。未登录用户可只传 productName 先试跑,返回前 2 条线索;登录后传 productId 才会落客户池。支持 douyin/xiaohongshu/kuaishou。多关键词 2 窗口并行(抖音);小红书/快手更保守。" +
|
|
416
483
|
"挖到客户时会在用户桌面生成一份战报网页文件(结果里的 reportFile),汇报时务必告诉用户文件位置和首推客户及理由(topPick)。" +
|
|
417
484
|
"结果里的 contentAngles 是从这批评论提炼的内容选题方向(拍什么/为什么/客户原话),汇报后可主动提议「要不要根据这些评论写下一条视频脚本」。",
|
|
418
485
|
inputSchema: {
|
|
@@ -422,7 +489,12 @@ function createMcpServer() {
|
|
|
422
489
|
.min(1)
|
|
423
490
|
.max(6)
|
|
424
491
|
.describe("搜索词列表,最多 6 个。"),
|
|
425
|
-
productId: zod_1.z.string().describe("
|
|
492
|
+
productId: zod_1.z.string().optional().describe("已登录用户的产品 id,用 list_products 获取;未登录试用可不传。"),
|
|
493
|
+
productName: zod_1.z.string().optional().describe("未登录试用时填写:用户卖的产品/服务名称。"),
|
|
494
|
+
productDescription: zod_1.z.string().optional().describe("未登录试用时填写:适合谁、解决什么问题、主要卖点。"),
|
|
495
|
+
sellingPoints: zod_1.z.array(zod_1.z.string()).max(8).optional().describe("未登录试用时可填:卖点列表。"),
|
|
496
|
+
problemSolved: zod_1.z.string().optional().describe("未登录试用时可填:解决的痛点。"),
|
|
497
|
+
targetPersona: zod_1.z.string().optional().describe("未登录试用时可填:目标人群。"),
|
|
426
498
|
maxVideosPerKeyword: zod_1.z
|
|
427
499
|
.number()
|
|
428
500
|
.int()
|
|
@@ -435,12 +507,12 @@ function createMcpServer() {
|
|
|
435
507
|
}, async (args) => {
|
|
436
508
|
const platform = (0, detect_platform_1.normalizePlatformId)(args.platform);
|
|
437
509
|
const keywords = args.keywords ?? [];
|
|
438
|
-
const
|
|
510
|
+
const productContext = productContextFromArgs(args);
|
|
439
511
|
const maxVideosPerKeyword = args.maxVideosPerKeyword;
|
|
440
512
|
const save = args.save;
|
|
441
513
|
const adapter = (0, registry_1.getPlatformAdapter)(platform);
|
|
442
|
-
if (!
|
|
443
|
-
return jsonText({ ok: false, code: "
|
|
514
|
+
if (!hasProductContext(productContext)) {
|
|
515
|
+
return jsonText({ ok: false, code: "NO_PRODUCT_CONTEXT", userHint: productContextHint() });
|
|
444
516
|
}
|
|
445
517
|
if (!Array.isArray(keywords) || keywords.length === 0) {
|
|
446
518
|
return jsonText({ ok: false, code: "NO_KEYWORD", userHint: "缺少搜索词。" });
|
|
@@ -483,7 +555,7 @@ function createMcpServer() {
|
|
|
483
555
|
const itemUrl = v.contentUrl ?? v.videoUrl ?? "";
|
|
484
556
|
try {
|
|
485
557
|
const analysis = await (0, ppxc_client_1.analyzeComments)({
|
|
486
|
-
|
|
558
|
+
...productContext,
|
|
487
559
|
comments: v.comments,
|
|
488
560
|
videoUrl: itemUrl,
|
|
489
561
|
videoDesc: v.title,
|
|
@@ -510,14 +582,16 @@ function createMcpServer() {
|
|
|
510
582
|
return jsonText({
|
|
511
583
|
ok: false,
|
|
512
584
|
code: "PPXC_LOGIN_REQUIRED",
|
|
513
|
-
userHint:
|
|
585
|
+
userHint: productContext.productId
|
|
586
|
+
? "OPC 账号未登录或登录失效,请用 check_status_and_login(action=login_ppxc)登录后重试。"
|
|
587
|
+
: "试用分析暂时没跑通。可以先登录 OPC,或补充产品名称后重试。",
|
|
514
588
|
});
|
|
515
589
|
}
|
|
516
590
|
if (err instanceof ppxc_client_1.PpxcApiError && err.status === 402) {
|
|
517
591
|
return jsonText({
|
|
518
592
|
ok: false,
|
|
519
593
|
code: "INSUFFICIENT_CREDITS",
|
|
520
|
-
userHint: `${err.message}。请到
|
|
594
|
+
userHint: `${err.message}。请到 OPC 网页端给 AI 充电后再试。`,
|
|
521
595
|
partial: allLeads.length > 0
|
|
522
596
|
? { leads: allLeads, commentsAnalyzed, demandsFound, savedToPool }
|
|
523
597
|
: undefined,
|
|
@@ -580,7 +654,7 @@ function createMcpServer() {
|
|
|
580
654
|
const exported = (0, battle_report_1.exportBattleReport)({
|
|
581
655
|
kind: "search",
|
|
582
656
|
platformName: adapter.displayName,
|
|
583
|
-
productName: await productNameOf(productId),
|
|
657
|
+
productName: productContext.productName || (productContext.productId ? await productNameOf(productContext.productId) : undefined),
|
|
584
658
|
keywords: outcomes.map((o) => o.keyword),
|
|
585
659
|
summary: { itemsRead, commentsAnalyzed, demandsFound, highIntentCount },
|
|
586
660
|
leads: reportLeads,
|
|
@@ -630,14 +704,14 @@ function createMcpServer() {
|
|
|
630
704
|
? {
|
|
631
705
|
reportFile: report.file,
|
|
632
706
|
reportHint: paywallLocked
|
|
633
|
-
? `已生成战报(网页文件),只展示了解锁的前 ${visibleAllLeads.length}
|
|
707
|
+
? `已生成战报(网页文件),只展示了解锁的前 ${visibleAllLeads.length} 个客户;其余已锁。开通套餐后,重新查询客户池或重新生成战报即可拿到完整结果。文件在:${report.file}`
|
|
634
708
|
: `已生成一份客户战报(网页文件,含每个词的成绩和话术,可转发同事照着跟进),放在:${report.file}`,
|
|
635
709
|
}
|
|
636
710
|
: {}),
|
|
637
711
|
});
|
|
638
712
|
});
|
|
639
713
|
registerTool("suggest_search_keywords", {
|
|
640
|
-
description: "给 productId,返回
|
|
714
|
+
description: "给 productId,返回 OPC 想词委员会为该产品生成的精选搜索词(带词型分类和推荐理由)。" +
|
|
641
715
|
"词单通常在产品创建/编辑时已自动生成,本工具默认直接读现成结果;没有现成词单时会触发一轮生成并等待(约 1 分钟)。" +
|
|
642
716
|
"开搜前先调它,从主力词里挑 3~6 个传给 search_keyword_for_leads,比现场编词命中率高得多。" +
|
|
643
717
|
"regenerate=true 强制重新生成(消耗用户电力,仅当用户明确要求换一批词时使用)。",
|
|
@@ -684,7 +758,7 @@ function createMcpServer() {
|
|
|
684
758
|
return jsonText({
|
|
685
759
|
ok: false,
|
|
686
760
|
code: "COMMITTEE_PENDING",
|
|
687
|
-
userHint: "
|
|
761
|
+
userHint: "搜词还在生成,请过一两分钟再调用本工具读取结果。",
|
|
688
762
|
});
|
|
689
763
|
}
|
|
690
764
|
const list = formatCommitteeKeywords(current.keywords);
|
|
@@ -703,7 +777,7 @@ function createMcpServer() {
|
|
|
703
777
|
return jsonText({
|
|
704
778
|
ok: false,
|
|
705
779
|
code: "PPXC_LOGIN_REQUIRED",
|
|
706
|
-
userHint: "
|
|
780
|
+
userHint: "OPC 账号未登录或登录失效,请先用 check_status_and_login(action=login_ppxc)登录。",
|
|
707
781
|
});
|
|
708
782
|
}
|
|
709
783
|
if (err instanceof ppxc_client_1.PpxcApiError && err.status === 403) {
|
|
@@ -720,8 +794,12 @@ function createMcpServer() {
|
|
|
720
794
|
});
|
|
721
795
|
}
|
|
722
796
|
});
|
|
797
|
+
registerTool("get_workflow_manifest", {
|
|
798
|
+
description: "读取 OPC 后端最新动态工作流、能力清单和 Skill 升级提示。每次开始找客户或复盘前优先调用;如果失败,可以继续使用 MCP 内置流程。",
|
|
799
|
+
inputSchema: {},
|
|
800
|
+
}, async () => jsonText(await workflowManifestPayload()));
|
|
723
801
|
registerTool("query_leads", {
|
|
724
|
-
description: "查
|
|
802
|
+
description: "查 OPC 客户池里已挖到的潜在客户(只读)。支持按产品、来源搜索词、最近天数、跟进状态过滤," +
|
|
725
803
|
"默认返回最新 30 条,每条带昵称、评论原文、意向、判断理由、跟进话术、跟进状态。" +
|
|
726
804
|
"用户问「之前找到的客户 / 高意向的有哪些 / 某个词搜出来的客户」时用它。",
|
|
727
805
|
inputSchema: {
|
|
@@ -742,7 +820,8 @@ function createMcpServer() {
|
|
|
742
820
|
const limit = Math.max(1, Math.min(100, args.limit ?? 30));
|
|
743
821
|
try {
|
|
744
822
|
const since = days ? new Date(Date.now() - days * 86400000).toISOString() : undefined;
|
|
745
|
-
const { rows,
|
|
823
|
+
const { rows, paywall } = await (0, ppxc_client_1.queryLeads)({ productId, keyword, since });
|
|
824
|
+
const paywallLocked = paywall.locked === true;
|
|
746
825
|
const filtered = status ? rows.filter((r) => r.status === status) : rows;
|
|
747
826
|
const byStatus = {};
|
|
748
827
|
const byIntent = {};
|
|
@@ -769,17 +848,17 @@ function createMcpServer() {
|
|
|
769
848
|
}));
|
|
770
849
|
const baseHint = filtered.length === 0
|
|
771
850
|
? "没查到符合条件的客户。可以放宽条件,或先用 search_keyword_for_leads 去挖新客户。"
|
|
772
|
-
: `共 ${filtered.length} 条符合条件(按入池时间从新到旧,返回前 ${leads.length} 条)。完整客户池在
|
|
851
|
+
: `共 ${filtered.length} 条符合条件(按入池时间从新到旧,返回前 ${leads.length} 条)。完整客户池在 OPC 网页端可看。`;
|
|
773
852
|
return jsonText({
|
|
774
853
|
ok: true,
|
|
775
854
|
totalMatched: filtered.length,
|
|
776
855
|
returned: leads.length,
|
|
777
856
|
byStatus,
|
|
778
857
|
byIntent,
|
|
779
|
-
paywall
|
|
858
|
+
paywall,
|
|
780
859
|
leads,
|
|
781
860
|
hint: paywallLocked
|
|
782
|
-
? `${baseHint}
|
|
861
|
+
? `${baseHint}(你当前是体验版,已返回可预览的客户;${paywall.unlockHint ?? "开通套餐后重新查询即可解锁完整结果。"})`
|
|
783
862
|
: baseHint,
|
|
784
863
|
});
|
|
785
864
|
}
|
|
@@ -788,7 +867,7 @@ function createMcpServer() {
|
|
|
788
867
|
return jsonText({
|
|
789
868
|
ok: false,
|
|
790
869
|
code: "PPXC_LOGIN_REQUIRED",
|
|
791
|
-
userHint: "
|
|
870
|
+
userHint: "OPC 账号未登录或登录失效,请先用 check_status_and_login(action=login_ppxc)登录。",
|
|
792
871
|
});
|
|
793
872
|
}
|
|
794
873
|
if (err instanceof ppxc_client_1.PpxcApiError && err.status === 403) {
|
|
@@ -802,8 +881,160 @@ function createMcpServer() {
|
|
|
802
881
|
});
|
|
803
882
|
}
|
|
804
883
|
});
|
|
884
|
+
registerTool("mark_lead_feedback", {
|
|
885
|
+
description: "记录用户对某条客户线索的主观判断。准不准由用户说了算;用户说「这个准 / 不准 / 太泛 / 像客户 / 像路人」时调用,用于后端持续学习。",
|
|
886
|
+
inputSchema: {
|
|
887
|
+
leadId: zod_1.z.string().describe("客户线索 id,来自 query_leads 或找客户结果。"),
|
|
888
|
+
tag: zod_1.z
|
|
889
|
+
.enum(["accurate", "inaccurate", "too_broad", "feels_like_buyer", "feels_like_passerby"])
|
|
890
|
+
.describe("用户判断:accurate=准;inaccurate=不准;too_broad=太泛;feels_like_buyer=像客户;feels_like_passerby=像路人。"),
|
|
891
|
+
reason: zod_1.z.string().optional().describe("用户补充原因,最多会由后端截取保存。"),
|
|
892
|
+
},
|
|
893
|
+
}, async (args) => {
|
|
894
|
+
const leadId = stringArg(args, "leadId");
|
|
895
|
+
const tag = args.tag;
|
|
896
|
+
const reason = stringArg(args, "reason");
|
|
897
|
+
if (!leadId || !tag) {
|
|
898
|
+
return jsonText({ ok: false, code: "BAD_INPUT", userHint: "需要提供 leadId 和反馈类型。" });
|
|
899
|
+
}
|
|
900
|
+
try {
|
|
901
|
+
await (0, ppxc_client_1.markLeadFeedback)({ leadId, tag, reason });
|
|
902
|
+
return jsonText({
|
|
903
|
+
ok: true,
|
|
904
|
+
leadId,
|
|
905
|
+
tag,
|
|
906
|
+
hint: "已记录你的判断。后端会把这类反馈用于后续找客户和想词学习。",
|
|
907
|
+
});
|
|
908
|
+
}
|
|
909
|
+
catch (err) {
|
|
910
|
+
if (err instanceof ppxc_client_1.PpxcApiError && err.status === 401) {
|
|
911
|
+
return jsonText({
|
|
912
|
+
ok: false,
|
|
913
|
+
code: "PPXC_LOGIN_REQUIRED",
|
|
914
|
+
userHint: "需要先登录 OPC,才能把反馈写入你的客户池。请调用 check_status_and_login(action=login_ppxc)。",
|
|
915
|
+
});
|
|
916
|
+
}
|
|
917
|
+
if (err instanceof ppxc_client_1.PpxcApiError && err.status === 403) {
|
|
918
|
+
return jsonText({ ok: false, code: "NO_LEAD_ACCESS", userHint: "这条客户线索不属于当前账号,不能标记。" });
|
|
919
|
+
}
|
|
920
|
+
return jsonText({
|
|
921
|
+
ok: false,
|
|
922
|
+
code: "INTERNAL",
|
|
923
|
+
userHint: "记录线索反馈时出错了,请稍后重试。",
|
|
924
|
+
detail: err instanceof Error ? err.message : String(err),
|
|
925
|
+
});
|
|
926
|
+
}
|
|
927
|
+
});
|
|
928
|
+
registerTool("update_lead_status", {
|
|
929
|
+
description: "更新客户线索跟进状态。用户说「已联系 / 已成交 / 没转化 / 忽略」时调用,用于形成真实跟进闭环和后续复盘。",
|
|
930
|
+
inputSchema: {
|
|
931
|
+
leadId: zod_1.z.string().describe("客户线索 id,来自 query_leads 或找客户结果。"),
|
|
932
|
+
status: zod_1.z
|
|
933
|
+
.enum(["待处理", "已联系", "已转化", "未转化", "忽略"])
|
|
934
|
+
.describe("新的跟进状态。"),
|
|
935
|
+
},
|
|
936
|
+
}, async (args) => {
|
|
937
|
+
const leadId = stringArg(args, "leadId");
|
|
938
|
+
const status = args.status;
|
|
939
|
+
if (!leadId || !status) {
|
|
940
|
+
return jsonText({ ok: false, code: "BAD_INPUT", userHint: "需要提供 leadId 和跟进状态。" });
|
|
941
|
+
}
|
|
942
|
+
try {
|
|
943
|
+
await (0, ppxc_client_1.updateLeadStatus)({ leadId, status });
|
|
944
|
+
return jsonText({
|
|
945
|
+
ok: true,
|
|
946
|
+
leadId,
|
|
947
|
+
status,
|
|
948
|
+
hint: `已把这条客户标记为「${status}」。后续可以用 review_followup_queue 复盘待处理和成交情况。`,
|
|
949
|
+
});
|
|
950
|
+
}
|
|
951
|
+
catch (err) {
|
|
952
|
+
if (err instanceof ppxc_client_1.PpxcApiError && err.status === 401) {
|
|
953
|
+
return jsonText({
|
|
954
|
+
ok: false,
|
|
955
|
+
code: "PPXC_LOGIN_REQUIRED",
|
|
956
|
+
userHint: "需要先登录 OPC,才能更新客户池状态。请调用 check_status_and_login(action=login_ppxc)。",
|
|
957
|
+
});
|
|
958
|
+
}
|
|
959
|
+
if (err instanceof ppxc_client_1.PpxcApiError && err.status === 403) {
|
|
960
|
+
return jsonText({ ok: false, code: "NO_LEAD_ACCESS", userHint: "这条客户线索不属于当前账号,不能更新。" });
|
|
961
|
+
}
|
|
962
|
+
return jsonText({
|
|
963
|
+
ok: false,
|
|
964
|
+
code: "INTERNAL",
|
|
965
|
+
userHint: "更新跟进状态时出错了,请稍后重试。",
|
|
966
|
+
detail: err instanceof Error ? err.message : String(err),
|
|
967
|
+
});
|
|
968
|
+
}
|
|
969
|
+
});
|
|
970
|
+
registerTool("review_followup_queue", {
|
|
971
|
+
description: "复盘客户池跟进队列:统计待处理、已联系、已转化、未转化和忽略,并列出下一批最该处理的客户。用户问「昨天那批怎么样 / 哪些还没跟 / 成交情况」时调用。",
|
|
972
|
+
inputSchema: {
|
|
973
|
+
productId: zod_1.z.string().optional().describe("产品 id;不传则查当前账号全部产品。"),
|
|
974
|
+
days: zod_1.z.number().int().min(1).max(90).optional().describe("只看最近 N 天入池客户,默认 7 天。"),
|
|
975
|
+
limit: zod_1.z.number().int().min(1).max(50).optional().describe("每组最多返回几条样例,默认 5。"),
|
|
976
|
+
},
|
|
977
|
+
}, async (args) => {
|
|
978
|
+
const productId = stringArg(args, "productId");
|
|
979
|
+
const days = Math.max(1, Math.min(90, args.days ?? 7));
|
|
980
|
+
const limit = Math.max(1, Math.min(50, args.limit ?? 5));
|
|
981
|
+
try {
|
|
982
|
+
const since = new Date(Date.now() - days * 86400000).toISOString();
|
|
983
|
+
const { rows, paywall } = await (0, ppxc_client_1.queryLeads)({ productId, since });
|
|
984
|
+
const byStatus = {};
|
|
985
|
+
for (const row of rows) {
|
|
986
|
+
byStatus[row.status] = (byStatus[row.status] ?? 0) + 1;
|
|
987
|
+
}
|
|
988
|
+
const pick = (status) => rows
|
|
989
|
+
.filter((row) => row.status === status)
|
|
990
|
+
.slice(0, limit)
|
|
991
|
+
.map((row) => ({
|
|
992
|
+
id: row.id,
|
|
993
|
+
nickname: row.user_nickname,
|
|
994
|
+
intent: row.urgency,
|
|
995
|
+
demandType: row.demand_type,
|
|
996
|
+
comment: row.comment_text,
|
|
997
|
+
script: row.script,
|
|
998
|
+
...(row.sale_score !== undefined ? { saleScore: row.sale_score } : {}),
|
|
999
|
+
...(row.source_keyword ? { fromKeyword: row.source_keyword } : {}),
|
|
1000
|
+
}));
|
|
1001
|
+
const pending = pick("待处理");
|
|
1002
|
+
const contacted = pick("已联系");
|
|
1003
|
+
const converted = pick("已转化");
|
|
1004
|
+
return jsonText({
|
|
1005
|
+
ok: true,
|
|
1006
|
+
days,
|
|
1007
|
+
total: rows.length,
|
|
1008
|
+
byStatus,
|
|
1009
|
+
paywall,
|
|
1010
|
+
nextActions: {
|
|
1011
|
+
pending,
|
|
1012
|
+
contactedNeedsResult: contacted,
|
|
1013
|
+
converted,
|
|
1014
|
+
},
|
|
1015
|
+
hint: rows.length === 0
|
|
1016
|
+
? "最近没有可复盘的客户。可以先跑一次 search_keyword_for_leads 或 analyze_video_comments。"
|
|
1017
|
+
: `最近 ${days} 天有 ${rows.length} 条客户记录。优先处理「待处理」客户;已联系的客户要回填已转化或未转化,这样后端才能继续学习。`,
|
|
1018
|
+
});
|
|
1019
|
+
}
|
|
1020
|
+
catch (err) {
|
|
1021
|
+
if (err instanceof ppxc_client_1.PpxcApiError && err.status === 401) {
|
|
1022
|
+
return jsonText({
|
|
1023
|
+
ok: false,
|
|
1024
|
+
code: "PPXC_LOGIN_REQUIRED",
|
|
1025
|
+
userHint: "需要先登录 OPC,才能复盘客户池。请调用 check_status_and_login(action=login_ppxc)。",
|
|
1026
|
+
});
|
|
1027
|
+
}
|
|
1028
|
+
return jsonText({
|
|
1029
|
+
ok: false,
|
|
1030
|
+
code: "INTERNAL",
|
|
1031
|
+
userHint: "复盘客户池时出错了,请稍后重试。",
|
|
1032
|
+
detail: err instanceof Error ? err.message : String(err),
|
|
1033
|
+
});
|
|
1034
|
+
}
|
|
1035
|
+
});
|
|
805
1036
|
registerTool("export_diagnostics", {
|
|
806
|
-
description: "用户反馈「不好用 / 出错了 / 需要排查」时调用:把本机运行日志打包成一个诊断文件(默认放到桌面),告诉用户文件位置,让用户发给
|
|
1037
|
+
description: "用户反馈「不好用 / 出错了 / 需要排查」时调用:把本机运行日志打包成一个诊断文件(默认放到桌面),告诉用户文件位置,让用户发给 OPC 支持人员。不含账号密码等敏感信息。",
|
|
807
1038
|
inputSchema: {},
|
|
808
1039
|
}, async () => {
|
|
809
1040
|
const result = (0, diagnostics_1.exportDiagnostics)();
|
|
@@ -818,7 +1049,7 @@ function createMcpServer() {
|
|
|
818
1049
|
return jsonText({
|
|
819
1050
|
ok: true,
|
|
820
1051
|
file: result.file,
|
|
821
|
-
hint: `诊断文件已放到:${result.file}。请把这个文件发给
|
|
1052
|
+
hint: `诊断文件已放到:${result.file}。请把这个文件发给 OPC 支持人员,里面只有运行记录,没有账号密码。`,
|
|
822
1053
|
});
|
|
823
1054
|
});
|
|
824
1055
|
return server;
|
package/package.json
CHANGED
|
@@ -1,8 +1,24 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ppxc-leads-mcp",
|
|
3
|
-
"productName": "
|
|
4
|
-
"version": "0.1.
|
|
5
|
-
"description": "
|
|
3
|
+
"productName": "Social Leads Signal MCP",
|
|
4
|
+
"version": "0.1.11",
|
|
5
|
+
"description": "Detect high-intent customer signals from social comments",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"mcp",
|
|
8
|
+
"lead-generation",
|
|
9
|
+
"sales-leads",
|
|
10
|
+
"social-media-leads",
|
|
11
|
+
"comment-analysis",
|
|
12
|
+
"intent-detection",
|
|
13
|
+
"customer-discovery",
|
|
14
|
+
"crm",
|
|
15
|
+
"sales-assistant",
|
|
16
|
+
"douyin",
|
|
17
|
+
"xiaohongshu",
|
|
18
|
+
"kuaishou",
|
|
19
|
+
"social-listening",
|
|
20
|
+
"follow-up-scripts"
|
|
21
|
+
],
|
|
6
22
|
"license": "UNLICENSED",
|
|
7
23
|
"homepage": "https://opc1.me/download/mcp",
|
|
8
24
|
"main": "dist/main.js",
|
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: ppxc-find-customers
|
|
3
|
-
description:
|
|
3
|
+
description: OPC 评论线索雷达:找客户、销售线索、评论分析、关键词、客户池、跟进话术、小红书获客、抖音获客、快手获客、短视频获客、评论区获客、客户发现。用于从抖音/小红书/快手公开评论里识别购买意向、AI 销售线索、高意向客户和可跟进客户名单。当用户说「找客户」「获客」「分析评论区」「谁想买我的产品」「从小红书/抖音/快手找销售线索」「整理客户名单」时使用。要求已接入 ppxc-leads-mcp 的 MCP 工具。
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
#
|
|
6
|
+
# OPC 评论线索雷达 · 标准工作流
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
你是用户的社媒获客助手。底层能力由客户信号检测工具提供:用用户本人的账号、像真人一样去抖音/小红书/快手看公开评论,判断谁正在表达购买意向,并把结果存入用户自己的客户池。
|
|
9
|
+
|
|
10
|
+
统一定位句:OPC 评论线索雷达是一款找客户 Agent Skill / MCP 工具,帮助商家从抖音、小红书、快手公开评论中识别购买意向、销售线索和可跟进客户名单。
|
|
11
|
+
|
|
12
|
+
## 命名和故障口径
|
|
13
|
+
|
|
14
|
+
- 对用户称呼这套能力为「OPC 评论线索雷达」或「评论线索雷达」。
|
|
15
|
+
- 不要把它叫成「PPXC 后台」「PPXC 后端」「本机后台」。用户不需要、也不能自己启动一个 PPXC 后台。
|
|
16
|
+
- MCP 工具不可用时,判断为「连接器没有启用 / MCP 配置没有生效 / 宿主还没重启」,不要说「后台没起来」。
|
|
17
|
+
- 不要一上来要求用户登录 OPC。先让用户看到试用结果:用产品/服务描述 + 平台链接或关键词跑一次,展示前 2 条客户线索;用户要保存、看完整名单或解锁更多时,再引导登录 OPC。
|
|
9
18
|
|
|
10
19
|
## 第 0 步:自检与自动接线(工具不可用时才走)
|
|
11
20
|
|
|
@@ -29,48 +38,114 @@ description: 用 PPXC 找客户小组件(MCP)从抖音/小红书/快手评
|
|
|
29
38
|
- **Claude 桌面版**:macOS `~/Library/Application Support/Claude/claude_desktop_config.json`;Windows `%APPDATA%\Claude\claude_desktop_config.json`
|
|
30
39
|
- 其他标准 MCP(stdio)宿主:在其 MCP 设置里按同样格式加一条
|
|
31
40
|
|
|
32
|
-
3.
|
|
33
|
-
4.
|
|
34
|
-
5.
|
|
41
|
+
3. **告诉用户**:「OPC 评论线索雷达的 MCP 配置已经加好了。首次启动时,智能体会按这条配置拉起 MCP 运行包(约一两分钟,取决于网络)。」
|
|
42
|
+
4. **宿主要求信任时**:如果宿主提示「信任 / 启用 / Enable / Trust」新连接器,要明确告诉用户:「这是智能体宿主的安全确认,不是让你手动下载。请在连接器管理里信任/启用 `ppxc-leads` 或 `ppxc-find-customers`,点完回来告诉我,我继续试跑找客户。」不要把用户甩去自己研究配置。
|
|
43
|
+
5. **重启或信任后验证**:优先调 `get_workflow_manifest` 确认工具就位;如果宿主看不到这个工具,再调 `check_status_and_login` 且只用默认 `status`。确认后从第 1 步继续,**不要**因此弹 OPC 登录窗。
|
|
44
|
+
6. **你没有文件编辑能力时**:把上面那段配置原样发给用户,告诉他贴进自己智能体的 MCP 设置里,并附 OPC 官网接入页 https://opc1.me/download/mcp(有逐家图文步骤)。注意:这个页面只是接入说明,不是登录窗口。
|
|
45
|
+
|
|
46
|
+
## 动态工作流优先(每次开始都先做,但不要弹登录窗)
|
|
47
|
+
|
|
48
|
+
本 Skill 不是完整业务逻辑的唯一来源。OPC 后端会持续进化找客户流程,所以每次开始找客户、复盘客户池或处理用户反馈前,必须先读一次当前动态工作流:
|
|
49
|
+
|
|
50
|
+
1. 优先调 `get_workflow_manifest` 读取最新作战手册。
|
|
51
|
+
2. 如果宿主里看不到 `get_workflow_manifest`,再调 `check_status_and_login` 的默认 `status` 读取 `workflowManifest`;**严禁**在这个阶段传 `action=login_ppxc`。
|
|
52
|
+
3. 如果动态工作流读取失败,不要中断找客户;继续按本文内置流程执行,并告诉用户“后端动态工作流暂时不可用,先用本地流程继续”。
|
|
53
|
+
4. 如果返回里有 `skill.updateHint` 或 `skill.updateCommand`,在合适时机提醒用户:“OPC 评论线索雷达 Skill 有新版流程,可按官网或这条命令更新。”
|
|
54
|
+
|
|
55
|
+
关键原则:Skill 负责触发和基本兜底,最新找客户流程以后端 `workflowManifest` 为准。
|
|
35
56
|
|
|
36
57
|
## 标准流程(按顺序)
|
|
37
58
|
|
|
38
|
-
### 第 1
|
|
59
|
+
### 第 1 步:先收产品上下文,不先登录 OPC
|
|
60
|
+
|
|
61
|
+
用户说“测试一下 / 帮我找客户 / 扫描评论 / 分析评论区”时,先拿试用扫描所需的最少信息:
|
|
39
62
|
|
|
40
|
-
|
|
63
|
+
- 产品/服务:至少要有 `productName`,能补 `productDescription / sellingPoints / targetPersona` 更好。
|
|
64
|
+
- 平台:抖音 / 小红书 / 快手,用户没说就问一句。
|
|
65
|
+
- 入口:用户给了视频/笔记链接就直接分析链接;没给链接就要 1~3 个关键词,或根据产品描述先建议 1~3 个朴素搜索词给用户确认。
|
|
41
66
|
|
|
42
|
-
|
|
43
|
-
- 目标平台未登录 → `action=login_douyin / login_xiaohongshu / login_kuaishou` 弹扫码窗,请用户用对应 App 扫码
|
|
67
|
+
不要一上来调 `check_status_and_login(action=login_ppxc)`,也不要先调 `list_products`。OPC 登录只在用户要看剩余线索、保存完整名单或查询客户池时发生。
|
|
44
68
|
|
|
45
|
-
### 第 2
|
|
69
|
+
### 第 2 步:平台登录只在抓评论需要时处理
|
|
46
70
|
|
|
47
|
-
|
|
71
|
+
试用扫描也需要借用户自己的平台登录态抓公开评论,但这不是 OPC 登录:
|
|
48
72
|
|
|
49
|
-
|
|
73
|
+
- 先直接调 `search_keyword_for_leads` 或 `analyze_video_comments`,传 `productName/productDescription` 走未登录试用模式。
|
|
74
|
+
- 如果工具返回 `LOGIN_REQUIRED`,只针对对应平台调 `check_status_and_login`:`action=login_douyin / login_xiaohongshu / login_kuaishou`,请用户用对应 App 扫码。
|
|
75
|
+
- OPC 账号未登录 → 继续试用扫描,不弹 OPC 登录窗。
|
|
76
|
+
- 如果弹出的窗口是“接入说明页”而不是登录表单,告诉用户这是配置地址误填或旧包问题:先关闭窗口,更新到新版 `ppxc-leads-mcp`,再重新调用对应动作。
|
|
50
77
|
|
|
51
|
-
|
|
78
|
+
### 第 3 步:已登录完整模式才拿产品
|
|
79
|
+
|
|
80
|
+
优先让用户先看到结果:
|
|
81
|
+
|
|
82
|
+
- 未登录或用户只是试试看 → 不调 `list_products`。请用户给一句产品/服务描述,至少要有 `productName`,能补 `productDescription / sellingPoints / targetPersona` 更好。
|
|
83
|
+
- 已登录且用户明确要用已保存产品 → 调 `list_products`。只有一个产品直接用;有多个时把名字列给用户选,**不要替用户猜**。
|
|
84
|
+
|
|
85
|
+
### 第 4 步:先要词,再开搜
|
|
86
|
+
|
|
87
|
+
不要在未登录试用阶段卡住用户:
|
|
88
|
+
|
|
89
|
+
- 已登录且有 `productId` → 先调 `suggest_search_keywords`(传 productId),从返回的主力词里挑 **3~6 个**,搭配不同词型更好(比如:1 个泛需求词 + 2 个人群痛点词 + 1 个决策对比词)。把你挑的词和理由告诉用户,用户有自己想加的词就一起带上。
|
|
90
|
+
- 未登录试用 → 让用户给 1~3 个想搜的词,或根据用户的产品描述先建议 1~3 个朴素搜索词给他确认。不要为了想词委员会要求他先登录。
|
|
52
91
|
|
|
53
92
|
`regenerate=true` 会重新生成并消耗用户电力——只有用户明确说「换一批词」才用。
|
|
54
93
|
|
|
55
|
-
### 第
|
|
94
|
+
### 第 5 步:开搜
|
|
95
|
+
|
|
96
|
+
调 `search_keyword_for_leads`。平台听用户的;用户没说就问一句,不要默认猜。
|
|
56
97
|
|
|
57
|
-
|
|
98
|
+
- 已登录完整模式:传 `keywords + productId + platform`,结果会落客户池。
|
|
99
|
+
- 未登录试用模式:传 `keywords + productName/productDescription + platform`,结果只展示前 2 条线索,不落客户池。
|
|
58
100
|
|
|
59
101
|
开搜前告诉用户:这一步要 2~3 分钟,会在后台用隐藏窗口干活。
|
|
60
102
|
|
|
61
|
-
|
|
103
|
+
如果用户给的是具体的视频/笔记链接,跳过想词和搜索,直接调 `analyze_video_comments`:
|
|
104
|
+
|
|
105
|
+
- 已登录完整模式:传 `videoUrl + productId`。
|
|
106
|
+
- 未登录试用模式:传 `videoUrl + productName/productDescription`。
|
|
62
107
|
|
|
63
|
-
### 第
|
|
108
|
+
### 第 6 步:汇报成果(固定格式)
|
|
64
109
|
|
|
65
110
|
按这个顺序说,不要把原始 JSON 念出来:
|
|
66
111
|
|
|
67
112
|
1. **一句总结**:直接用返回里的 `summary.verdict`(已含首推客户及理由)。
|
|
68
113
|
2. **前 5 名**:每人一行——昵称、意向、需求类型、一句评论原话。
|
|
69
114
|
3. **战报文件**:如果返回里有 `reportFile`,务必告诉用户「完整战报(含可复制的跟进话术)已放到桌面:文件路径」,提醒可以转发给同事照着跟进。
|
|
70
|
-
4. **下一步提示**:提醒完整名单和历史记录在
|
|
71
|
-
5.
|
|
115
|
+
4. **下一步提示**:提醒完整名单和历史记录在 OPC 网页端客户池。
|
|
116
|
+
5. **转化动作(重要)**:如果返回里 `paywall.locked` 为真,说明已经先给用户看到了前几个完整客户。要**如实、不啰嗦**地转达:「这次挖到 N 个,先给你看前 2 个完整线索(含话术和主页入口),其余 X 个已锁。登录/开通后可以保存到客户池并解锁完整名单」。用 `paywall.unlockHint` 的话术,别夸大、别假装全给了。
|
|
117
|
+
6. **登录时机**:用户说「看剩下的」「保存」「查客户池」「继续跟进」时,再调 `check_status_and_login`,参数 `action=login_ppxc`,让他登录 OPC。不要在试用扫描前做这一步。
|
|
72
118
|
|
|
73
|
-
### 第
|
|
119
|
+
### 第 6.1 步:收集用户判断(持续学习的关键)
|
|
120
|
+
|
|
121
|
+
准不准不是系统说了算,是用户说了算。汇报完客户名单后,主动问一句:
|
|
122
|
+
|
|
123
|
+
> 「这批线索里有没有明显准 / 不准 / 太泛的?你告诉我,我会记录下来,让后面越找越贴近你的客户。」
|
|
124
|
+
|
|
125
|
+
用户给出判断时调用 `mark_lead_feedback`:
|
|
126
|
+
|
|
127
|
+
- 用户说“这个准 / 这个对” → `tag=accurate`
|
|
128
|
+
- 用户说“这个不准 / 不是客户” → `tag=inaccurate`
|
|
129
|
+
- 用户说“太泛了 / 太宽了” → `tag=too_broad`
|
|
130
|
+
- 用户说“这个像客户,但还不确定” → `tag=feels_like_buyer`
|
|
131
|
+
- 用户说“像路人 / 看热闹的” → `tag=feels_like_passerby`
|
|
132
|
+
|
|
133
|
+
用户反馈可以只针对 1 条,不要强迫他给整批打分。每次反馈都要带 `leadId`;如果当前汇报里没显示 id,就先用 `query_leads` 查出对应线索再标记。
|
|
134
|
+
|
|
135
|
+
### 第 6.2 步:记录跟进结果(成交闭环的关键)
|
|
136
|
+
|
|
137
|
+
系统不能保证成交,只能保证把可跟进机会识别、排序、提醒和复盘。真正是否成交,要靠用户跟进后回填。
|
|
138
|
+
|
|
139
|
+
用户说出跟进进展时调用 `update_lead_status`:
|
|
140
|
+
|
|
141
|
+
- “我去联系了 / 已经回复了” → `status=已联系`
|
|
142
|
+
- “成交了 / 加微信了 / 付钱了” → `status=已转化`
|
|
143
|
+
- “没戏 / 不买 / 没回复” → `status=未转化`
|
|
144
|
+
- “这条不用管 / 跳过” → `status=忽略`
|
|
145
|
+
|
|
146
|
+
更新后提醒用户:这些状态会进入后端学习和复盘,下一轮会更贴近他的真实客户。
|
|
147
|
+
|
|
148
|
+
### 第 6.5 步:内容彩蛋(挖到客户后主动提议)
|
|
74
149
|
|
|
75
150
|
每次找客户的返回里都带 `contentAngles`——从这批评论提炼的内容选题方向(每条含:拍什么角度、为什么、客户原话)。汇报完客户名单后,**主动加一句**:
|
|
76
151
|
|
|
@@ -83,19 +158,27 @@ description: 用 PPXC 找客户小组件(MCP)从抖音/小红书/快手评
|
|
|
83
158
|
- 一次给 1~3 条不同角度的脚本,每条含:一句话钩子 + 3~5 句口播 + 一句行动引导;
|
|
84
159
|
- 风格贴合平台(抖音口语化、小红书种草感)。
|
|
85
160
|
|
|
86
|
-
|
|
161
|
+
这一步是「客户信号」到「内容获客」的飞轮:评论既识别了这批销售线索,又指明了下一条吸引同类客户的内容。**别强推**——用户不接就跳过。
|
|
162
|
+
|
|
163
|
+
### 第 7 步(隔天/复盘场景):查战果、换词
|
|
164
|
+
|
|
165
|
+
用户问「之前挖到的客户怎么样了」「昨天那批有跟进吗」「哪些还没跟」时优先调 `review_followup_queue`,再按需要调 `query_leads` 看明细。
|
|
166
|
+
|
|
167
|
+
复盘固定说三件事:
|
|
87
168
|
|
|
88
|
-
|
|
169
|
+
1. 还有多少待处理。
|
|
170
|
+
2. 哪些已联系但还没回填结果。
|
|
171
|
+
3. 哪些已转化 / 未转化,下一轮该保留或淘汰哪些词。
|
|
89
172
|
|
|
90
|
-
|
|
173
|
+
复盘逻辑:连续两轮不出客户的词建议淘汰;连续出现“用户标记不准”的类型,要在下一轮主动避开;用户标记准或已转化的类型,要在下一轮加权。
|
|
91
174
|
|
|
92
175
|
## 硬性注意事项
|
|
93
176
|
|
|
94
177
|
- **额度**:每个平台每天有安全抓取额度(抖音约 20 个词、小红书/快手各约 10 个)。返回 `DAILY_LIMITED` 时如实告诉用户「今天这个平台的安全额度用完了,明天再继续」,不要换平台硬刷同一批词。
|
|
95
178
|
- **验证码**:返回 `VERIFICATION_REQUIRED` 时,平台已弹出验证窗口,请用户人工完成验证后再重试。**绝不**换词重试或反复发起。
|
|
96
179
|
- **间隔**:两次抓取之间系统强制间隔半分钟,返回 `RATE_LIMITED` 时等一下再试,不要连发。
|
|
97
|
-
- **出问题**:用户说「不好用 / 出错了」时调 `export_diagnostics`,告诉用户诊断文件位置,请他发给
|
|
98
|
-
- **电力**:返回 `INSUFFICIENT_CREDITS` 时引导用户去
|
|
180
|
+
- **出问题**:用户说「不好用 / 出错了」时调 `export_diagnostics`,告诉用户诊断文件位置,请他发给 OPC 支持人员。
|
|
181
|
+
- **电力**:返回 `INSUFFICIENT_CREDITS` 时引导用户去 OPC 网页端充电。
|
|
99
182
|
- 所有工具返回里的 `userHint` 都是写好的人话,可以直接转述给用户。
|
|
100
183
|
|
|
101
184
|
## 汇报示例
|
|
@@ -107,4 +190,4 @@ description: 用 PPXC 找客户小组件(MCP)从抖音/小红书/快手评
|
|
|
107
190
|
> 2. Momo(高意向 · 竞品不满):“用了某大牌的防晒整张脸闷痘……”
|
|
108
191
|
> 3. ……
|
|
109
192
|
>
|
|
110
|
-
> 完整战报已放到你桌面(含每个人的跟进话术,可直接复制):
|
|
193
|
+
> 完整战报已放到你桌面(含每个人的跟进话术,可直接复制):OPC客户战报-xxxx.html,可以转给同事照着跟进。全部 12 人已存入 OPC 客户池,网页端随时可看。
|