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.loadURL(url).catch((err) => {
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 clickables = Array.from(document.querySelectorAll('button, a, [role="button"], div'));
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
- const t = String(el.innerText || el.textContent || '').trim();
154
- if (/^登录$|扫码登录|手机号登录/.test(t)) { el.click(); await sleep(800); break; }
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
- return { ok: true };
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ppxc-leads-mcp",
3
3
  "productName": "PPXC Leads MCP",
4
- "version": "0.1.5",
4
+ "version": "0.1.7",
5
5
  "description": "PPXC 找客户能力的 MCP 工具包:智能体可调用的抖音/小红书/快手评论客户发现工具",
6
6
  "license": "UNLICENSED",
7
7
  "homepage": "https://opc1.me/download/mcp",