ppxc-leads-mcp 0.1.0

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/README.md +115 -0
  2. package/dist/backend/config.js +13 -0
  3. package/dist/backend/ppxc-client.js +156 -0
  4. package/dist/backend/ppxc-login-window.js +168 -0
  5. package/dist/backend/token-store.js +65 -0
  6. package/dist/browser/comments.js +9 -0
  7. package/dist/browser/douyin-runner.js +15 -0
  8. package/dist/browser/kernel/electron-profile.js +32 -0
  9. package/dist/browser/kernel/logger.js +57 -0
  10. package/dist/browser/kernel/page-scripts/index.js +1422 -0
  11. package/dist/browser/kernel/runner-page-manager.js +145 -0
  12. package/dist/browser/kernel/runner-page-session.js +1465 -0
  13. package/dist/browser/kernel/runner-page-session.search-parser.js +187 -0
  14. package/dist/browser/kernel/runner-page-session.user-agent.js +32 -0
  15. package/dist/browser/platform-runner.js +312 -0
  16. package/dist/browser/platforms/detect-platform.js +33 -0
  17. package/dist/browser/platforms/douyin/adapter.js +162 -0
  18. package/dist/browser/platforms/douyin/comments.js +130 -0
  19. package/dist/browser/platforms/kuaishou/adapter.js +178 -0
  20. package/dist/browser/platforms/kuaishou/comments.js +170 -0
  21. package/dist/browser/platforms/registry.js +23 -0
  22. package/dist/browser/platforms/shared/cdp-json-waiter.js +75 -0
  23. package/dist/browser/platforms/types.js +3 -0
  24. package/dist/browser/platforms/xiaohongshu/adapter.js +233 -0
  25. package/dist/browser/platforms/xiaohongshu/comments.js +184 -0
  26. package/dist/browser/usage-throttle.js +72 -0
  27. package/dist/main.js +64 -0
  28. package/dist/mcp/battle-report.js +325 -0
  29. package/dist/mcp/content-insights.js +66 -0
  30. package/dist/mcp/diagnostics.js +79 -0
  31. package/dist/mcp/server.js +829 -0
  32. package/dist/version.js +19 -0
  33. package/package.json +43 -0
  34. package/scripts/launch-mcp.cjs +96 -0
  35. package/skills/ppxc-find-customers/SKILL.md +110 -0
@@ -0,0 +1,75 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.waitForJsonResponses = waitForJsonResponses;
4
+ const logger_1 = require("../../kernel/logger");
5
+ const log = logger_1.logger.scope("cdp-json-waiter");
6
+ async function waitForJsonResponses(wc, urlMatcher, timeoutMs, maxResponses = 3) {
7
+ const results = [];
8
+ let settled = false;
9
+ let timer = null;
10
+ let finishResolve = null;
11
+ const finish = () => {
12
+ if (settled)
13
+ return;
14
+ settled = true;
15
+ if (timer)
16
+ clearTimeout(timer);
17
+ try {
18
+ if (dbg?.isAttached())
19
+ dbg.detach();
20
+ }
21
+ catch {
22
+ }
23
+ finishResolve?.(results);
24
+ };
25
+ const dbg = wc.debugger;
26
+ try {
27
+ if (!dbg.isAttached())
28
+ dbg.attach("1.3");
29
+ await dbg.sendCommand("Network.enable");
30
+ }
31
+ catch (err) {
32
+ log.warn("cdp attach failed", err instanceof Error ? err.message : String(err));
33
+ return results;
34
+ }
35
+ const pendingBodies = new Map();
36
+ const onMessage = (_e, method, params) => {
37
+ if (method === "Network.responseReceived") {
38
+ const response = params.response;
39
+ const url = String(response?.url ?? "");
40
+ const requestId = String(params.requestId ?? "");
41
+ if (!url || !requestId || !urlMatcher(url))
42
+ return;
43
+ pendingBodies.set(requestId, url);
44
+ }
45
+ if (method === "Network.loadingFinished") {
46
+ const requestId = String(params.requestId ?? "");
47
+ if (!pendingBodies.has(requestId))
48
+ return;
49
+ pendingBodies.delete(requestId);
50
+ void dbg
51
+ .sendCommand("Network.getResponseBody", { requestId })
52
+ .then((bodyRes) => {
53
+ let text = String(bodyRes.body ?? "");
54
+ if (bodyRes.base64Encoded) {
55
+ text = Buffer.from(text, "base64").toString("utf8");
56
+ }
57
+ try {
58
+ const json = JSON.parse(text);
59
+ results.push(json);
60
+ if (results.length >= maxResponses)
61
+ finish();
62
+ }
63
+ catch {
64
+ }
65
+ })
66
+ .catch(() => undefined);
67
+ }
68
+ };
69
+ dbg.on("message", onMessage);
70
+ return await new Promise((resolve) => {
71
+ finishResolve = resolve;
72
+ timer = setTimeout(finish, Math.max(500, timeoutMs));
73
+ });
74
+ }
75
+ //# sourceMappingURL=cdp-json-waiter.js.map
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1,233 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.xiaohongshuAdapter = void 0;
4
+ const runner_page_manager_1 = require("../../kernel/runner-page-manager");
5
+ const cdp_json_waiter_1 = require("../shared/cdp-json-waiter");
6
+ const comments_1 = require("./comments");
7
+ const CHROME_UA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36";
8
+ const SESSION = {
9
+ partition: "persist:ppxc-mcp-xhs",
10
+ cookieDomain: ".xiaohongshu.com",
11
+ windowTitle: "PPXC 小红书",
12
+ userAgent: CHROME_UA,
13
+ enableSearchSniffer: false,
14
+ loginCookieNames: ["web_session"],
15
+ homeUrl: "https://www.xiaohongshu.com/",
16
+ };
17
+ const RISK = {
18
+ maxVideosPerKeyword: 4,
19
+ totalContentBudget: 8,
20
+ minCommentCount: 2,
21
+ maxPerAuthor: 2,
22
+ perItemMinDelayMs: 2000,
23
+ perItemMaxDelayMs: 4000,
24
+ slotStaggerMs: 12000,
25
+ dailyQuotaUnits: 10,
26
+ };
27
+ function sleep(ms) {
28
+ return new Promise((r) => setTimeout(r, ms));
29
+ }
30
+ async function probePage(session) {
31
+ const raw = await session
32
+ .getWebContents()
33
+ ?.executeJavaScript(comments_1.XHS_PROBE_SCRIPT, true)
34
+ .catch(() => null);
35
+ if (!raw || typeof raw !== "object")
36
+ return "unknown";
37
+ return String(raw.status ?? "unknown");
38
+ }
39
+ async function readPageLoginState(session) {
40
+ const raw = await session
41
+ .getWebContents()
42
+ ?.executeJavaScript(comments_1.XHS_LOGIN_STATE_SCRIPT, true)
43
+ .catch(() => null);
44
+ if (!raw || typeof raw !== "object")
45
+ return null;
46
+ const obj = raw;
47
+ if (!obj.ok)
48
+ return null;
49
+ return Boolean(obj.loggedIn);
50
+ }
51
+ async function ensureOnXhsPage(session) {
52
+ if (/xiaohongshu\.com/i.test(session.currentUrl()))
53
+ return;
54
+ try {
55
+ await session.loadUrl(`${SESSION.homeUrl}explore`);
56
+ }
57
+ catch {
58
+ }
59
+ await sleep(2500);
60
+ }
61
+ exports.xiaohongshuAdapter = {
62
+ id: "xiaohongshu",
63
+ displayName: "小红书",
64
+ sessionConfig: SESSION,
65
+ risk: RISK,
66
+ createManager(defaultVisible) {
67
+ return new runner_page_manager_1.RunnerPageManager({
68
+ maxConcurrent: 2,
69
+ defaultVisible,
70
+ navigationTimeoutMs: 45000,
71
+ sessionDefaults: SESSION,
72
+ });
73
+ },
74
+ async isLoggedIn(session) {
75
+ if (!(await session.hasAnyLoginCookies(SESSION.loginCookieNames)))
76
+ return false;
77
+ await ensureOnXhsPage(session);
78
+ const state = await readPageLoginState(session);
79
+ return state === true;
80
+ },
81
+ async startLogin(manager, slotId, timeoutMs) {
82
+ const session = manager.acquire(slotId, { visible: true });
83
+ session.reveal();
84
+ try {
85
+ await ensureOnXhsPage(session);
86
+ if ((await readPageLoginState(session)) === true)
87
+ return { loggedIn: true };
88
+ await session.getWebContents()?.executeJavaScript(comments_1.XHS_OPEN_LOGIN_SCRIPT, true);
89
+ session.reveal();
90
+ const deadline = Date.now() + Math.max(15000, timeoutMs);
91
+ let polls = 0;
92
+ while (Date.now() < deadline) {
93
+ await sleep(2500);
94
+ polls += 1;
95
+ if ((await readPageLoginState(session)) === true)
96
+ return { loggedIn: true };
97
+ if (polls % 8 === 0) {
98
+ try {
99
+ await session.loadUrl(`${SESSION.homeUrl}explore`);
100
+ }
101
+ catch {
102
+ }
103
+ await sleep(2000);
104
+ if ((await readPageLoginState(session)) === true)
105
+ return { loggedIn: true };
106
+ }
107
+ }
108
+ return { loggedIn: false };
109
+ }
110
+ finally {
111
+ session.hide();
112
+ }
113
+ },
114
+ detectContentUrl(input) {
115
+ return (/xiaohongshu\.com|xhslink\.com|xhs\.cn/i.test(input) || Boolean((0, comments_1.extractNoteId)(input)));
116
+ },
117
+ async resolveContentId(input, session) {
118
+ const direct = (0, comments_1.extractNoteId)(input);
119
+ if (direct)
120
+ return direct;
121
+ if ((0, comments_1.isXhsShortLink)(input)) {
122
+ try {
123
+ await session.loadUrl(input);
124
+ }
125
+ catch {
126
+ }
127
+ const deadline = Date.now() + 15000;
128
+ while (Date.now() < deadline) {
129
+ await sleep(1000);
130
+ const fromUrl = (0, comments_1.extractNoteId)(session.currentUrl());
131
+ if (fromUrl)
132
+ return fromUrl;
133
+ }
134
+ }
135
+ throw new Error(`无法从链接里识别出小红书笔记:${input}`);
136
+ },
137
+ buildContentUrl(contentId) {
138
+ return `https://www.xiaohongshu.com/explore/${contentId}`;
139
+ },
140
+ preferredContentUrl(input, contentId, session) {
141
+ const raw = String(input ?? "");
142
+ if (/xsec_token=/.test(raw) && raw.includes(contentId))
143
+ return raw;
144
+ const cur = session.currentUrl();
145
+ if (/xsec_token=/.test(cur) && cur.includes(contentId))
146
+ return cur;
147
+ return null;
148
+ },
149
+ async fetchComments(session, contentId, contentUrl, _maxComments, fetchWaitMs) {
150
+ const wc = session.getWebContents();
151
+ if (!wc)
152
+ return { rawCount: 0, hasMore: false };
153
+ const capturePromise = (0, cdp_json_waiter_1.waitForJsonResponses)(wc, (url) => /comment\/page|comment\/list|comments/i.test(url), fetchWaitMs, 2);
154
+ try {
155
+ await session.loadUrl(contentUrl);
156
+ }
157
+ catch {
158
+ }
159
+ await sleep(2000);
160
+ await wc.executeJavaScript(comments_1.XHS_SCROLL_COMMENTS_SCRIPT, true).catch(() => undefined);
161
+ await sleep(1500);
162
+ const captured = await capturePromise;
163
+ for (const json of captured) {
164
+ const parsed = (0, comments_1.commentsFromXhsJson)(contentId, json);
165
+ if (parsed.length > 0) {
166
+ return { json, rawCount: parsed.length, hasMore: false };
167
+ }
168
+ }
169
+ const fallback = await wc
170
+ .executeJavaScript(`(() => {
171
+ try {
172
+ const s = window.__INITIAL_STATE__ || window.__INITIAL_SSR_STATE__ || null;
173
+ return s ? JSON.stringify(s) : null;
174
+ } catch { return null; }
175
+ })()`, true)
176
+ .catch(() => null);
177
+ if (fallback && typeof fallback === "string") {
178
+ try {
179
+ const json = JSON.parse(fallback);
180
+ const parsed = (0, comments_1.commentsFromXhsJson)(contentId, json);
181
+ if (parsed.length > 0)
182
+ return { json, rawCount: parsed.length, hasMore: false };
183
+ }
184
+ catch {
185
+ }
186
+ }
187
+ return { rawCount: 0, hasMore: false };
188
+ },
189
+ parseComments: comments_1.commentsFromXhsJson,
190
+ buildSearchUrl(keyword) {
191
+ return `https://www.xiaohongshu.com/search_result?keyword=${encodeURIComponent(keyword)}&source=web_search_result_notes`;
192
+ },
193
+ async awaitSearchBatch(session, keyword, fetchWaitMs) {
194
+ const wc = session.getWebContents();
195
+ if (!wc)
196
+ return null;
197
+ const capturePromise = (0, cdp_json_waiter_1.waitForJsonResponses)(wc, (url) => /search\/notes|search\/note|search_result/i.test(url), fetchWaitMs, 3);
198
+ session.navigateNoWait(exports.xiaohongshuAdapter.buildSearchUrl(keyword));
199
+ await sleep(2500);
200
+ await wc.executeJavaScript(comments_1.XHS_SCROLL_COMMENTS_SCRIPT, true).catch(() => undefined);
201
+ const captured = await capturePromise;
202
+ let items = [];
203
+ for (const json of captured) {
204
+ items = (0, comments_1.searchItemsFromXhsJson)(json).map((it) => ({
205
+ contentId: it.contentId,
206
+ title: it.title,
207
+ authorName: it.authorName,
208
+ authorSecUid: it.authorSecUid,
209
+ commentCount: it.commentCount,
210
+ contentUrl: it.contentUrl,
211
+ }));
212
+ if (items.length > 0)
213
+ break;
214
+ }
215
+ if (items.length === 0) {
216
+ const status = await probePage(session);
217
+ if (status === "verification_required")
218
+ return { items: [], verifyCheck: true };
219
+ return null;
220
+ }
221
+ return { items };
222
+ },
223
+ async probeVerification(session) {
224
+ return (await probePage(session)) === "verification_required";
225
+ },
226
+ async openLoginPanel(session) {
227
+ await session.getWebContents()?.executeJavaScript(comments_1.XHS_OPEN_LOGIN_SCRIPT, true);
228
+ },
229
+ async getProfileDisplayName(_session) {
230
+ return null;
231
+ },
232
+ };
233
+ //# sourceMappingURL=adapter.js.map
@@ -0,0 +1,184 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.XHS_LOGIN_STATE_SCRIPT = exports.XHS_SCROLL_COMMENTS_SCRIPT = exports.XHS_OPEN_LOGIN_SCRIPT = exports.XHS_PROBE_SCRIPT = void 0;
4
+ exports.extractNoteId = extractNoteId;
5
+ exports.isXhsShortLink = isXhsShortLink;
6
+ exports.commentsFromXhsJson = commentsFromXhsJson;
7
+ exports.searchItemsFromXhsJson = searchItemsFromXhsJson;
8
+ const COMMENT_HAS_MEANINGFUL_CHAR = /[\p{Script=Han}A-Za-z0-9]/u;
9
+ const COMMENT_MIN_TEXT_LENGTH = 4;
10
+ function shouldKeepComment(text, diggCount) {
11
+ const trimmed = String(text ?? "").trim();
12
+ if (!trimmed)
13
+ return false;
14
+ if (!COMMENT_HAS_MEANINGFUL_CHAR.test(trimmed))
15
+ return false;
16
+ if (trimmed.length < COMMENT_MIN_TEXT_LENGTH && diggCount <= 0)
17
+ return false;
18
+ return true;
19
+ }
20
+ function extractNoteId(input) {
21
+ const raw = String(input ?? "").trim();
22
+ if (!raw)
23
+ return null;
24
+ const explore = raw.match(/\/explore\/([a-zA-Z0-9]+)/);
25
+ if (explore)
26
+ return explore[1];
27
+ const discovery = raw.match(/\/discovery\/item\/([a-zA-Z0-9]+)/);
28
+ if (discovery)
29
+ return discovery[1];
30
+ const item = raw.match(/\/item\/([a-zA-Z0-9]+)/);
31
+ if (item)
32
+ return item[1];
33
+ if (/^[a-f0-9]{24}$/i.test(raw))
34
+ return raw;
35
+ return null;
36
+ }
37
+ function isXhsShortLink(input) {
38
+ return /xhslink\.com|xhs\.cn/i.test(String(input ?? ""));
39
+ }
40
+ function commentsFromXhsJson(contentId, json) {
41
+ if (!json || typeof json !== "object")
42
+ return [];
43
+ const root = json;
44
+ const data = root.data ?? root;
45
+ const comments = Array.isArray(data.comments)
46
+ ? data.comments
47
+ : Array.isArray(data.items)
48
+ ? data.items
49
+ : [];
50
+ const out = [];
51
+ for (const raw of comments) {
52
+ if (!raw || typeof raw !== "object")
53
+ continue;
54
+ const item = raw;
55
+ const userInfo = item.user_info && typeof item.user_info === "object"
56
+ ? item.user_info
57
+ : item.user && typeof item.user === "object"
58
+ ? item.user
59
+ : {};
60
+ const cid = String(item.id ?? item.comment_id ?? item.cid ?? "").trim();
61
+ const text = String(item.content ?? item.text ?? item.note ?? "").trim();
62
+ if (!text)
63
+ continue;
64
+ const diggCount = Number.isFinite(Number(item.like_count ?? item.liked_count))
65
+ ? Math.max(0, Math.floor(Number(item.like_count ?? item.liked_count)))
66
+ : 0;
67
+ if (!shouldKeepComment(text, diggCount))
68
+ continue;
69
+ const userId = String(userInfo.user_id ?? userInfo.userId ?? userInfo.id ?? "").trim();
70
+ const nickname = String(userInfo.nickname ?? userInfo.name ?? "未知用户").trim() || "未知用户";
71
+ out.push({
72
+ contentId,
73
+ awemeId: contentId,
74
+ cid: cid || `${contentId}_${out.length}`,
75
+ text,
76
+ nickname,
77
+ userSecUid: userId || undefined,
78
+ userProfileUrl: userId ? `https://www.xiaohongshu.com/user/profile/${userId}` : undefined,
79
+ avatarUrl: String(userInfo.image ?? userInfo.avatar ?? "").trim() || undefined,
80
+ createTime: Number.isFinite(Number(item.create_time ?? item.time))
81
+ ? Math.max(0, Math.floor(Number(item.create_time ?? item.time) / (Number(item.create_time) > 1e12 ? 1000 : 1)))
82
+ : 0,
83
+ diggCount,
84
+ replyCount: Number.isFinite(Number(item.sub_comment_count ?? item.sub_comments))
85
+ ? Math.max(0, Math.floor(Number(item.sub_comment_count ?? item.sub_comments)))
86
+ : 0,
87
+ ipLabel: String(item.ip_location ?? userInfo.ip_location ?? "").slice(0, 64),
88
+ });
89
+ }
90
+ return out;
91
+ }
92
+ function searchItemsFromXhsJson(json) {
93
+ if (!json || typeof json !== "object")
94
+ return [];
95
+ const root = json;
96
+ const data = root.data ?? root;
97
+ const items = Array.isArray(data.items) ? data.items : Array.isArray(data.notes) ? data.notes : [];
98
+ const out = [];
99
+ for (const raw of items) {
100
+ if (!raw || typeof raw !== "object")
101
+ continue;
102
+ const item = raw;
103
+ const noteCard = item.note_card && typeof item.note_card === "object"
104
+ ? item.note_card
105
+ : item;
106
+ const id = String(noteCard.id ?? noteCard.note_id ?? item.id ?? "").trim();
107
+ if (!id)
108
+ continue;
109
+ const user = noteCard.user && typeof noteCard.user === "object"
110
+ ? noteCard.user
111
+ : item.user && typeof item.user === "object"
112
+ ? item.user
113
+ : {};
114
+ const interact = noteCard.interact_info && typeof noteCard.interact_info === "object"
115
+ ? noteCard.interact_info
116
+ : {};
117
+ const xsecToken = String(item.xsec_token ?? noteCard.xsec_token ?? "").trim();
118
+ const contentUrl = xsecToken
119
+ ? `https://www.xiaohongshu.com/explore/${id}?xsec_token=${encodeURIComponent(xsecToken)}&xsec_source=pc_search`
120
+ : `https://www.xiaohongshu.com/explore/${id}`;
121
+ out.push({
122
+ contentId: id,
123
+ title: String(noteCard.title ?? noteCard.desc ?? item.title ?? "").slice(0, 200),
124
+ authorName: String(user.nickname ?? user.name ?? "").trim(),
125
+ authorSecUid: String(user.user_id ?? user.userId ?? "").trim() || undefined,
126
+ commentCount: Number(interact.comment_count ?? noteCard.comment_count ?? 0) || 0,
127
+ contentUrl,
128
+ });
129
+ }
130
+ return out;
131
+ }
132
+ exports.XHS_PROBE_SCRIPT = `(async () => {
133
+ try {
134
+ const url = String(location.href || '');
135
+ const title = String(document.title || '');
136
+ const body = String(document.body && document.body.innerText || '').slice(0, 4000);
137
+ const hasCaptcha = /验证|captcha|滑块|安全验证|risk|verify/i.test(body + title + url);
138
+ const hasLogin = /登录|扫码|sign in|login/i.test(body + title) && !/(?:^|;\\s*)web_session=/.test(document.cookie);
139
+ if (hasCaptcha) return { ok: true, status: 'verification_required' };
140
+ if (hasLogin) return { ok: true, status: 'login_required' };
141
+ const loggedIn = /(?:^|;\\s*)web_session=/.test(document.cookie || '');
142
+ if (loggedIn) return { ok: true, status: 'logged_in' };
143
+ return { ok: true, status: 'unknown' };
144
+ } catch (e) {
145
+ return { ok: false, status: 'unknown', message: String(e && e.message || e) };
146
+ }
147
+ })();`;
148
+ exports.XHS_OPEN_LOGIN_SCRIPT = `(async () => {
149
+ try {
150
+ const sleep = (ms) => new Promise(r => setTimeout(r, ms));
151
+ const clickables = Array.from(document.querySelectorAll('button, a, [role="button"], div'));
152
+ for (const el of clickables) {
153
+ const t = String(el.innerText || el.textContent || '').trim();
154
+ if (/^登录$|扫码登录|手机号登录/.test(t)) { el.click(); await sleep(800); break; }
155
+ }
156
+ return { ok: true };
157
+ } catch (e) { return { ok: false }; }
158
+ })();`;
159
+ exports.XHS_SCROLL_COMMENTS_SCRIPT = `(async () => {
160
+ try {
161
+ for (let i = 0; i < 4; i++) {
162
+ window.scrollBy({ top: window.innerHeight * 0.8, behavior: 'instant' });
163
+ await new Promise(r => setTimeout(r, 600));
164
+ }
165
+ const el = document.querySelector('[class*="comment"], [class*="Comment"], #comment-list, .comments-container');
166
+ if (el) el.scrollIntoView({ behavior: 'instant', block: 'center' });
167
+ return { ok: true };
168
+ } catch (e) { return { ok: false }; }
169
+ })();`;
170
+ exports.XHS_LOGIN_STATE_SCRIPT = `(() => {
171
+ try {
172
+ const st = window.__INITIAL_STATE__ || null;
173
+ const u = st && st.user;
174
+ if (!u) return { ok: false, loggedIn: false };
175
+ let v = u.loggedIn;
176
+ if (v && typeof v === 'object') {
177
+ v = ('value' in v) ? v.value : (('_value' in v) ? v._value : false);
178
+ }
179
+ return { ok: true, loggedIn: !!v };
180
+ } catch (e) {
181
+ return { ok: false, loggedIn: false };
182
+ }
183
+ })();`;
184
+ //# sourceMappingURL=comments.js.map
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.consumeCrawlQuota = consumeCrawlQuota;
7
+ const electron_1 = require("electron");
8
+ const node_fs_1 = require("node:fs");
9
+ const node_path_1 = __importDefault(require("node:path"));
10
+ const logger_1 = require("./kernel/logger");
11
+ const log = logger_1.logger.scope("usage-throttle");
12
+ const MIN_INTERVAL_MS = 30000;
13
+ function stateFilePath() {
14
+ return node_path_1.default.join(electron_1.app.getPath("userData"), "usage-throttle.json");
15
+ }
16
+ function todayKey() {
17
+ const d = new Date();
18
+ const mm = String(d.getMonth() + 1).padStart(2, "0");
19
+ const dd = String(d.getDate()).padStart(2, "0");
20
+ return `${d.getFullYear()}-${mm}-${dd}`;
21
+ }
22
+ function readState() {
23
+ try {
24
+ const raw = (0, node_fs_1.readFileSync)(stateFilePath(), "utf8");
25
+ const parsed = JSON.parse(raw);
26
+ if (typeof parsed.day === "string" && typeof parsed.lastAcceptedAt === "number") {
27
+ const usedByPlatform = parsed.usedByPlatform && typeof parsed.usedByPlatform === "object"
28
+ ? parsed.usedByPlatform
29
+ : typeof parsed.used === "number"
30
+ ? { douyin: parsed.used }
31
+ : {};
32
+ return {
33
+ day: parsed.day,
34
+ usedByPlatform,
35
+ lastAcceptedAt: parsed.lastAcceptedAt,
36
+ };
37
+ }
38
+ }
39
+ catch {
40
+ }
41
+ return { day: todayKey(), usedByPlatform: {}, lastAcceptedAt: 0 };
42
+ }
43
+ function writeState(state) {
44
+ try {
45
+ (0, node_fs_1.writeFileSync)(stateFilePath(), JSON.stringify(state));
46
+ }
47
+ catch (err) {
48
+ log.warn("persist throttle state failed", err instanceof Error ? err.message : String(err));
49
+ }
50
+ }
51
+ function consumeCrawlQuota(platform, units, dailyLimit) {
52
+ const now = Date.now();
53
+ let state = readState();
54
+ if (state.day !== todayKey()) {
55
+ state = { day: todayKey(), usedByPlatform: {}, lastAcceptedAt: 0 };
56
+ }
57
+ if (state.lastAcceptedAt > 0 && now - state.lastAcceptedAt < MIN_INTERVAL_MS) {
58
+ const waitSec = Math.ceil((MIN_INTERVAL_MS - (now - state.lastAcceptedAt)) / 1000);
59
+ log.info("crawl call rejected by interval gate", { platform, waitSec });
60
+ return { ok: false, reason: "interval", waitSec };
61
+ }
62
+ const used = state.usedByPlatform[platform] ?? 0;
63
+ if (used + units > dailyLimit) {
64
+ log.info("crawl call rejected by daily gate", { platform, used, units, limit: dailyLimit });
65
+ return { ok: false, reason: "daily", usedToday: used, limit: dailyLimit };
66
+ }
67
+ state.usedByPlatform[platform] = used + units;
68
+ state.lastAcceptedAt = now;
69
+ writeState(state);
70
+ return { ok: true, usedToday: state.usedByPlatform[platform] ?? 0, limit: dailyLimit };
71
+ }
72
+ //# sourceMappingURL=usage-throttle.js.map
package/dist/main.js ADDED
@@ -0,0 +1,64 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const logger_1 = require("./browser/kernel/logger");
4
+ const consoleLogger = logger_1.logger.scope("console");
5
+ console.log = (...args) => consoleLogger.info(...args);
6
+ console.info = (...args) => consoleLogger.info(...args);
7
+ console.warn = (...args) => consoleLogger.warn(...args);
8
+ console.debug = (...args) => consoleLogger.info(...args);
9
+ console.error = (...args) => consoleLogger.error(...args);
10
+ const electron_1 = require("electron");
11
+ const electron_profile_1 = require("./browser/kernel/electron-profile");
12
+ const platform_runner_1 = require("./browser/platform-runner");
13
+ const server_1 = require("./mcp/server");
14
+ const version_1 = require("./version");
15
+ const log = logger_1.logger.scope("main");
16
+ (0, electron_profile_1.configureDesktopElectronProfile)(electron_1.app, {
17
+ profileName: process.env.PPXC_MCP_PROFILE_NAME?.trim() || "PPXC Leads MCP",
18
+ logUserDataPathOnce: true,
19
+ log: (msg) => log.info(msg),
20
+ });
21
+ if (process.platform === "darwin" && electron_1.app.dock) {
22
+ electron_1.app.dock.hide();
23
+ }
24
+ let stdioClosed = false;
25
+ function quitAfterStdioClose(reason) {
26
+ if (stdioClosed)
27
+ return;
28
+ stdioClosed = true;
29
+ log.info(`MCP stdio closed (${reason}), quitting`);
30
+ void (0, platform_runner_1.shutdownRunner)().finally(() => {
31
+ if (electron_1.app.isReady()) {
32
+ electron_1.app.quit();
33
+ return;
34
+ }
35
+ electron_1.app.exit(0);
36
+ });
37
+ setTimeout(() => electron_1.app.exit(0), 1500).unref();
38
+ }
39
+ process.stdin.on("end", () => quitAfterStdioClose("end"));
40
+ process.stdin.on("close", () => quitAfterStdioClose("close"));
41
+ const gotLock = electron_1.app.requestSingleInstanceLock();
42
+ if (!gotLock) {
43
+ log.error("another PPXC Leads MCP instance is already running, exiting");
44
+ electron_1.app.exit(1);
45
+ }
46
+ else {
47
+ electron_1.app.on("window-all-closed", () => {
48
+ });
49
+ electron_1.app.on("before-quit", () => {
50
+ void (0, platform_runner_1.shutdownRunner)();
51
+ });
52
+ electron_1.app
53
+ .whenReady()
54
+ .then(async () => {
55
+ log.info(`electron ready, starting MCP stdio server (ppxc-leads-mcp v${version_1.OWN_VERSION})`);
56
+ await (0, server_1.startMcpServer)();
57
+ log.info("MCP server connected over stdio");
58
+ })
59
+ .catch((err) => {
60
+ log.error("failed to start MCP server", err instanceof Error ? err.message : String(err));
61
+ electron_1.app.exit(1);
62
+ });
63
+ }
64
+ //# sourceMappingURL=main.js.map