@gobing-ai/knowledge-kit 0.1.13 → 0.1.14

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.13",
9
+ "version": "0.1.14",
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.13",
3
+ "version": "0.1.14",
4
4
  "description": "An ingest → create → publish content pipeline CLI (Bun).",
5
5
  "repository": {
6
6
  "type": "git",
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "kk",
3
- "version": "0.1.13",
3
+ "version": "0.1.14",
4
4
  "description": "knowledge-kit Claude Code plugin: skills, commands, subagents, hooks, and rules"
5
5
  }
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env bun
2
2
  /** Supervise one scheduled daily run and persist its terminal outcome even after a deadline. */
3
3
  import { spawn, spawnSync } from 'node:child_process';
4
- import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
4
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
5
5
  import { dirname, join, resolve } from 'node:path';
6
6
  import { parseArgs } from 'node:util';
7
7
 
@@ -17,6 +17,7 @@ const { values } = parseArgs({
17
17
  'required-channels': { type: 'string', default: 'wechat,locales,cms-resync' },
18
18
  'wechat-live': { type: 'boolean', default: false },
19
19
  workflow: { type: 'string', default: 'kk-daily-ai-voice2' },
20
+ 'generate-only': { type: 'boolean', default: false },
20
21
  window: { type: 'string' },
21
22
  'publish-preset': { type: 'string' },
22
23
  },
@@ -33,12 +34,18 @@ const runId = values['run-id'] ?? `daily-ai-voice-${stamp}-${now.getTime()}`;
33
34
  const workDir = join(resolve(values['works-dir'] ?? join(repo, '.works')), 'kk-daily-ai-voice2', stamp);
34
35
  const receiptPath = resolve(values.receipt ?? join(workDir, 'schedule-receipt.json'));
35
36
  const startedAt = now.toISOString();
37
+ mkdirSync(workDir, { recursive: true });
36
38
  mkdirSync(dirname(receiptPath), { recursive: true });
37
39
  // voice3 shares the voice2 workspace (F08); only the vars and the publish seam differ.
38
40
  const isVoice3 = values.workflow === 'kk-daily-ai-voice3';
41
+ const generateOnly = values['generate-only'] === true;
42
+ if (generateOnly && !isVoice3) {
43
+ console.error('daily-scheduled-run: --generate-only requires --workflow kk-daily-ai-voice3');
44
+ process.exit(2);
45
+ }
39
46
  const vars = isVoice3
40
47
  ? {
41
- auto_publish: 'true',
48
+ auto_publish: generateOnly ? 'false' : 'true',
42
49
  ...(values['publish-preset'] !== undefined ? { publish_preset: values['publish-preset'] } : {}),
43
50
  ...(values.window ? { window: values.window } : {}),
44
51
  skip_review: 'true',
@@ -312,6 +319,37 @@ const missingRequiredChannels = isVoice3
312
319
  const rows = channelCompleteness.filter((row) => row.channel === channel);
313
320
  return rows.length !== 1 || rows[0]?.state !== 'done';
314
321
  });
322
+ /** Generate-only receipts use the same fail-closed bundle admission as posting and validation. */
323
+ function inspectGenerationOnly(): { status: string; reason: string } {
324
+ const inspector = join(repo, 'scripts/daily-ai-news.ts');
325
+ if (!existsSync(inspector)) return { status: 'incomplete', reason: `bundle inspector not found: ${inspector}` };
326
+ // The child is terminal, but our initial receipt still says running. Mark the inspection phase so
327
+ // the shared admission check does not mistake this supervisor for a concurrent active run.
328
+ writeFileSync(receiptPath, JSON.stringify({ runId, startedAt, status: 'inspecting', workDir }, null, 2));
329
+ const probe = spawnSync(process.execPath, [inspector, 'inspect', '--work-dir', workDir, '--run-date', stamp], {
330
+ cwd: repo,
331
+ encoding: 'utf-8',
332
+ timeout: 30000,
333
+ maxBuffer: 1024 * 1024,
334
+ });
335
+ try {
336
+ const parsed = JSON.parse(probe.stdout) as { status?: unknown; reason?: unknown };
337
+ if (typeof parsed.status === 'string') {
338
+ return {
339
+ status: probe.status === 0 && parsed.status === 'complete' ? 'complete' : 'incomplete',
340
+ reason: typeof parsed.reason === 'string' ? parsed.reason : `inspector exited ${probe.status ?? 1}`,
341
+ };
342
+ }
343
+ } catch {}
344
+ return {
345
+ status: 'incomplete',
346
+ reason: probe.stderr.trim() || probe.error?.message || `bundle inspector exited ${probe.status ?? 1}`,
347
+ };
348
+ }
349
+ const generationInspection =
350
+ generateOnly && workflowStatus === 'done' && !runSummary?.startsWith('no-content:')
351
+ ? inspectGenerationOnly()
352
+ : null;
315
353
  // A terminal green workflow can mean "no content" or failed delivery. Make that observable to
316
354
  // the scheduler instead of equating the workflow's process exit with a published edition.
317
355
  // A voice3 outcome classified 'full' already proves every non-dummy row succeeded (0326 R2).
@@ -320,14 +358,20 @@ const status =
320
358
  ? workflowStatus
321
359
  : runSummary?.startsWith('no-content:')
322
360
  ? 'no-content'
323
- : primaryPublishStatus === 'full' && !(!isVoice3 && missingRequiredChannels.length > 0)
324
- ? 'done'
325
- : 'incomplete';
361
+ : generateOnly
362
+ ? generationInspection?.status === 'complete'
363
+ ? 'done'
364
+ : 'incomplete'
365
+ : primaryPublishStatus === 'full' && !(!isVoice3 && missingRequiredChannels.length > 0)
366
+ ? 'done'
367
+ : 'incomplete';
326
368
  const receipt = {
327
369
  runId,
328
370
  startedAt,
329
371
  completedAt: new Date().toISOString(),
330
372
  status,
373
+ generateOnly,
374
+ generationInspection,
331
375
  exitCode: timedOut ? 124 : interrupted ? 143 : exitCode !== 0 ? exitCode : status === 'done' ? 0 : 4,
332
376
  workDir,
333
377
  timeoutMs,
@@ -21263,7 +21263,10 @@ class WeiboBrowserTransport {
21263
21263
  }
21264
21264
  async probeCoverAttached(page, selector, baseline) {
21265
21265
  const count = await this.countImages(page, selector);
21266
- return count > baseline ? `cover-attached (${count} images)` : null;
21266
+ if (count > baseline)
21267
+ return `cover-attached (${count} images)`;
21268
+ const preview = Number(await page.evaluate(`() => document.querySelectorAll('section:not(:has(.n-modal)) .cover-preview, .cover-preview, img.cover-img').length`));
21269
+ return Number.isFinite(preview) && preview > 0 ? `cover-attached (cover-preview x${preview})` : null;
21267
21270
  }
21268
21271
  async readToasts(page) {
21269
21272
  return parseStringArray(await page.evaluate(toastTextsScript()));
@@ -21451,7 +21454,13 @@ function openCoverLibraryScript() {
21451
21454
  tab.click();
21452
21455
  }
21453
21456
  }
21454
- for (let i = 0; i < 15 && !document.querySelector('input[type="file"]'); i += 1) await sleep(200);
21457
+ for (let i = 0; i < 30 && !document.querySelector('input[type="file"]'); i += 1) await sleep(200);
21458
+ // The \u56FE\u7247\u5E93 tab lazy-loads (upload button + items mount ~5-8s after the tab click; the fixed
21459
+ // 3s window could race it, and a raced tab leaves fileInputs=0 \u2192 no cover attachment).
21460
+ for (let i = 0; i < 40; i += 1) {
21461
+ if (document.querySelector('.n-modal img, .n-modal input[type="file"]')) break;
21462
+ await sleep(250);
21463
+ }
21455
21464
  return true;
21456
21465
  })()`;
21457
21466
  }
@@ -21479,7 +21488,11 @@ function confirmCoverModalScript() {
21479
21488
  }
21480
21489
  await sleep(200);
21481
21490
  }
21482
- await sleep(1800);
21491
+ // The \u4E0B\u4E00\u6B65 opens the \u56FE\u7247\u88C1\u526A crop modal (\u4E0A\u4E00\u6B65/\u786E\u5B9A), which takes several seconds to
21492
+ // render \u2014 poll for it before clicking, then wait for the whole modal chain to close.
21493
+ for (let i = 0; i < 40 && !Array.from(document.querySelectorAll('.n-modal button')).find((b) => (b.textContent || '').trim() === '\u786E\u5B9A'); i += 1) {
21494
+ await sleep(250);
21495
+ }
21483
21496
  for (let i = 0; i < 15; i += 1) {
21484
21497
  const okBtn = Array.from(document.querySelectorAll('.n-modal button')).find(
21485
21498
  (b) => (b.textContent || '').trim() === '\u786E\u5B9A' && !b.disabled,
@@ -400,10 +400,16 @@ export class WeiboBrowserTransport implements PublishTransport {
400
400
  await page.evaluate(confirmCoverModalScript());
401
401
  }
402
402
  // A requested cover needs a fresh observed attachment, not a successful upload call.
403
- const attached = await expectOutcome(() => this.probeCoverAttached(page, bodySelector, baseline), {
404
- timeoutMs: this.stepTimeoutMs,
405
- label: 'weibo article cover attachment',
406
- });
403
+ const attached = await expectOutcome(
404
+ () =>
405
+ // Cover settles in the v5 publish-settings panel (.cover-preview) — outside the
406
+ // body editor scope the body-image count probes, so accept the panel preview too.
407
+ this.probeCoverAttached(page, bodySelector, baseline),
408
+ {
409
+ timeoutMs: this.stepTimeoutMs,
410
+ label: 'weibo article cover attachment',
411
+ },
412
+ );
407
413
  if (!attached.ok) return fail(attached.error);
408
414
  }
409
415
 
@@ -563,7 +569,15 @@ export class WeiboBrowserTransport implements PublishTransport {
563
569
 
564
570
  private async probeCoverAttached(page: Page, selector: string, baseline: number): Promise<string | null> {
565
571
  const count = await this.countImages(page, selector);
566
- return count > baseline ? `cover-attached (${count} images)` : null;
572
+ if (count > baseline) return `cover-attached (${count} images)`;
573
+ // The v5 editor routes a modal-picked cover into the publish-settings panel
574
+ // (.cover-preview / img.cover-img) — outside the body scope, so the body count never moves.
575
+ const preview = Number(
576
+ await page.evaluate(
577
+ `() => document.querySelectorAll('section:not(:has(.n-modal)) .cover-preview, .cover-preview, img.cover-img').length`,
578
+ ),
579
+ );
580
+ return Number.isFinite(preview) && preview > 0 ? `cover-attached (cover-preview x${preview})` : null;
567
581
  }
568
582
 
569
583
  /** Visible toast texts before a save, so an already-visible toast cannot satisfy the draft rail. */
@@ -812,7 +826,13 @@ function openCoverLibraryScript(): string {
812
826
  tab.click();
813
827
  }
814
828
  }
815
- for (let i = 0; i < 15 && !document.querySelector('input[type="file"]'); i += 1) await sleep(200);
829
+ for (let i = 0; i < 30 && !document.querySelector('input[type="file"]'); i += 1) await sleep(200);
830
+ // The 图片库 tab lazy-loads (upload button + items mount ~5-8s after the tab click; the fixed
831
+ // 3s window could race it, and a raced tab leaves fileInputs=0 → no cover attachment).
832
+ for (let i = 0; i < 40; i += 1) {
833
+ if (document.querySelector('.n-modal img, .n-modal input[type="file"]')) break;
834
+ await sleep(250);
835
+ }
816
836
  return true;
817
837
  })()`;
818
838
  }
@@ -842,7 +862,11 @@ function confirmCoverModalScript(): string {
842
862
  }
843
863
  await sleep(200);
844
864
  }
845
- await sleep(1800);
865
+ // The 下一步 opens the 图片裁剪 crop modal (上一步/确定), which takes several seconds to
866
+ // render — poll for it before clicking, then wait for the whole modal chain to close.
867
+ for (let i = 0; i < 40 && !Array.from(document.querySelectorAll('.n-modal button')).find((b) => (b.textContent || '').trim() === '确定'); i += 1) {
868
+ await sleep(250);
869
+ }
846
870
  for (let i = 0; i < 15; i += 1) {
847
871
  const okBtn = Array.from(document.querySelectorAll('.n-modal button')).find(
848
872
  (b) => (b.textContent || '').trim() === '确定' && !b.disabled,