ppxc-leads-mcp 0.1.3 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -0
- package/dist/browser/kernel/runner-page-session.js +75 -1
- package/dist/browser/platforms/xiaohongshu/comments.js +33 -4
- package/dist/main.js +5 -3
- package/dist/mcp/diagnostics.js +3 -0
- package/dist/mcp/server.js +16 -2
- package/package.json +3 -2
- package/scripts/launch-mcp.cjs +29 -1
- package/skills/ppxc-find-customers/SKILL.md +1 -1
package/README.md
CHANGED
|
@@ -128,6 +128,26 @@ macOS 或 Windows,Node.js 18+,以及一个 PPXC 账号。目标平台账号
|
|
|
128
128
|
|
|
129
129
|
详见 [`docs/开发手册.md` §8](./docs/开发手册.md)。小红书/快手初版比抖音更保守。
|
|
130
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
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
可选值:`direct`(默认)、`system`、`auto_detect`。如果 VPN 是全局 TUN / 全局路由模式,应用内 `direct` 不能绕过 VPN,需要在 VPN 客户端里配置分流或临时关闭全局路由。
|
|
142
|
+
|
|
143
|
+
也可以只覆盖单个平台:
|
|
144
|
+
|
|
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
|
+
```
|
|
150
|
+
|
|
131
151
|
## 仓库关系(重要)
|
|
132
152
|
|
|
133
153
|
本仓库**独立**,不与 `/Users/jianshi/ppxc`(网页 + 后端)、`/Users/jianshi/ppxc-desktop`(桌面客户端稳定线)共享代码、分支、git 历史。
|
|
@@ -136,6 +136,77 @@ const DOUYIN_SEARCH_HOOK_BINDING = "__OPC1_DOUYIN_SEARCH_CAPTURE__";
|
|
|
136
136
|
const MAX_PAGE_HOOK_PAYLOAD_CHARS = 5 * 1024 * 1024;
|
|
137
137
|
const ENABLE_SEARCH_SNIFFER = process.env.OPC1_DISABLE_DOUYIN_SEARCH_SNIFFER !== "1";
|
|
138
138
|
const ENABLE_PAGE_SEARCH_HOOK = process.env.OPC1_DISABLE_DOUYIN_PAGE_SEARCH_HOOK !== "1";
|
|
139
|
+
function normalizePlatformProxyMode(rawValue) {
|
|
140
|
+
const raw = String(rawValue ?? "").trim().toLowerCase();
|
|
141
|
+
if (!raw)
|
|
142
|
+
return null;
|
|
143
|
+
if (raw === "direct" || raw === "system" || raw === "auto_detect")
|
|
144
|
+
return raw;
|
|
145
|
+
if (raw === "auto-detect" || raw === "auto")
|
|
146
|
+
return "auto_detect";
|
|
147
|
+
if (raw === "off" || raw === "0" || raw === "false")
|
|
148
|
+
return "system";
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
function platformProxyEnvKey(partition) {
|
|
152
|
+
if (partition.includes("kuaishou"))
|
|
153
|
+
return "PPXC_MCP_PLATFORM_PROXY_KUAISHOU";
|
|
154
|
+
if (partition.includes("xhs"))
|
|
155
|
+
return "PPXC_MCP_PLATFORM_PROXY_XIAOHONGSHU";
|
|
156
|
+
if (partition.includes("douyin"))
|
|
157
|
+
return "PPXC_MCP_PLATFORM_PROXY_DOUYIN";
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
function defaultPlatformProxyMode(partition) {
|
|
161
|
+
if (partition.includes("kuaishou"))
|
|
162
|
+
return "system";
|
|
163
|
+
return "direct";
|
|
164
|
+
}
|
|
165
|
+
function resolvePlatformProxyMode(partition) {
|
|
166
|
+
const platformKey = platformProxyEnvKey(partition);
|
|
167
|
+
const platformOverride = platformKey
|
|
168
|
+
? normalizePlatformProxyMode(process.env[platformKey])
|
|
169
|
+
: null;
|
|
170
|
+
if (platformOverride)
|
|
171
|
+
return platformOverride;
|
|
172
|
+
const globalOverride = normalizePlatformProxyMode(process.env.PPXC_MCP_PLATFORM_PROXY);
|
|
173
|
+
if (globalOverride)
|
|
174
|
+
return globalOverride;
|
|
175
|
+
const raw = String(process.env.PPXC_MCP_PLATFORM_PROXY ?? "")
|
|
176
|
+
.trim()
|
|
177
|
+
.toLowerCase();
|
|
178
|
+
if (raw) {
|
|
179
|
+
log.warn("unknown PPXC_MCP_PLATFORM_PROXY value, use platform default", {
|
|
180
|
+
value: raw,
|
|
181
|
+
partition,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
if (platformKey) {
|
|
185
|
+
const platformRaw = String(process.env[platformKey] ?? "").trim().toLowerCase();
|
|
186
|
+
if (platformRaw) {
|
|
187
|
+
log.warn("unknown platform proxy override, use platform default", {
|
|
188
|
+
key: platformKey,
|
|
189
|
+
value: platformRaw,
|
|
190
|
+
partition,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return defaultPlatformProxyMode(partition);
|
|
195
|
+
}
|
|
196
|
+
async function applyPlatformProxyPolicy(ses, slotId, partition) {
|
|
197
|
+
const mode = resolvePlatformProxyMode(partition);
|
|
198
|
+
try {
|
|
199
|
+
await ses.setProxy({ mode });
|
|
200
|
+
log.info(`slot ${slotId} platform proxy applied`, { mode, partition });
|
|
201
|
+
}
|
|
202
|
+
catch (err) {
|
|
203
|
+
log.warn(`slot ${slotId} platform proxy apply failed`, {
|
|
204
|
+
mode,
|
|
205
|
+
partition,
|
|
206
|
+
msg: err instanceof Error ? err.message : String(err),
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
}
|
|
139
210
|
function readBoundedIntEnv(name, fallback, min, max) {
|
|
140
211
|
const parsed = Math.floor(Number(process.env[name]));
|
|
141
212
|
if (!Number.isFinite(parsed))
|
|
@@ -273,6 +344,7 @@ class RunnerPageSession {
|
|
|
273
344
|
backgroundThrottling: false,
|
|
274
345
|
},
|
|
275
346
|
});
|
|
347
|
+
this._networkPolicyReady = applyPlatformProxyPolicy(this._win.webContents.session, this._slotId, this._partition);
|
|
276
348
|
this._win.webContents.setAudioMuted(true);
|
|
277
349
|
this._win.webContents.on("did-attach-webview", (_e, attached) => {
|
|
278
350
|
try {
|
|
@@ -523,6 +595,7 @@ class RunnerPageSession {
|
|
|
523
595
|
async loadUrl(url) {
|
|
524
596
|
return await withDouyinNavigationPermit(this._slotId, async () => {
|
|
525
597
|
this._assertAlive();
|
|
598
|
+
await this._networkPolicyReady;
|
|
526
599
|
const wc = this._win.webContents;
|
|
527
600
|
const timeoutMs = this._navigationTimeoutMs;
|
|
528
601
|
let timer = null;
|
|
@@ -586,7 +659,8 @@ class RunnerPageSession {
|
|
|
586
659
|
}
|
|
587
660
|
navigateNoWait(url) {
|
|
588
661
|
this._assertAlive();
|
|
589
|
-
this._win.webContents
|
|
662
|
+
const wc = this._win.webContents;
|
|
663
|
+
void this._networkPolicyReady.then(() => wc.loadURL(url)).catch((err) => {
|
|
590
664
|
const raw = err instanceof Error ? err : new Error(String(err));
|
|
591
665
|
if (isNavigationAbortedError(raw)) {
|
|
592
666
|
log.info(`slot ${this._slotId} navigateNoWait aborted url=${url} msg=${raw.message}`);
|
|
@@ -148,12 +148,41 @@ exports.XHS_PROBE_SCRIPT = `(async () => {
|
|
|
148
148
|
exports.XHS_OPEN_LOGIN_SCRIPT = `(async () => {
|
|
149
149
|
try {
|
|
150
150
|
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
|
|
151
|
-
const
|
|
151
|
+
const visible = (el) => {
|
|
152
|
+
const rect = el && el.getBoundingClientRect ? el.getBoundingClientRect() : null;
|
|
153
|
+
const style = el ? window.getComputedStyle(el) : null;
|
|
154
|
+
return !!rect && rect.width > 0 && rect.height > 0 && style && style.visibility !== 'hidden' && style.display !== 'none';
|
|
155
|
+
};
|
|
156
|
+
const clickables = Array.from(document.querySelectorAll('button, [role="button"], a'));
|
|
152
157
|
for (const el of clickables) {
|
|
153
|
-
|
|
154
|
-
|
|
158
|
+
if (!visible(el)) continue;
|
|
159
|
+
const t = String(el.innerText || el.textContent || el.getAttribute('aria-label') || '').trim();
|
|
160
|
+
const href = String(el.getAttribute('href') || '');
|
|
161
|
+
if (/发布|创作|publish|creator/i.test(t + ' ' + href)) continue;
|
|
162
|
+
if (/^(登录|扫码登录|手机号登录)$/.test(t)) {
|
|
163
|
+
el.click();
|
|
164
|
+
await sleep(800);
|
|
165
|
+
return { ok: true, clicked: true, text: t };
|
|
166
|
+
}
|
|
155
167
|
}
|
|
156
|
-
|
|
168
|
+
const exactTextNodes = [];
|
|
169
|
+
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
|
|
170
|
+
while (exactTextNodes.length < 20) {
|
|
171
|
+
const node = walker.nextNode();
|
|
172
|
+
if (!node) break;
|
|
173
|
+
if (String(node.nodeValue || '').trim() === '登录') exactTextNodes.push(node);
|
|
174
|
+
}
|
|
175
|
+
for (const node of exactTextNodes) {
|
|
176
|
+
const el = node.parentElement && node.parentElement.closest('button, [role="button"], a, div');
|
|
177
|
+
if (!el || !visible(el)) continue;
|
|
178
|
+
const href = String(el.getAttribute('href') || '');
|
|
179
|
+
const text = String(el.innerText || el.textContent || '').trim();
|
|
180
|
+
if (/发布|创作|publish|creator/i.test(text + ' ' + href)) continue;
|
|
181
|
+
el.click();
|
|
182
|
+
await sleep(800);
|
|
183
|
+
return { ok: true, clicked: true, text };
|
|
184
|
+
}
|
|
185
|
+
return { ok: true, clicked: false };
|
|
157
186
|
} catch (e) { return { ok: false }; }
|
|
158
187
|
})();`;
|
|
159
188
|
exports.XHS_SCROLL_COMMENTS_SCRIPT = `(async () => {
|
package/dist/main.js
CHANGED
|
@@ -36,8 +36,10 @@ function quitAfterStdioClose(reason) {
|
|
|
36
36
|
});
|
|
37
37
|
setTimeout(() => electron_1.app.exit(0), 1500).unref();
|
|
38
38
|
}
|
|
39
|
-
process.
|
|
40
|
-
process.stdin.on("
|
|
39
|
+
if (!process.env.PPXC_MCP_STDIN_FD) {
|
|
40
|
+
process.stdin.on("end", () => quitAfterStdioClose("end"));
|
|
41
|
+
process.stdin.on("close", () => quitAfterStdioClose("close"));
|
|
42
|
+
}
|
|
41
43
|
const gotLock = electron_1.app.requestSingleInstanceLock();
|
|
42
44
|
if (!gotLock) {
|
|
43
45
|
log.error("another PPXC Leads MCP instance is already running, exiting");
|
|
@@ -53,7 +55,7 @@ else {
|
|
|
53
55
|
.whenReady()
|
|
54
56
|
.then(async () => {
|
|
55
57
|
log.info(`electron ready, starting MCP stdio server (ppxc-leads-mcp v${version_1.OWN_VERSION})`);
|
|
56
|
-
await (0, server_1.startMcpServer)();
|
|
58
|
+
await (0, server_1.startMcpServer)(quitAfterStdioClose);
|
|
57
59
|
log.info("MCP server connected over stdio");
|
|
58
60
|
})
|
|
59
61
|
.catch((err) => {
|
package/dist/mcp/diagnostics.js
CHANGED
|
@@ -27,6 +27,9 @@ function readLogTail() {
|
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
function pickOutputDir() {
|
|
30
|
+
const override = process.env.PPXC_MCP_DIAGNOSTICS_DIR?.trim();
|
|
31
|
+
if (override)
|
|
32
|
+
return override;
|
|
30
33
|
for (const name of ["desktop", "downloads"]) {
|
|
31
34
|
try {
|
|
32
35
|
const dir = electron_1.app.getPath(name);
|
package/dist/mcp/server.js
CHANGED
|
@@ -4,6 +4,7 @@ exports.createMcpServer = createMcpServer;
|
|
|
4
4
|
exports.startMcpServer = startMcpServer;
|
|
5
5
|
const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
6
6
|
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
7
|
+
const node_fs_1 = require("node:fs");
|
|
7
8
|
const zod_1 = require("zod");
|
|
8
9
|
const platform_runner_1 = require("../browser/platform-runner");
|
|
9
10
|
const detect_platform_1 = require("../browser/platforms/detect-platform");
|
|
@@ -822,9 +823,22 @@ function createMcpServer() {
|
|
|
822
823
|
});
|
|
823
824
|
return server;
|
|
824
825
|
}
|
|
825
|
-
|
|
826
|
+
function mcpInputStream(onInputClosed) {
|
|
827
|
+
const fdRaw = process.env.PPXC_MCP_STDIN_FD?.trim();
|
|
828
|
+
if (!fdRaw)
|
|
829
|
+
return process.stdin;
|
|
830
|
+
const fd = Number(fdRaw);
|
|
831
|
+
if (!Number.isInteger(fd) || fd < 0) {
|
|
832
|
+
throw new Error(`invalid PPXC_MCP_STDIN_FD: ${fdRaw}`);
|
|
833
|
+
}
|
|
834
|
+
const stream = (0, node_fs_1.createReadStream)("", { fd, autoClose: false });
|
|
835
|
+
stream.on("end", () => onInputClosed?.(`fd${fd}-end`));
|
|
836
|
+
stream.on("close", () => onInputClosed?.(`fd${fd}-close`));
|
|
837
|
+
return stream;
|
|
838
|
+
}
|
|
839
|
+
async function startMcpServer(onInputClosed) {
|
|
826
840
|
const server = createMcpServer();
|
|
827
|
-
const transport = new stdio_js_1.StdioServerTransport();
|
|
841
|
+
const transport = new stdio_js_1.StdioServerTransport(mcpInputStream(onInputClosed), process.stdout);
|
|
828
842
|
await server.connect(transport);
|
|
829
843
|
}
|
|
830
844
|
//# sourceMappingURL=server.js.map
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ppxc-leads-mcp",
|
|
3
3
|
"productName": "PPXC Leads MCP",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.7",
|
|
5
5
|
"description": "PPXC 找客户能力的 MCP 工具包:智能体可调用的抖音/小红书/快手评论客户发现工具",
|
|
6
6
|
"license": "UNLICENSED",
|
|
7
7
|
"homepage": "https://opc1.me/download/mcp",
|
|
@@ -34,7 +34,8 @@
|
|
|
34
34
|
"smoke:e2e": "npm run build && electron dist/scripts/smoke-e2e-main.js",
|
|
35
35
|
"smoke:backend": "npm run build && electron dist/scripts/smoke-backend-tools-main.js",
|
|
36
36
|
"regression:static": "npm run build && node dist/scripts/regression-static-main.js",
|
|
37
|
-
"spike:probe": "npm run build && PPXC_MCP_PAGE_VISIBLE=${PPXC_MCP_PAGE_VISIBLE:-1} electron dist/scripts/spike-platform-probe-main.js"
|
|
37
|
+
"spike:probe": "npm run build && PPXC_MCP_PAGE_VISIBLE=${PPXC_MCP_PAGE_VISIBLE:-1} electron dist/scripts/spike-platform-probe-main.js",
|
|
38
|
+
"smoke:published-mcp": "node scripts/smoke-published-mcp.cjs"
|
|
38
39
|
},
|
|
39
40
|
"devDependencies": {
|
|
40
41
|
"@types/node": "^25.5.0",
|
package/scripts/launch-mcp.cjs
CHANGED
|
@@ -88,11 +88,39 @@ if (!existsSync(mainJs)) {
|
|
|
88
88
|
const env = { ...process.env };
|
|
89
89
|
delete env.ELECTRON_RUN_AS_NODE;
|
|
90
90
|
|
|
91
|
+
const useAuxStdinPipe = process.platform === "win32";
|
|
92
|
+
if (useAuxStdinPipe) {
|
|
93
|
+
env.PPXC_MCP_STDIN_FD = "3";
|
|
94
|
+
}
|
|
95
|
+
|
|
91
96
|
const child = spawn(electronBin, [mainJs], {
|
|
92
|
-
|
|
97
|
+
// Windows 上 GUI 形态的 Electron 读取 fd0 可能立刻收到 EOF,导致 MCP server
|
|
98
|
+
// 启动后马上退出。Windows 改用额外 fd3 承载 MCP 输入;stdout/stderr 仍
|
|
99
|
+
// 直接继承,stdout 完整保留给 MCP 协议输出。
|
|
100
|
+
stdio: useAuxStdinPipe ? ["ignore", "inherit", "inherit", "pipe"] : ["pipe", "inherit", "inherit"],
|
|
93
101
|
env,
|
|
94
102
|
});
|
|
95
103
|
|
|
104
|
+
const mcpInput = useAuxStdinPipe ? child.stdio[3] : child.stdin;
|
|
105
|
+
if (!mcpInput) {
|
|
106
|
+
process.stderr.write("[ppxc-leads-mcp] 启动失败:无法建立 MCP 输入管道。\n");
|
|
107
|
+
process.exit(1);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
process.stdin.resume();
|
|
111
|
+
process.stdin.on("data", (chunk) => {
|
|
112
|
+
mcpInput.write(chunk);
|
|
113
|
+
});
|
|
114
|
+
process.stdin.on("end", () => {
|
|
115
|
+
mcpInput.end();
|
|
116
|
+
});
|
|
117
|
+
process.stdin.on("close", () => {
|
|
118
|
+
mcpInput.end();
|
|
119
|
+
});
|
|
120
|
+
mcpInput.on("error", () => {
|
|
121
|
+
/* child 已退出或 stdin 已关闭,忽略 EPIPE 等清理期错误 */
|
|
122
|
+
});
|
|
123
|
+
|
|
96
124
|
let exiting = false;
|
|
97
125
|
|
|
98
126
|
function exitWithChild(code, signal) {
|
|
@@ -31,7 +31,7 @@ description: 用 PPXC 找客户小组件(MCP)从抖音/小红书/快手评
|
|
|
31
31
|
|
|
32
32
|
3. **告诉用户**:「我把 PPXC 找客户小组件的配置加好了,重启/刷新一下智能体就能用。首次使用会自动下载运行环境(约一两分钟,取决于网络)。」
|
|
33
33
|
4. **重启后验证**:调 `check_status_and_login` 确认工具就位,然后从第 1 步继续。
|
|
34
|
-
5. **你没有文件编辑能力时**:把上面那段配置原样发给用户,告诉他贴进自己智能体的 MCP 设置里,并附 PPXC
|
|
34
|
+
5. **你没有文件编辑能力时**:把上面那段配置原样发给用户,告诉他贴进自己智能体的 MCP 设置里,并附 PPXC 官网接入页 https://opc1.me/download/mcp(有逐家图文步骤)。
|
|
35
35
|
|
|
36
36
|
## 标准流程(按顺序)
|
|
37
37
|
|