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,162 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.douyinAdapter = void 0;
4
+ const runner_page_manager_1 = require("../../kernel/runner-page-manager");
5
+ const runner_page_session_user_agent_1 = require("../../kernel/runner-page-session.user-agent");
6
+ const comments_1 = require("./comments");
7
+ const SEARCH_SORT_TYPE = "0";
8
+ const SEARCH_PUBLISH_TIME = "180";
9
+ const SESSION = {
10
+ partition: "persist:ppxc-mcp-douyin",
11
+ cookieDomain: ".douyin.com",
12
+ windowTitle: "PPXC Douyin",
13
+ userAgent: (0, runner_page_session_user_agent_1.resolveDouyinUserAgent)(process.platform, process.versions),
14
+ enableSearchSniffer: true,
15
+ loginCookieNames: ["sessionid", "sessionid_ss", "sid_guard"],
16
+ homeUrl: "https://www.douyin.com/",
17
+ };
18
+ const RISK = {
19
+ maxVideosPerKeyword: 8,
20
+ totalContentBudget: 12,
21
+ minCommentCount: 3,
22
+ maxPerAuthor: 2,
23
+ perItemMinDelayMs: 1200,
24
+ perItemMaxDelayMs: 2600,
25
+ slotStaggerMs: 8000,
26
+ dailyQuotaUnits: 20,
27
+ };
28
+ function sleep(ms) {
29
+ return new Promise((r) => setTimeout(r, ms));
30
+ }
31
+ exports.douyinAdapter = {
32
+ id: "douyin",
33
+ displayName: "抖音",
34
+ sessionConfig: SESSION,
35
+ risk: RISK,
36
+ createManager(defaultVisible) {
37
+ return new runner_page_manager_1.RunnerPageManager({
38
+ maxConcurrent: 2,
39
+ defaultVisible,
40
+ navigationTimeoutMs: 45000,
41
+ sessionDefaults: {
42
+ partition: SESSION.partition,
43
+ cookieDomain: SESSION.cookieDomain,
44
+ windowTitle: SESSION.windowTitle,
45
+ userAgent: SESSION.userAgent,
46
+ enableSearchSniffer: SESSION.enableSearchSniffer,
47
+ },
48
+ });
49
+ },
50
+ async isLoggedIn(session) {
51
+ const snap = await session.getSessionCookieSnapshot();
52
+ return Boolean(snap.hasSessionId || snap.hasSessionIdSs || snap.hasSidGuard);
53
+ },
54
+ async startLogin(manager, slotId, timeoutMs) {
55
+ const session = manager.acquire(slotId, { visible: true });
56
+ session.reveal();
57
+ try {
58
+ if (await exports.douyinAdapter.isLoggedIn(session))
59
+ return { loggedIn: true };
60
+ await session.loadUrl(SESSION.homeUrl);
61
+ await session.openLoginPanel().catch(() => null);
62
+ session.reveal();
63
+ const deadline = Date.now() + Math.max(15000, timeoutMs);
64
+ while (Date.now() < deadline) {
65
+ await sleep(2500);
66
+ if (await exports.douyinAdapter.isLoggedIn(session)) {
67
+ const name = await exports.douyinAdapter.getProfileDisplayName(session);
68
+ return { loggedIn: true, displayName: name };
69
+ }
70
+ }
71
+ return { loggedIn: false };
72
+ }
73
+ finally {
74
+ session.hide();
75
+ }
76
+ },
77
+ detectContentUrl(input) {
78
+ return detectPlatformFromUrl(input) === "douyin" || Boolean((0, comments_1.extractAwemeId)(input));
79
+ },
80
+ async resolveContentId(input, session) {
81
+ const direct = (0, comments_1.extractAwemeId)(input);
82
+ if (direct)
83
+ return direct;
84
+ if ((0, comments_1.isDouyinShortLink)(input)) {
85
+ let resolved = null;
86
+ try {
87
+ const loaded = await session.loadUrl(input);
88
+ resolved = (0, comments_1.extractAwemeId)(loaded.finalUrl);
89
+ }
90
+ catch {
91
+ }
92
+ if (!resolved) {
93
+ await sleep(1500);
94
+ resolved = (0, comments_1.extractAwemeId)(session.currentUrl());
95
+ }
96
+ if (resolved)
97
+ return resolved;
98
+ }
99
+ throw new Error(`无法从链接里识别出抖音视频:${input}`);
100
+ },
101
+ buildContentUrl(contentId) {
102
+ return `https://www.douyin.com/video/${contentId}`;
103
+ },
104
+ async fetchComments(session, contentId, _contentUrl, maxComments, fetchWaitMs) {
105
+ const fetched = await session.fetchJsonInPage((0, comments_1.buildCommentUrl)(contentId, 0, maxComments), fetchWaitMs);
106
+ if (!fetched.ok || !fetched.json || typeof fetched.json !== "object") {
107
+ return { rawCount: 0, hasMore: false };
108
+ }
109
+ const json = fetched.json;
110
+ const rawCount = Array.isArray(json.comments) ? json.comments.length : 0;
111
+ return { json, rawCount, hasMore: Boolean(json.has_more) };
112
+ },
113
+ parseComments: comments_1.commentsFromJson,
114
+ buildSearchUrl(keyword) {
115
+ const params = new URLSearchParams({
116
+ type: "video",
117
+ sort_type: SEARCH_SORT_TYPE,
118
+ publish_time: SEARCH_PUBLISH_TIME,
119
+ });
120
+ return `https://www.douyin.com/search/${encodeURIComponent(keyword)}?${params.toString()}`;
121
+ },
122
+ async awaitSearchBatch(session, keyword, fetchWaitMs) {
123
+ session.clearSearchBuffer({ resolveWaiters: true });
124
+ session.navigateNoWait(exports.douyinAdapter.buildSearchUrl(keyword));
125
+ const batch = await session.awaitNextSearchBatch(fetchWaitMs);
126
+ if (!batch)
127
+ return null;
128
+ const items = batch.items
129
+ .filter((it) => it.awemeId)
130
+ .map((it) => ({
131
+ contentId: String(it.awemeId),
132
+ title: it.title ?? "",
133
+ authorName: it.authorName ?? "",
134
+ authorSecUid: it.authorSecUid,
135
+ commentCount: it.commentCount ?? 0,
136
+ contentUrl: `https://www.douyin.com/video/${it.awemeId}`,
137
+ }));
138
+ return { items, verifyCheck: batch.verifyCheck };
139
+ },
140
+ async probeVerification(session) {
141
+ const probe = await session.probe().catch(() => null);
142
+ return probe?.status === "verification_required";
143
+ },
144
+ async openLoginPanel(session) {
145
+ await session.openLoginPanel().catch(() => undefined);
146
+ },
147
+ async getProfileDisplayName(session) {
148
+ try {
149
+ const snap = await session.getDouyinProfileSnapshot();
150
+ return snap.displayName;
151
+ }
152
+ catch {
153
+ return null;
154
+ }
155
+ },
156
+ };
157
+ function detectPlatformFromUrl(input) {
158
+ if (/douyin\.com|v\.douyin\.com/i.test(input))
159
+ return "douyin";
160
+ return null;
161
+ }
162
+ //# sourceMappingURL=adapter.js.map
@@ -0,0 +1,130 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.commentsFromJson = commentsFromJson;
4
+ exports.buildCommentUrl = buildCommentUrl;
5
+ exports.extractAwemeId = extractAwemeId;
6
+ exports.isDouyinShortLink = isDouyinShortLink;
7
+ const COMMENT_HAS_MEANINGFUL_CHAR = /[\p{Script=Han}A-Za-z0-9]/u;
8
+ const COMMENT_MIN_TEXT_LENGTH = 4;
9
+ function shouldKeepComment(text, diggCount) {
10
+ const trimmed = String(text ?? "").trim();
11
+ if (!trimmed)
12
+ return false;
13
+ if (!COMMENT_HAS_MEANINGFUL_CHAR.test(trimmed))
14
+ return false;
15
+ if (trimmed.length < COMMENT_MIN_TEXT_LENGTH && diggCount <= 0)
16
+ return false;
17
+ return true;
18
+ }
19
+ function optionalString(value, maxLength) {
20
+ const text = String(value ?? "").trim();
21
+ return text ? text.slice(0, maxLength) : undefined;
22
+ }
23
+ function readAvatarUrl(value) {
24
+ const obj = value && typeof value === "object" && !Array.isArray(value)
25
+ ? value
26
+ : null;
27
+ const list = obj && Array.isArray(obj.url_list) ? obj.url_list : [];
28
+ const first = list.length > 0 ? String(list[0] ?? "").trim() : "";
29
+ return first || undefined;
30
+ }
31
+ function pickIpLabel(comment, user) {
32
+ const candidates = [
33
+ comment.ip_label,
34
+ comment.ipLabel,
35
+ comment.ip_location,
36
+ comment.ipLocation,
37
+ user.ip_label,
38
+ user.ipLabel,
39
+ user.ip_location,
40
+ user.ipLocation,
41
+ ];
42
+ for (const value of candidates) {
43
+ const text = String(value ?? "").trim();
44
+ if (text)
45
+ return text.slice(0, 64);
46
+ }
47
+ return "";
48
+ }
49
+ function commentsFromJson(contentId, json) {
50
+ if (!json || typeof json !== "object")
51
+ return [];
52
+ const record = json;
53
+ if (Number(record.status_code ?? 0) !== 0)
54
+ return [];
55
+ const comments = Array.isArray(record.comments) ? record.comments : [];
56
+ const out = [];
57
+ for (const raw of comments) {
58
+ if (!raw || typeof raw !== "object")
59
+ continue;
60
+ const item = raw;
61
+ const user = item.user && typeof item.user === "object" && !Array.isArray(item.user)
62
+ ? item.user
63
+ : {};
64
+ const cid = String(item.cid ?? "").trim();
65
+ const text = String(item.text ?? "").trim();
66
+ if (!cid || !text)
67
+ continue;
68
+ const diggCount = Number.isFinite(Number(item.digg_count))
69
+ ? Math.max(0, Math.floor(Number(item.digg_count)))
70
+ : 0;
71
+ if (!shouldKeepComment(text, diggCount))
72
+ continue;
73
+ const secUid = optionalString(user.sec_uid, 128);
74
+ out.push({
75
+ contentId,
76
+ awemeId: contentId,
77
+ cid,
78
+ text,
79
+ nickname: String(user.nickname ?? item.nickname ?? "").trim() || "未知用户",
80
+ userSecUid: secUid,
81
+ userProfileUrl: secUid ? `https://www.douyin.com/user/${secUid}` : undefined,
82
+ avatarUrl: readAvatarUrl(user.avatar_thumb),
83
+ createTime: Number.isFinite(Number(item.create_time))
84
+ ? Math.max(0, Math.floor(Number(item.create_time)))
85
+ : 0,
86
+ diggCount,
87
+ replyCount: Number.isFinite(Number(item.reply_comment_total ?? item.reply_count))
88
+ ? Math.max(0, Math.floor(Number(item.reply_comment_total ?? item.reply_count)))
89
+ : 0,
90
+ ipLabel: pickIpLabel(item, user),
91
+ });
92
+ }
93
+ return out;
94
+ }
95
+ function buildCommentUrl(awemeId, cursor, count) {
96
+ const params = new URLSearchParams({
97
+ aweme_id: awemeId,
98
+ cursor: String(cursor),
99
+ count: String(count),
100
+ item_type: "0",
101
+ });
102
+ return `https://www.douyin.com/aweme/v1/web/comment/list/?${params.toString()}`;
103
+ }
104
+ function extractAwemeId(input) {
105
+ const raw = String(input ?? "").trim();
106
+ if (!raw)
107
+ return null;
108
+ if (/^\d{15,25}$/.test(raw))
109
+ return raw;
110
+ const videoMatch = raw.match(/\/video\/(\d{15,25})/);
111
+ if (videoMatch)
112
+ return videoMatch[1];
113
+ const noteMatch = raw.match(/\/note\/(\d{15,25})/);
114
+ if (noteMatch)
115
+ return noteMatch[1];
116
+ try {
117
+ const url = new URL(raw);
118
+ const modalId = url.searchParams.get("modal_id");
119
+ if (modalId && /^\d{15,25}$/.test(modalId))
120
+ return modalId;
121
+ }
122
+ catch {
123
+ }
124
+ const anyDigits = raw.match(/(\d{15,25})/);
125
+ return anyDigits ? anyDigits[1] : null;
126
+ }
127
+ function isDouyinShortLink(input) {
128
+ return /v\.douyin\.com|\/share\//i.test(String(input ?? ""));
129
+ }
130
+ //# sourceMappingURL=comments.js.map
@@ -0,0 +1,178 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.kuaishouAdapter = 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-kuaishou",
10
+ cookieDomain: ".kuaishou.com",
11
+ windowTitle: "PPXC 快手",
12
+ userAgent: CHROME_UA,
13
+ enableSearchSniffer: false,
14
+ loginCookieNames: ["kuaishou.server.webday7_st", "kuaishou.server.web_st"],
15
+ homeUrl: "https://www.kuaishou.com/",
16
+ };
17
+ const RISK = {
18
+ maxVideosPerKeyword: 4,
19
+ totalContentBudget: 8,
20
+ minCommentCount: 2,
21
+ maxPerAuthor: 2,
22
+ perItemMinDelayMs: 1800,
23
+ perItemMaxDelayMs: 3500,
24
+ slotStaggerMs: 10000,
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.KS_PROBE_SCRIPT, true)
34
+ .catch(() => null);
35
+ if (!raw || typeof raw !== "object")
36
+ return "unknown";
37
+ return String(raw.status ?? "unknown");
38
+ }
39
+ exports.kuaishouAdapter = {
40
+ id: "kuaishou",
41
+ displayName: "快手",
42
+ sessionConfig: SESSION,
43
+ risk: RISK,
44
+ createManager(defaultVisible) {
45
+ return new runner_page_manager_1.RunnerPageManager({
46
+ maxConcurrent: 2,
47
+ defaultVisible,
48
+ navigationTimeoutMs: 45000,
49
+ sessionDefaults: SESSION,
50
+ });
51
+ },
52
+ async isLoggedIn(session) {
53
+ const wc = session.getWebContents();
54
+ if (!wc)
55
+ return false;
56
+ try {
57
+ const cookies = await wc.session.cookies.get({});
58
+ return cookies.some((c) => /^kuaishou\.server\.web(day\d+)?_st$/.test(c.name) &&
59
+ /kuaishou\.com$/.test(String(c.domain ?? "")));
60
+ }
61
+ catch {
62
+ return false;
63
+ }
64
+ },
65
+ async startLogin(manager, slotId, timeoutMs) {
66
+ const session = manager.acquire(slotId, { visible: true });
67
+ session.reveal();
68
+ try {
69
+ if (await exports.kuaishouAdapter.isLoggedIn(session))
70
+ return { loggedIn: true };
71
+ await session.loadUrl(SESSION.homeUrl);
72
+ await session.getWebContents()?.executeJavaScript(comments_1.KS_OPEN_LOGIN_SCRIPT, true);
73
+ session.reveal();
74
+ const deadline = Date.now() + Math.max(15000, timeoutMs);
75
+ while (Date.now() < deadline) {
76
+ await sleep(2500);
77
+ if (await exports.kuaishouAdapter.isLoggedIn(session))
78
+ return { loggedIn: true };
79
+ }
80
+ return { loggedIn: false };
81
+ }
82
+ finally {
83
+ session.hide();
84
+ }
85
+ },
86
+ detectContentUrl(input) {
87
+ return /kuaishou\.com|gifshow\.com|ksurl\.cn/i.test(input) || Boolean((0, comments_1.extractPhotoId)(input));
88
+ },
89
+ async resolveContentId(input, session) {
90
+ const direct = (0, comments_1.extractPhotoId)(input);
91
+ if (direct)
92
+ return direct;
93
+ if ((0, comments_1.isKsShortLink)(input)) {
94
+ try {
95
+ await session.loadUrl(input);
96
+ }
97
+ catch {
98
+ }
99
+ const deadline = Date.now() + 15000;
100
+ while (Date.now() < deadline) {
101
+ await sleep(1000);
102
+ const fromUrl = (0, comments_1.extractPhotoId)(session.currentUrl());
103
+ if (fromUrl)
104
+ return fromUrl;
105
+ }
106
+ }
107
+ throw new Error(`无法从链接里识别出快手视频:${input}`);
108
+ },
109
+ buildContentUrl(contentId) {
110
+ return `https://www.kuaishou.com/short-video/${contentId}`;
111
+ },
112
+ async fetchComments(session, contentId, contentUrl, _maxComments, fetchWaitMs) {
113
+ const wc = session.getWebContents();
114
+ if (!wc)
115
+ return { rawCount: 0, hasMore: false };
116
+ const capturePromise = (0, cdp_json_waiter_1.waitForJsonResponses)(wc, (url) => /kuaishou\.com\/graphql|rest\/[^?]*comment|comment\/list|commentList/i.test(url), fetchWaitMs, 10);
117
+ try {
118
+ await session.loadUrl(contentUrl);
119
+ }
120
+ catch {
121
+ }
122
+ await sleep(2000);
123
+ await wc.executeJavaScript(comments_1.KS_SCROLL_SCRIPT, true).catch(() => undefined);
124
+ await sleep(1500);
125
+ const captured = await capturePromise;
126
+ for (const json of captured) {
127
+ const parsed = (0, comments_1.commentsFromKsJson)(contentId, json);
128
+ if (parsed.length > 0) {
129
+ return { json, rawCount: parsed.length, hasMore: false };
130
+ }
131
+ }
132
+ return { rawCount: 0, hasMore: false };
133
+ },
134
+ parseComments: comments_1.commentsFromKsJson,
135
+ buildSearchUrl(keyword) {
136
+ return `https://www.kuaishou.com/search/video?searchKey=${encodeURIComponent(keyword)}`;
137
+ },
138
+ async awaitSearchBatch(session, keyword, fetchWaitMs) {
139
+ const wc = session.getWebContents();
140
+ if (!wc)
141
+ return null;
142
+ const capturePromise = (0, cdp_json_waiter_1.waitForJsonResponses)(wc, (url) => /rest\/v\/search\/feed|search\/photo|search\/video|graphql/i.test(url), fetchWaitMs, 3);
143
+ session.navigateNoWait(exports.kuaishouAdapter.buildSearchUrl(keyword));
144
+ await sleep(2500);
145
+ await wc.executeJavaScript(comments_1.KS_SCROLL_SCRIPT, true).catch(() => undefined);
146
+ const captured = await capturePromise;
147
+ let items = [];
148
+ for (const json of captured) {
149
+ items = (0, comments_1.searchItemsFromKsJson)(json).map((it) => ({
150
+ contentId: it.contentId,
151
+ title: it.title,
152
+ authorName: it.authorName,
153
+ authorSecUid: it.authorSecUid,
154
+ commentCount: it.commentCount,
155
+ contentUrl: it.contentUrl,
156
+ }));
157
+ if (items.length > 0)
158
+ break;
159
+ }
160
+ if (items.length === 0) {
161
+ const status = await probePage(session);
162
+ if (status === "verification_required")
163
+ return { items: [], verifyCheck: true };
164
+ return null;
165
+ }
166
+ return { items };
167
+ },
168
+ async probeVerification(session) {
169
+ return (await probePage(session)) === "verification_required";
170
+ },
171
+ async openLoginPanel(session) {
172
+ await session.getWebContents()?.executeJavaScript(comments_1.KS_OPEN_LOGIN_SCRIPT, true);
173
+ },
174
+ async getProfileDisplayName(_session) {
175
+ return null;
176
+ },
177
+ };
178
+ //# sourceMappingURL=adapter.js.map
@@ -0,0 +1,170 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.KS_SCROLL_SCRIPT = exports.KS_OPEN_LOGIN_SCRIPT = exports.KS_PROBE_SCRIPT = void 0;
4
+ exports.extractPhotoId = extractPhotoId;
5
+ exports.isKsShortLink = isKsShortLink;
6
+ exports.commentsFromKsJson = commentsFromKsJson;
7
+ exports.searchItemsFromKsJson = searchItemsFromKsJson;
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 extractPhotoId(input) {
21
+ const raw = String(input ?? "").trim();
22
+ if (!raw)
23
+ return null;
24
+ const short = raw.match(/\/short-video\/([a-zA-Z0-9_-]+)/);
25
+ if (short)
26
+ return short[1];
27
+ const photo = raw.match(/\/fw\/photo\/([a-zA-Z0-9_-]+)/);
28
+ if (photo)
29
+ return photo[1];
30
+ const f = raw.match(/\/f\/([a-zA-Z0-9_-]+)/);
31
+ if (f)
32
+ return f[1];
33
+ if (/^[a-zA-Z0-9_-]{8,64}$/.test(raw))
34
+ return raw;
35
+ return null;
36
+ }
37
+ function isKsShortLink(input) {
38
+ return /ksurl\.cn|v\.kuaishou\.com|gifshow\.com/i.test(String(input ?? ""));
39
+ }
40
+ function commentsFromKsJson(contentId, json) {
41
+ if (!json || typeof json !== "object")
42
+ return [];
43
+ const root = json;
44
+ const data = root.data ?? root;
45
+ const vision = data.visionCommentList && typeof data.visionCommentList === "object"
46
+ ? data.visionCommentList
47
+ : data;
48
+ const comments = Array.isArray(vision.rootCommentsV2) && vision.rootCommentsV2.length > 0
49
+ ? vision.rootCommentsV2
50
+ : Array.isArray(vision.rootComments)
51
+ ? vision.rootComments
52
+ : Array.isArray(data.rootComments)
53
+ ? data.rootComments
54
+ : Array.isArray(data.comments)
55
+ ? data.comments
56
+ : [];
57
+ const out = [];
58
+ for (const raw of comments) {
59
+ if (!raw || typeof raw !== "object")
60
+ continue;
61
+ const item = raw;
62
+ const author = item.author && typeof item.author === "object"
63
+ ? item.author
64
+ : {};
65
+ const cid = String(item.commentId ?? item.comment_id ?? item.cid ?? item.id ?? "").trim();
66
+ const text = String(item.content ?? item.text ?? item.comment ?? "").trim();
67
+ if (!text)
68
+ continue;
69
+ const diggCount = Number.isFinite(Number(item.likedCount ?? item.like_count))
70
+ ? Math.max(0, Math.floor(Number(item.likedCount ?? item.like_count)))
71
+ : 0;
72
+ if (!shouldKeepComment(text, diggCount))
73
+ continue;
74
+ const userId = String(item.authorId ?? author.id ?? author.userId ?? author.user_id ?? "").trim();
75
+ const nickname = String(item.authorName ?? author.name ?? author.user_name ?? "未知用户").trim() ||
76
+ "未知用户";
77
+ out.push({
78
+ contentId,
79
+ awemeId: contentId,
80
+ cid: cid || `${contentId}_${out.length}`,
81
+ text,
82
+ nickname,
83
+ userSecUid: userId || undefined,
84
+ userProfileUrl: userId ? `https://www.kuaishou.com/profile/${userId}` : undefined,
85
+ avatarUrl: String(item.headurl ?? author.headurl ?? author.avatar ?? "").trim() || undefined,
86
+ createTime: Number.isFinite(Number(item.timestamp ?? item.time))
87
+ ? Math.max(0, Math.floor(Number(item.timestamp ?? item.time) / (Number(item.timestamp) > 1e12 ? 1000 : 1)))
88
+ : 0,
89
+ diggCount,
90
+ replyCount: Number.isFinite(Number(item.subCommentCount ?? item.reply_count))
91
+ ? Math.max(0, Math.floor(Number(item.subCommentCount ?? item.reply_count)))
92
+ : item.hasSubComments === true
93
+ ? 1
94
+ : 0,
95
+ ipLabel: String(item.ip ?? item.ip_location ?? "").slice(0, 64),
96
+ });
97
+ }
98
+ return out;
99
+ }
100
+ function searchItemsFromKsJson(json) {
101
+ if (!json || typeof json !== "object")
102
+ return [];
103
+ const root = json;
104
+ const data = root.data ?? root;
105
+ const feeds = Array.isArray(data.feeds) ? data.feeds : Array.isArray(data.list) ? data.list : [];
106
+ const out = [];
107
+ for (const raw of feeds) {
108
+ if (!raw || typeof raw !== "object")
109
+ continue;
110
+ const item = raw;
111
+ const photo = item.photo && typeof item.photo === "object"
112
+ ? item.photo
113
+ : item;
114
+ const id = String(photo.id ?? photo.photoId ?? item.id ?? "").trim();
115
+ if (!id)
116
+ continue;
117
+ const author = item.author && typeof item.author === "object"
118
+ ? item.author
119
+ : photo.userInfo && typeof photo.userInfo === "object"
120
+ ? photo.userInfo
121
+ : photo.author && typeof photo.author === "object"
122
+ ? photo.author
123
+ : {};
124
+ const commentInfo = item.comment && typeof item.comment === "object"
125
+ ? item.comment
126
+ : {};
127
+ const commentCount = Number(commentInfo.us_c ?? photo.commentCount ?? photo.comment_count ?? 0) || 0;
128
+ out.push({
129
+ contentId: id,
130
+ title: String(photo.caption ?? photo.title ?? item.title ?? "").slice(0, 200),
131
+ authorName: String(author.name ?? author.user_name ?? "").trim(),
132
+ authorSecUid: String(author.id ?? author.userId ?? "").trim() || undefined,
133
+ commentCount,
134
+ contentUrl: `https://www.kuaishou.com/short-video/${id}`,
135
+ });
136
+ }
137
+ return out;
138
+ }
139
+ exports.KS_PROBE_SCRIPT = `(async () => {
140
+ try {
141
+ const body = String(document.body && document.body.innerText || '').slice(0, 4000);
142
+ const title = String(document.title || '');
143
+ if (/验证|captcha|滑块|安全/i.test(body + title)) return { ok: true, status: 'verification_required' };
144
+ // 注意:主站会话票(kuaishou.server.webday7_st)是 httpOnly,document.cookie 读不到,
145
+ // 页内只能靠界面特征粗判;权威登录判断在主进程 cookies API。
146
+ const hasLoginWall = /登录即可享受|立即登录|扫码登录/.test(body);
147
+ if (hasLoginWall) return { ok: true, status: 'login_required' };
148
+ return { ok: true, status: 'unknown' };
149
+ } catch (e) {
150
+ return { ok: false, status: 'unknown' };
151
+ }
152
+ })();`;
153
+ exports.KS_OPEN_LOGIN_SCRIPT = `(async () => {
154
+ try {
155
+ const els = Array.from(document.querySelectorAll('button, a, [role="button"], div'));
156
+ for (const el of els) {
157
+ const t = String(el.innerText || el.textContent || '').trim();
158
+ if (/登录|扫码/.test(t)) { el.click(); await new Promise(r => setTimeout(r, 800)); break; }
159
+ }
160
+ return { ok: true };
161
+ } catch { return { ok: false }; }
162
+ })();`;
163
+ exports.KS_SCROLL_SCRIPT = `(async () => {
164
+ for (let i = 0; i < 3; i++) {
165
+ window.scrollBy({ top: window.innerHeight * 0.7, behavior: 'instant' });
166
+ await new Promise(r => setTimeout(r, 500));
167
+ }
168
+ return { ok: true };
169
+ })();`;
170
+ //# sourceMappingURL=comments.js.map
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.kuaishouAdapter = exports.xiaohongshuAdapter = exports.douyinAdapter = void 0;
4
+ exports.getPlatformAdapter = getPlatformAdapter;
5
+ exports.listPlatformAdapters = listPlatformAdapters;
6
+ const adapter_1 = require("./douyin/adapter");
7
+ Object.defineProperty(exports, "douyinAdapter", { enumerable: true, get: function () { return adapter_1.douyinAdapter; } });
8
+ const adapter_2 = require("./kuaishou/adapter");
9
+ Object.defineProperty(exports, "kuaishouAdapter", { enumerable: true, get: function () { return adapter_2.kuaishouAdapter; } });
10
+ const adapter_3 = require("./xiaohongshu/adapter");
11
+ Object.defineProperty(exports, "xiaohongshuAdapter", { enumerable: true, get: function () { return adapter_3.xiaohongshuAdapter; } });
12
+ const ADAPTERS = {
13
+ douyin: adapter_1.douyinAdapter,
14
+ xiaohongshu: adapter_3.xiaohongshuAdapter,
15
+ kuaishou: adapter_2.kuaishouAdapter,
16
+ };
17
+ function getPlatformAdapter(platform) {
18
+ return ADAPTERS[platform];
19
+ }
20
+ function listPlatformAdapters() {
21
+ return Object.values(ADAPTERS);
22
+ }
23
+ //# sourceMappingURL=registry.js.map