ppxc-leads-mcp 0.1.5 → 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
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/package.json
CHANGED