ppxc-leads-mcp 0.1.7 → 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.
Files changed (35) hide show
  1. package/LICENSE.md +11 -0
  2. package/README.md +56 -130
  3. package/dist/backend/config.js +11 -3
  4. package/dist/backend/ppxc-client.js +83 -8
  5. package/dist/backend/ppxc-login-window.js +3 -4
  6. package/dist/backend/token-store.js +0 -1
  7. package/dist/browser/comments.js +0 -1
  8. package/dist/browser/douyin-runner.js +0 -1
  9. package/dist/browser/kernel/electron-profile.js +0 -1
  10. package/dist/browser/kernel/logger.js +0 -1
  11. package/dist/browser/kernel/page-scripts/index.js +0 -1
  12. package/dist/browser/kernel/runner-page-manager.js +0 -1
  13. package/dist/browser/kernel/runner-page-session.js +0 -1
  14. package/dist/browser/kernel/runner-page-session.search-parser.js +0 -1
  15. package/dist/browser/kernel/runner-page-session.user-agent.js +0 -1
  16. package/dist/browser/platform-runner.js +0 -1
  17. package/dist/browser/platforms/detect-platform.js +0 -1
  18. package/dist/browser/platforms/douyin/adapter.js +0 -1
  19. package/dist/browser/platforms/douyin/comments.js +0 -1
  20. package/dist/browser/platforms/kuaishou/adapter.js +0 -1
  21. package/dist/browser/platforms/kuaishou/comments.js +0 -1
  22. package/dist/browser/platforms/registry.js +0 -1
  23. package/dist/browser/platforms/shared/cdp-json-waiter.js +0 -1
  24. package/dist/browser/platforms/types.js +0 -1
  25. package/dist/browser/platforms/xiaohongshu/adapter.js +0 -1
  26. package/dist/browser/platforms/xiaohongshu/comments.js +0 -1
  27. package/dist/browser/usage-throttle.js +0 -1
  28. package/dist/main.js +0 -1
  29. package/dist/mcp/battle-report.js +5 -4
  30. package/dist/mcp/content-insights.js +0 -1
  31. package/dist/mcp/diagnostics.js +0 -1
  32. package/dist/mcp/server.js +272 -45
  33. package/dist/version.js +0 -1
  34. package/package.json +19 -10
  35. package/skills/ppxc-find-customers/SKILL.md +94 -22
package/LICENSE.md ADDED
@@ -0,0 +1,11 @@
1
+ # OPC 评论线索雷达 MCP Proprietary License
2
+
3
+ Copyright (c) OPC. All rights reserved.
4
+
5
+ This package is provided only for use with the OPC service.
6
+
7
+ You may install and run this package to connect an MCP-capable assistant to your own OPC account.
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 OPC.
10
+
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,20 +1,27 @@
1
- # PPXC Leads MCP
1
+ # OPC 评论线索雷达 MCP
2
2
 
3
- PPXC「从评论区发现高意向客户」的能力,做成智能体可直接调用的 MCP 工具包。
3
+ OPC 评论线索雷达是一款找客户 Agent Skill / MCP 工具,帮助商家从抖音、小红书、快手公开评论中识别购买意向、销售线索和可跟进客户名单。
4
4
 
5
- 发布包:<https://www.npmjs.com/package/ppxc-leads-mcp>
6
- 官网接入页:<https://opc1.me/download/mcp>
7
- 公开仓库:<https://github.com/yuanjian068yuan/ppxc-leads-mcp>
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.
8
6
 
9
- > **先读这两份,再动手:**
10
- > 1. [`AGENTS.md`](./AGENTS.md) — 本仓库的硬性边界规则(AI 协作员必读第一文件)
11
- > 2. [`docs/开发手册.md`](./docs/开发手册.md) — 架构、工具契约、平台适配层、验收标准
12
- >
13
- > 产品化(从本地半成品到正式可分发小组件)的路线:[`docs/产品化规划-2026-06-11.md`](./docs/产品化规划-2026-06-11.md)
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.
14
8
 
15
- ## 安装(一行配置)
9
+ Package: <https://www.npmjs.com/package/ppxc-leads-mcp>
10
+ Setup guide: <https://opc1.me/download/mcp>
16
11
 
17
- 在支持 MCP 的智能体(Claude 桌面版 / Cursor / 其他)配置里加一段:
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
+
22
+ ## Install MCP
23
+
24
+ Add this server to an MCP-capable host:
18
25
 
19
26
  ```json
20
27
  {
@@ -27,11 +34,7 @@
27
34
  }
28
35
  ```
29
36
 
30
- - 首次使用会自动下载运行环境(约 100MB)。如果下载失败,换网络重试;国内网络可在 `env` 里额外加 `ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/`。
31
- - 默认连 PPXC 生产站;开发联调时在 `env` 里加 `PPXC_API_BASE` 指向本地后端。
32
- - 装好后对智能体说「检查登录状态」,按提示完成 PPXC 登录和平台扫码即可使用。
33
-
34
- Windows 宿主如果不能直接执行 `npx`,用:
37
+ If a Windows host cannot execute `npx` directly, use:
35
38
 
36
39
  ```json
37
40
  {
@@ -44,136 +47,59 @@ Windows 宿主如果不能直接执行 `npx`,用:
44
47
  }
45
48
  ```
46
49
 
47
- ## 一句话定位
48
-
49
- 用户在智能体里一行配置装好本工具包;首次登录 PPXC 账号 + 按需扫各平台二维码;之后对智能体说「分析这条内容 / 用这个词在小红书找客户」,工具包用自带的隐藏浏览器内核抓取数据、送 PPXC 后端分析,把「总体判断 + 高意向客户 + 跟进话术 + 内容选题」直接返回给智能体。
50
-
51
- ## 支持的平台
52
-
53
- | 平台 | 链接分析评论 | 关键词搜索 | 数据获取方式 |
54
- |---|---|---|---|
55
- | 抖音 | ✅ | ✅ | 页内 fetch + 搜索 sniffer(已真机验证) |
56
- | 小红书 | ✅ 真机验证通过(2026-06-11) | ✅ 真机验证通过(2026-06-11) | CDP 监听 + 滚动;笔记页需带 xsec_token 签名链接 |
57
- | 快手 | ✅ 真机验证通过(2026-06-11) | ✅ 真机验证通过(2026-06-11) | CDP 监听 graphql(rootCommentsV2);搜索走 /rest/v/search/feed |
58
-
59
- ## 它不是什么
50
+ First launch downloads the local browser runtime. Node.js 18 or newer is required.
60
51
 
61
- - **不是**桌面客户端:没有产品界面,唯一允许出现的窗口是登录窗和验证码窗
62
- - **不是**浏览器插件:不依赖用户手动安装插件
63
- - **不是**爬虫平台:借用户本人登录态、保守频率、撞验证就请真人处理,不做任何过验证 / 共享账号的灰色手段
52
+ ## Install Skill
64
53
 
65
- ## 隐私与安全
54
+ The official workflow skill is distributed in:
66
55
 
67
- - 平台登录态和 PPXC 登录凭证只保存在用户本机。
68
- - 不上传平台 token / Cookie,不把它们写进日志或诊断包。
69
- - 评论文本会通过 HTTPS 发送到 PPXC 后端做 AI 意向判断和话术生成。
70
- - 分析出的客户结果会写入用户自己的 PPXC 客户池。
71
- - 遇到验证码或平台安全验证时,组件只会弹出窗口让用户本人处理,不做自动绕过。
72
-
73
- ## 形态(方案 A:自带浏览器内核的 CDP MCP)
74
-
75
- ```
76
- 智能体(Claude / Cursor / ...)
77
- │ MCP 协议(stdio)
78
-
79
- 本工具包(无界面后台进程)
80
- ├── MCP 协议层 ← 7 个 tool,platform 参数 / 链接自动识别
81
- ├── 浏览器执行层 ← 平台插座 + 三平台方言件 + 隐藏窗口内核
82
- └── 后端对接层 ← 调 PPXC 生产后端的同步分析接口
56
+ ```text
57
+ skills/ppxc-find-customers/SKILL.md
83
58
  ```
84
59
 
85
- ## 工具
86
-
87
- | 工具 | 用户场景 | 状态 |
88
- |---|---|---|
89
- | `check_status_and_login` | 检查 PPXC + 抖音/小红书/快手登录;`login_*` 弹对应扫码窗 | 已接入三平台 |
90
- | `list_products` | 列出账号下产品,拿 productId | 第二期 |
91
- | `analyze_video_comments` | 给内容链接 + 产品 → 读评论 → AI 分析 → 战报 + 入池;platform 可省略 | 抖音已验;小红书/快手待真机 |
92
- | `search_keyword_for_leads` | 给关键词 + **platform** → 搜内容 → 读评论 → 分析 → 汇总战报 | 抖音已验;小红书/快手待真机 |
93
- | `suggest_search_keywords` | 开搜前先要词:读后端想词委员会为产品生成的精选搜索词(带词型+理由) | 0.2 新增 |
94
- | `query_leads` | 问「之前挖到的客户 / 高意向有哪些」→ 只读查客户池,支持按词/天数/状态筛 | 0.2 新增 |
95
- | `export_diagnostics` | 用户说「不好用 / 要反馈」→ 把运行日志打包成桌面上的诊断文件 | 已实测 |
96
-
97
- ## FAQ
98
-
99
- ### 需要什么环境?
100
-
101
- macOS 或 Windows,Node.js 18+,以及一个 PPXC 账号。目标平台账号由用户本人扫码登录。
102
-
103
- ### 为什么不能云托管?
104
-
105
- 本工具依赖用户本机的平台登录态和扫码/验证码窗口,必须在用户自己的电脑上运行。市场上架时请选择 Local / stdio 模式。
60
+ Install the whole `skills/ppxc-find-customers/` folder into your assistant's skill directory, or import the folder through the host's skill UI. For Codex Desktop / Codex CLI, the usual destination is:
106
61
 
107
- ### 第一次启动慢怎么办?
108
-
109
- 首次会下载 Electron 浏览器内核,约 100MB。下载失败通常是网络问题,换网络重试即可;国内网络可按官网 FAQ 加镜像环境变量。
110
-
111
- ### 能不能自动私信客户?
112
-
113
- 不做。工具只帮助识别潜在客户、生成跟进话术和主页/来源链接,具体触达由用户本人判断和执行。
114
-
115
- ### 怎么反馈问题?
116
-
117
- 对智能体说「导出诊断信息」,它会在桌面生成脱敏诊断文件,发给 PPXC 支持人员即可。
118
-
119
- ## 桌面战报(0.2)
120
-
121
- 两件分析工具挖到客户后,会在用户桌面自动生成一份**客户战报**(自包含网页文件,离线可开):总览数字、行动清单(先跟谁 + 为什么)、客户卡片(评论原话 / 判断理由 / 一键复制话术 / 主页直达)、每个词的成绩单。可转发给同事直接照着跟进。
122
-
123
- ## 官方技能说明书(教智能体怎么用)
124
-
125
- [`skills/ppxc-find-customers/SKILL.md`](./skills/ppxc-find-customers/SKILL.md) 是给智能体的标准工作流:先查登录 → 选产品 → 要词 → 开搜 → 固定格式汇报(含战报文件位置)→ 隔天复盘换词,并写死了额度 / 验证码 / 电力的应对话术。装进 Claude / Cursor 等宿主的技能目录即可,npm 包里也随包分发。
126
-
127
- ## 风控(按平台分日额度 + 全局 30 秒间隔)
128
-
129
- 详见 [`docs/开发手册.md` §8](./docs/开发手册.md)。小红书/快手初版比抖音更保守。
130
-
131
- ## 网络代理
132
-
133
- 平台网页登录窗口(抖音 / 小红书 / 快手)会单独设置 Electron 代理策略。抖音 / 小红书默认 `direct`,避免被系统 HTTP 代理或代理软件拖慢登录二维码加载;快手默认 `system`,因为实测快手在部分 Clash/TUN 网络下走 `direct` 会导航超时,而系统代理配合直连规则更稳定。该设置只影响平台 BrowserWindow,不影响 MCP stdio 或 PPXC 后端 API 调用。
134
-
135
- 如需恢复系统代理:
136
-
137
- ```bash
138
- PPXC_MCP_PLATFORM_PROXY=system npx -y ppxc-leads-mcp
62
+ ```text
63
+ ~/.codex/skills/ppxc-find-customers/
139
64
  ```
140
65
 
141
- 可选值:`direct`(默认)、`system`、`auto_detect`。如果 VPN 是全局 TUN / 全局路由模式,应用内 `direct` 不能绕过 VPN,需要在 VPN 客户端里配置分流或临时关闭全局路由。
142
-
143
- 也可以只覆盖单个平台:
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.
144
67
 
145
- ```bash
146
- PPXC_MCP_PLATFORM_PROXY_KUAISHOU=direct npx -y ppxc-leads-mcp
147
- PPXC_MCP_PLATFORM_PROXY_XIAOHONGSHU=system npx -y ppxc-leads-mcp
148
- PPXC_MCP_PLATFORM_PROXY_DOUYIN=system npx -y ppxc-leads-mcp
149
- ```
68
+ ## Tools
150
69
 
151
- ## 仓库关系(重要)
70
+ The package exposes eleven MCP tools:
152
71
 
153
- 本仓库**独立**,不与 `/Users/jianshi/ppxc`(网页 + 后端)、`/Users/jianshi/ppxc-desktop`(桌面客户端稳定线)共享代码、分支、git 历史。
72
+ - `check_status_and_login`
73
+ - `get_workflow_manifest`
74
+ - `list_products`
75
+ - `suggest_search_keywords`
76
+ - `search_keyword_for_leads`
77
+ - `analyze_video_comments`
78
+ - `query_leads`
79
+ - `mark_lead_feedback`
80
+ - `update_lead_status`
81
+ - `review_followup_queue`
82
+ - `export_diagnostics`
154
83
 
155
- - 后端:本仓库作为 PPXC 后端的**客户端**调用;分析接口 `POST /api/v2/comments/analyze` 在 `ppxc` 仓库,支持 `userProfileUrl` 按平台传入主页链接。
156
- - 桌面客户端:从 `ppxc-desktop` 稳定线**单向搬运**执行内核,搬完解耦。
84
+ ## Privacy And Safety
157
85
 
158
- 详细边界见 [`AGENTS.md`](./AGENTS.md)。
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.
90
+ - Captchas, QR-code login, and security checks are handled by the user in a local window. This package does not bypass verification.
91
+ - The tool does not send private messages or automate outreach.
159
92
 
160
- ## 开发状态
93
+ ## Troubleshooting
161
94
 
162
- **抖音三期主链路均已真机验证**(链接分析 + 关键词搜索 + 入池)。
95
+ If the assistant says the MCP tools are unavailable, restart or refresh the MCP host after adding the config.
163
96
 
164
- 🟡 **多平台适配层(2026-06-11)**:
97
+ If startup is slow, wait for the browser runtime download to finish. In mainland China networks, the setup guide includes a mirror configuration for Electron downloads.
165
98
 
166
- - 阶段 A:`platform-runner` + `platforms/*` 插座架构;抖音逻辑平移;`typecheck` + `build` + `regression:static` 通过。
167
- - 阶段 B:**小红书真机验证通过**——扫码登录、关键词搜索(17 笔记/20 评论)、短链笔记拉评论(10 评论 + 主页链接)三链路实测 OK。探路结论:① 访客也有 web_session cookie,登录态必须读页面状态;② 笔记页必须带 xsec_token 签名链接,裸链接会被「暂时无法浏览」拦截;③ 短链跳转在慢网络下需轮询等待。MCP 工具层端到端(含后端分析+入池)待联调。
168
- - 阶段 C:**快手真机验证通过**——扫码登录、关键词搜索(20 视频/18 评论)、单视频链接拉评论(13 评论 + 主页链接)三链路实测 OK。探路结论:① 扫码先发 passToken(id.kuaishou.com),主站会话票叫 `kuaishou.server.webday7_st`(带灰度后缀且 httpOnly),userId/did 都不能当登录凭证;② 搜索接口是 `/rest/v/search/feed`(作者在 feed 级、评论数字段不可靠);③ 视频页评论走 graphql `visionCommentList`,真数据在 `rootCommentsV2`(老字段是空壳)。MCP 工具层端到端(含后端分析+入池)待联调。
169
- - 阶段 D:后端 `userProfileUrl` 优先使用 MCP 传入的主页链接;开发手册/README 已更新。
99
+ If a platform asks for QR-code login or verification, complete it manually in the local window, then retry the assistant request.
170
100
 
171
- 验证命令:
101
+ If something fails, ask the assistant to run `export_diagnostics` and send the generated diagnostic file to OPC support.
172
102
 
173
- ```bash
174
- npm run regression:static # 静态回归(无需登录)
175
- npm run smoke:comments # 抖音 smoke(需登录 + PPXC_MCP_VIDEO_URL)
176
- PPXC_MCP_PLATFORM=xiaohongshu PPXC_MCP_LOGIN=1 npm run spike:probe # 小红书探路
177
- ```
103
+ ## License
178
104
 
179
- 运行与验证步骤:见 [`docs/第一期-运行与验证.md`](./docs/第一期-运行与验证.md)。
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,10 +4,18 @@ 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);
12
21
  }
13
- //# sourceMappingURL=config.js.map
@@ -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,6 +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
  }
157
- //# sourceMappingURL=ppxc-client.js.map
@@ -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) => {
@@ -165,4 +165,3 @@ async function logoutPpxc() {
165
165
  log.warn("clear login partition failed", err instanceof Error ? err.message : String(err));
166
166
  }
167
167
  }
168
- //# sourceMappingURL=ppxc-login-window.js.map
@@ -62,4 +62,3 @@ function clearToken() {
62
62
  catch {
63
63
  }
64
64
  }
65
- //# sourceMappingURL=token-store.js.map
@@ -6,4 +6,3 @@ Object.defineProperty(exports, "buildCommentUrl", { enumerable: true, get: funct
6
6
  Object.defineProperty(exports, "commentsFromJson", { enumerable: true, get: function () { return comments_1.commentsFromJson; } });
7
7
  Object.defineProperty(exports, "extractAwemeId", { enumerable: true, get: function () { return comments_1.extractAwemeId; } });
8
8
  Object.defineProperty(exports, "isDouyinShortLink", { enumerable: true, get: function () { return comments_1.isDouyinShortLink; } });
9
- //# sourceMappingURL=comments.js.map
@@ -12,4 +12,3 @@ Object.defineProperty(exports, "startDouyinLogin", { enumerable: true, get: func
12
12
  Object.defineProperty(exports, "startPlatformLogin", { enumerable: true, get: function () { return platform_runner_1.startPlatformLogin; } });
13
13
  Object.defineProperty(exports, "shutdownRunner", { enumerable: true, get: function () { return platform_runner_1.shutdownRunner; } });
14
14
  Object.defineProperty(exports, "mapSearchResultForDouyin", { enumerable: true, get: function () { return platform_runner_1.mapSearchResultForDouyin; } });
15
- //# sourceMappingURL=douyin-runner.js.map
@@ -29,4 +29,3 @@ function configureDesktopElectronProfile(app, options) {
29
29
  }
30
30
  return { profileName, userDataDir };
31
31
  }
32
- //# sourceMappingURL=electron-profile.js.map
@@ -54,4 +54,3 @@ exports.logger = {
54
54
  scope: createScopedLogger,
55
55
  };
56
56
  exports.LOG_FILE_PATH = LOG_FILE;
57
- //# sourceMappingURL=logger.js.map
@@ -1419,4 +1419,3 @@ exports.DOUYIN_PRESCROLL_SCRIPT = `(async () => {
1419
1419
  return { ok: false, message: (err && err.message) || String(err) };
1420
1420
  }
1421
1421
  })();`;
1422
- //# sourceMappingURL=index.js.map
@@ -142,4 +142,3 @@ class RunnerPageManager {
142
142
  }
143
143
  }
144
144
  exports.RunnerPageManager = RunnerPageManager;
145
- //# sourceMappingURL=runner-page-manager.js.map
@@ -1536,4 +1536,3 @@ async function safeJson(wc, script, label, timeoutMs) {
1536
1536
  };
1537
1537
  }
1538
1538
  }
1539
- //# sourceMappingURL=runner-page-session.js.map
@@ -184,4 +184,3 @@ function asNumber(value) {
184
184
  }
185
185
  return undefined;
186
186
  }
187
- //# sourceMappingURL=runner-page-session.search-parser.js.map
@@ -29,4 +29,3 @@ function resolveDouyinUserAgent(platform = process.platform, electronVersions =
29
29
  return buildUserAgent("Macintosh; Intel Mac OS X 10_15_7", chromeVersion);
30
30
  }
31
31
  }
32
- //# sourceMappingURL=runner-page-session.user-agent.js.map
@@ -309,4 +309,3 @@ function mapSearchResultForDouyin(result) {
309
309
  totalComments: result.totalComments,
310
310
  };
311
311
  }
312
- //# sourceMappingURL=platform-runner.js.map
@@ -30,4 +30,3 @@ function normalizePlatformId(explicit, contentUrl) {
30
30
  }
31
31
  return "douyin";
32
32
  }
33
- //# sourceMappingURL=detect-platform.js.map
@@ -159,4 +159,3 @@ function detectPlatformFromUrl(input) {
159
159
  return "douyin";
160
160
  return null;
161
161
  }
162
- //# sourceMappingURL=adapter.js.map
@@ -127,4 +127,3 @@ function extractAwemeId(input) {
127
127
  function isDouyinShortLink(input) {
128
128
  return /v\.douyin\.com|\/share\//i.test(String(input ?? ""));
129
129
  }
130
- //# sourceMappingURL=comments.js.map
@@ -175,4 +175,3 @@ exports.kuaishouAdapter = {
175
175
  return null;
176
176
  },
177
177
  };
178
- //# sourceMappingURL=adapter.js.map
@@ -167,4 +167,3 @@ exports.KS_SCROLL_SCRIPT = `(async () => {
167
167
  }
168
168
  return { ok: true };
169
169
  })();`;
170
- //# sourceMappingURL=comments.js.map
@@ -20,4 +20,3 @@ function getPlatformAdapter(platform) {
20
20
  function listPlatformAdapters() {
21
21
  return Object.values(ADAPTERS);
22
22
  }
23
- //# sourceMappingURL=registry.js.map
@@ -72,4 +72,3 @@ async function waitForJsonResponses(wc, urlMatcher, timeoutMs, maxResponses = 3)
72
72
  timer = setTimeout(finish, Math.max(500, timeoutMs));
73
73
  });
74
74
  }
75
- //# sourceMappingURL=cdp-json-waiter.js.map
@@ -1,3 +1,2 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- //# sourceMappingURL=types.js.map
@@ -230,4 +230,3 @@ exports.xiaohongshuAdapter = {
230
230
  return null;
231
231
  },
232
232
  };
233
- //# sourceMappingURL=adapter.js.map
@@ -210,4 +210,3 @@ exports.XHS_LOGIN_STATE_SCRIPT = `(() => {
210
210
  return { ok: false, loggedIn: false };
211
211
  }
212
212
  })();`;
213
- //# sourceMappingURL=comments.js.map
@@ -69,4 +69,3 @@ function consumeCrawlQuota(platform, units, dailyLimit) {
69
69
  writeState(state);
70
70
  return { ok: true, usedToday: state.usedByPlatform[platform] ?? 0, limit: dailyLimit };
71
71
  }
72
- //# sourceMappingURL=usage-throttle.js.map
package/dist/main.js CHANGED
@@ -63,4 +63,3 @@ else {
63
63
  electron_1.app.exit(1);
64
64
  });
65
65
  }
66
- //# sourceMappingURL=main.js.map
@@ -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()) {
@@ -322,4 +324,3 @@ function exportBattleReport(input) {
322
324
  return { ok: false, error: msg };
323
325
  }
324
326
  }
325
- //# sourceMappingURL=battle-report.js.map
@@ -63,4 +63,3 @@ function deriveContentAngles(leads, topN = 3) {
63
63
  angles.sort((a, b) => b.count - a.count);
64
64
  return angles.slice(0, topN);
65
65
  }
66
- //# sourceMappingURL=content-insights.js.map
@@ -79,4 +79,3 @@ function exportDiagnostics() {
79
79
  return { ok: false, error: msg };
80
80
  }
81
81
  }
82
- //# sourceMappingURL=diagnostics.js.map