@gobing-ai/knowledge-kit 0.1.10 → 0.1.11
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/.claude-plugin/marketplace.json +1 -1
- package/dist/index.js +128 -25
- package/package.json +1 -1
- package/plugins/kk/plugin.json +1 -1
- package/plugins/kk/scripts/daily-scheduled-run.ts +298 -0
- package/plugins/kk/scripts/daily-stages.ts +1057 -108
- package/plugins/kk/scripts/kk-workflow-stages.ts +76 -10
- package/plugins/kk/scripts/safety-artifact-check.ts +235 -0
- package/plugins/kk/workflows/kk-daily-ai-voice2.yaml +168 -121
- package/plugins/kk/workflows/kk-daily-safety.yaml +129 -50
- package/plugins/publishings/bilibili-pub/dist/index.js +2 -1
- package/plugins/publishings/bilibili-pub/dist/profile-check.js +2 -1
- package/plugins/publishings/cnblogs-pub/dist/index.js +2 -1
- package/plugins/publishings/cnblogs-pub/dist/profile-check.js +2 -1
- package/plugins/publishings/csdn-pub/dist/index.js +2 -1
- package/plugins/publishings/csdn-pub/dist/profile-check.js +2 -1
- package/plugins/publishings/infoq-pub/dist/index.js +2 -1
- package/plugins/publishings/infoq-pub/dist/profile-check.js +2 -1
- package/plugins/publishings/jike-pub/dist/index.js +2 -1
- package/plugins/publishings/jike-pub/dist/profile-check.js +2 -1
- package/plugins/publishings/juejin-pub/dist/index.js +2 -1
- package/plugins/publishings/juejin-pub/dist/profile-check.js +2 -1
- package/plugins/publishings/podcast-pub/dist/index.js +46 -8
- package/plugins/publishings/podcast-pub/src/index.ts +62 -5
- package/plugins/publishings/shipinhao-pub/dist/index.js +2 -1
- package/plugins/publishings/shipinhao-pub/dist/profile-check.js +2 -1
- package/plugins/publishings/sspai-pub/dist/index.js +2 -1
- package/plugins/publishings/sspai-pub/dist/profile-check.js +2 -1
- package/plugins/publishings/substack-pub/dist/index.js +2 -1
- package/plugins/publishings/substack-pub/dist/profile-check.js +2 -1
- package/plugins/publishings/surfdash-pub/dist/index.js +460 -52
- package/plugins/publishings/surfdash-pub/src/digest-check.ts +85 -19
- package/plugins/publishings/surfdash-pub/src/index.ts +589 -64
- package/plugins/publishings/wechatmp-pub/dist/index.js +933 -98
- package/plugins/publishings/wechatmp-pub/dist/profile-check.js +60 -3
- package/plugins/publishings/wechatmp-pub/src/flow.ts +1125 -85
- package/plugins/publishings/wechatmp-pub/src/index.ts +112 -30
- package/plugins/publishings/wechatmp-pub/src/page.ts +105 -0
- package/plugins/publishings/weibo-pub/dist/index.js +2 -1
- package/plugins/publishings/weibo-pub/dist/profile-check.js +2 -1
- package/plugins/publishings/x-pub/dist/index.js +2 -1
- package/plugins/publishings/x-pub/dist/profile-check.js +2 -1
- package/plugins/publishings/xhs-pub/dist/index.js +2 -1
- package/plugins/publishings/xhs-pub/dist/profile-check.js +2 -1
- package/plugins/publishings/xiaoyuzhou-pub/dist/index.js +2 -1
- package/plugins/publishings/xiaoyuzhou-pub/dist/profile-check.js +2 -1
package/dist/index.js
CHANGED
|
@@ -37862,9 +37862,11 @@ var init_dist6 = __esm(() => {
|
|
|
37862
37862
|
// ../../packages/publish-harness/src/bot-fs.ts
|
|
37863
37863
|
import {
|
|
37864
37864
|
chmodSync,
|
|
37865
|
+
closeSync,
|
|
37865
37866
|
copyFileSync,
|
|
37866
37867
|
lstatSync,
|
|
37867
37868
|
mkdirSync as mkdirSync4,
|
|
37869
|
+
openSync,
|
|
37868
37870
|
readdirSync as readdirSync4,
|
|
37869
37871
|
realpathSync as realpathSync5,
|
|
37870
37872
|
rmdirSync
|
|
@@ -37906,6 +37908,9 @@ function rmdir(path) {
|
|
|
37906
37908
|
function chmod(path, mode) {
|
|
37907
37909
|
chmodSync(path, mode);
|
|
37908
37910
|
}
|
|
37911
|
+
function createExclusiveFile(path) {
|
|
37912
|
+
closeSync(openSync(path, "wx", 384));
|
|
37913
|
+
}
|
|
37909
37914
|
function copyProfileEntry(src, dest, options = {}) {
|
|
37910
37915
|
const stat = lstat(src);
|
|
37911
37916
|
if (stat === null)
|
|
@@ -38790,6 +38795,8 @@ __export(exports_browser, {
|
|
|
38790
38795
|
canonicalize: () => canonicalize,
|
|
38791
38796
|
captureSessionFacts: () => captureSessionFacts,
|
|
38792
38797
|
checkChannelSession: () => checkChannelSession,
|
|
38798
|
+
chmod: () => chmod,
|
|
38799
|
+
createExclusiveFile: () => createExclusiveFile,
|
|
38793
38800
|
createKkBrowserLauncher: () => createKkBrowserLauncher,
|
|
38794
38801
|
createShadowFinder: () => createShadowFinder,
|
|
38795
38802
|
enterDelegatedLease: () => enterDelegatedLease,
|
|
@@ -38807,6 +38814,7 @@ __export(exports_browser, {
|
|
|
38807
38814
|
loginBot: () => loginBot,
|
|
38808
38815
|
logoutBot: () => logoutBot,
|
|
38809
38816
|
markdownToHtml: () => markdownToHtml,
|
|
38817
|
+
mkdirExclusive: () => mkdirExclusive,
|
|
38810
38818
|
normalizeBotDefaultUrl: () => normalizeBotDefaultUrl,
|
|
38811
38819
|
normalizeSessionExpiry: () => normalizeSessionExpiry,
|
|
38812
38820
|
normalizeSessionIdentity: () => normalizeSessionIdentity,
|
|
@@ -38821,6 +38829,7 @@ __export(exports_browser, {
|
|
|
38821
38829
|
resolveBotProfileDir: () => resolveBotProfileDir,
|
|
38822
38830
|
resolveChannelProfileDir: () => resolveChannelProfileDir,
|
|
38823
38831
|
resolveSelector: () => resolveSelector,
|
|
38832
|
+
rmdir: () => rmdir,
|
|
38824
38833
|
runProfileCli: () => runProfileCli,
|
|
38825
38834
|
uploadFile: () => uploadFile,
|
|
38826
38835
|
verifyLeaseHeld: () => verifyLeaseHeld,
|
|
@@ -39625,6 +39634,7 @@ var init_browser = __esm(() => {
|
|
|
39625
39634
|
init_dist();
|
|
39626
39635
|
init_bot();
|
|
39627
39636
|
init_bot_fs();
|
|
39637
|
+
init_bot_fs();
|
|
39628
39638
|
init_bot_transaction();
|
|
39629
39639
|
init_src();
|
|
39630
39640
|
init_profile_dir2();
|
|
@@ -48182,11 +48192,47 @@ async function prepareAction(options) {
|
|
|
48182
48192
|
failLoud2("kk.daily.prepare.failed", error);
|
|
48183
48193
|
}
|
|
48184
48194
|
}
|
|
48185
|
-
|
|
48186
|
-
|
|
48187
|
-
|
|
48188
|
-
|
|
48189
|
-
|
|
48195
|
+
var PRIMARY_PUBLISH_TARGETS = ["surfdash-pub", "podcast-pub"];
|
|
48196
|
+
function isRecord2(value) {
|
|
48197
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
48198
|
+
}
|
|
48199
|
+
function failedPrimaryTarget(target, reason) {
|
|
48200
|
+
return { target, ok: false, status: "failed", error: reason };
|
|
48201
|
+
}
|
|
48202
|
+
function readPrimaryReceipt(path, expectedTarget) {
|
|
48203
|
+
if (!existsSync5(path)) {
|
|
48204
|
+
const issue = `${expectedTarget} receipt file is missing`;
|
|
48205
|
+
return { targets: [failedPrimaryTarget(expectedTarget, issue)], issue };
|
|
48206
|
+
}
|
|
48207
|
+
let parsed;
|
|
48208
|
+
try {
|
|
48209
|
+
parsed = parseJson3(readFileSync7(path, "utf-8"));
|
|
48210
|
+
} catch {
|
|
48211
|
+
const issue = `${expectedTarget} receipt is not valid JSON`;
|
|
48212
|
+
return { targets: [failedPrimaryTarget(expectedTarget, issue)], issue };
|
|
48213
|
+
}
|
|
48214
|
+
if (!isRecord2(parsed) || !Array.isArray(parsed.targets)) {
|
|
48215
|
+
const issue = `${expectedTarget} receipt has no targets array`;
|
|
48216
|
+
return { targets: [failedPrimaryTarget(expectedTarget, issue)], issue };
|
|
48217
|
+
}
|
|
48218
|
+
const rawTargets = parsed.targets;
|
|
48219
|
+
const targets = rawTargets.filter(isRecord2);
|
|
48220
|
+
const issues = [];
|
|
48221
|
+
if (targets.length !== rawTargets.length) {
|
|
48222
|
+
issues.push(`${expectedTarget} receipt contains malformed target records`);
|
|
48223
|
+
}
|
|
48224
|
+
if (targets.length === 0) {
|
|
48225
|
+
issues.push(`${expectedTarget} receipt targets array is empty`);
|
|
48226
|
+
}
|
|
48227
|
+
const observedExpected = targets.some((entry) => entry.target === expectedTarget);
|
|
48228
|
+
if (!observedExpected) {
|
|
48229
|
+
targets.push(failedPrimaryTarget(expectedTarget, `${expectedTarget} receipt has no named target record`));
|
|
48230
|
+
issues.push(`${expectedTarget} receipt has no named target record`);
|
|
48231
|
+
}
|
|
48232
|
+
return { targets, issue: issues.length === 0 ? undefined : issues.join("; ") };
|
|
48233
|
+
}
|
|
48234
|
+
function isPublishedPrimaryTarget(entry) {
|
|
48235
|
+
return PRIMARY_PUBLISH_TARGETS.some((target) => target === entry.target) && entry.ok === true && entry.status === "published";
|
|
48190
48236
|
}
|
|
48191
48237
|
async function publishClassifyAction(options) {
|
|
48192
48238
|
try {
|
|
@@ -48196,14 +48242,36 @@ async function publishClassifyAction(options) {
|
|
|
48196
48242
|
const statusPath = join7(dir, `${runDate}_17_publish_publish-status.txt`);
|
|
48197
48243
|
const surfdash = join7(dir, `${runDate}_14_publish-surfdash_result.json`);
|
|
48198
48244
|
const podcast = join7(dir, `${runDate}_16_publish-podcast_result.json`);
|
|
48199
|
-
const
|
|
48200
|
-
const targets =
|
|
48201
|
-
const
|
|
48245
|
+
const receipts = [readPrimaryReceipt(surfdash, "surfdash-pub"), readPrimaryReceipt(podcast, "podcast-pub")];
|
|
48246
|
+
const targets = receipts.flatMap((receipt) => receipt.targets);
|
|
48247
|
+
const receiptIssues = receipts.flatMap((receipt) => receipt.issue === undefined ? [] : [receipt.issue]);
|
|
48248
|
+
const okCount = targets.filter(isPublishedPrimaryTarget).length;
|
|
48202
48249
|
const total = targets.length;
|
|
48203
|
-
const
|
|
48250
|
+
const exactPrimarySet = receiptIssues.length === 0 && targets.length === PRIMARY_PUBLISH_TARGETS.length && PRIMARY_PUBLISH_TARGETS.every((target) => targets.filter((entry) => entry.target === target).length === 1) && targets.every(isPublishedPrimaryTarget);
|
|
48251
|
+
let status = exactPrimarySet ? "full" : okCount > 0 ? "partial" : "none";
|
|
48204
48252
|
mkdirSync3(dir, { recursive: true });
|
|
48205
|
-
|
|
48206
|
-
|
|
48253
|
+
const writeClassification = (completenessError) => {
|
|
48254
|
+
const result = {
|
|
48255
|
+
ok: status === "full",
|
|
48256
|
+
targets,
|
|
48257
|
+
...receiptIssues.length === 0 ? {} : { issues: receiptIssues },
|
|
48258
|
+
...completenessError === undefined ? {} : { completenessError }
|
|
48259
|
+
};
|
|
48260
|
+
writeFileSync4(resultPath, `${JSON.stringify(result, null, 2)}`);
|
|
48261
|
+
writeFileSync4(statusPath, status);
|
|
48262
|
+
};
|
|
48263
|
+
writeClassification();
|
|
48264
|
+
const writeSummary = (missing, completenessError) => {
|
|
48265
|
+
writeFileSync4(join7(workDir, "publish-classify.summary.txt"), `${JSON.stringify({
|
|
48266
|
+
status,
|
|
48267
|
+
published: okCount,
|
|
48268
|
+
targets: total,
|
|
48269
|
+
...receiptIssues.length === 0 ? {} : { issues: receiptIssues },
|
|
48270
|
+
...completenessError === undefined ? {} : { completenessError },
|
|
48271
|
+
missing
|
|
48272
|
+
})}
|
|
48273
|
+
`);
|
|
48274
|
+
};
|
|
48207
48275
|
writeFileSync4(join7(workDir, "publish-classify.summary.txt"), `${JSON.stringify({ status, published: okCount, targets: total })}
|
|
48208
48276
|
`);
|
|
48209
48277
|
if (options.foldTail === true) {
|
|
@@ -48219,20 +48287,45 @@ async function publishClassifyAction(options) {
|
|
|
48219
48287
|
failLoud2("kk.daily.publish-classify.patch-failed", new Error(`exit ${code}`));
|
|
48220
48288
|
}
|
|
48221
48289
|
}
|
|
48222
|
-
|
|
48290
|
+
let completenessError;
|
|
48291
|
+
let completenessCode = 1;
|
|
48292
|
+
try {
|
|
48293
|
+
completenessCode = runStage(["daily-publish-completeness", workDir, runDate]);
|
|
48294
|
+
} catch (error) {
|
|
48295
|
+
completenessError = `completeness stage failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
48296
|
+
}
|
|
48223
48297
|
const completenessPath = join7(dir, `${runDate}_17_publish_completeness.json`);
|
|
48224
48298
|
let missing = [];
|
|
48225
|
-
if (
|
|
48299
|
+
if (completenessError === undefined && completenessCode !== 0) {
|
|
48300
|
+
completenessError = `completeness stage exited ${completenessCode}`;
|
|
48301
|
+
} else if (completenessError === undefined && !existsSync5(completenessPath)) {
|
|
48302
|
+
completenessError = "completeness result artifact is missing";
|
|
48303
|
+
} else if (completenessError === undefined) {
|
|
48226
48304
|
try {
|
|
48227
48305
|
const parsed = parseJson3(readFileSync7(completenessPath, "utf-8"));
|
|
48228
|
-
if (Array.isArray(parsed
|
|
48229
|
-
|
|
48306
|
+
if (!isRecord2(parsed) || !Array.isArray(parsed.missing) || !parsed.missing.every((item) => typeof item === "string")) {
|
|
48307
|
+
completenessError = "completeness result has no valid missing[] probe";
|
|
48308
|
+
} else {
|
|
48309
|
+
missing = parsed.missing;
|
|
48230
48310
|
}
|
|
48231
48311
|
} catch {
|
|
48232
|
-
|
|
48312
|
+
completenessError = "completeness result is not valid JSON";
|
|
48233
48313
|
}
|
|
48234
48314
|
}
|
|
48235
|
-
|
|
48315
|
+
if (completenessError !== undefined)
|
|
48316
|
+
missing = ["completeness"];
|
|
48317
|
+
if (completenessError !== undefined && status === "full")
|
|
48318
|
+
status = "partial";
|
|
48319
|
+
writeClassification(completenessError);
|
|
48320
|
+
writeSummary(missing, completenessError);
|
|
48321
|
+
consoleOutput.write(JSON.stringify({
|
|
48322
|
+
status,
|
|
48323
|
+
published: okCount,
|
|
48324
|
+
targets: total,
|
|
48325
|
+
missing,
|
|
48326
|
+
...receiptIssues.length === 0 ? {} : { issues: receiptIssues },
|
|
48327
|
+
...completenessError === undefined ? {} : { completenessError }
|
|
48328
|
+
}));
|
|
48236
48329
|
} catch (error) {
|
|
48237
48330
|
if (error instanceof CommanderError)
|
|
48238
48331
|
throw error;
|
|
@@ -52761,7 +52854,8 @@ async function readSucceeded(fs, receiptsDir, id) {
|
|
|
52761
52854
|
}
|
|
52762
52855
|
}
|
|
52763
52856
|
async function readLinkMap(fs, receiptsDir, rows) {
|
|
52764
|
-
const
|
|
52857
|
+
const real = {};
|
|
52858
|
+
const all = {};
|
|
52765
52859
|
for (const source of LINK_SOURCES) {
|
|
52766
52860
|
const row = rows.find((candidate) => candidate.plugin === source.plugin && candidate.tier === 0);
|
|
52767
52861
|
if (row === undefined)
|
|
@@ -52771,11 +52865,14 @@ async function readLinkMap(fs, receiptsDir, rows) {
|
|
|
52771
52865
|
continue;
|
|
52772
52866
|
try {
|
|
52773
52867
|
const receipt = JSON.parse(await fs.readFile(path));
|
|
52774
|
-
if (receipt.status
|
|
52775
|
-
|
|
52868
|
+
if (receipt.status !== "succeeded" || !receipt.url)
|
|
52869
|
+
continue;
|
|
52870
|
+
all[source.receipt] = receipt.url;
|
|
52871
|
+
if (receipt.dummy !== true)
|
|
52872
|
+
real[source.receipt] = receipt.url;
|
|
52776
52873
|
} catch {}
|
|
52777
52874
|
}
|
|
52778
|
-
return
|
|
52875
|
+
return { real, all };
|
|
52779
52876
|
}
|
|
52780
52877
|
async function sendPublishMatrix(args, deps = {}) {
|
|
52781
52878
|
const fs = deps.fs ?? createNodeFileSystem();
|
|
@@ -52826,7 +52923,7 @@ async function sendPublishMatrix(args, deps = {}) {
|
|
|
52826
52923
|
}
|
|
52827
52924
|
continue;
|
|
52828
52925
|
}
|
|
52829
|
-
const
|
|
52926
|
+
const linkMaps = tier === 0 ? { real: {}, all: {} } : await readLinkMap(fs, receiptsDir, plan.rows);
|
|
52830
52927
|
const sendRowsInput = rows.map((row) => ({
|
|
52831
52928
|
key: row.id,
|
|
52832
52929
|
plugin: row.plugin,
|
|
@@ -52851,12 +52948,13 @@ async function sendPublishMatrix(args, deps = {}) {
|
|
|
52851
52948
|
if (tier === 0)
|
|
52852
52949
|
return { payloadPath: row.payloadPath };
|
|
52853
52950
|
const raw = await fs.exists(row.payloadPath) ? await fs.readFile(row.payloadPath) : "";
|
|
52951
|
+
const links = row.mode === "dummy" ? linkMaps.all : linkMaps.real;
|
|
52854
52952
|
let resolved = raw;
|
|
52855
52953
|
for (const source of LINK_SOURCES) {
|
|
52856
52954
|
const url = links[source.receipt];
|
|
52857
52955
|
if (url === undefined)
|
|
52858
52956
|
continue;
|
|
52859
|
-
resolved = resolved.split(source.placeholder).join(url);
|
|
52957
|
+
resolved = resolved.split(source.placeholder).join(JSON.stringify(url).slice(1, -1));
|
|
52860
52958
|
}
|
|
52861
52959
|
if (LINK_SOURCES.some((source) => resolved.includes(source.placeholder))) {
|
|
52862
52960
|
return { error: "unresolved-link" };
|
|
@@ -52988,7 +53086,12 @@ async function runPublishMatrixRssGate(args, deps = {}) {
|
|
|
52988
53086
|
};
|
|
52989
53087
|
let xml;
|
|
52990
53088
|
try {
|
|
52991
|
-
const read = deps.readFeed ?? (async (url) =>
|
|
53089
|
+
const read = deps.readFeed ?? (async (url) => {
|
|
53090
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(30000) });
|
|
53091
|
+
if (!res.ok)
|
|
53092
|
+
throw new Error(`HTTP ${res.status}`);
|
|
53093
|
+
return res.text();
|
|
53094
|
+
});
|
|
52992
53095
|
xml = await read(feedUrl);
|
|
52993
53096
|
} catch (err) {
|
|
52994
53097
|
return write({
|
|
@@ -53127,7 +53230,7 @@ async function runPublishMatrixBundle(args, deps = {}) {
|
|
|
53127
53230
|
}
|
|
53128
53231
|
const hasOwnedVariant = Object.keys(variants).some((key) => key === "article" || key === "episode");
|
|
53129
53232
|
if (!hasOwnedVariant) {
|
|
53130
|
-
echoError("publish-matrix-bundle: no article/episode variant \u2014 the publication contract needs
|
|
53233
|
+
echoError("publish-matrix-bundle: no article/episode variant \u2014 the publication contract needs at least one");
|
|
53131
53234
|
return 1;
|
|
53132
53235
|
}
|
|
53133
53236
|
const articleTitle = await readVariantTitle(fs, workDir, variants);
|
package/package.json
CHANGED
package/plugins/kk/plugin.json
CHANGED
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/** Supervise one scheduled daily run and persist its terminal outcome even after a deadline. */
|
|
3
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
4
|
+
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
5
|
+
import { dirname, join, resolve } from 'node:path';
|
|
6
|
+
import { parseArgs } from 'node:util';
|
|
7
|
+
|
|
8
|
+
const { values } = parseArgs({
|
|
9
|
+
options: {
|
|
10
|
+
repo: { type: 'string', default: process.cwd() },
|
|
11
|
+
'works-dir': { type: 'string' },
|
|
12
|
+
'run-id': { type: 'string' },
|
|
13
|
+
'timeout-ms': { type: 'string', default: '13500000' },
|
|
14
|
+
receipt: { type: 'string' },
|
|
15
|
+
agent: { type: 'string', default: 'inline' },
|
|
16
|
+
'retry-agent': { type: 'string', default: 'claude' },
|
|
17
|
+
'required-channels': { type: 'string', default: 'wechat,locales,cms-resync' },
|
|
18
|
+
'wechat-live': { type: 'boolean', default: false },
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
const timeoutMs = Number(values['timeout-ms']);
|
|
22
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0) {
|
|
23
|
+
console.error('daily-scheduled-run: --timeout-ms must be a positive integer');
|
|
24
|
+
process.exit(2);
|
|
25
|
+
}
|
|
26
|
+
const repo = resolve(values.repo);
|
|
27
|
+
const now = new Date();
|
|
28
|
+
const stamp = `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, '0')}${String(now.getDate()).padStart(2, '0')}`;
|
|
29
|
+
const runId = values['run-id'] ?? `daily-ai-voice-${stamp}-${now.getTime()}`;
|
|
30
|
+
const workDir = join(resolve(values['works-dir'] ?? join(repo, '.works')), 'kk-daily-ai-voice2', stamp);
|
|
31
|
+
const receiptPath = resolve(values.receipt ?? join(workDir, 'schedule-receipt.json'));
|
|
32
|
+
const startedAt = now.toISOString();
|
|
33
|
+
mkdirSync(dirname(receiptPath), { recursive: true });
|
|
34
|
+
const vars = {
|
|
35
|
+
publish_enabled: 'true',
|
|
36
|
+
skip_review: 'true',
|
|
37
|
+
agent: values.agent,
|
|
38
|
+
agent_retry: values['retry-agent'],
|
|
39
|
+
wechat_live: values['wechat-live'] === true ? 'true' : 'false',
|
|
40
|
+
work_dir: workDir,
|
|
41
|
+
run_date: stamp,
|
|
42
|
+
};
|
|
43
|
+
writeFileSync(receiptPath, JSON.stringify({ runId, startedAt, status: 'running', workDir }, null, 2));
|
|
44
|
+
let interrupted = false;
|
|
45
|
+
let timedOut = false;
|
|
46
|
+
let spawnError = '';
|
|
47
|
+
let forceKill: ReturnType<typeof setTimeout> | undefined;
|
|
48
|
+
const child = spawn(
|
|
49
|
+
'spur',
|
|
50
|
+
[
|
|
51
|
+
'--no-logo',
|
|
52
|
+
'workflow',
|
|
53
|
+
'run',
|
|
54
|
+
'plugins/kk/workflows/kk-daily-ai-voice2.yaml',
|
|
55
|
+
'--run-id',
|
|
56
|
+
runId,
|
|
57
|
+
'--vars',
|
|
58
|
+
JSON.stringify(vars),
|
|
59
|
+
'--quiet',
|
|
60
|
+
],
|
|
61
|
+
{ cwd: repo, detached: true, stdio: 'inherit' },
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const ownedPids = new Set<number>();
|
|
65
|
+
/** Snapshot descendants before the root exits, including workers that created their own groups. */
|
|
66
|
+
function rememberDescendants(): void {
|
|
67
|
+
if (child.pid === undefined) return;
|
|
68
|
+
ownedPids.add(child.pid);
|
|
69
|
+
const snapshot = spawnSync('ps', ['-axo', 'pid=,ppid='], { encoding: 'utf-8', timeout: 5000 });
|
|
70
|
+
const rows = snapshot.stdout
|
|
71
|
+
.trim()
|
|
72
|
+
.split('\n')
|
|
73
|
+
.map((line) => line.trim().split(/\s+/).map(Number));
|
|
74
|
+
const descendants = new Set([child.pid]);
|
|
75
|
+
let added = true;
|
|
76
|
+
while (added) {
|
|
77
|
+
added = false;
|
|
78
|
+
for (const [pid, parent] of rows) {
|
|
79
|
+
if (pid !== undefined && parent !== undefined && descendants.has(parent) && !descendants.has(pid)) {
|
|
80
|
+
descendants.add(pid);
|
|
81
|
+
added = true;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
for (const pid of descendants) ownedPids.add(pid);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Stop only the recorded owned tree; process-group signals also catch late same-group forks. */
|
|
89
|
+
function signalOwned(signal: NodeJS.Signals): void {
|
|
90
|
+
if (child.pid === undefined) return;
|
|
91
|
+
rememberDescendants();
|
|
92
|
+
for (const pid of [...ownedPids].reverse()) {
|
|
93
|
+
try {
|
|
94
|
+
process.kill(-pid, signal);
|
|
95
|
+
} catch {}
|
|
96
|
+
try {
|
|
97
|
+
process.kill(pid, signal);
|
|
98
|
+
} catch {}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function terminate(): void {
|
|
103
|
+
signalOwned('SIGTERM');
|
|
104
|
+
if (forceKill === undefined) {
|
|
105
|
+
forceKill = setTimeout(() => {
|
|
106
|
+
signalOwned('SIGKILL');
|
|
107
|
+
}, 300);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const onSignal = (): void => {
|
|
111
|
+
interrupted = true;
|
|
112
|
+
terminate();
|
|
113
|
+
};
|
|
114
|
+
process.once('SIGTERM', onSignal);
|
|
115
|
+
process.once('SIGINT', onSignal);
|
|
116
|
+
const deadline = setTimeout(() => {
|
|
117
|
+
timedOut = true;
|
|
118
|
+
terminate();
|
|
119
|
+
}, timeoutMs);
|
|
120
|
+
const exitCode = await new Promise<number>((done) => {
|
|
121
|
+
child.once('error', (error) => {
|
|
122
|
+
spawnError = error.message;
|
|
123
|
+
done(1);
|
|
124
|
+
});
|
|
125
|
+
child.once('close', (code) => done(code ?? 1));
|
|
126
|
+
});
|
|
127
|
+
clearTimeout(deadline);
|
|
128
|
+
if (forceKill !== undefined) clearTimeout(forceKill);
|
|
129
|
+
// A killed shell can leave owned descendants that ignore TERM. Always finish the group before
|
|
130
|
+
// recording a terminal outcome; no detached grandchild may continue to publish after a timeout.
|
|
131
|
+
if (child.pid !== undefined && (timedOut || interrupted || exitCode !== 0)) {
|
|
132
|
+
terminate();
|
|
133
|
+
await Bun.sleep(300);
|
|
134
|
+
signalOwned('SIGKILL');
|
|
135
|
+
await Bun.sleep(100);
|
|
136
|
+
}
|
|
137
|
+
const workflowStatus = timedOut ? 'timed-out' : interrupted ? 'interrupted' : exitCode === 0 ? 'done' : 'failed';
|
|
138
|
+
const primaryPublishStatus = readOptional(join(workDir, '31-publish', `${stamp}_17_publish_publish-status.txt`));
|
|
139
|
+
const runSummary = readOptional(join(workDir, 'run-report.summary.txt'));
|
|
140
|
+
const cleanup: { command: string; exitCode: number | null }[] = [];
|
|
141
|
+
let reservationRecovery: { status: string; reason: string; released?: number } = {
|
|
142
|
+
status: 'retained',
|
|
143
|
+
reason: 'workflow completed; publication settlement owns its reservations',
|
|
144
|
+
};
|
|
145
|
+
if (workflowStatus !== 'done' || runSummary?.startsWith('no-content:')) {
|
|
146
|
+
// The caller gives the nested safety run this exact id. Unknown/already-terminal ids are
|
|
147
|
+
// harmless cleanup failures; they never override the original workflow failure.
|
|
148
|
+
if (workflowStatus !== 'done') {
|
|
149
|
+
for (const id of [`${runId}-safety`, runId]) {
|
|
150
|
+
const result = spawnSync('spur', ['workflow', 'cancel', id, '--json'], {
|
|
151
|
+
cwd: repo,
|
|
152
|
+
encoding: 'utf-8',
|
|
153
|
+
timeout: 20000,
|
|
154
|
+
});
|
|
155
|
+
cleanup.push({ command: `spur workflow cancel ${id}`, exitCode: result.status });
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
// Only a complete trace proving no channel send began permits recycling reservations. A
|
|
159
|
+
// missing receipt alone is insufficient: a remote send may have succeeded before it crashed.
|
|
160
|
+
const trace = spawnSync('spur', ['workflow', 'trace', runId, '--json'], {
|
|
161
|
+
cwd: repo,
|
|
162
|
+
encoding: 'utf-8',
|
|
163
|
+
timeout: 20000,
|
|
164
|
+
});
|
|
165
|
+
reservationRecovery = { status: 'retained', reason: 'publication state could not be established' };
|
|
166
|
+
try {
|
|
167
|
+
const parsed = JSON.parse(trace.stdout) as {
|
|
168
|
+
run?: { runId?: string; workflowName?: string; status?: string; startedAt?: string };
|
|
169
|
+
events?: { kind?: string; node?: string; status?: string }[];
|
|
170
|
+
};
|
|
171
|
+
const sendNodes = new Set([
|
|
172
|
+
'publish-surfdash',
|
|
173
|
+
'publish-podcast',
|
|
174
|
+
'xhs-publish',
|
|
175
|
+
'wechat-publish',
|
|
176
|
+
'translate-localize',
|
|
177
|
+
'translate',
|
|
178
|
+
'translate-retry',
|
|
179
|
+
'translate-sync',
|
|
180
|
+
'publish-classify',
|
|
181
|
+
]);
|
|
182
|
+
const validTrace =
|
|
183
|
+
trace.status === 0 &&
|
|
184
|
+
parsed.run?.runId === runId &&
|
|
185
|
+
parsed.run?.workflowName === 'kk-daily-ai-voice2' &&
|
|
186
|
+
parsed.run?.status === (workflowStatus === 'done' ? 'done' : 'failed') &&
|
|
187
|
+
Date.parse(parsed.run.startedAt ?? '') >= Date.parse(startedAt) - 1000 &&
|
|
188
|
+
Date.parse(parsed.run.startedAt ?? '') <= Date.now() &&
|
|
189
|
+
Array.isArray(parsed.events) &&
|
|
190
|
+
parsed.events.length > 0;
|
|
191
|
+
const sendStarted = parsed.events?.some(
|
|
192
|
+
(event) => event.kind === 'action' && sendNodes.has(event.node ?? '') && event.status !== 'skipped',
|
|
193
|
+
);
|
|
194
|
+
if (validTrace && !sendStarted) {
|
|
195
|
+
// Cancellation is durable before reuse. Require a second stable snapshot after the
|
|
196
|
+
// owned processes are gone, so an in-flight or partially finalized row cannot pass.
|
|
197
|
+
await Bun.sleep(200);
|
|
198
|
+
const secondTrace = spawnSync('spur', ['workflow', 'trace', runId, '--json'], {
|
|
199
|
+
cwd: repo,
|
|
200
|
+
encoding: 'utf-8',
|
|
201
|
+
timeout: 20000,
|
|
202
|
+
});
|
|
203
|
+
const second = JSON.parse(secondTrace.stdout) as typeof parsed;
|
|
204
|
+
if (
|
|
205
|
+
secondTrace.status !== 0 ||
|
|
206
|
+
JSON.stringify(second.run) !== JSON.stringify(parsed.run) ||
|
|
207
|
+
JSON.stringify(second.events) !== JSON.stringify(parsed.events)
|
|
208
|
+
) {
|
|
209
|
+
throw new Error('failed-run trace is not stable after cancellation');
|
|
210
|
+
}
|
|
211
|
+
const release = spawnSync('kk', ['store', 'release', '--topic', 'ai-news', '--run', runId], {
|
|
212
|
+
cwd: repo,
|
|
213
|
+
encoding: 'utf-8',
|
|
214
|
+
timeout: 20000,
|
|
215
|
+
});
|
|
216
|
+
cleanup.push({ command: 'kk store release (verified pre-publication failure)', exitCode: release.status });
|
|
217
|
+
const result = release.status === 0 ? (JSON.parse(release.stdout) as { released?: unknown }) : null;
|
|
218
|
+
reservationRecovery =
|
|
219
|
+
result !== null && typeof result.released === 'number'
|
|
220
|
+
? { status: 'released', reason: 'trace proves no channel send began', released: result.released }
|
|
221
|
+
: { status: 'retained', reason: 'reservation release did not produce a valid receipt' };
|
|
222
|
+
} else if (sendStarted) {
|
|
223
|
+
reservationRecovery = { status: 'retained', reason: 'a channel send began; preserve uncertain delivery' };
|
|
224
|
+
}
|
|
225
|
+
} catch {
|
|
226
|
+
// Unreadable/truncated evidence or an unexpected CLI shape must never cause reuse of a
|
|
227
|
+
// potentially aired story. Keep the reservation and the original failure visible.
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
const ledger = spawnSync('kk', ['ledger', 'sync', '--workflow', 'kk-daily-ai-voice2', '--work-dir', workDir], {
|
|
231
|
+
cwd: repo,
|
|
232
|
+
encoding: 'utf-8',
|
|
233
|
+
timeout: 20000,
|
|
234
|
+
});
|
|
235
|
+
cleanup.push({ command: 'kk ledger sync', exitCode: ledger.status });
|
|
236
|
+
function readOptional(path: string): string | null {
|
|
237
|
+
try {
|
|
238
|
+
return readFileSync(path, 'utf-8').trim();
|
|
239
|
+
} catch {
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
const requiredChannels = values['required-channels']
|
|
244
|
+
.split(',')
|
|
245
|
+
.map((channel) => channel.trim())
|
|
246
|
+
.filter(Boolean);
|
|
247
|
+
let channelCompleteness: { channel: string; state: string; detail?: string }[] = [];
|
|
248
|
+
try {
|
|
249
|
+
const parsed = JSON.parse(
|
|
250
|
+
readOptional(join(workDir, '31-publish', `${stamp}_17_publish_completeness.json`)) ?? 'null',
|
|
251
|
+
) as { rows?: unknown } | null;
|
|
252
|
+
if (Array.isArray(parsed?.rows)) {
|
|
253
|
+
channelCompleteness = parsed.rows.filter(
|
|
254
|
+
(row): row is { channel: string; state: string; detail?: string } =>
|
|
255
|
+
row !== null &&
|
|
256
|
+
typeof row === 'object' &&
|
|
257
|
+
typeof row.channel === 'string' &&
|
|
258
|
+
typeof row.state === 'string',
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
} catch {}
|
|
262
|
+
const missingRequiredChannels = requiredChannels.filter((channel) => {
|
|
263
|
+
const rows = channelCompleteness.filter((row) => row.channel === channel);
|
|
264
|
+
return rows.length !== 1 || rows[0]?.state !== 'done';
|
|
265
|
+
});
|
|
266
|
+
// A terminal green workflow can mean "no content" or failed delivery. Make that observable to
|
|
267
|
+
// the scheduler instead of equating the workflow's process exit with a published edition.
|
|
268
|
+
const status =
|
|
269
|
+
workflowStatus !== 'done'
|
|
270
|
+
? workflowStatus
|
|
271
|
+
: runSummary?.startsWith('no-content:')
|
|
272
|
+
? 'no-content'
|
|
273
|
+
: primaryPublishStatus === 'full' && missingRequiredChannels.length === 0
|
|
274
|
+
? 'done'
|
|
275
|
+
: 'incomplete';
|
|
276
|
+
const receipt = {
|
|
277
|
+
runId,
|
|
278
|
+
startedAt,
|
|
279
|
+
completedAt: new Date().toISOString(),
|
|
280
|
+
status,
|
|
281
|
+
exitCode: timedOut ? 124 : interrupted ? 143 : exitCode !== 0 ? exitCode : status === 'done' ? 0 : 4,
|
|
282
|
+
workDir,
|
|
283
|
+
timeoutMs,
|
|
284
|
+
agent: values.agent,
|
|
285
|
+
retryAgent: values['retry-agent'],
|
|
286
|
+
error: spawnError || null,
|
|
287
|
+
primaryPublishStatus,
|
|
288
|
+
channelCompleteness,
|
|
289
|
+
missingRequiredChannels,
|
|
290
|
+
runSummary,
|
|
291
|
+
reservationRecovery,
|
|
292
|
+
cleanup,
|
|
293
|
+
};
|
|
294
|
+
writeFileSync(receiptPath, `${JSON.stringify(receipt, null, 2)}\n`);
|
|
295
|
+
console.log(JSON.stringify(receipt));
|
|
296
|
+
process.removeListener('SIGTERM', onSignal);
|
|
297
|
+
process.removeListener('SIGINT', onSignal);
|
|
298
|
+
process.exit(receipt.exitCode);
|