@clize/clize 0.33.0 → 0.34.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.
@@ -39,6 +39,20 @@ const REFERENCE_HOSTS = new Set([
39
39
  "ahrefs.com", "hubspot.com", "cloudflare.com", "amazon.com", "google.com",
40
40
  "nvidia.com", "intel.com", "cisco.com",
41
41
  ]);
42
+ /** 独立发布者:媒体 / 评测 / 软件目录 —— 榜单不是为自家产品写的。只收有把握的;
43
+ * 带编辑部的厂商(zapier / hubspot 之类)不在这里:它们仍是卖家,归属按路径规则落 vendor,
44
+ * 「vendor 也可能收你」这层判断留给 skill,机器不替它下结论。 */
45
+ const PUBLISHER_HOSTS = new Set([
46
+ "g2.com", "capterra.com", "getapp.com", "softwareadvice.com", "trustradius.com",
47
+ "alternativeto.net", "sourceforge.net", "slashdot.org", "saasworthy.com", "producthunt.com",
48
+ "techradar.com", "pcmag.com", "zdnet.com", "cnet.com", "theverge.com", "wired.com",
49
+ "forbes.com", "techcrunch.com", "businessinsider.com", "nytimes.com", "tomsguide.com",
50
+ "techrepublic.com", "makeuseof.com", "lifewire.com", "digitaltrends.com", "engadget.com",
51
+ "mashable.com", "geekflare.com", "hackernoon.com", "cloudwards.net", "websiteplanet.com",
52
+ "toolify.ai", "futurepedia.io", "theresanaiforthat.com", "unite.ai", "aitoolreport.com",
53
+ ]);
54
+ /** 产品站上的内容栏目:榜单挂在这些路径下 = 厂商博客写的榜单。 */
55
+ const VENDOR_CONTENT_PATH_RE = /\/(blog|resources?|learn|guides?|insights|library|articles?|academy|hub|magazine|knowledge|kb)(\/|$)/i;
42
56
  /** 榜单标题:`best|top|N 个|vs|alternatives|compared` 家族。数字+Best 是最强信号。 */
43
57
  const LISTICLE_RE = /\b(\d+\s+)?(best|top|greatest)\b|\balternatives?\b|\bvs\.?\b|\bcompared\b|\bcomparison\b|\broundup\b/i;
44
58
  /** 定义型标题:`what is X` / `guide to` / `introduction` / 纯词条。 */
@@ -124,8 +138,34 @@ export function classifySerp(keyword, items) {
124
138
  const parasiteTargets = verdict === "listicle_window"
125
139
  ? classified
126
140
  .filter((it) => it.type === "listicle")
127
- .map((it) => ({ host: it.host, pos: it.pos, title: it.title }))
141
+ .map((it) => ({ host: it.host, pos: it.pos, title: it.title, owner: ownerOf(keyword, it, classified) }))
128
142
  : [];
129
143
  return { verdict, items: classified, parasiteTargets, counts };
130
144
  }
145
+ /** 榜单页归谁(见 TargetOwner)。四条机械证据,按强弱排:查询词里的品牌就是这个站(官方自己的
146
+ * 「X 的替代品」页)> 同根域在本页另占非榜单席位 > `alternative(s)` 型标题(tabledi 走查:
147
+ * `nanonets alternative` 的 9 个榜单席全是竞品自家的替代页,没有一个是独立评测)> 内容栏目路径。 */
148
+ export function ownerOf(keyword, it, page) {
149
+ if (inSet(PUBLISHER_HOSTS, it.host))
150
+ return "publisher";
151
+ const root = rootOf(it.host);
152
+ const brand = brandOf(it.host);
153
+ if (brand.length >= 4 && keyword.toLowerCase().replace(/[^a-z0-9]/g, "").includes(brand))
154
+ return "vendor";
155
+ const sellsHere = page.some((o) => o !== it && rootOf(o.host) === root && !LISTICLE_RE.test(o.title) && !inSet(UGC_HOSTS, o.host));
156
+ if (sellsHere)
157
+ return "vendor";
158
+ if (/\balternatives?\b/i.test(it.title))
159
+ return "vendor";
160
+ let path = it.url;
161
+ try {
162
+ path = new URL(it.url).pathname;
163
+ }
164
+ catch {
165
+ /* 不是完整 URL 就按原串匹配 */
166
+ }
167
+ if (VENDOR_CONTENT_PATH_RE.test(path))
168
+ return "vendor";
169
+ return "unknown";
170
+ }
131
171
  //# sourceMappingURL=seo-classify.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"seo-classify.js","sourceRoot":"","sources":["../../src/core/seo-classify.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"seo-classify.js","sourceRoot":"","sources":["../../src/core/seo-classify.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,214 @@
1
+ /** `latest` 取曝光最大的多少页。 */
2
+ export const PAGES_TOP = 20;
3
+ /** 曝光 mover(词与页共用):两窗口曝光的小值 ≤ 大值 × 此比例(腰斩 / 翻倍)。 */
4
+ export const IMPRESSIONS_MOVE_RATIO = 0.5;
5
+ /** 曝光 mover 的曝光下限(两窗口取大):十几次曝光的抖动不是塌方。 */
6
+ export const IMPRESSIONS_MOVE_MIN = 20;
7
+ export const PAGES_MOVERS_MAX = 20;
8
+ /** `unseen` 列多少页。 */
9
+ export const UNSEEN_PAGES_MAX = 20;
10
+ /** 一轮 check 最多查多少个 URL 的收录态(配额 2,000/天/property;按需挑,不全量扫)。 */
11
+ export const INSPECT_MAX = 30;
12
+ const PROBE_TIMEOUT_MS = 8000;
13
+ /** Workers 单次调用同时打开的出站连接上限是 6,再高也只是排队。 */
14
+ const PROBE_CONCURRENCY = 6;
15
+ const SITEMAP_MAX_URLS = 5000;
16
+ const SITEMAP_MAX_CHILDREN = 10;
17
+ const UA = "clize-seo-check/1 (+https://clize.ai)";
18
+ /** 比对用的 URL 归一:GSC 的 page 与 sitemap 的 loc 常差一个尾斜杠 / 主机大小写 / fragment。 */
19
+ export function normalizePageUrl(u) {
20
+ try {
21
+ const x = new URL(u.trim());
22
+ x.hash = "";
23
+ x.hostname = x.hostname.toLowerCase();
24
+ let p = x.pathname;
25
+ if (p.length > 1 && p.endsWith("/"))
26
+ p = p.slice(0, -1);
27
+ return `${x.protocol}//${x.host}${p}${x.search}`;
28
+ }
29
+ catch {
30
+ return u.trim();
31
+ }
32
+ }
33
+ /** 曝光量的 mover 判定(词与页同一把尺):腰斩 / 翻倍 / 从无到有 / 从有到无,且大的一侧 ≥ 下限。
34
+ * prev 为 null(上一窗口没这行)按 0 算 —— 调用方负责在「上一窗口整段为空」时不用这条尺。 */
35
+ export function isImpressionsMove(prev, cur) {
36
+ const a = prev ?? 0;
37
+ const hi = Math.max(a, cur);
38
+ const lo = Math.min(a, cur);
39
+ return hi >= IMPRESSIONS_MOVE_MIN && lo <= hi * IMPRESSIONS_MOVE_RATIO;
40
+ }
41
+ // ---- sitemap ----
42
+ function decodeXml(s) {
43
+ return s
44
+ .replace(/&amp;/g, "&")
45
+ .replace(/&lt;/g, "<")
46
+ .replace(/&gt;/g, ">")
47
+ .replace(/&quot;/g, '"')
48
+ .replace(/&#39;/g, "'");
49
+ }
50
+ export function parseUrlset(xml) {
51
+ const out = [];
52
+ const re = /<url\b[^>]*>([\s\S]*?)<\/url>/gi;
53
+ let m;
54
+ while ((m = re.exec(xml)) && out.length < SITEMAP_MAX_URLS) {
55
+ const block = m[1] ?? "";
56
+ const loc = /<loc>\s*([^<]+?)\s*<\/loc>/i.exec(block)?.[1];
57
+ if (!loc)
58
+ continue;
59
+ const lastmod = /<lastmod>\s*([^<]+?)\s*<\/lastmod>/i.exec(block)?.[1] ?? null;
60
+ out.push({ loc: decodeXml(loc), lastmod });
61
+ }
62
+ return out;
63
+ }
64
+ function parseIndex(xml) {
65
+ const out = [];
66
+ const re = /<sitemap\b[^>]*>([\s\S]*?)<\/sitemap>/gi;
67
+ let m;
68
+ while ((m = re.exec(xml)) && out.length < SITEMAP_MAX_CHILDREN) {
69
+ const loc = /<loc>\s*([^<]+?)\s*<\/loc>/i.exec(m[1] ?? "")?.[1];
70
+ if (loc)
71
+ out.push(decodeXml(loc));
72
+ }
73
+ return out;
74
+ }
75
+ async function fetchText(url, fetchImpl) {
76
+ try {
77
+ const res = await fetchImpl(url, {
78
+ headers: { "user-agent": UA, accept: "application/xml,text/xml,text/plain,*/*" },
79
+ signal: AbortSignal.timeout(PROBE_TIMEOUT_MS),
80
+ redirect: "follow",
81
+ });
82
+ if (!res.ok)
83
+ return null;
84
+ return await res.text();
85
+ }
86
+ catch {
87
+ return null;
88
+ }
89
+ }
90
+ /** 读站点 sitemap:先 /sitemap.xml,没有再看 robots.txt 的 `Sitemap:` 行;索引型 sitemap 展开子表。
91
+ * 任何失败都是「没读到」(urls 空、source null),不抛 —— 它只是页面面的一个来源,不该拖垮 check。 */
92
+ export async function readSitemap(domain, fetchImpl = fetch) {
93
+ const candidates = [`https://${domain}/sitemap.xml`];
94
+ const tryOne = async (url) => {
95
+ const xml = await fetchText(url, fetchImpl);
96
+ if (!xml)
97
+ return [];
98
+ let urls = parseUrlset(xml);
99
+ if (!urls.length)
100
+ for (const child of parseIndex(xml)) {
101
+ const cx = await fetchText(child, fetchImpl);
102
+ if (cx)
103
+ urls = urls.concat(parseUrlset(cx));
104
+ if (urls.length >= SITEMAP_MAX_URLS)
105
+ break;
106
+ }
107
+ return urls.slice(0, SITEMAP_MAX_URLS);
108
+ };
109
+ const first = await tryOne(candidates[0] ?? "");
110
+ if (first.length)
111
+ return { urls: first, source: candidates[0] ?? null };
112
+ const robots = await fetchText(`https://${domain}/robots.txt`, fetchImpl);
113
+ if (robots)
114
+ for (const line of robots.split(/\r?\n/)) {
115
+ const m = /^\s*sitemap:\s*(\S+)/i.exec(line);
116
+ if (m?.[1] && !candidates.includes(m[1]))
117
+ candidates.push(m[1]);
118
+ }
119
+ for (const url of candidates.slice(1, 4)) {
120
+ const urls = await tryOne(url);
121
+ if (urls.length)
122
+ return { urls, source: url };
123
+ }
124
+ return { urls: [], source: null };
125
+ }
126
+ // ---- 实况状态 ----
127
+ async function probeOne(url, fetchImpl) {
128
+ const go = async (method) => {
129
+ const res = await fetchImpl(url, {
130
+ method,
131
+ redirect: "follow",
132
+ headers: { "user-agent": UA, accept: "text/html,*/*" },
133
+ signal: AbortSignal.timeout(PROBE_TIMEOUT_MS),
134
+ });
135
+ if (method === "GET")
136
+ await res.body?.cancel().catch(() => undefined);
137
+ return res;
138
+ };
139
+ try {
140
+ let res = await go("HEAD");
141
+ // 有些源站 / CDN 对 HEAD 回 405/501,或对陌生 UA 的 HEAD 回 403 —— 用 GET 再问一次才是页的真实状态。
142
+ if (res.status === 405 || res.status === 501 || res.status === 403)
143
+ res = await go("GET");
144
+ const finalUrl = res.url && normalizePageUrl(res.url) !== normalizePageUrl(url) ? res.url : null;
145
+ return { status: res.status, finalUrl };
146
+ }
147
+ catch {
148
+ return { status: null, finalUrl: null };
149
+ }
150
+ }
151
+ /** 一批 URL 的实时 HTTP 状态(跟随跳转)。失败的是 null,不是错 —— 探不到也是一种读数。 */
152
+ export async function probeStatus(urls, fetchImpl = fetch) {
153
+ const out = new Map();
154
+ const queue = [...new Set(urls)];
155
+ const worker = async () => {
156
+ for (let u = queue.shift(); u !== undefined; u = queue.shift())
157
+ out.set(u, await probeOne(u, fetchImpl));
158
+ };
159
+ await Promise.all(Array.from({ length: Math.min(PROBE_CONCURRENCY, queue.length) }, worker));
160
+ return out;
161
+ }
162
+ // ---- 挑页(纯函数)----
163
+ /** 从两窗口的页级聚合 + sitemap 挑出三组页。状态 / 收录态字段留 null,由调用方按预算填。
164
+ * `prev` 整段为空(冷启动、补拉也空)时不出 movers:没有对照就没有「变化」,别把每页都报成 mover。 */
165
+ export function selectPages(cur, prev, sitemap) {
166
+ const row = (page) => {
167
+ const c = cur.get(page);
168
+ const p = prev.get(page);
169
+ return {
170
+ page,
171
+ clicks: c?.clicks ?? 0,
172
+ impressions: c?.impressions ?? 0,
173
+ position: c?.position ?? null,
174
+ prevClicks: p ? p.clicks : null,
175
+ prevImpressions: p ? p.impressions : null,
176
+ status: null,
177
+ finalUrl: null,
178
+ index: null,
179
+ };
180
+ };
181
+ const byPage = (x, y) => (x.page < y.page ? -1 : x.page > y.page ? 1 : 0);
182
+ const latest = [...cur.keys()]
183
+ .map(row)
184
+ .filter((r) => r.impressions > 0)
185
+ .sort((x, y) => y.impressions - x.impressions || byPage(x, y))
186
+ .slice(0, PAGES_TOP);
187
+ const movers = prev.size
188
+ ? [...new Set([...cur.keys(), ...prev.keys()])]
189
+ .map(row)
190
+ .filter((r) => isImpressionsMove(r.prevImpressions, r.impressions))
191
+ .sort((x, y) => Math.abs(y.impressions - (y.prevImpressions ?? 0)) - Math.abs(x.impressions - (x.prevImpressions ?? 0)) ||
192
+ byPage(x, y))
193
+ .slice(0, PAGES_MOVERS_MAX)
194
+ : [];
195
+ const seen = new Set([...cur.entries()].filter(([, a]) => a.impressions > 0).map(([p]) => normalizePageUrl(p)));
196
+ const dedupe = new Set();
197
+ const unseen = sitemap
198
+ .filter((u) => {
199
+ const n = normalizePageUrl(u.loc);
200
+ if (seen.has(n) || dedupe.has(n))
201
+ return false;
202
+ dedupe.add(n);
203
+ return true;
204
+ })
205
+ .sort((x, y) => {
206
+ const a = x.lastmod ?? "";
207
+ const b = y.lastmod ?? "";
208
+ return a < b ? 1 : a > b ? -1 : x.loc < y.loc ? -1 : x.loc > y.loc ? 1 : 0;
209
+ })
210
+ .slice(0, UNSEEN_PAGES_MAX)
211
+ .map((u) => ({ page: u.loc, lastmod: u.lastmod, status: null, finalUrl: null, index: null }));
212
+ return { latest, movers, unseen };
213
+ }
214
+ //# sourceMappingURL=seo-pages.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"seo-pages.js","sourceRoot":"","sources":["../../src/core/seo-pages.ts"],"names":[],"mappings":"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"}
package/dist/core/seo.js CHANGED
@@ -4,6 +4,7 @@ import { DataForSeo, KEYWORDS_PER_CALL, normalizeHost, } from "../providers/seo/
4
4
  import { classifySerp } from "./seo-classify.js";
5
5
  import { Gsc, toGscProperty } from "../providers/seo/gsc.js";
6
6
  import { readTraffic } from "./seo-traffic.js";
7
+ import { INSPECT_MAX, IMPRESSIONS_MOVE_MIN, isImpressionsMove, probeStatus, readSitemap, selectPages, } from "./seo-pages.js";
7
8
  /** 对客加价:成本 × (1 + 4) = ×5。刻意不复用 platform.billing.markupPct(0.2)——
8
9
  * 那是给域名 / 媒体等大额供给定的,套在美分级数据上会让定价失真(一次 serp 收 8 厘)。 */
9
10
  export const SEO_MARKUP_PCT = 4;
@@ -558,6 +559,9 @@ export async function serp(ctx, db, opts) {
558
559
  }
559
560
  /** `pre_emergence` 要求的最低曝光:低于此量与噪声不可分(同 GSC 新词回填的阈值口径)。 */
560
561
  const SIGNAL_MIN_IMPRESSIONS = 10;
562
+ /** `pre_emergence` 的第二条件:窗口曝光 ≥ 月量 × 此倍数 = 量表明显没跟上需求。
563
+ * 2 倍留足了「4 周窗口 vs 月量」和「en-US 量 vs 全球曝光」两种口径差,不会把正常词误标。 */
564
+ const PRE_EMERGENCE_SV_RATIO = 2;
561
565
  /** `authority_limited` 要求的曝光量级 —— 少量曝光 + 零点击只是没人搜,不是授权度问题。 */
562
566
  const AUTHORITY_LIMITED_MIN_IMPRESSIONS = 50;
563
567
  /** `authority_limited` 的位次下界(GSC 均位次)。50+ 基本落在第 5 页之后,点击天然趋零。 */
@@ -567,13 +571,18 @@ const DEMAND_NO_SURFACE_MIN_SV = 50;
567
571
  /** `signals` 里最多留几条 `demand_no_surface`。**这条形态在冷启动站上是常态不是意外**
568
572
  * ——0.29.0 发布日对 clize.ai 实测:18 条 signals 里 17 条是它,一份"每行都说同一句话"
569
573
  * 的清单不是压缩,是把词表又抄了一遍。另两个形态是真稀有,不设上限。
570
- * 被截掉的词不会消失:它们本来就全在 `rank.unseen` 里,notes 说明还有多少条。 */
574
+ * 被截掉的词不会消失:它们本来就全在 `rank.unseen` 里,notes 说明还有多少条。
575
+ * **留哪 5 条(0.34.0 改)**:按 KD 升序、同 KD 按量降序 —— 留最可打的,不是量最大的。
576
+ * 按量截会把 40,500 的品类头词顶到最前(tabledi 的 `pivot table`),那是永远赢不了的词,
577
+ * agent 只能把它从跟踪表里删掉来解围 —— 让机器的截断规则逼出删词,是规则错了。 */
571
578
  const DEMAND_NO_SURFACE_MAX = 5;
572
579
  /** 形态判定:纯函数、纯阈值,给定同样三个数字永远给同样的答案(agent 可自行复核)。 */
573
580
  export function signalForm(a) {
574
581
  if (a.position === null)
575
582
  return a.sv !== null && a.sv >= DEMAND_NO_SURFACE_MIN_SV ? "demand_no_surface" : null;
576
- if (a.sv === 0 && a.impressions >= SIGNAL_MIN_IMPRESSIONS)
583
+ if (a.sv !== null &&
584
+ a.impressions >= SIGNAL_MIN_IMPRESSIONS &&
585
+ (a.sv === 0 || a.impressions >= a.sv * PRE_EMERGENCE_SV_RATIO))
577
586
  return "pre_emergence";
578
587
  if (a.clicks === 0 &&
579
588
  a.impressions >= AUTHORITY_LIMITED_MIN_IMPRESSIONS &&
@@ -639,7 +648,7 @@ async function upsertDomain(ctx, db, domain, cfg) {
639
648
  }
640
649
  async function readKeywords(db, domain) {
641
650
  const { results } = await db
642
- .prepare("SELECT keyword, locale, sv, kd, ai_sv, cpc, source, last_refreshed_at FROM seo_keywords WHERE domain=? ORDER BY id")
651
+ .prepare("SELECT keyword, locale, sv, kd, ai_sv, cpc, source, saved_at, last_refreshed_at FROM seo_keywords WHERE domain=? ORDER BY id")
643
652
  .bind(domain)
644
653
  .all();
645
654
  return results;
@@ -884,6 +893,77 @@ async function aggregateGscPages(db, domain, from, to) {
884
893
  out.set(query, { topPage: c.topPage || null, pages: c.pages });
885
894
  return out;
886
895
  }
896
+ /** 页级聚合(一个窗口):曝光 / 点击 / 曝光加权均位次。与 `aggregateGsc` 同一批明细,按 page 而不是 query 分组。 */
897
+ async function aggregateGscByPage(db, domain, from, to) {
898
+ const { results } = await db
899
+ .prepare("SELECT page, SUM(clicks) AS clicks, SUM(impressions) AS impressions," +
900
+ " SUM(position*impressions) AS pw FROM seo_gsc_daily" +
901
+ " WHERE domain=? AND date>=? AND date<=? GROUP BY page")
902
+ .bind(domain, from, to)
903
+ .all();
904
+ const out = new Map();
905
+ for (const r of results) {
906
+ if (!r.page)
907
+ continue;
908
+ out.set(r.page, {
909
+ clicks: r.clicks,
910
+ impressions: r.impressions,
911
+ position: r.impressions ? Number((r.pw / r.impressions).toFixed(1)) : null,
912
+ });
913
+ }
914
+ return out;
915
+ }
916
+ /** 页面面(0.34.0):每页两窗口曝光 × 实况状态 × 收录态。GSC 段成功后才有(数据都在明细表里)。
917
+ * 探测与 inspection 的任何失败只降级成 note,不中断 check;收录态按预算分配:
918
+ * 塌方的 movers 先(「页丢了还是掉了」)→ sitemap 里零曝光的新页(「Google 收了没」)→ 头部页。 */
919
+ async function pagesSection(db, domain, gscClient, property, win, notes) {
920
+ const cur = await aggregateGscByPage(db, domain, win.from, win.to);
921
+ const prev = await aggregateGscByPage(db, domain, win.prevFrom, win.prevTo);
922
+ const sitemap = await readSitemap(domain);
923
+ if (!sitemap.urls.length)
924
+ notes.push("pages: no sitemap found (/sitemap.xml or a Sitemap: line in robots.txt) — pages Google was never shown cannot be listed; publish one to get index status for pages without impressions");
925
+ const face = selectPages(cur, prev, sitemap.urls);
926
+ const probe = await probeStatus([...face.latest, ...face.movers].map((r) => r.page).concat(face.unseen.map((u) => u.page)));
927
+ for (const r of [...face.latest, ...face.movers, ...face.unseen]) {
928
+ const s = probe.get(r.page);
929
+ if (s) {
930
+ r.status = s.status;
931
+ r.finalUrl = s.finalUrl;
932
+ }
933
+ }
934
+ // 收录态:按需分配的配额(INSPECT_MAX/轮),顺序 = 问题的轻重。
935
+ const order = [];
936
+ const push = (r) => {
937
+ if (!order.some((o) => o.page === r.page))
938
+ order.push(r);
939
+ };
940
+ face.movers.filter((m) => m.impressions < (m.prevImpressions ?? 0)).forEach(push);
941
+ face.unseen.forEach(push);
942
+ face.latest.forEach(push);
943
+ const queue = order.slice(0, INSPECT_MAX);
944
+ const candidates = queue.length;
945
+ let stop = null;
946
+ let done = 0;
947
+ const run = async () => {
948
+ for (let r = queue.shift(); r && !stop; r = queue.shift()) {
949
+ try {
950
+ r.index = await gscClient.inspectUrl(property, r.page);
951
+ done++;
952
+ }
953
+ catch (e) {
954
+ // 403(权限不够)/ 429(配额)是整轮性的:记一次、停手;单个 URL 的其他失败留 null 继续。
955
+ if (e instanceof ClizeError && (e.status === 403 || e.status === 429))
956
+ stop = e.message;
957
+ }
958
+ }
959
+ };
960
+ await Promise.all(Array.from({ length: Math.min(4, queue.length) }, run));
961
+ if (stop)
962
+ notes.push(`pages: index status unavailable — ${stop}`);
963
+ else if (candidates)
964
+ notes.push(`pages: index status checked for ${done} of ${order.length} candidate page(s) — collapsed movers first, then the newest sitemap URLs without impressions, then top pages (${INSPECT_MAX} per check)`);
965
+ return { ...face, sitemap: { source: sitemap.source, urls: sitemap.urls.length } };
966
+ }
887
967
  /** 位次段:词表 join GSC 明细。纯 D1 读,零上游调用。 */
888
968
  async function rankSection(db, domain, tracked, win, notes) {
889
969
  const cur = await aggregateGsc(db, domain, win.from, win.to);
@@ -892,26 +972,54 @@ async function rankSection(db, domain, tracked, win, notes) {
892
972
  const wanted = new Set(tracked.map((k) => k.keyword));
893
973
  const series = await weeklySeries(db, domain, wanted, win.to);
894
974
  const latest = [];
975
+ const vanished = [];
895
976
  const unseen = [];
977
+ const pending = [];
896
978
  const signals = [];
897
979
  for (const k of tracked) {
898
980
  const a = cur.get(k.keyword);
981
+ const p = prev.get(k.keyword);
899
982
  if (!a) {
900
- // GSC 本窗口没见过这个词。**只列名,零成本** —— 对它做深抓只会一遍遍确认「仍未见」。
983
+ // 三种「本窗口没见」要分开(0.34.0):
984
+ // · 上一窗口见过 → 从有到无,是最重的 mover(塌方形),同时仍如实列在 unseen;
985
+ // · 词是窗口关闭之后才入表的 → 还没被测过(pending),不进 unseen、不判形态 ——
986
+ // tabledi 2026-09-02 把「窗口早于上线的零」读成「靶打偏」,就是这一步没分开;
987
+ // · 其余 → unseen(只列名,零成本)+ demand_no_surface 形态照旧。
988
+ if (p) {
989
+ vanished.push({
990
+ keyword: k.keyword,
991
+ position: null,
992
+ clicks: 0,
993
+ impressions: 0,
994
+ prevImpressions: p.impressions,
995
+ prevClicks: p.clicks,
996
+ topPage: null,
997
+ pages: 0,
998
+ delta: null,
999
+ series: series.get(k.keyword) ?? [],
1000
+ sv: k.sv,
1001
+ kd: k.kd,
1002
+ form: null,
1003
+ });
1004
+ }
1005
+ else if (k.saved_at.slice(0, 10) > win.to) {
1006
+ pending.push({ keyword: k.keyword, addedAt: k.saved_at.slice(0, 10) });
1007
+ continue;
1008
+ }
901
1009
  unseen.push(k.keyword);
902
- // 但"量表说有需求 × Google 一次曝光都没给"本身是个形态,不该只当空行。
903
1010
  const form = signalForm({ sv: k.sv, position: null, impressions: 0, clicks: 0 });
904
1011
  if (form)
905
1012
  signals.push({ keyword: k.keyword, form, sv: k.sv, kd: k.kd, position: null, impressions: 0, clicks: 0 });
906
1013
  continue;
907
1014
  }
908
- const p = prev.get(k.keyword);
909
1015
  const form = signalForm({ sv: k.sv, position: a.position, impressions: a.impressions, clicks: a.clicks });
910
1016
  latest.push({
911
1017
  keyword: k.keyword,
912
1018
  position: a.position,
913
1019
  clicks: a.clicks,
914
1020
  impressions: a.impressions,
1021
+ prevImpressions: p ? p.impressions : null,
1022
+ prevClicks: p ? p.clicks : null,
915
1023
  topPage: byPage.get(k.keyword)?.topPage ?? null,
916
1024
  pages: byPage.get(k.keyword)?.pages ?? 0,
917
1025
  // 位次变小 = 前进,所以 delta = 旧 - 新(正数是好消息)。上一窗口没数据 → null。
@@ -932,26 +1040,43 @@ async function rankSection(db, domain, tracked, win, notes) {
932
1040
  clicks: a.clicks,
933
1041
  });
934
1042
  }
935
- latest.sort((x, y) => x.position - y.position);
936
- const movers = latest
1043
+ latest.sort((x, y) => (x.position ?? Infinity) - (y.position ?? Infinity));
1044
+ // movers 两个轴(0.34.0):位次(±3 以内是均值抖动)与曝光量(腰斩 / 翻倍,大的一侧 ≥ 20)。
1045
+ // 只看位次是 tabledi 走查暴露的盲点:去重页三个月 3,088 曝光塌到窗口 50,delta 显示 -0.5。
1046
+ // 上一窗口整段为空时不用曝光尺(没有对照就没有「变化」,别把每个词都报成 mover)。
1047
+ const byPosition = latest
937
1048
  .filter((r) => r.delta != null && Math.abs(r.delta) >= RANK_MOVER_MIN)
938
1049
  .sort((x, y) => Math.abs(y.delta ?? 0) - Math.abs(x.delta ?? 0));
1050
+ const byImpressions = prev.size
1051
+ ? latest
1052
+ .filter((r) => !byPosition.includes(r) && isImpressionsMove(r.prevImpressions, r.impressions))
1053
+ .sort((x, y) => Math.abs(y.impressions - (y.prevImpressions ?? 0)) - Math.abs(x.impressions - (x.prevImpressions ?? 0)))
1054
+ : [];
1055
+ const gone = vanished
1056
+ .filter((r) => (r.prevImpressions ?? 0) >= IMPRESSIONS_MOVE_MIN)
1057
+ .sort((x, y) => (y.prevImpressions ?? 0) - (x.prevImpressions ?? 0));
1058
+ const movers = [...byPosition, ...byImpressions, ...gone];
939
1059
  // 形态排序 = 值得先看的在前:能立刻动手的 pre_emergence,再是要换打法的 authority_limited。
940
1060
  const FORM_ORDER = {
941
1061
  pre_emergence: 0,
942
1062
  authority_limited: 1,
943
1063
  demand_no_surface: 2,
944
1064
  };
945
- signals.sort((x, y) => FORM_ORDER[x.form] - FORM_ORDER[y.form] || y.impressions - x.impressions || (y.sv ?? 0) - (x.sv ?? 0));
1065
+ // demand_no_surface 内部按「最可打」排(KD 升序、同 KD 量降序);另两形态按曝光降序。
1066
+ const attackable = (x, y) => (x.kd ?? Infinity) - (y.kd ?? Infinity) || (y.sv ?? 0) - (x.sv ?? 0);
1067
+ signals.sort((x, y) => FORM_ORDER[x.form] - FORM_ORDER[y.form] ||
1068
+ (x.form === "demand_no_surface" ? attackable(x, y) : y.impressions - x.impressions || (y.sv ?? 0) - (x.sv ?? 0)));
946
1069
  // demand_no_surface 在冷启动站上是常态,留全量会把两条真稀有的形态淹掉。截断要说出来。
947
1070
  const noSurface = signals.filter((x) => x.form === "demand_no_surface");
948
1071
  if (noSurface.length > DEMAND_NO_SURFACE_MAX) {
949
1072
  const drop = new Set(noSurface.slice(DEMAND_NO_SURFACE_MAX).map((x) => x.keyword));
950
1073
  for (let i = signals.length - 1; i >= 0; i--)
951
- if (drop.has(signals[i].keyword))
1074
+ if (drop.has(signals[i]?.keyword ?? ""))
952
1075
  signals.splice(i, 1);
953
- notes.push(`signals: ${noSurface.length} keywords have demand but no impressions this window — listing the ${DEMAND_NO_SURFACE_MAX} with the most volume; the rest are in rank.unseen`);
1076
+ notes.push(`signals: ${noSurface.length} keywords have demand but no impressions this window — listing the ${DEMAND_NO_SURFACE_MAX} most attackable (lowest KD, then volume); the rest are in rank.unseen`);
954
1077
  }
1078
+ if (pending.length)
1079
+ notes.push(`rank: ${pending.length} keyword(s) were enrolled after this window closed (${win.to}) — no reading yet: they are in rank.pending, not in unseen, and take no signal form until a window that includes them`);
955
1080
  return {
956
1081
  source: "gsc",
957
1082
  window: { from: win.from, to: win.to },
@@ -959,6 +1084,7 @@ async function rankSection(db, domain, tracked, win, notes) {
959
1084
  latest,
960
1085
  movers,
961
1086
  unseen,
1087
+ pending,
962
1088
  signals,
963
1089
  };
964
1090
  }
@@ -1120,6 +1246,15 @@ async function gscSection(ctx, db, domain, conf, kwRows, notes, win, now) {
1120
1246
  " ON CONFLICT(domain, keyword, locale) DO NOTHING")
1121
1247
  .bind(domain, q.query, DEFAULT_LOCALE, "gsc_backfill", savedAt)
1122
1248
  .run();
1249
+ // 页面面:明细已落库(本窗口 + 补拉的上一窗口),这里再按 page 聚合一次 + 实况探测 + 收录态。
1250
+ // 自己失败只降级(pages: null + note),不毁掉上面已经算好的 GSC 段。
1251
+ let pages = null;
1252
+ try {
1253
+ pages = await pagesSection(db, domain, gscClient, property, win, notes);
1254
+ }
1255
+ catch (e) {
1256
+ notes.push(`pages: unavailable this round (${e instanceof Error ? e.message : String(e)})`);
1257
+ }
1123
1258
  return {
1124
1259
  property,
1125
1260
  window: { from: win.from, to: win.to },
@@ -1127,6 +1262,7 @@ async function gscSection(ctx, db, domain, conf, kwRows, notes, win, now) {
1127
1262
  impressions: byQuery.reduce((s, q) => s + q.impressions, 0),
1128
1263
  topQueries: byQuery.slice(0, 15),
1129
1264
  newQueries: fresh.map((q) => ({ query: q.query, impressions: q.impressions, position: q.position })),
1265
+ pages,
1130
1266
  };
1131
1267
  }
1132
1268
  //# sourceMappingURL=seo.js.map