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.
- package/README.md +115 -0
- package/dist/backend/config.js +13 -0
- package/dist/backend/ppxc-client.js +156 -0
- package/dist/backend/ppxc-login-window.js +168 -0
- package/dist/backend/token-store.js +65 -0
- package/dist/browser/comments.js +9 -0
- package/dist/browser/douyin-runner.js +15 -0
- package/dist/browser/kernel/electron-profile.js +32 -0
- package/dist/browser/kernel/logger.js +57 -0
- package/dist/browser/kernel/page-scripts/index.js +1422 -0
- package/dist/browser/kernel/runner-page-manager.js +145 -0
- package/dist/browser/kernel/runner-page-session.js +1465 -0
- package/dist/browser/kernel/runner-page-session.search-parser.js +187 -0
- package/dist/browser/kernel/runner-page-session.user-agent.js +32 -0
- package/dist/browser/platform-runner.js +312 -0
- package/dist/browser/platforms/detect-platform.js +33 -0
- package/dist/browser/platforms/douyin/adapter.js +162 -0
- package/dist/browser/platforms/douyin/comments.js +130 -0
- package/dist/browser/platforms/kuaishou/adapter.js +178 -0
- package/dist/browser/platforms/kuaishou/comments.js +170 -0
- package/dist/browser/platforms/registry.js +23 -0
- package/dist/browser/platforms/shared/cdp-json-waiter.js +75 -0
- package/dist/browser/platforms/types.js +3 -0
- package/dist/browser/platforms/xiaohongshu/adapter.js +233 -0
- package/dist/browser/platforms/xiaohongshu/comments.js +184 -0
- package/dist/browser/usage-throttle.js +72 -0
- package/dist/main.js +64 -0
- package/dist/mcp/battle-report.js +325 -0
- package/dist/mcp/content-insights.js +66 -0
- package/dist/mcp/diagnostics.js +79 -0
- package/dist/mcp/server.js +829 -0
- package/dist/version.js +19 -0
- package/package.json +43 -0
- package/scripts/launch-mcp.cjs +96 -0
- package/skills/ppxc-find-customers/SKILL.md +110 -0
|
@@ -0,0 +1,325 @@
|
|
|
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.rankReason = rankReason;
|
|
7
|
+
exports.renderBattleReport = renderBattleReport;
|
|
8
|
+
exports.exportBattleReport = exportBattleReport;
|
|
9
|
+
const node_fs_1 = require("node:fs");
|
|
10
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
11
|
+
const electron_1 = require("electron");
|
|
12
|
+
const logger_1 = require("../browser/kernel/logger");
|
|
13
|
+
const version_1 = require("../version");
|
|
14
|
+
const content_insights_1 = require("./content-insights");
|
|
15
|
+
const log = logger_1.logger.scope("battle-report");
|
|
16
|
+
const INTENT_LABELS = { high: "高意向", medium: "中意向", low: "低意向" };
|
|
17
|
+
const INTENT_CLASSES = { high: "hi", medium: "mid", low: "low" };
|
|
18
|
+
function intentLabel(intent) {
|
|
19
|
+
return INTENT_LABELS[intent] ?? intent;
|
|
20
|
+
}
|
|
21
|
+
function esc(s) {
|
|
22
|
+
return String(s ?? "")
|
|
23
|
+
.replace(/&/g, "&")
|
|
24
|
+
.replace(/</g, "<")
|
|
25
|
+
.replace(/>/g, ">")
|
|
26
|
+
.replace(/"/g, """);
|
|
27
|
+
}
|
|
28
|
+
function rankReason(lead) {
|
|
29
|
+
const parts = [];
|
|
30
|
+
parts.push(intentLabel(lead.intent));
|
|
31
|
+
if (lead.demandType)
|
|
32
|
+
parts.push(lead.demandType);
|
|
33
|
+
if (typeof lead.saleScore === "number")
|
|
34
|
+
parts.push(`销售分 ${lead.saleScore}`);
|
|
35
|
+
if (lead.ipLabel)
|
|
36
|
+
parts.push(`IP ${lead.ipLabel}`);
|
|
37
|
+
return parts.join(" · ");
|
|
38
|
+
}
|
|
39
|
+
function pickOutputDir() {
|
|
40
|
+
for (const name of ["desktop", "downloads"]) {
|
|
41
|
+
try {
|
|
42
|
+
const dir = electron_1.app.getPath(name);
|
|
43
|
+
if (dir)
|
|
44
|
+
return dir;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return electron_1.app.getPath("userData");
|
|
50
|
+
}
|
|
51
|
+
function timestampLabel() {
|
|
52
|
+
const d = new Date();
|
|
53
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
54
|
+
return `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}-${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`;
|
|
55
|
+
}
|
|
56
|
+
const STYLE = `
|
|
57
|
+
:root {
|
|
58
|
+
--ink:#26332d; --muted:#7c8a83; --green:#0e9f6e; --green-deep:#046c4e;
|
|
59
|
+
--mint:#e4f5ed; --card:#ffffff; --line:#e9efeb;
|
|
60
|
+
--shadow:0 2px 12px rgba(38,72,58,.06);
|
|
61
|
+
}
|
|
62
|
+
* { box-sizing:border-box; margin:0; padding:0; }
|
|
63
|
+
body { font-family:-apple-system,BlinkMacSystemFont,"PingFang SC","Microsoft YaHei",sans-serif;
|
|
64
|
+
color:var(--ink); background:linear-gradient(180deg,#f3f9f5 0%,#fbfcfa 360px); line-height:1.7; }
|
|
65
|
+
.page { max-width:820px; margin:0 auto; padding:52px 28px 72px; }
|
|
66
|
+
header { text-align:center; margin-bottom:34px; }
|
|
67
|
+
.brand { display:inline-block; font-size:12px; letter-spacing:.22em; color:var(--green-deep);
|
|
68
|
+
background:var(--mint); border-radius:999px; padding:5px 16px; font-weight:700; }
|
|
69
|
+
h1 { font-size:30px; font-weight:800; margin-top:14px; letter-spacing:.02em; }
|
|
70
|
+
.meta { margin-top:14px; font-size:13px; color:var(--muted); display:flex; flex-wrap:wrap;
|
|
71
|
+
justify-content:center; gap:8px; }
|
|
72
|
+
.meta span { background:var(--card); border:1px solid var(--line); border-radius:999px; padding:4px 14px; }
|
|
73
|
+
.stats { display:grid; grid-template-columns:repeat(auto-fit,minmax(140px,1fr)); gap:14px; margin:30px 0 6px; }
|
|
74
|
+
.stat { border-radius:18px; padding:18px 20px; box-shadow:var(--shadow); background:var(--card); }
|
|
75
|
+
.stat:nth-child(odd) { background:#f0faf5; }
|
|
76
|
+
.stat b { display:block; font-size:28px; font-weight:800; color:var(--green-deep); line-height:1.25; }
|
|
77
|
+
.stat span { font-size:13px; color:var(--muted); }
|
|
78
|
+
h2 { font-size:17px; font-weight:800; margin:40px 0 16px; display:flex; align-items:center; gap:9px; }
|
|
79
|
+
h2::before { content:""; width:9px; height:9px; border-radius:50%; background:var(--green); flex-shrink:0; }
|
|
80
|
+
.todo { background:var(--card); border-radius:18px; box-shadow:var(--shadow); padding:8px 0; }
|
|
81
|
+
.todo li { list-style:none; display:flex; gap:14px; align-items:baseline; padding:12px 22px;
|
|
82
|
+
border-bottom:1px solid var(--line); font-size:15px; }
|
|
83
|
+
.todo li:last-child { border-bottom:none; }
|
|
84
|
+
.todo .no { display:inline-flex; align-items:center; justify-content:center; width:24px; height:24px;
|
|
85
|
+
border-radius:50%; background:var(--mint); color:var(--green-deep); font-weight:800;
|
|
86
|
+
font-size:13px; flex-shrink:0; transform:translateY(4px); }
|
|
87
|
+
.todo .why { color:var(--muted); font-size:13px; }
|
|
88
|
+
.card { background:var(--card); border-radius:18px; box-shadow:var(--shadow); padding:22px 24px;
|
|
89
|
+
margin-bottom:16px; page-break-inside:avoid; }
|
|
90
|
+
.card-top { display:flex; flex-wrap:wrap; align-items:center; gap:8px 10px; margin-bottom:12px; }
|
|
91
|
+
.rank { font-weight:800; color:#b8c6bf; font-size:14px; }
|
|
92
|
+
.nick { font-weight:700; font-size:16px; }
|
|
93
|
+
.badge { font-size:12px; padding:3px 11px; border-radius:999px; white-space:nowrap;
|
|
94
|
+
background:#f2f6f3; color:#5f6f67; }
|
|
95
|
+
.badge.hi { background:#def7ec; color:#03543f; font-weight:700; }
|
|
96
|
+
.badge.mid { background:#fdf3dd; color:#8a5a0b; }
|
|
97
|
+
.quote { background:#f6faf7; border-radius:14px; padding:12px 18px; font-size:15px; margin:10px 0;
|
|
98
|
+
color:#3d4d45; }
|
|
99
|
+
.label { font-size:12px; color:var(--muted); letter-spacing:.1em; margin-top:14px; }
|
|
100
|
+
.reason { font-size:14px; margin-top:3px; }
|
|
101
|
+
.script { background:var(--mint); border-radius:14px; padding:14px 16px; font-size:14px; margin-top:5px;
|
|
102
|
+
color:var(--green-deep); display:flex; justify-content:space-between; gap:12px; align-items:flex-start; }
|
|
103
|
+
.copy { flex-shrink:0; font-size:12px; border:none; color:#fff; background:var(--green);
|
|
104
|
+
padding:6px 14px; cursor:pointer; border-radius:999px; font-weight:600; }
|
|
105
|
+
.copy:active { background:var(--green-deep); }
|
|
106
|
+
.egg { background:linear-gradient(135deg,#effaf3 0%,#e4f5ed 100%); border-radius:18px;
|
|
107
|
+
padding:24px 26px; margin-top:14px; box-shadow:var(--shadow); }
|
|
108
|
+
.egg-kicker { font-size:12px; letter-spacing:.16em; color:var(--green-deep); font-weight:700; }
|
|
109
|
+
.egg-title { font-size:18px; font-weight:800; margin:8px 0 4px; }
|
|
110
|
+
.egg-sub { font-size:13px; color:var(--muted); margin-bottom:14px; }
|
|
111
|
+
.angle { background:rgba(255,255,255,.75); border-radius:14px; padding:14px 16px; margin-bottom:10px; }
|
|
112
|
+
.angle-head { display:flex; align-items:baseline; gap:10px; flex-wrap:wrap; }
|
|
113
|
+
.angle-name { font-weight:800; font-size:15px; color:var(--green-deep); }
|
|
114
|
+
.angle-cnt { font-size:12px; color:var(--muted); }
|
|
115
|
+
.angle-why { font-size:13px; color:#3d4d45; margin-top:5px; }
|
|
116
|
+
.angle-q { font-size:12px; color:var(--muted); margin-top:6px; line-height:1.8; }
|
|
117
|
+
.egg-cta { margin-top:8px; font-size:14px; color:var(--green-deep); font-weight:600; }
|
|
118
|
+
.lock { background:#fff7ed; border:1px dashed #f0a868; border-radius:18px; padding:24px 26px; margin-top:14px;
|
|
119
|
+
text-align:center; }
|
|
120
|
+
.lock-icon { font-size:26px; }
|
|
121
|
+
.lock-title { font-size:18px; font-weight:800; color:#9a3412; margin:6px 0 4px; }
|
|
122
|
+
.lock-sub { font-size:14px; color:#7c5a3a; line-height:1.7; }
|
|
123
|
+
.lock-rows { display:flex; justify-content:center; gap:12px; flex-wrap:wrap; margin:14px 0 4px; }
|
|
124
|
+
.lock-chip { background:#fff; border:1px solid #f0d9bf; border-radius:999px; padding:6px 16px; font-size:13px; color:#9a3412; }
|
|
125
|
+
.lock-cta { display:inline-block; margin-top:14px; background:#ea7a26; color:#fff; font-weight:700;
|
|
126
|
+
padding:10px 26px; border-radius:999px; font-size:14px; }
|
|
127
|
+
.links { margin-top:12px; font-size:13px; display:flex; gap:18px; flex-wrap:wrap; }
|
|
128
|
+
.links a { color:var(--green-deep); text-decoration:none; border-bottom:1px dashed currentColor; padding-bottom:1px; }
|
|
129
|
+
.tablewrap { background:var(--card); border-radius:18px; box-shadow:var(--shadow); overflow:hidden; }
|
|
130
|
+
table { width:100%; border-collapse:collapse; font-size:14px; }
|
|
131
|
+
th,td { text-align:left; padding:12px 22px; border-bottom:1px solid var(--line); }
|
|
132
|
+
th { font-size:12px; color:var(--muted); letter-spacing:.1em; font-weight:600; background:#f7fbf8; }
|
|
133
|
+
tr:last-child td { border-bottom:none; }
|
|
134
|
+
footer { margin-top:52px; text-align:center; color:var(--muted); font-size:13px; line-height:2; }
|
|
135
|
+
@media print { body { background:#fff; } .copy { display:none; } .page { padding:0; }
|
|
136
|
+
.card,.todo,.stat,.tablewrap { box-shadow:none; border:1px solid var(--line); } }
|
|
137
|
+
`;
|
|
138
|
+
const COPY_SCRIPT = `
|
|
139
|
+
function ppxcCopy(btn) {
|
|
140
|
+
var text = btn.parentElement.querySelector("span").innerText;
|
|
141
|
+
function done() { btn.innerText = "已复制"; setTimeout(function(){ btn.innerText = "复制话术"; }, 1500); }
|
|
142
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
143
|
+
navigator.clipboard.writeText(text).then(done).catch(function(){ fallback(); });
|
|
144
|
+
} else { fallback(); }
|
|
145
|
+
function fallback() {
|
|
146
|
+
var ta = document.createElement("textarea");
|
|
147
|
+
ta.value = text; document.body.appendChild(ta); ta.select();
|
|
148
|
+
try { document.execCommand("copy"); done(); } catch (e) { /* 手动选中复制 */ }
|
|
149
|
+
document.body.removeChild(ta);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
`;
|
|
153
|
+
function renderLeadCard(lead, rank) {
|
|
154
|
+
const intentCls = INTENT_CLASSES[lead.intent] ?? "low";
|
|
155
|
+
const badges = [
|
|
156
|
+
`<span class="badge ${intentCls}">${esc(intentLabel(lead.intent))}</span>`,
|
|
157
|
+
lead.demandType ? `<span class="badge">${esc(lead.demandType)}</span>` : "",
|
|
158
|
+
typeof lead.saleScore === "number" ? `<span class="badge">销售分 ${esc(lead.saleScore)}</span>` : "",
|
|
159
|
+
lead.ipLabel ? `<span class="badge">IP ${esc(lead.ipLabel)}</span>` : "",
|
|
160
|
+
lead.fromKeyword ? `<span class="badge">搜「${esc(lead.fromKeyword)}」来的</span>` : "",
|
|
161
|
+
].filter(Boolean).join("");
|
|
162
|
+
const links = [
|
|
163
|
+
lead.profileUrl ? `<a href="${esc(lead.profileUrl)}" target="_blank">打开他的主页</a>` : "",
|
|
164
|
+
lead.fromContent ? `<a href="${esc(lead.fromContent)}" target="_blank">他评论的内容</a>` : "",
|
|
165
|
+
].filter(Boolean).join("");
|
|
166
|
+
return `
|
|
167
|
+
<div class="card">
|
|
168
|
+
<div class="card-top">
|
|
169
|
+
<span class="rank">${rank.toString().padStart(2, "0")}</span>
|
|
170
|
+
<span class="nick">${esc(lead.nickname || "(未留昵称)")}</span>
|
|
171
|
+
${badges}
|
|
172
|
+
</div>
|
|
173
|
+
<div class="quote">“${esc(lead.comment)}”</div>
|
|
174
|
+
<div class="label">为什么是潜在客户</div>
|
|
175
|
+
<div class="reason">${esc(lead.reason)}</div>
|
|
176
|
+
${lead.script ? `
|
|
177
|
+
<div class="label">建议跟进话术(可直接发)</div>
|
|
178
|
+
<div class="script"><span>${esc(lead.script)}</span><button class="copy" onclick="ppxcCopy(this)">复制话术</button></div>` : ""}
|
|
179
|
+
${links ? `<div class="links">${links}</div>` : ""}
|
|
180
|
+
</div>`;
|
|
181
|
+
}
|
|
182
|
+
function renderContentEgg(angles) {
|
|
183
|
+
if (angles.length === 0)
|
|
184
|
+
return "";
|
|
185
|
+
const items = angles
|
|
186
|
+
.map((a) => {
|
|
187
|
+
const quotes = a.quotes.length > 0
|
|
188
|
+
? `<div class="angle-q">客户原话:${a.quotes.map((q) => `“${esc(q)}”`).join(" ")}</div>`
|
|
189
|
+
: "";
|
|
190
|
+
return `
|
|
191
|
+
<div class="angle">
|
|
192
|
+
<div class="angle-head">
|
|
193
|
+
<span class="angle-name">${esc(a.angle)}</span>
|
|
194
|
+
<span class="angle-cnt">${a.count} 条评论属于「${esc(a.demandType)}」</span>
|
|
195
|
+
</div>
|
|
196
|
+
<div class="angle-why">${esc(a.rationale)}</div>
|
|
197
|
+
${quotes}
|
|
198
|
+
</div>`;
|
|
199
|
+
})
|
|
200
|
+
.join("\n");
|
|
201
|
+
return `
|
|
202
|
+
<div class="egg">
|
|
203
|
+
<div class="egg-kicker">✦ 彩蛋</div>
|
|
204
|
+
<div class="egg-title">这批评论还告诉你:下一条拍什么</div>
|
|
205
|
+
<div class="egg-sub">同样这些评论,藏着你的下一条获客内容选题——客户用自己的话说出了想看什么。</div>
|
|
206
|
+
${items}
|
|
207
|
+
<div class="egg-cta">想直接拿脚本?对智能体说「根据这些评论帮我写下一条视频脚本」。</div>
|
|
208
|
+
</div>`;
|
|
209
|
+
}
|
|
210
|
+
function renderPaywall(input) {
|
|
211
|
+
const pw = input.paywall;
|
|
212
|
+
if (!pw || pw.lockedCount <= 0)
|
|
213
|
+
return "";
|
|
214
|
+
const intents = pw.lockedIntents;
|
|
215
|
+
const chips = intents
|
|
216
|
+
? [
|
|
217
|
+
intents.high > 0 ? `<span class="lock-chip">高意向 ${intents.high} 个</span>` : "",
|
|
218
|
+
intents.medium > 0 ? `<span class="lock-chip">中意向 ${intents.medium} 个</span>` : "",
|
|
219
|
+
intents.low > 0 ? `<span class="lock-chip">低意向 ${intents.low} 个</span>` : "",
|
|
220
|
+
].filter(Boolean).join("")
|
|
221
|
+
: "";
|
|
222
|
+
return `
|
|
223
|
+
<div class="lock">
|
|
224
|
+
<div class="lock-icon">🔒</div>
|
|
225
|
+
<div class="lock-title">还有 ${pw.lockedCount} 个潜在客户没解锁</div>
|
|
226
|
+
<div class="lock-sub">${esc(pw.unlockHint || "开通套餐后,这些客户的评论、跟进话术和主页链接全部解锁。")}</div>
|
|
227
|
+
${chips ? `<div class="lock-rows">${chips}</div>` : ""}
|
|
228
|
+
<div class="lock-cta">开通套餐 · 解锁全部客户</div>
|
|
229
|
+
</div>`;
|
|
230
|
+
}
|
|
231
|
+
function renderBattleReport(input, generatedAt = new Date()) {
|
|
232
|
+
const s = input.summary;
|
|
233
|
+
const sourceLine = input.kind === "search"
|
|
234
|
+
? `搜索词:${(input.keywords ?? []).map((k) => `「${k}」`).join(" ")}`
|
|
235
|
+
: `分析内容:${input.contentUrl ?? ""}`;
|
|
236
|
+
const sorted = [...input.leads].sort((a, b) => (b.saleScore ?? 0) - (a.saleScore ?? 0));
|
|
237
|
+
const todo = sorted.slice(0, 5);
|
|
238
|
+
const contentEgg = renderContentEgg((0, content_insights_1.deriveContentAngles)(input.leads));
|
|
239
|
+
const statBlocks = [
|
|
240
|
+
typeof s.itemsRead === "number" ? `<div class="stat"><b>${s.itemsRead}</b><span>看过的内容</span></div>` : "",
|
|
241
|
+
`<div class="stat"><b>${s.commentsAnalyzed}</b><span>读过的评论</span></div>`,
|
|
242
|
+
`<div class="stat"><b>${s.demandsFound}</b><span>潜在客户</span></div>`,
|
|
243
|
+
`<div class="stat"><b>${s.highIntentCount}</b><span>高意向</span></div>`,
|
|
244
|
+
`<div class="stat"><b>${input.savedToPool}</b><span>已存入客户池</span></div>`,
|
|
245
|
+
].filter(Boolean).join("");
|
|
246
|
+
const perKeywordTable = input.kind === "search" && (input.perKeyword?.length ?? 0) > 0
|
|
247
|
+
? `
|
|
248
|
+
<h2>每个词的成绩</h2>
|
|
249
|
+
<div class="tablewrap">
|
|
250
|
+
<table>
|
|
251
|
+
<tr><th>搜索词</th><th>看过的内容</th><th>挖到的客户</th></tr>
|
|
252
|
+
${(input.perKeyword ?? [])
|
|
253
|
+
.map((k) => k.ok
|
|
254
|
+
? `<tr><td>${esc(k.keyword)}</td><td>${k.itemsRead ?? 0}</td><td>${k.demands ?? 0}</td></tr>`
|
|
255
|
+
: `<tr><td>${esc(k.keyword)}</td><td colspan="2">这次没跑成</td></tr>`)
|
|
256
|
+
.join("\n ")}
|
|
257
|
+
</table>
|
|
258
|
+
</div>`
|
|
259
|
+
: "";
|
|
260
|
+
const todoSection = todo.length > 0
|
|
261
|
+
? `
|
|
262
|
+
<h2>行动清单 · 先跟这 ${todo.length} 个</h2>
|
|
263
|
+
<ul class="todo">
|
|
264
|
+
${todo
|
|
265
|
+
.map((l, i) => `<li><span class="no">${i + 1}</span><span><b>${esc(l.nickname || "(未留昵称)")}</b> <span class="why">${esc(rankReason(l))}</span></span></li>`)
|
|
266
|
+
.join("\n ")}
|
|
267
|
+
</ul>`
|
|
268
|
+
: "";
|
|
269
|
+
const cards = sorted.length > 0
|
|
270
|
+
? `
|
|
271
|
+
<h2>客户明细(按成交可能性排序)</h2>
|
|
272
|
+
${sorted.map((l, i) => renderLeadCard(l, i + 1)).join("\n")}`
|
|
273
|
+
: `<h2>客户明细</h2><div class="card">这次没有挖到明确的潜在客户。可以换一批搜索词再试。</div>`;
|
|
274
|
+
const dup = (input.skippedDuplicates ?? 0) > 0
|
|
275
|
+
? `(另有 ${input.skippedDuplicates} 条与客户池里已有的客户重复,没有重复入池)`
|
|
276
|
+
: "";
|
|
277
|
+
return `<!DOCTYPE html>
|
|
278
|
+
<html lang="zh-CN">
|
|
279
|
+
<head>
|
|
280
|
+
<meta charset="utf-8">
|
|
281
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
282
|
+
<title>PPXC 客户战报 · ${esc(generatedAt.toLocaleDateString("zh-CN"))}</title>
|
|
283
|
+
<style>${STYLE}</style>
|
|
284
|
+
</head>
|
|
285
|
+
<body>
|
|
286
|
+
<div class="page">
|
|
287
|
+
<header>
|
|
288
|
+
<div class="brand">PPXC · 找客户战报</div>
|
|
289
|
+
<h1>${esc(input.platformName)}找客户成果</h1>
|
|
290
|
+
<div class="meta">
|
|
291
|
+
${input.productName ? `<span>产品:${esc(input.productName)}</span>` : ""}
|
|
292
|
+
<span>${esc(sourceLine)}</span>
|
|
293
|
+
<span>生成时间:${esc(generatedAt.toLocaleString("zh-CN"))}</span>
|
|
294
|
+
</div>
|
|
295
|
+
</header>
|
|
296
|
+
<div class="stats">${statBlocks}</div>
|
|
297
|
+
${todoSection}
|
|
298
|
+
${cards}
|
|
299
|
+
${renderPaywall(input)}
|
|
300
|
+
${perKeywordTable}
|
|
301
|
+
${contentEgg}
|
|
302
|
+
<footer>
|
|
303
|
+
<div>同批客户已存入 PPXC 网页端客户池,可随时回看与标记跟进结果${dup}</div>
|
|
304
|
+
<div>PPXC 找客户小组件 v${esc(version_1.OWN_VERSION)} 生成</div>
|
|
305
|
+
</footer>
|
|
306
|
+
</div>
|
|
307
|
+
<script>${COPY_SCRIPT}</script>
|
|
308
|
+
</body>
|
|
309
|
+
</html>`;
|
|
310
|
+
}
|
|
311
|
+
function exportBattleReport(input) {
|
|
312
|
+
try {
|
|
313
|
+
const html = renderBattleReport(input);
|
|
314
|
+
const outFile = node_path_1.default.join(pickOutputDir(), `PPXC客户战报-${timestampLabel()}.html`);
|
|
315
|
+
(0, node_fs_1.writeFileSync)(outFile, html, "utf8");
|
|
316
|
+
log.info("battle report exported", { leads: input.leads.length, kind: input.kind });
|
|
317
|
+
return { ok: true, file: outFile };
|
|
318
|
+
}
|
|
319
|
+
catch (err) {
|
|
320
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
321
|
+
log.error("battle report export failed", msg);
|
|
322
|
+
return { ok: false, error: msg };
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
//# sourceMappingURL=battle-report.js.map
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.deriveContentAngles = deriveContentAngles;
|
|
4
|
+
const ANGLE_MAP = {
|
|
5
|
+
购买咨询: {
|
|
6
|
+
angle: "选购指南 / 答疑科普",
|
|
7
|
+
rationale: "很多人在纠结怎么选、能不能买,拍一条把选购标准讲清楚,直接接住这波咨询",
|
|
8
|
+
},
|
|
9
|
+
主动寻求: {
|
|
10
|
+
angle: "产品种草 / 直接推荐",
|
|
11
|
+
rationale: "有人在主动找方案、求推荐,拍一条种草内容把产品摆到他们面前",
|
|
12
|
+
},
|
|
13
|
+
痛点暴露: {
|
|
14
|
+
angle: "痛点共鸣开场",
|
|
15
|
+
rationale: "这批人正被同一个问题困扰,用他们的原话做开场钩子,最容易戳中",
|
|
16
|
+
},
|
|
17
|
+
竞品不满: {
|
|
18
|
+
angle: "对比 / 替代测评",
|
|
19
|
+
rationale: "有人对现有方案不满、在找替代,拍一条对比测评正好承接这股不满",
|
|
20
|
+
},
|
|
21
|
+
场景暴露: {
|
|
22
|
+
angle: "场景带入内容",
|
|
23
|
+
rationale: "这批人身份场景高度匹配,围绕这个具体场景拍,代入感最强",
|
|
24
|
+
},
|
|
25
|
+
隐性需求: {
|
|
26
|
+
angle: "需求挑明 / 教育型",
|
|
27
|
+
rationale: "他们的需求还很含蓄,拍一条把这个需求挑明的科普,把潜在需求变成明确需求",
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
const FALLBACK_ANGLE = {
|
|
31
|
+
angle: "话题切入内容",
|
|
32
|
+
rationale: "这批评论集中在这个话题上,可以围绕它做一条内容",
|
|
33
|
+
};
|
|
34
|
+
function trimQuote(text, max = 40) {
|
|
35
|
+
const clean = String(text ?? "").replace(/\s+/g, " ").trim();
|
|
36
|
+
return clean.length > max ? `${clean.slice(0, max)}…` : clean;
|
|
37
|
+
}
|
|
38
|
+
function deriveContentAngles(leads, topN = 3) {
|
|
39
|
+
const groups = new Map();
|
|
40
|
+
for (const lead of leads) {
|
|
41
|
+
const t = (lead.demandType || "").trim();
|
|
42
|
+
if (!t || t === "无")
|
|
43
|
+
continue;
|
|
44
|
+
const arr = groups.get(t) ?? [];
|
|
45
|
+
arr.push(lead);
|
|
46
|
+
groups.set(t, arr);
|
|
47
|
+
}
|
|
48
|
+
const angles = [];
|
|
49
|
+
for (const [demandType, items] of groups) {
|
|
50
|
+
const map = ANGLE_MAP[demandType] ?? FALLBACK_ANGLE;
|
|
51
|
+
const quotes = items
|
|
52
|
+
.map((l) => trimQuote(l.comment))
|
|
53
|
+
.filter((q) => q.length >= 4)
|
|
54
|
+
.slice(0, 3);
|
|
55
|
+
angles.push({
|
|
56
|
+
demandType,
|
|
57
|
+
count: items.length,
|
|
58
|
+
angle: map.angle,
|
|
59
|
+
rationale: map.rationale,
|
|
60
|
+
quotes,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
angles.sort((a, b) => b.count - a.count);
|
|
64
|
+
return angles.slice(0, topN);
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=content-insights.js.map
|
|
@@ -0,0 +1,79 @@
|
|
|
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.exportDiagnostics = exportDiagnostics;
|
|
7
|
+
const node_fs_1 = require("node:fs");
|
|
8
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
+
const electron_1 = require("electron");
|
|
10
|
+
const logger_1 = require("../browser/kernel/logger");
|
|
11
|
+
const config_1 = require("../backend/config");
|
|
12
|
+
const ppxc_login_window_1 = require("../backend/ppxc-login-window");
|
|
13
|
+
const version_1 = require("../version");
|
|
14
|
+
const log = logger_1.logger.scope("diagnostics");
|
|
15
|
+
const MAX_LOG_BYTES = 512 * 1024;
|
|
16
|
+
function readLogTail() {
|
|
17
|
+
try {
|
|
18
|
+
const full = (0, node_fs_1.readFileSync)(logger_1.LOG_FILE_PATH, "utf8");
|
|
19
|
+
if (full.length <= MAX_LOG_BYTES)
|
|
20
|
+
return full;
|
|
21
|
+
const tail = full.slice(-MAX_LOG_BYTES);
|
|
22
|
+
const firstNewline = tail.indexOf("\n");
|
|
23
|
+
return `(日志较长,以下为最近一段)\n${tail.slice(firstNewline + 1)}`;
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return "(没有读到日志文件)";
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function pickOutputDir() {
|
|
30
|
+
for (const name of ["desktop", "downloads"]) {
|
|
31
|
+
try {
|
|
32
|
+
const dir = electron_1.app.getPath(name);
|
|
33
|
+
if (dir)
|
|
34
|
+
return dir;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return electron_1.app.getPath("userData");
|
|
40
|
+
}
|
|
41
|
+
function timestampLabel() {
|
|
42
|
+
const d = new Date();
|
|
43
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
44
|
+
return `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}-${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`;
|
|
45
|
+
}
|
|
46
|
+
function exportDiagnostics() {
|
|
47
|
+
try {
|
|
48
|
+
const backendHost = (() => {
|
|
49
|
+
try {
|
|
50
|
+
return new URL((0, config_1.getApiBase)()).host;
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return "unknown";
|
|
54
|
+
}
|
|
55
|
+
})();
|
|
56
|
+
const header = [
|
|
57
|
+
"PPXC 找客户小组件 · 诊断信息",
|
|
58
|
+
`导出时间:${new Date().toLocaleString("zh-CN")}`,
|
|
59
|
+
`组件版本:${version_1.OWN_VERSION}`,
|
|
60
|
+
`系统:${process.platform} ${process.arch}`,
|
|
61
|
+
`内核版本:electron ${process.versions.electron} / node ${process.versions.node}`,
|
|
62
|
+
`后端:${backendHost}`,
|
|
63
|
+
`PPXC 登录:${(0, ppxc_login_window_1.isPpxcLoggedIn)() ? "已登录" : "未登录"}`,
|
|
64
|
+
"",
|
|
65
|
+
"──── 以下为运行日志 ────",
|
|
66
|
+
"",
|
|
67
|
+
].join("\n");
|
|
68
|
+
const outFile = node_path_1.default.join(pickOutputDir(), `PPXC诊断-${timestampLabel()}.txt`);
|
|
69
|
+
(0, node_fs_1.writeFileSync)(outFile, header + readLogTail(), "utf8");
|
|
70
|
+
log.info("diagnostics exported", { bytes: header.length });
|
|
71
|
+
return { ok: true, file: outFile };
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
75
|
+
log.error("diagnostics export failed", msg);
|
|
76
|
+
return { ok: false, error: msg };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=diagnostics.js.map
|