@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.
package/package.json
CHANGED
package/plugins/kk/plugin.json
CHANGED
|
@@ -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
|
-
:
|
|
324
|
-
? '
|
|
325
|
-
|
|
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
|
-
|
|
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 <
|
|
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
|
-
|
|
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(
|
|
404
|
-
|
|
405
|
-
|
|
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
|
-
|
|
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 <
|
|
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
|
-
|
|
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,
|