ppxc-leads-mcp 0.1.8 → 0.1.10

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 CHANGED
@@ -1,11 +1,11 @@
1
- # PPXC Leads MCP Proprietary License
1
+ # OPC 评论线索雷达 MCP Proprietary License
2
2
 
3
- Copyright (c) PPXC. All rights reserved.
3
+ Copyright (c) OPC. All rights reserved.
4
4
 
5
- This package is provided only for use with the PPXC service.
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 PPXC account.
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 PPXC.
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. PPXC may update, limit, suspend, or discontinue access to service-backed functionality according to PPXC account, billing, safety, and abuse-control rules.
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
- # PPXC Leads MCP
1
+ # OPC 评论线索雷达 MCP
2
2
 
3
- PPXC Leads MCP lets an MCP-capable assistant help a PPXC user find potential customers from public comments on supported social platforms.
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: check login, choose product, suggest keywords, search for leads, query the customer pool, and export diagnostics when needed.
66
+ MCP installation exposes the tools. Skill installation teaches the assistant the safe workflow: verify the connector and platform login, run a trial analysis from a product description, show the first customer signals, then ask the user to log in only when they want to save, unlock, or query the full customer pool.
53
67
 
54
68
  ## Tools
55
69
 
56
- The package exposes seven MCP tools:
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 PPXC login credentials stay on the user's machine.
69
- - Platform tokens and cookies are not uploaded to PPXC and are not written to diagnostics.
70
- - Public comment text is sent to PPXC over HTTPS for customer-intent analysis, follow-up suggestions, and customer-pool storage.
71
- - Results are saved only to the user's own PPXC customer pool.
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 PPXC support.
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 PPXC service, but copying, modifying, reverse engineering, republishing, or redistributing it is not allowed except with written permission from PPXC.
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.
@@ -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().replace(/\/+$/, "");
8
- return raw;
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, "PPXC 未登录");
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, "PPXC 登录已失效");
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`, { method: "POST", headers: authHeaders(), body: JSON.stringify(body) }, ANALYZE_TIMEOUT_MS);
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
- return { rows: Array.isArray(data) ? data : [], paywallLocked };
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: "后端地址无效,请配置 PPXC_API_BASE" };
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: "登录 PPXC",
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
- <div class="lock-cta">开通套餐 · 解锁全部客户</div>
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()) {
@@ -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];
@@ -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: "检查抖音/小红书/快手 + PPXC 登录态。action=login_douyin|login_xiaohongshu|login_kuaishou 弹对应平台扫码窗;action=login_ppxc 弹 PPXC 账号登录窗;默认只查状态。",
194
+ description: "检查 OPC 账号 + 抖音/小红书/快手登录态。action=login_douyin|login_xiaohongshu|login_kuaishou 弹对应平台扫码窗;action=login_ppxc 弹 OPC 账号登录窗;默认只查状态。",
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=只查;login_*=弹对应平台登录窗;login_ppxc=PPXC 账号登录。"),
205
+ .describe("status=只查;login_*=弹对应平台登录窗;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,8 +239,9 @@ function createMcpServer() {
184
239
  platforms,
185
240
  ppxcLoggedIn: (0, ppxc_login_window_1.isPpxcLoggedIn)(),
186
241
  hint: result.ok
187
- ? "PPXC 账号已登录。"
188
- : `PPXC 登录未完成:${result.message ?? "请重试"}`,
242
+ ? "OPC 账号已登录。"
243
+ : `OPC 登录未完成:${result.message ?? "请重试"}`,
244
+ workflowManifest,
189
245
  logFile: logger_1.LOG_FILE_PATH,
190
246
  });
191
247
  }
@@ -198,14 +254,15 @@ function createMcpServer() {
198
254
  }
199
255
  }
200
256
  if (!ppxc)
201
- missing.push("PPXC 账号(action=login_ppxc)");
257
+ missing.push("OPC 账号(action=login_ppxc)");
202
258
  return jsonText({
203
259
  ok: true,
204
260
  platforms,
205
261
  ppxcLoggedIn: ppxc,
206
262
  hint: missing.length === 0
207
- ? "各平台与 PPXC 都已登录,可以开始找客户了。"
263
+ ? "OPC 账号与各平台都已登录,可以开始找客户了。"
208
264
  : `还需要登录:${missing.join("、")}`,
265
+ workflowManifest,
209
266
  logFile: logger_1.LOG_FILE_PATH,
210
267
  });
211
268
  }
@@ -227,7 +284,7 @@ function createMcpServer() {
227
284
  }
228
285
  });
229
286
  registerTool("list_products", {
230
- description: "列出当前 PPXC 账号下的产品,返回 id 和名称。分析评论前用它拿到 productId。",
287
+ description: "列出当前 OPC 账号下的产品,返回 id 和名称。分析评论前用它拿到 productId。",
231
288
  inputSchema: {},
232
289
  }, async () => {
233
290
  try {
@@ -236,7 +293,7 @@ function createMcpServer() {
236
293
  ok: true,
237
294
  products,
238
295
  hint: products.length === 0
239
- ? "这个账号下还没有产品,请先到 PPXC 网页端创建一个产品。"
296
+ ? "这个账号下还没有产品,请先到 OPC 网页端创建一个产品。"
240
297
  : "把其中一个产品的 id 作为 productId 传给 analyze_video_comments。",
241
298
  });
242
299
  }
@@ -245,7 +302,7 @@ function createMcpServer() {
245
302
  return jsonText({
246
303
  ok: false,
247
304
  code: "PPXC_LOGIN_REQUIRED",
248
- userHint: "PPXC 未登录或登录失效,请先用 check_status_and_login(action=login_ppxc)登录。",
305
+ userHint: "OPC 账号未登录或登录失效,请先用 check_status_and_login(action=login_ppxc)登录。",
249
306
  });
250
307
  }
251
308
  return jsonText({
@@ -257,12 +314,17 @@ function createMcpServer() {
257
314
  }
258
315
  });
259
316
  registerTool("analyze_video_comments", {
260
- description: "给一条抖音/小红书/快手内容链接 + productId:读评论 → PPXC AI 分析 → 客户战报 + 落客户池。platform 可省略,会从链接自动识别。" +
317
+ description: "给一条抖音/小红书/快手内容链接 + productId 或 productName:读评论 → OPC AI 分析 → 客户战报。未登录用户可只传 productName 先试跑,返回前 2 条线索;登录后传 productId 才会落客户池。platform 可省略,会从链接自动识别。" +
261
318
  "挖到客户时会在用户桌面生成一份战报网页文件(结果里的 reportFile),汇报时务必告诉用户文件位置和首推客户及理由(topPick)。" +
262
319
  "结果里的 contentAngles 是从这批评论提炼的内容选题方向(拍什么/为什么/客户原话),汇报后可主动提议「要不要根据这些评论写下一条视频脚本」。",
263
320
  inputSchema: {
264
321
  videoUrl: zod_1.z.string().describe("内容链接(抖音视频/小红书笔记/快手视频)或 ID。"),
265
- productId: zod_1.z.string().describe("产品 id,用 list_products 获取。"),
322
+ productId: zod_1.z.string().optional().describe("已登录用户的产品 id,用 list_products 获取;未登录试用可不传。"),
323
+ productName: zod_1.z.string().optional().describe("未登录试用时填写:用户卖的产品/服务名称。"),
324
+ productDescription: zod_1.z.string().optional().describe("未登录试用时填写:适合谁、解决什么问题、主要卖点。"),
325
+ sellingPoints: zod_1.z.array(zod_1.z.string()).max(8).optional().describe("未登录试用时可填:卖点列表。"),
326
+ problemSolved: zod_1.z.string().optional().describe("未登录试用时可填:解决的痛点。"),
327
+ targetPersona: zod_1.z.string().optional().describe("未登录试用时可填:目标人群。"),
266
328
  platform: PLATFORM_ZOD
267
329
  .optional()
268
330
  .describe("可选:douyin | xiaohongshu | kuaishou。不传则从链接自动识别。"),
@@ -271,15 +333,15 @@ function createMcpServer() {
271
333
  },
272
334
  }, async (args) => {
273
335
  const videoUrl = args.videoUrl;
274
- const productId = args.productId;
336
+ const productContext = productContextFromArgs(args);
275
337
  const maxComments = args.maxComments;
276
338
  const save = args.save;
277
339
  const platform = (0, detect_platform_1.normalizePlatformId)(args.platform, videoUrl);
278
- if (!productId || typeof productId !== "string") {
340
+ if (!hasProductContext(productContext)) {
279
341
  return jsonText({
280
342
  ok: false,
281
- code: "NO_PRODUCT",
282
- userHint: "缺少 productId。请先用 list_products 选一个产品。",
343
+ code: "NO_PRODUCT_CONTEXT",
344
+ userHint: productContextHint(),
283
345
  });
284
346
  }
285
347
  let fetched;
@@ -307,7 +369,7 @@ function createMcpServer() {
307
369
  }
308
370
  try {
309
371
  const analysis = await (0, ppxc_client_1.analyzeComments)({
310
- productId,
372
+ ...productContext,
311
373
  comments: fetched.comments,
312
374
  videoUrl: fetched.contentUrl,
313
375
  save: save !== false,
@@ -321,7 +383,7 @@ function createMcpServer() {
321
383
  const exported = (0, battle_report_1.exportBattleReport)({
322
384
  kind: "link",
323
385
  platformName: (0, registry_1.getPlatformAdapter)(platform).displayName,
324
- productName: await productNameOf(productId),
386
+ productName: productContext.productName || (productContext.productId ? await productNameOf(productContext.productId) : undefined),
325
387
  contentUrl: fetched.contentUrl,
326
388
  summary: analysis.summary,
327
389
  leads: reportLeads,
@@ -365,7 +427,7 @@ function createMcpServer() {
365
427
  ? {
366
428
  reportFile: report.file,
367
429
  reportHint: paywallLocked
368
- ? `已生成战报(网页文件),里面只展示了解锁的前 ${reportLeads.length} 个客户;其余已锁,开通套餐后全部解锁。文件在:${report.file}`
430
+ ? `已生成战报(网页文件),里面只展示了解锁的前 ${reportLeads.length} 个客户;其余已锁。开通套餐后,重新查询客户池或重新生成战报即可拿到完整结果。文件在:${report.file}`
369
431
  : `已生成一份客户战报(网页文件,含话术,可转发同事照着跟进),放在:${report.file}`,
370
432
  }
371
433
  : {}),
@@ -377,14 +439,16 @@ function createMcpServer() {
377
439
  return jsonText({
378
440
  ok: false,
379
441
  code: "PPXC_LOGIN_REQUIRED",
380
- userHint: "PPXC 未登录或登录失效,请用 check_status_and_login(action=login_ppxc)登录后重试。",
442
+ userHint: productContext.productId
443
+ ? "OPC 账号未登录或登录失效,请用 check_status_and_login(action=login_ppxc)登录后重试。"
444
+ : "试用分析暂时没跑通。可以先登录 OPC,或补充产品名称后重试。",
381
445
  });
382
446
  }
383
447
  if (err.status === 402) {
384
448
  return jsonText({
385
449
  ok: false,
386
450
  code: "INSUFFICIENT_CREDITS",
387
- userHint: `${err.message}。请到 PPXC 网页端给 AI 充电后再试。`,
451
+ userHint: `${err.message}。请到 OPC 网页端给 AI 充电后再试。`,
388
452
  });
389
453
  }
390
454
  if (err.status === 403) {
@@ -398,21 +462,21 @@ function createMcpServer() {
398
462
  return jsonText({
399
463
  ok: false,
400
464
  code: "BACKEND_RATE_LIMITED",
401
- userHint: `后端提示请求太频繁:${err.message}`,
465
+ userHint: `OPC 服务提示请求太频繁:${err.message}`,
402
466
  });
403
467
  }
404
- return jsonText({ ok: false, code: "BACKEND_ERROR", userHint: `后端分析失败:${err.message}` });
468
+ return jsonText({ ok: false, code: "BACKEND_ERROR", userHint: `OPC 分析服务失败:${err.message}` });
405
469
  }
406
470
  return jsonText({
407
471
  ok: false,
408
472
  code: "INTERNAL",
409
- userHint: "送后端分析时内部出错,请稍后重试。",
473
+ userHint: "送 OPC 分析服务时内部出错,请稍后重试。",
410
474
  detail: err instanceof Error ? err.message : String(err),
411
475
  });
412
476
  }
413
477
  });
414
478
  registerTool("search_keyword_for_leads", {
415
- description: "关键词 + productId + platform:搜内容 → 读评论 → AI 分析 → 汇总战报。支持 douyin/xiaohongshu/kuaishou。多关键词 2 窗口并行(抖音);小红书/快手更保守。" +
479
+ description: "关键词 + productId 或 productName + platform:搜内容 → 读评论 → AI 分析 → 汇总战报。未登录用户可只传 productName 先试跑,返回前 2 条线索;登录后传 productId 才会落客户池。支持 douyin/xiaohongshu/kuaishou。多关键词 2 窗口并行(抖音);小红书/快手更保守。" +
416
480
  "挖到客户时会在用户桌面生成一份战报网页文件(结果里的 reportFile),汇报时务必告诉用户文件位置和首推客户及理由(topPick)。" +
417
481
  "结果里的 contentAngles 是从这批评论提炼的内容选题方向(拍什么/为什么/客户原话),汇报后可主动提议「要不要根据这些评论写下一条视频脚本」。",
418
482
  inputSchema: {
@@ -422,7 +486,12 @@ function createMcpServer() {
422
486
  .min(1)
423
487
  .max(6)
424
488
  .describe("搜索词列表,最多 6 个。"),
425
- productId: zod_1.z.string().describe("产品 id,用 list_products 获取。"),
489
+ productId: zod_1.z.string().optional().describe("已登录用户的产品 id,用 list_products 获取;未登录试用可不传。"),
490
+ productName: zod_1.z.string().optional().describe("未登录试用时填写:用户卖的产品/服务名称。"),
491
+ productDescription: zod_1.z.string().optional().describe("未登录试用时填写:适合谁、解决什么问题、主要卖点。"),
492
+ sellingPoints: zod_1.z.array(zod_1.z.string()).max(8).optional().describe("未登录试用时可填:卖点列表。"),
493
+ problemSolved: zod_1.z.string().optional().describe("未登录试用时可填:解决的痛点。"),
494
+ targetPersona: zod_1.z.string().optional().describe("未登录试用时可填:目标人群。"),
426
495
  maxVideosPerKeyword: zod_1.z
427
496
  .number()
428
497
  .int()
@@ -435,12 +504,12 @@ function createMcpServer() {
435
504
  }, async (args) => {
436
505
  const platform = (0, detect_platform_1.normalizePlatformId)(args.platform);
437
506
  const keywords = args.keywords ?? [];
438
- const productId = args.productId;
507
+ const productContext = productContextFromArgs(args);
439
508
  const maxVideosPerKeyword = args.maxVideosPerKeyword;
440
509
  const save = args.save;
441
510
  const adapter = (0, registry_1.getPlatformAdapter)(platform);
442
- if (!productId || typeof productId !== "string") {
443
- return jsonText({ ok: false, code: "NO_PRODUCT", userHint: "缺少 productId。" });
511
+ if (!hasProductContext(productContext)) {
512
+ return jsonText({ ok: false, code: "NO_PRODUCT_CONTEXT", userHint: productContextHint() });
444
513
  }
445
514
  if (!Array.isArray(keywords) || keywords.length === 0) {
446
515
  return jsonText({ ok: false, code: "NO_KEYWORD", userHint: "缺少搜索词。" });
@@ -483,7 +552,7 @@ function createMcpServer() {
483
552
  const itemUrl = v.contentUrl ?? v.videoUrl ?? "";
484
553
  try {
485
554
  const analysis = await (0, ppxc_client_1.analyzeComments)({
486
- productId,
555
+ ...productContext,
487
556
  comments: v.comments,
488
557
  videoUrl: itemUrl,
489
558
  videoDesc: v.title,
@@ -510,14 +579,16 @@ function createMcpServer() {
510
579
  return jsonText({
511
580
  ok: false,
512
581
  code: "PPXC_LOGIN_REQUIRED",
513
- userHint: "PPXC 未登录或登录失效,请用 check_status_and_login(action=login_ppxc)登录后重试。",
582
+ userHint: productContext.productId
583
+ ? "OPC 账号未登录或登录失效,请用 check_status_and_login(action=login_ppxc)登录后重试。"
584
+ : "试用分析暂时没跑通。可以先登录 OPC,或补充产品名称后重试。",
514
585
  });
515
586
  }
516
587
  if (err instanceof ppxc_client_1.PpxcApiError && err.status === 402) {
517
588
  return jsonText({
518
589
  ok: false,
519
590
  code: "INSUFFICIENT_CREDITS",
520
- userHint: `${err.message}。请到 PPXC 网页端给 AI 充电后再试。`,
591
+ userHint: `${err.message}。请到 OPC 网页端给 AI 充电后再试。`,
521
592
  partial: allLeads.length > 0
522
593
  ? { leads: allLeads, commentsAnalyzed, demandsFound, savedToPool }
523
594
  : undefined,
@@ -580,7 +651,7 @@ function createMcpServer() {
580
651
  const exported = (0, battle_report_1.exportBattleReport)({
581
652
  kind: "search",
582
653
  platformName: adapter.displayName,
583
- productName: await productNameOf(productId),
654
+ productName: productContext.productName || (productContext.productId ? await productNameOf(productContext.productId) : undefined),
584
655
  keywords: outcomes.map((o) => o.keyword),
585
656
  summary: { itemsRead, commentsAnalyzed, demandsFound, highIntentCount },
586
657
  leads: reportLeads,
@@ -630,14 +701,14 @@ function createMcpServer() {
630
701
  ? {
631
702
  reportFile: report.file,
632
703
  reportHint: paywallLocked
633
- ? `已生成战报(网页文件),只展示了解锁的前 ${visibleAllLeads.length} 个客户;其余已锁,开通套餐后全部解锁。文件在:${report.file}`
704
+ ? `已生成战报(网页文件),只展示了解锁的前 ${visibleAllLeads.length} 个客户;其余已锁。开通套餐后,重新查询客户池或重新生成战报即可拿到完整结果。文件在:${report.file}`
634
705
  : `已生成一份客户战报(网页文件,含每个词的成绩和话术,可转发同事照着跟进),放在:${report.file}`,
635
706
  }
636
707
  : {}),
637
708
  });
638
709
  });
639
710
  registerTool("suggest_search_keywords", {
640
- description: "给 productId,返回 PPXC 想词委员会为该产品生成的精选搜索词(带词型分类和推荐理由)。" +
711
+ description: "给 productId,返回 OPC 想词委员会为该产品生成的精选搜索词(带词型分类和推荐理由)。" +
641
712
  "词单通常在产品创建/编辑时已自动生成,本工具默认直接读现成结果;没有现成词单时会触发一轮生成并等待(约 1 分钟)。" +
642
713
  "开搜前先调它,从主力词里挑 3~6 个传给 search_keyword_for_leads,比现场编词命中率高得多。" +
643
714
  "regenerate=true 强制重新生成(消耗用户电力,仅当用户明确要求换一批词时使用)。",
@@ -684,7 +755,7 @@ function createMcpServer() {
684
755
  return jsonText({
685
756
  ok: false,
686
757
  code: "COMMITTEE_PENDING",
687
- userHint: "搜词还在后台生成,请过一两分钟再调用本工具读取结果。",
758
+ userHint: "搜词还在生成,请过一两分钟再调用本工具读取结果。",
688
759
  });
689
760
  }
690
761
  const list = formatCommitteeKeywords(current.keywords);
@@ -703,7 +774,7 @@ function createMcpServer() {
703
774
  return jsonText({
704
775
  ok: false,
705
776
  code: "PPXC_LOGIN_REQUIRED",
706
- userHint: "PPXC 未登录或登录失效,请先用 check_status_and_login(action=login_ppxc)登录。",
777
+ userHint: "OPC 账号未登录或登录失效,请先用 check_status_and_login(action=login_ppxc)登录。",
707
778
  });
708
779
  }
709
780
  if (err instanceof ppxc_client_1.PpxcApiError && err.status === 403) {
@@ -720,8 +791,12 @@ function createMcpServer() {
720
791
  });
721
792
  }
722
793
  });
794
+ registerTool("get_workflow_manifest", {
795
+ description: "读取 OPC 后端最新动态工作流、能力清单和 Skill 升级提示。每次开始找客户或复盘前优先调用;如果失败,可以继续使用 MCP 内置流程。",
796
+ inputSchema: {},
797
+ }, async () => jsonText(await workflowManifestPayload()));
723
798
  registerTool("query_leads", {
724
- description: "查 PPXC 客户池里已挖到的潜在客户(只读)。支持按产品、来源搜索词、最近天数、跟进状态过滤," +
799
+ description: "查 OPC 客户池里已挖到的潜在客户(只读)。支持按产品、来源搜索词、最近天数、跟进状态过滤," +
725
800
  "默认返回最新 30 条,每条带昵称、评论原文、意向、判断理由、跟进话术、跟进状态。" +
726
801
  "用户问「之前找到的客户 / 高意向的有哪些 / 某个词搜出来的客户」时用它。",
727
802
  inputSchema: {
@@ -742,7 +817,8 @@ function createMcpServer() {
742
817
  const limit = Math.max(1, Math.min(100, args.limit ?? 30));
743
818
  try {
744
819
  const since = days ? new Date(Date.now() - days * 86400000).toISOString() : undefined;
745
- const { rows, paywallLocked } = await (0, ppxc_client_1.queryLeads)({ productId, keyword, since });
820
+ const { rows, paywall } = await (0, ppxc_client_1.queryLeads)({ productId, keyword, since });
821
+ const paywallLocked = paywall.locked === true;
746
822
  const filtered = status ? rows.filter((r) => r.status === status) : rows;
747
823
  const byStatus = {};
748
824
  const byIntent = {};
@@ -769,17 +845,17 @@ function createMcpServer() {
769
845
  }));
770
846
  const baseHint = filtered.length === 0
771
847
  ? "没查到符合条件的客户。可以放宽条件,或先用 search_keyword_for_leads 去挖新客户。"
772
- : `共 ${filtered.length} 条符合条件(按入池时间从新到旧,返回前 ${leads.length} 条)。完整客户池在 PPXC 网页端可看。`;
848
+ : `共 ${filtered.length} 条符合条件(按入池时间从新到旧,返回前 ${leads.length} 条)。完整客户池在 OPC 网页端可看。`;
773
849
  return jsonText({
774
850
  ok: true,
775
851
  totalMatched: filtered.length,
776
852
  returned: leads.length,
777
853
  byStatus,
778
854
  byIntent,
779
- paywall: paywallLocked ? { locked: true } : { locked: false },
855
+ paywall,
780
856
  leads,
781
857
  hint: paywallLocked
782
- ? `${baseHint}(你当前是体验版,名单里的跟进话术和联系方式已锁,开通套餐后解锁。)`
858
+ ? `${baseHint}(你当前是体验版,已返回可预览的客户;${paywall.unlockHint ?? "开通套餐后重新查询即可解锁完整结果。"})`
783
859
  : baseHint,
784
860
  });
785
861
  }
@@ -788,7 +864,7 @@ function createMcpServer() {
788
864
  return jsonText({
789
865
  ok: false,
790
866
  code: "PPXC_LOGIN_REQUIRED",
791
- userHint: "PPXC 未登录或登录失效,请先用 check_status_and_login(action=login_ppxc)登录。",
867
+ userHint: "OPC 账号未登录或登录失效,请先用 check_status_and_login(action=login_ppxc)登录。",
792
868
  });
793
869
  }
794
870
  if (err instanceof ppxc_client_1.PpxcApiError && err.status === 403) {
@@ -802,8 +878,160 @@ function createMcpServer() {
802
878
  });
803
879
  }
804
880
  });
881
+ registerTool("mark_lead_feedback", {
882
+ description: "记录用户对某条客户线索的主观判断。准不准由用户说了算;用户说「这个准 / 不准 / 太泛 / 像客户 / 像路人」时调用,用于后端持续学习。",
883
+ inputSchema: {
884
+ leadId: zod_1.z.string().describe("客户线索 id,来自 query_leads 或找客户结果。"),
885
+ tag: zod_1.z
886
+ .enum(["accurate", "inaccurate", "too_broad", "feels_like_buyer", "feels_like_passerby"])
887
+ .describe("用户判断:accurate=准;inaccurate=不准;too_broad=太泛;feels_like_buyer=像客户;feels_like_passerby=像路人。"),
888
+ reason: zod_1.z.string().optional().describe("用户补充原因,最多会由后端截取保存。"),
889
+ },
890
+ }, async (args) => {
891
+ const leadId = stringArg(args, "leadId");
892
+ const tag = args.tag;
893
+ const reason = stringArg(args, "reason");
894
+ if (!leadId || !tag) {
895
+ return jsonText({ ok: false, code: "BAD_INPUT", userHint: "需要提供 leadId 和反馈类型。" });
896
+ }
897
+ try {
898
+ await (0, ppxc_client_1.markLeadFeedback)({ leadId, tag, reason });
899
+ return jsonText({
900
+ ok: true,
901
+ leadId,
902
+ tag,
903
+ hint: "已记录你的判断。后端会把这类反馈用于后续找客户和想词学习。",
904
+ });
905
+ }
906
+ catch (err) {
907
+ if (err instanceof ppxc_client_1.PpxcApiError && err.status === 401) {
908
+ return jsonText({
909
+ ok: false,
910
+ code: "PPXC_LOGIN_REQUIRED",
911
+ userHint: "需要先登录 OPC,才能把反馈写入你的客户池。请调用 check_status_and_login(action=login_ppxc)。",
912
+ });
913
+ }
914
+ if (err instanceof ppxc_client_1.PpxcApiError && err.status === 403) {
915
+ return jsonText({ ok: false, code: "NO_LEAD_ACCESS", userHint: "这条客户线索不属于当前账号,不能标记。" });
916
+ }
917
+ return jsonText({
918
+ ok: false,
919
+ code: "INTERNAL",
920
+ userHint: "记录线索反馈时出错了,请稍后重试。",
921
+ detail: err instanceof Error ? err.message : String(err),
922
+ });
923
+ }
924
+ });
925
+ registerTool("update_lead_status", {
926
+ description: "更新客户线索跟进状态。用户说「已联系 / 已成交 / 没转化 / 忽略」时调用,用于形成真实跟进闭环和后续复盘。",
927
+ inputSchema: {
928
+ leadId: zod_1.z.string().describe("客户线索 id,来自 query_leads 或找客户结果。"),
929
+ status: zod_1.z
930
+ .enum(["待处理", "已联系", "已转化", "未转化", "忽略"])
931
+ .describe("新的跟进状态。"),
932
+ },
933
+ }, async (args) => {
934
+ const leadId = stringArg(args, "leadId");
935
+ const status = args.status;
936
+ if (!leadId || !status) {
937
+ return jsonText({ ok: false, code: "BAD_INPUT", userHint: "需要提供 leadId 和跟进状态。" });
938
+ }
939
+ try {
940
+ await (0, ppxc_client_1.updateLeadStatus)({ leadId, status });
941
+ return jsonText({
942
+ ok: true,
943
+ leadId,
944
+ status,
945
+ hint: `已把这条客户标记为「${status}」。后续可以用 review_followup_queue 复盘待处理和成交情况。`,
946
+ });
947
+ }
948
+ catch (err) {
949
+ if (err instanceof ppxc_client_1.PpxcApiError && err.status === 401) {
950
+ return jsonText({
951
+ ok: false,
952
+ code: "PPXC_LOGIN_REQUIRED",
953
+ userHint: "需要先登录 OPC,才能更新客户池状态。请调用 check_status_and_login(action=login_ppxc)。",
954
+ });
955
+ }
956
+ if (err instanceof ppxc_client_1.PpxcApiError && err.status === 403) {
957
+ return jsonText({ ok: false, code: "NO_LEAD_ACCESS", userHint: "这条客户线索不属于当前账号,不能更新。" });
958
+ }
959
+ return jsonText({
960
+ ok: false,
961
+ code: "INTERNAL",
962
+ userHint: "更新跟进状态时出错了,请稍后重试。",
963
+ detail: err instanceof Error ? err.message : String(err),
964
+ });
965
+ }
966
+ });
967
+ registerTool("review_followup_queue", {
968
+ description: "复盘客户池跟进队列:统计待处理、已联系、已转化、未转化和忽略,并列出下一批最该处理的客户。用户问「昨天那批怎么样 / 哪些还没跟 / 成交情况」时调用。",
969
+ inputSchema: {
970
+ productId: zod_1.z.string().optional().describe("产品 id;不传则查当前账号全部产品。"),
971
+ days: zod_1.z.number().int().min(1).max(90).optional().describe("只看最近 N 天入池客户,默认 7 天。"),
972
+ limit: zod_1.z.number().int().min(1).max(50).optional().describe("每组最多返回几条样例,默认 5。"),
973
+ },
974
+ }, async (args) => {
975
+ const productId = stringArg(args, "productId");
976
+ const days = Math.max(1, Math.min(90, args.days ?? 7));
977
+ const limit = Math.max(1, Math.min(50, args.limit ?? 5));
978
+ try {
979
+ const since = new Date(Date.now() - days * 86400000).toISOString();
980
+ const { rows, paywall } = await (0, ppxc_client_1.queryLeads)({ productId, since });
981
+ const byStatus = {};
982
+ for (const row of rows) {
983
+ byStatus[row.status] = (byStatus[row.status] ?? 0) + 1;
984
+ }
985
+ const pick = (status) => rows
986
+ .filter((row) => row.status === status)
987
+ .slice(0, limit)
988
+ .map((row) => ({
989
+ id: row.id,
990
+ nickname: row.user_nickname,
991
+ intent: row.urgency,
992
+ demandType: row.demand_type,
993
+ comment: row.comment_text,
994
+ script: row.script,
995
+ ...(row.sale_score !== undefined ? { saleScore: row.sale_score } : {}),
996
+ ...(row.source_keyword ? { fromKeyword: row.source_keyword } : {}),
997
+ }));
998
+ const pending = pick("待处理");
999
+ const contacted = pick("已联系");
1000
+ const converted = pick("已转化");
1001
+ return jsonText({
1002
+ ok: true,
1003
+ days,
1004
+ total: rows.length,
1005
+ byStatus,
1006
+ paywall,
1007
+ nextActions: {
1008
+ pending,
1009
+ contactedNeedsResult: contacted,
1010
+ converted,
1011
+ },
1012
+ hint: rows.length === 0
1013
+ ? "最近没有可复盘的客户。可以先跑一次 search_keyword_for_leads 或 analyze_video_comments。"
1014
+ : `最近 ${days} 天有 ${rows.length} 条客户记录。优先处理「待处理」客户;已联系的客户要回填已转化或未转化,这样后端才能继续学习。`,
1015
+ });
1016
+ }
1017
+ catch (err) {
1018
+ if (err instanceof ppxc_client_1.PpxcApiError && err.status === 401) {
1019
+ return jsonText({
1020
+ ok: false,
1021
+ code: "PPXC_LOGIN_REQUIRED",
1022
+ userHint: "需要先登录 OPC,才能复盘客户池。请调用 check_status_and_login(action=login_ppxc)。",
1023
+ });
1024
+ }
1025
+ return jsonText({
1026
+ ok: false,
1027
+ code: "INTERNAL",
1028
+ userHint: "复盘客户池时出错了,请稍后重试。",
1029
+ detail: err instanceof Error ? err.message : String(err),
1030
+ });
1031
+ }
1032
+ });
805
1033
  registerTool("export_diagnostics", {
806
- description: "用户反馈「不好用 / 出错了 / 需要排查」时调用:把本机运行日志打包成一个诊断文件(默认放到桌面),告诉用户文件位置,让用户发给 PPXC 支持人员。不含账号密码等敏感信息。",
1034
+ description: "用户反馈「不好用 / 出错了 / 需要排查」时调用:把本机运行日志打包成一个诊断文件(默认放到桌面),告诉用户文件位置,让用户发给 OPC 支持人员。不含账号密码等敏感信息。",
807
1035
  inputSchema: {},
808
1036
  }, async () => {
809
1037
  const result = (0, diagnostics_1.exportDiagnostics)();
@@ -818,7 +1046,7 @@ function createMcpServer() {
818
1046
  return jsonText({
819
1047
  ok: true,
820
1048
  file: result.file,
821
- hint: `诊断文件已放到:${result.file}。请把这个文件发给 PPXC 支持人员,里面只有运行记录,没有账号密码。`,
1049
+ hint: `诊断文件已放到:${result.file}。请把这个文件发给 OPC 支持人员,里面只有运行记录,没有账号密码。`,
822
1050
  });
823
1051
  });
824
1052
  return server;
package/package.json CHANGED
@@ -1,8 +1,24 @@
1
1
  {
2
2
  "name": "ppxc-leads-mcp",
3
- "productName": "PPXC Leads MCP",
4
- "version": "0.1.8",
5
- "description": "PPXC 找客户能力的 MCP 本地连接器",
3
+ "productName": "Social Leads Signal MCP",
4
+ "version": "0.1.10",
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: PPXC 找客户小组件(MCP)从抖音/小红书/快手评论区挖潜在客户的标准工作流。当用户说「帮我找客户」「谁想买我的产品」「分析这条视频/笔记的评论区」「用XX词搜搜看」「之前挖到的客户怎么样了」时使用。要求已接入 ppxc-leads-mcp 的 MCP 工具。
3
+ description: OPC 评论线索雷达:找客户、销售线索、评论分析、关键词、客户池、跟进话术、小红书获客、抖音获客、快手获客、短视频获客、评论区获客、客户发现。用于从抖音/小红书/快手公开评论里识别购买意向、AI 销售线索、高意向客户和可跟进客户名单。当用户说「找客户」「获客」「分析评论区」「谁想买我的产品」「从小红书/抖音/快手找销售线索」「整理客户名单」时使用。要求已接入 ppxc-leads-mcp 的 MCP 工具。
4
4
  ---
5
5
 
6
- # PPXC 找客户 · 标准工作流
6
+ # OPC 评论线索雷达 · 标准工作流
7
7
 
8
- 你是用户的找客户助手。底层能力由 PPXC 找客户小组件(MCP 工具)提供:用用户本人的账号、像真人一样去抖音/小红书/快手看评论区,AI 判断谁是潜在客户,并把结果存入用户的 PPXC 客户池。
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,36 +38,61 @@ 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. **告诉用户**:「我把 PPXC 找客户小组件的配置加好了,重启/刷新一下智能体就能用。首次使用会自动下载运行环境(约一两分钟,取决于网络)。」
33
- 4. **重启后验证**:调 `check_status_and_login` 确认工具就位,然后从第 1 步继续。
34
- 5. **你没有文件编辑能力时**:把上面那段配置原样发给用户,告诉他贴进自己智能体的 MCP 设置里,并附 PPXC 官网接入页 https://opc1.me/download/mcp(有逐家图文步骤)。
41
+ 3. **告诉用户**:「OPC 评论线索雷达的 MCP 配置已经加好了。首次启动时,智能体会按这条配置拉起 MCP 运行包(约一两分钟,取决于网络)。」
42
+ 4. **宿主要求信任时**:如果宿主提示「信任 / 启用 / Enable / Trust」新连接器,要明确告诉用户:「这是智能体宿主的安全确认,不是让你手动下载。请在连接器管理里信任/启用 `ppxc-leads` `ppxc-find-customers`,点完回来告诉我,我继续试跑找客户。」不要把用户甩去自己研究配置。
43
+ 5. **重启或信任后验证**:调 `check_status_and_login` 确认工具就位,然后从第 1 步继续。
44
+ 6. **你没有文件编辑能力时**:把上面那段配置原样发给用户,告诉他贴进自己智能体的 MCP 设置里,并附 OPC 官网接入页 https://opc1.me/download/mcp(有逐家图文步骤)。注意:这个页面只是接入说明,不是登录窗口。
45
+
46
+ ## 动态工作流优先(每次开始都先做)
47
+
48
+ 本 Skill 不是完整业务逻辑的唯一来源。OPC 后端会持续进化找客户流程,所以每次开始找客户、复盘客户池或处理用户反馈前,必须先读一次当前动态工作流:
49
+
50
+ 1. 优先调 `check_status_and_login`。它会返回 `workflowManifest`,里面包含当前能力清单、工作流版本、Skill 升级提示。
51
+ 2. 如果宿主里能看到 `get_workflow_manifest`,也可以直接调用它读取最新作战手册。
52
+ 3. 如果动态工作流读取失败,不要中断找客户;继续按本文内置流程执行,并告诉用户“后端动态工作流暂时不可用,先用本地流程继续”。
53
+ 4. 如果返回里有 `skill.updateHint` 或 `skill.updateCommand`,在合适时机提醒用户:“OPC 评论线索雷达 Skill 有新版流程,可按官网或这条命令更新。”
54
+
55
+ 关键原则:Skill 负责触发和基本兜底,最新找客户流程以后端 `workflowManifest` 为准。
35
56
 
36
57
  ## 标准流程(按顺序)
37
58
 
38
- ### 第 1 步:确认登录
59
+ ### 第 1 步:确认工具和平台登录
39
60
 
40
- `check_status_and_login`(默认 status)。缺哪个登录就引导用户补哪个:
61
+ 先调 `check_status_and_login`(默认 status)确认 MCP 工具可用。这里不要因为 OPC 账号未登录就停下来。
41
62
 
42
- - PPXC 账号未登录 → `action=login_ppxc` 弹登录窗
43
- - 目标平台未登录 → `action=login_douyin / login_xiaohongshu / login_kuaishou` 弹扫码窗,请用户用对应 App 扫码
63
+ - 目标平台未登录立刻调 `check_status_and_login`,参数 `action=login_douyin / login_xiaohongshu / login_kuaishou`,弹扫码窗,请用户用对应 App 扫码。
64
+ - OPC 账号未登录 暂时不弹登录窗。先继续做试用分析,等用户看到线索、要保存或看完整名单时,再调 `action=login_ppxc`。
65
+ - 如果弹出的窗口是“接入说明页”而不是登录表单,告诉用户这是配置地址误填或旧包问题:先关闭窗口,更新到新版 `ppxc-leads-mcp`,再重新调用 `action=login_ppxc`。
44
66
 
45
- ### 第 2 步:确认产品
67
+ ### 第 2 步:拿到产品上下文
46
68
 
47
- 调 `list_products`。只有一个产品直接用;有多个时把名字列给用户选,**不要替用户猜**。
69
+ 优先让用户先看到结果:
70
+
71
+ - 未登录或用户只是试试看 → 不调 `list_products`。请用户给一句产品/服务描述,至少要有 `productName`,能补 `productDescription / sellingPoints / targetPersona` 更好。
72
+ - 已登录且用户明确要用已保存产品 → 调 `list_products`。只有一个产品直接用;有多个时把名字列给用户选,**不要替用户猜**。
48
73
 
49
74
  ### 第 3 步:先要词,再开搜
50
75
 
51
- 不要自己现场编搜索词。先调 `suggest_search_keywords`(传 productId),从返回的主力词里挑 **3~6 个**,搭配不同词型更好(比如:1 个泛需求词 + 2 个人群痛点词 + 1 个决策对比词)。把你挑的词和理由告诉用户,用户有自己想加的词就一起带上。
76
+ 不要在未登录试用阶段卡住用户:
77
+
78
+ - 已登录且有 `productId` → 先调 `suggest_search_keywords`(传 productId),从返回的主力词里挑 **3~6 个**,搭配不同词型更好(比如:1 个泛需求词 + 2 个人群痛点词 + 1 个决策对比词)。把你挑的词和理由告诉用户,用户有自己想加的词就一起带上。
79
+ - 未登录试用 → 让用户给 1~3 个想搜的词,或根据用户的产品描述先建议 1~3 个朴素搜索词给他确认。不要为了想词委员会要求他先登录。
52
80
 
53
81
  `regenerate=true` 会重新生成并消耗用户电力——只有用户明确说「换一批词」才用。
54
82
 
55
83
  ### 第 4 步:开搜
56
84
 
57
- 调 `search_keyword_for_leads`(keywords + productId + platform)。平台听用户的;用户没说就问一句,不要默认猜。
85
+ 调 `search_keyword_for_leads`。平台听用户的;用户没说就问一句,不要默认猜。
86
+
87
+ - 已登录完整模式:传 `keywords + productId + platform`,结果会落客户池。
88
+ - 未登录试用模式:传 `keywords + productName/productDescription + platform`,结果只展示前 2 条线索,不落客户池。
58
89
 
59
90
  开搜前告诉用户:这一步要 2~3 分钟,会在后台用隐藏窗口干活。
60
91
 
61
- 如果用户给的是具体的视频/笔记链接,跳过 3、4 步,直接调 `analyze_video_comments`。
92
+ 如果用户给的是具体的视频/笔记链接,跳过想词和搜索,直接调 `analyze_video_comments`:
93
+
94
+ - 已登录完整模式:传 `videoUrl + productId`。
95
+ - 未登录试用模式:传 `videoUrl + productName/productDescription`。
62
96
 
63
97
  ### 第 5 步:汇报成果(固定格式)
64
98
 
@@ -67,8 +101,38 @@ description: 用 PPXC 找客户小组件(MCP)从抖音/小红书/快手评
67
101
  1. **一句总结**:直接用返回里的 `summary.verdict`(已含首推客户及理由)。
68
102
  2. **前 5 名**:每人一行——昵称、意向、需求类型、一句评论原话。
69
103
  3. **战报文件**:如果返回里有 `reportFile`,务必告诉用户「完整战报(含可复制的跟进话术)已放到桌面:文件路径」,提醒可以转发给同事照着跟进。
70
- 4. **下一步提示**:提醒完整名单和历史记录在 PPXC 网页端客户池。
71
- 5. **收费墙(重要)**:如果返回里 `paywall.locked` 为真,说明用户是体验版、只解锁了前几个完整客户。要**如实、不啰嗦**地转达:「这次挖到 N 个,体验版先给你看了前 2 个完整的(含话术和联系方式),其余 X 个已锁,开通套餐后全部解锁」。用 `paywall.unlockHint` 的话术,别夸大、别假装全给了。
104
+ 4. **下一步提示**:提醒完整名单和历史记录在 OPC 网页端客户池。
105
+ 5. **转化动作(重要)**:如果返回里 `paywall.locked` 为真,说明已经先给用户看到了前几个完整客户。要**如实、不啰嗦**地转达:「这次挖到 N 个,先给你看前 2 个完整线索(含话术和主页入口),其余 X 个已锁。登录/开通后可以保存到客户池并解锁完整名单」。用 `paywall.unlockHint` 的话术,别夸大、别假装全给了。
106
+ 6. **登录时机**:用户说「看剩下的」「保存」「查客户池」「继续跟进」时,再调 `check_status_and_login`,参数 `action=login_ppxc`,让他登录 OPC。
107
+
108
+ ### 第 5.1 步:收集用户判断(持续学习的关键)
109
+
110
+ 准不准不是系统说了算,是用户说了算。汇报完客户名单后,主动问一句:
111
+
112
+ > 「这批线索里有没有明显准 / 不准 / 太泛的?你告诉我,我会记录下来,让后面越找越贴近你的客户。」
113
+
114
+ 用户给出判断时调用 `mark_lead_feedback`:
115
+
116
+ - 用户说“这个准 / 这个对” → `tag=accurate`
117
+ - 用户说“这个不准 / 不是客户” → `tag=inaccurate`
118
+ - 用户说“太泛了 / 太宽了” → `tag=too_broad`
119
+ - 用户说“这个像客户,但还不确定” → `tag=feels_like_buyer`
120
+ - 用户说“像路人 / 看热闹的” → `tag=feels_like_passerby`
121
+
122
+ 用户反馈可以只针对 1 条,不要强迫他给整批打分。每次反馈都要带 `leadId`;如果当前汇报里没显示 id,就先用 `query_leads` 查出对应线索再标记。
123
+
124
+ ### 第 5.2 步:记录跟进结果(成交闭环的关键)
125
+
126
+ 系统不能保证成交,只能保证把可跟进机会识别、排序、提醒和复盘。真正是否成交,要靠用户跟进后回填。
127
+
128
+ 用户说出跟进进展时调用 `update_lead_status`:
129
+
130
+ - “我去联系了 / 已经回复了” → `status=已联系`
131
+ - “成交了 / 加微信了 / 付钱了” → `status=已转化`
132
+ - “没戏 / 不买 / 没回复” → `status=未转化`
133
+ - “这条不用管 / 跳过” → `status=忽略`
134
+
135
+ 更新后提醒用户:这些状态会进入后端学习和复盘,下一轮会更贴近他的真实客户。
72
136
 
73
137
  ### 第 5.5 步:内容彩蛋(挖到客户后主动提议)
74
138
 
@@ -83,19 +147,27 @@ description: 用 PPXC 找客户小组件(MCP)从抖音/小红书/快手评
83
147
  - 一次给 1~3 条不同角度的脚本,每条含:一句话钩子 + 3~5 句口播 + 一句行动引导;
84
148
  - 风格贴合平台(抖音口语化、小红书种草感)。
85
149
 
86
- 这一步是「找客户」到「做内容」的飞轮:评论既挖出了这批客户,又指明了下一条吸引同类客户的内容。**别强推**——用户不接就跳过。
150
+ 这一步是「客户信号」到「内容获客」的飞轮:评论既识别了这批销售线索,又指明了下一条吸引同类客户的内容。**别强推**——用户不接就跳过。
87
151
 
88
152
  ### 第 6 步(隔天/复盘场景):查战果、换词
89
153
 
90
- 用户问「之前挖到的客户怎么样了」「昨天那批有跟进吗」时调 `query_leads`(支持按产品/搜索词/天数/跟进状态筛)。复盘逻辑:连续两轮不出客户的词建议淘汰,回第 3 步补新词。
154
+ 用户问「之前挖到的客户怎么样了」「昨天那批有跟进吗」「哪些还没跟」时优先调 `review_followup_queue`,再按需要调 `query_leads` 看明细。
155
+
156
+ 复盘固定说三件事:
157
+
158
+ 1. 还有多少待处理。
159
+ 2. 哪些已联系但还没回填结果。
160
+ 3. 哪些已转化 / 未转化,下一轮该保留或淘汰哪些词。
161
+
162
+ 复盘逻辑:连续两轮不出客户的词建议淘汰;连续出现“用户标记不准”的类型,要在下一轮主动避开;用户标记准或已转化的类型,要在下一轮加权。
91
163
 
92
164
  ## 硬性注意事项
93
165
 
94
166
  - **额度**:每个平台每天有安全抓取额度(抖音约 20 个词、小红书/快手各约 10 个)。返回 `DAILY_LIMITED` 时如实告诉用户「今天这个平台的安全额度用完了,明天再继续」,不要换平台硬刷同一批词。
95
167
  - **验证码**:返回 `VERIFICATION_REQUIRED` 时,平台已弹出验证窗口,请用户人工完成验证后再重试。**绝不**换词重试或反复发起。
96
168
  - **间隔**:两次抓取之间系统强制间隔半分钟,返回 `RATE_LIMITED` 时等一下再试,不要连发。
97
- - **出问题**:用户说「不好用 / 出错了」时调 `export_diagnostics`,告诉用户诊断文件位置,请他发给 PPXC 支持人员。
98
- - **电力**:返回 `INSUFFICIENT_CREDITS` 时引导用户去 PPXC 网页端充电。
169
+ - **出问题**:用户说「不好用 / 出错了」时调 `export_diagnostics`,告诉用户诊断文件位置,请他发给 OPC 支持人员。
170
+ - **电力**:返回 `INSUFFICIENT_CREDITS` 时引导用户去 OPC 网页端充电。
99
171
  - 所有工具返回里的 `userHint` 都是写好的人话,可以直接转述给用户。
100
172
 
101
173
  ## 汇报示例
@@ -107,4 +179,4 @@ description: 用 PPXC 找客户小组件(MCP)从抖音/小红书/快手评
107
179
  > 2. Momo(高意向 · 竞品不满):“用了某大牌的防晒整张脸闷痘……”
108
180
  > 3. ……
109
181
  >
110
- > 完整战报已放到你桌面(含每个人的跟进话术,可直接复制):PPXC客户战报-xxxx.html,可以转给同事照着跟进。全部 12 人已存入 PPXC 客户池,网页端随时可看。
182
+ > 完整战报已放到你桌面(含每个人的跟进话术,可直接复制):OPC客户战报-xxxx.html,可以转给同事照着跟进。全部 12 人已存入 OPC 客户池,网页端随时可看。