@gobing-ai/knowledge-kit 0.1.1 → 0.1.2

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.
@@ -6,7 +6,7 @@
6
6
  },
7
7
  "plugins": [
8
8
  {
9
- "version": "0.1.1",
9
+ "version": "0.1.2",
10
10
  "name": "kk",
11
11
  "source": "./plugins/kk",
12
12
  "description": "knowledge-kit — a set of toolkit for knowledge management"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gobing-ai/knowledge-kit",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "An ingest → create → publish content pipeline CLI (Bun).",
5
5
  "repository": {
6
6
  "type": "git",
@@ -12395,6 +12395,27 @@ function minimumsFromEnv() {
12395
12395
  }
12396
12396
  return out;
12397
12397
  }
12398
+ async function readPublishCompleteness(path) {
12399
+ if (!path || path.trim() === "")
12400
+ return null;
12401
+ try {
12402
+ const parsed = JSON.parse(await Bun.file(path).text());
12403
+ const rows = parsed?.rows;
12404
+ if (!Array.isArray(rows) || rows.length === 0)
12405
+ return null;
12406
+ const missing = parsed.missing;
12407
+ return {
12408
+ rows: rows.filter((row) => row !== null && typeof row === "object").map((row) => ({
12409
+ channel: typeof row.channel === "string" ? row.channel : "?",
12410
+ state: typeof row.state === "string" ? row.state : "?",
12411
+ detail: typeof row.detail === "string" ? row.detail : ""
12412
+ })),
12413
+ missing: Array.isArray(missing) ? missing.filter((name) => typeof name === "string") : []
12414
+ };
12415
+ } catch {
12416
+ return null;
12417
+ }
12418
+ }
12398
12419
  async function readQcSummary(path) {
12399
12420
  if (!path || path.trim() === "")
12400
12421
  return null;
@@ -12561,6 +12582,19 @@ function renderReportMarkdown(docs, date, options = {}) {
12561
12582
  lines.push(`| ${issue.type} | ${issue.count} |`);
12562
12583
  lines.push("");
12563
12584
  }
12585
+ const completeness = options.completeness ?? null;
12586
+ if (completeness !== null) {
12587
+ lines.push("## \u53D1\u5E03\u5B8C\u6574\u6027", "");
12588
+ lines.push("| \u901A\u9053 | \u72B6\u6001 | \u8BF4\u660E |", "| --- | --- | --- |");
12589
+ for (const row of completeness.rows) {
12590
+ const detail = row.detail.replace(/\|/g, "\\|").replace(/\s+/g, " ").slice(0, 200);
12591
+ lines.push(`| ${row.channel} | ${row.state} | ${detail || "\u2014"} |`);
12592
+ }
12593
+ lines.push("");
12594
+ if (completeness.missing.length > 0) {
12595
+ lines.push(`**\u672A\u5B8C\u6210\u901A\u9053**: ${completeness.missing.join(", ")} \u2014 \u8BE6\u7EC6\u539F\u56E0\u89C1\u4E0A\u8868\uFF0C\u91CD\u8DD1\u5BF9\u5E94 stage \u53EF\u8865\u53D1\uFF08'kk stage daily-publish-<channel> \u2026'\uFF09\u3002`, "");
12596
+ }
12597
+ }
12564
12598
  const timing = options.timing ?? [];
12565
12599
  if (timing.length > 0) {
12566
12600
  lines.push("## \u6B65\u8017\u65F6", "");
@@ -12653,7 +12687,16 @@ async function processGeneratorIO(inputPath, outputPath) {
12653
12687
  const scriptSource = await readScriptSource(process.env.NEWS_REPORT_SCRIPT_FILE);
12654
12688
  const planAudit = await readPlanAudit(process.env.NEWS_REPORT_PLAN_AUDIT_FILE);
12655
12689
  const blended = await readBlendedCorpus(process.env.NEWS_REPORT_BLENDED_FILE);
12656
- const content = docsToContent(docs, dateFromEnv(), { rejected, timing, qc, scriptSource, planAudit, blended });
12690
+ const completeness = await readPublishCompleteness(process.env.NEWS_REPORT_COMPLETENESS_FILE);
12691
+ const content = docsToContent(docs, dateFromEnv(), {
12692
+ rejected,
12693
+ timing,
12694
+ qc,
12695
+ scriptSource,
12696
+ planAudit,
12697
+ blended,
12698
+ completeness
12699
+ });
12657
12700
  const fs = createNodeFileSystem();
12658
12701
  const outDir = dirname2(outputPath);
12659
12702
  if (outDir && outDir !== ".")
@@ -12692,6 +12735,7 @@ export {
12692
12735
  processGeneratorIO,
12693
12736
  readBlendedCorpus,
12694
12737
  readPlanAudit,
12738
+ readPublishCompleteness,
12695
12739
  readQcSummary,
12696
12740
  readRejectedAudit,
12697
12741
  readScriptSource,
@@ -27,6 +27,9 @@ import { echoError } from '@gobing-ai/ts-utils';
27
27
  * - `NEWS_REPORT_SCRIPT_FILE` — the script stage's Content path
28
28
  * (`24-script/<date>_08_script_content.json`, F613 R5); absent or metadata-less -> the
29
29
  * 脚本来源 line is omitted, never throw.
30
+ * - `NEWS_REPORT_COMPLETENESS_FILE` — the publish tail's channel ledger
31
+ * (`31-publish/<date>_17_publish_completeness.json`, written by
32
+ * `kk stage daily-publish-completeness`); absent -> the 发布完整性 section is omitted, never throw.
30
33
  * - `NEWS_REPORT_TIMING_FILE` — `<work_dir>/step-timing.json` JSONL of
31
34
  * {step, startedAt, endedAt} appended by the workflow shell steps; missing
32
35
  * or unreadable -> the timing section is omitted, never throw.
@@ -152,6 +155,49 @@ interface RejectedAudit {
152
155
  reasons: string[];
153
156
  }
154
157
 
158
+ /** One independent publish channel's terminal state, as the publish tail recorded it. */
159
+ export interface PublishCompletenessRow {
160
+ channel: string;
161
+ state: string;
162
+ detail: string;
163
+ }
164
+
165
+ export interface PublishCompleteness {
166
+ rows: PublishCompletenessRow[];
167
+ /** Channels that did not reach `done` — the ones the operator has to act on. */
168
+ missing: string[];
169
+ }
170
+
171
+ /**
172
+ * Read the publish tail's completeness ledger (`kk stage daily-publish-completeness`).
173
+ *
174
+ * Best-effort by contract like every other report input: absent, unreadable, malformed, or
175
+ * row-less -> `null` and the section is omitted. The ledger is the only place the independent
176
+ * channels (xhs, wechat, locales, cms-resync) surface — they never enter `publish_status` — so
177
+ * 20261016 a run that drafted nothing to WeChat/XHS reported only its scoring and QC.
178
+ */
179
+ export async function readPublishCompleteness(path: string | undefined): Promise<PublishCompleteness | null> {
180
+ if (!path || path.trim() === '') return null;
181
+ try {
182
+ const parsed: unknown = JSON.parse(await Bun.file(path).text());
183
+ const rows = (parsed as { rows?: unknown } | null)?.rows;
184
+ if (!Array.isArray(rows) || rows.length === 0) return null;
185
+ const missing = (parsed as { missing?: unknown }).missing;
186
+ return {
187
+ rows: rows
188
+ .filter((row): row is Record<string, unknown> => row !== null && typeof row === 'object')
189
+ .map((row) => ({
190
+ channel: typeof row.channel === 'string' ? row.channel : '?',
191
+ state: typeof row.state === 'string' ? row.state : '?',
192
+ detail: typeof row.detail === 'string' ? row.detail : '',
193
+ })),
194
+ missing: Array.isArray(missing) ? missing.filter((name): name is string => typeof name === 'string') : [],
195
+ };
196
+ } catch {
197
+ return null;
198
+ }
199
+ }
200
+
155
201
  /** Generate-stage audio QC summary rendered into the run report (task 0139 R5b). */
156
202
  export interface QcSummary {
157
203
  passed: boolean;
@@ -278,6 +324,7 @@ export function renderReportMarkdown(
278
324
  scriptSource?: ScriptSource | null;
279
325
  planAudit?: PlanAuditRow[] | null;
280
326
  blended?: BlendedDoc[] | null;
327
+ completeness?: PublishCompleteness | null;
281
328
  } = {},
282
329
  ): string {
283
330
  const lines: string[] = [`# 每日 AI 语音运行报告 — ${date}`, ''];
@@ -376,6 +423,26 @@ export function renderReportMarkdown(
376
423
  lines.push('');
377
424
  }
378
425
 
426
+ const completeness = options.completeness ?? null;
427
+ if (completeness !== null) {
428
+ // 20261016: the independent channels (xhs, wechat, locales, cms-resync) are non-blocking and
429
+ // never enter `publish_status`, so a run that drafted nothing to WeChat/XHS still reported
430
+ // `full`. Surface each channel's state here instead of leaving it to a separate JSON.
431
+ lines.push('## 发布完整性', '');
432
+ lines.push('| 通道 | 状态 | 说明 |', '| --- | --- | --- |');
433
+ for (const row of completeness.rows) {
434
+ const detail = row.detail.replace(/\|/g, '\\|').replace(/\s+/g, ' ').slice(0, 200);
435
+ lines.push(`| ${row.channel} | ${row.state} | ${detail || '—'} |`);
436
+ }
437
+ lines.push('');
438
+ if (completeness.missing.length > 0) {
439
+ lines.push(
440
+ `**未完成通道**: ${completeness.missing.join(', ')} — 详细原因见上表,重跑对应 stage 可补发('kk stage daily-publish-<channel> …')。`,
441
+ '',
442
+ );
443
+ }
444
+ }
445
+
379
446
  const timing = options.timing ?? [];
380
447
  if (timing.length > 0) {
381
448
  lines.push('## 步耗时', '');
@@ -498,6 +565,7 @@ export function docsToContent(
498
565
  scriptSource?: ScriptSource | null;
499
566
  planAudit?: PlanAuditRow[] | null;
500
567
  blended?: BlendedDoc[] | null;
568
+ completeness?: PublishCompleteness | null;
501
569
  },
502
570
  ): Content {
503
571
  return ContentSchema.parse({
@@ -522,7 +590,16 @@ export async function processGeneratorIO(inputPath: string, outputPath: string):
522
590
  const scriptSource = await readScriptSource(process.env.NEWS_REPORT_SCRIPT_FILE);
523
591
  const planAudit = await readPlanAudit(process.env.NEWS_REPORT_PLAN_AUDIT_FILE);
524
592
  const blended = await readBlendedCorpus(process.env.NEWS_REPORT_BLENDED_FILE);
525
- const content = docsToContent(docs, dateFromEnv(), { rejected, timing, qc, scriptSource, planAudit, blended });
593
+ const completeness = await readPublishCompleteness(process.env.NEWS_REPORT_COMPLETENESS_FILE);
594
+ const content = docsToContent(docs, dateFromEnv(), {
595
+ rejected,
596
+ timing,
597
+ qc,
598
+ scriptSource,
599
+ planAudit,
600
+ blended,
601
+ completeness,
602
+ });
526
603
  const fs = createNodeFileSystem();
527
604
  const outDir = dirname(outputPath);
528
605
  if (outDir && outDir !== '.') await fs.ensureDir(outDir);
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "kk",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "knowledge-kit Claude Code plugin: skills, commands, subagents, hooks, and rules"
5
5
  }
@@ -1509,6 +1509,14 @@ function dailyRunReport(args: string[]): void {
1509
1509
  NEWS_REPORT_BLENDED_FILE: join(absWorkDir, '10-ingest', `${runDate}_02_collect_blended.json`),
1510
1510
  // F613 R5: the script stage's Content, read best-effort (absent -> 脚本来源 omitted).
1511
1511
  NEWS_REPORT_SCRIPT_FILE: join(absWorkDir, '24-script', `${runDate}_08_script_content.json`),
1512
+ // 20261016: the independent publish channels (xhs/wechat/locales/cms-resync) never
1513
+ // enter publish_status, so the report is where an empty draft box becomes visible.
1514
+ // Written by the `daily-publish-completeness` stage that precedes this one.
1515
+ NEWS_REPORT_COMPLETENESS_FILE: join(
1516
+ absWorkDir,
1517
+ '31-publish',
1518
+ `${runDate}_17_publish_completeness.json`,
1519
+ ),
1512
1520
  NEWS_REPORT_DATE: runDate,
1513
1521
  },
1514
1522
  });
@@ -20828,6 +20828,8 @@ if (false) {}
20828
20828
  // ../../plugins/publishings/wechatmp-pub/src/profile-check.ts
20829
20829
  var USAGE = "usage: wechatmp profile-check [login|check|open] [--profile <dir>] [--url <url>] [--timeout <ms>] [--headed]";
20830
20830
  var LOGIN_TIMEOUT_MS = 180000;
20831
+ var PROFILE_OVERHEAD_MS = 8000;
20832
+ var MIN_READINESS_MS = 1000;
20831
20833
  async function main2(argv = process.argv.slice(2)) {
20832
20834
  let command;
20833
20835
  let profileDir;
@@ -20898,7 +20900,10 @@ async function main2(argv = process.argv.slice(2)) {
20898
20900
  }
20899
20901
  let verdict;
20900
20902
  try {
20901
- await checkWechatSession(profileDir, { headless: !headed, timeoutMs });
20903
+ await checkWechatSession(profileDir, {
20904
+ headless: !headed,
20905
+ timeoutMs: timeoutMs === undefined ? undefined : Math.max(MIN_READINESS_MS, timeoutMs - PROFILE_OVERHEAD_MS)
20906
+ });
20902
20907
  verdict = {
20903
20908
  status: "verified",
20904
20909
  profileDir,
@@ -19,6 +19,16 @@ import { CHANNEL, CONSOLE_URL, checkWechatSession, openWechatLogin, openWechatSe
19
19
  const USAGE =
20
20
  'usage: wechatmp profile-check [login|check|open] [--profile <dir>] [--url <url>] [--timeout <ms>] [--headed]';
21
21
  const LOGIN_TIMEOUT_MS = 180_000;
22
+ /**
23
+ * The one browser open, console navigation and context close a `check` performs around its readiness
24
+ * poll (headless ~3-4s, headed ~5-8s). `--timeout` is the WHOLE probe budget — kk SIGKILLs the process
25
+ * at that instant — so the poll must not spend all of it: a dead session then dies mid-report and
26
+ * surfaces as `probe exit 1 (malformed probe output)` instead of `refused` (observed 2026-10-16, where
27
+ * the 30s poll plus overhead overran `kk plugin check`'s 30s default).
28
+ */
29
+ const PROFILE_OVERHEAD_MS = 8_000;
30
+ /** Floor for the readiness poll, so a tiny `--timeout` still probes the console instead of refusing blind. */
31
+ const MIN_READINESS_MS = 1_000;
22
32
 
23
33
  interface WechatVerdict {
24
34
  status: 'verified' | 'refused';
@@ -106,7 +116,11 @@ export async function main(argv: string[] = process.argv.slice(2)): Promise<numb
106
116
 
107
117
  let verdict: WechatVerdict;
108
118
  try {
109
- await checkWechatSession(profileDir, { headless: !headed, timeoutMs });
119
+ await checkWechatSession(profileDir, {
120
+ headless: !headed,
121
+ timeoutMs:
122
+ timeoutMs === undefined ? undefined : Math.max(MIN_READINESS_MS, timeoutMs - PROFILE_OVERHEAD_MS),
123
+ });
110
124
  verdict = {
111
125
  status: 'verified',
112
126
  profileDir,
@@ -21269,6 +21269,10 @@ async function validateModeInput(operation, content) {
21269
21269
  }
21270
21270
 
21271
21271
  // ../../plugins/publishings/xhs-pub/src/page.ts
21272
+ var TRANSIENT_NAVIGATION = /net::ERR_(?:TIMED_OUT|CONNECTION_(?:RESET|CLOSED|TIMED_OUT|ABORTED|REFUSED|FAILED)|NETWORK_(?:CHANGED|IO_SUSPENDED)|EMPTY_RESPONSE|ADDRESS_UNREACHABLE|NAME_NOT_RESOLVED)/;
21273
+ function isTransientNavigationError(error) {
21274
+ return TRANSIENT_NAVIGATION.test(error instanceof Error ? error.message : String(error));
21275
+ }
21272
21276
  var POLL_MS = 500;
21273
21277
  var STEP_TIMEOUT_MS = 30000;
21274
21278
  var CREATOR_TAB_BAR = "div.creator-tab";
@@ -21314,7 +21318,16 @@ function wrapPlaywrightPage(raw, shadow) {
21314
21318
  return found;
21315
21319
  };
21316
21320
  return {
21317
- goto: (url, options) => pw.goto(url, options),
21321
+ goto: async (url, options) => {
21322
+ const navigate = () => pw.goto(url, { waitUntil: "domcontentloaded", ...options });
21323
+ try {
21324
+ await navigate();
21325
+ } catch (error) {
21326
+ if (!isTransientNavigationError(error))
21327
+ throw error;
21328
+ await navigate();
21329
+ }
21330
+ },
21318
21331
  locator: (selector) => ({
21319
21332
  count: () => pw.locator(selector).count(),
21320
21333
  first: () => {
@@ -20947,6 +20947,10 @@ if (false) {}
20947
20947
  var PUBLISH_URL = "https://creator.xiaohongshu.com/publish/publish";
20948
20948
 
20949
20949
  // ../../plugins/publishings/xhs-pub/src/page.ts
20950
+ var TRANSIENT_NAVIGATION = /net::ERR_(?:TIMED_OUT|CONNECTION_(?:RESET|CLOSED|TIMED_OUT|ABORTED|REFUSED|FAILED)|NETWORK_(?:CHANGED|IO_SUSPENDED)|EMPTY_RESPONSE|ADDRESS_UNREACHABLE|NAME_NOT_RESOLVED)/;
20951
+ function isTransientNavigationError(error) {
20952
+ return TRANSIENT_NAVIGATION.test(error instanceof Error ? error.message : String(error));
20953
+ }
20950
20954
  function wrapPlaywrightPage(raw, shadow) {
20951
20955
  const pw = raw;
20952
20956
  const onScreen = async (selector) => {
@@ -20968,7 +20972,16 @@ function wrapPlaywrightPage(raw, shadow) {
20968
20972
  return found;
20969
20973
  };
20970
20974
  return {
20971
- goto: (url, options) => pw.goto(url, options),
20975
+ goto: async (url, options) => {
20976
+ const navigate = () => pw.goto(url, { waitUntil: "domcontentloaded", ...options });
20977
+ try {
20978
+ await navigate();
20979
+ } catch (error) {
20980
+ if (!isTransientNavigationError(error))
20981
+ throw error;
20982
+ await navigate();
20983
+ }
20984
+ },
20972
20985
  locator: (selector) => ({
20973
20986
  count: () => pw.locator(selector).count(),
20974
20987
  first: () => {
@@ -10,6 +10,19 @@ import { waitUntil } from '@gobing-ai/publish-harness/browser';
10
10
  * Playwright page is part of this seam, never reached around it.
11
11
  */
12
12
 
13
+ /**
14
+ * Connect faults this origin produces intermittently (2026-10-16: `net::ERR_TIMED_OUT` at the
15
+ * publish page; 2026-10-13: the preflight's `Timeout 30000ms exceeded — waiting until "load"`).
16
+ * One retry absorbs the flake; a second failure is reported as it is, so a real outage still fails.
17
+ */
18
+ const TRANSIENT_NAVIGATION =
19
+ /net::ERR_(?:TIMED_OUT|CONNECTION_(?:RESET|CLOSED|TIMED_OUT|ABORTED|REFUSED|FAILED)|NETWORK_(?:CHANGED|IO_SUSPENDED)|EMPTY_RESPONSE|ADDRESS_UNREACHABLE|NAME_NOT_RESOLVED)/;
20
+
21
+ /** Whether a navigation failed for a reason a single retry can plausibly clear. */
22
+ function isTransientNavigationError(error: unknown): boolean {
23
+ return TRANSIENT_NAVIGATION.test(error instanceof Error ? error.message : String(error));
24
+ }
25
+
13
26
  export const POLL_MS = 500;
14
27
 
15
28
  // The shared poll (0184 R2); re-exported so flow/index keep importing it from here.
@@ -169,7 +182,18 @@ export function wrapPlaywrightPage(raw: unknown, shadow?: ShadowProbe): XhsPage
169
182
  };
170
183
 
171
184
  return {
172
- goto: (url, options) => pw.goto(url, options) as Promise<void>,
185
+ // `domcontentloaded`, never Playwright's `load` default: the console keeps long-lived
186
+ // subresources open, so a `load` wait turns a healthy visit into a timeout (2026-10-13
187
+ // preflight). Pinned here once, the way wechatmp-pub/src/page.ts already pins it.
188
+ goto: async (url, options) => {
189
+ const navigate = (): Promise<unknown> => pw.goto(url, { waitUntil: 'domcontentloaded', ...options });
190
+ try {
191
+ await navigate();
192
+ } catch (error) {
193
+ if (!isTransientNavigationError(error)) throw error;
194
+ await navigate();
195
+ }
196
+ },
173
197
  locator: (selector) => ({
174
198
  count: () => pw.locator(selector).count(),
175
199
  first: () => {