@gobing-ai/knowledge-kit 0.1.12 → 0.1.13
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 +16 -4
- package/package.json +1 -1
- package/plugins/kk/plugin.json +1 -1
- package/plugins/kk/publish-presets/daily-ai-voice-migration.json +3 -3
- package/plugins/kk/publish-presets/daily-ai-voice.json +4 -4
- package/plugins/kk/schedules/kk-daily-publish3-cn-peak.cron.tmpl +2 -2
- package/plugins/kk/scripts/daily-scheduled-run.ts +91 -41
- package/plugins/kk/scripts/daily-stages.ts +52 -9
- package/plugins/kk/skills/content-safety/references/platforms/podcast-salty.md +41 -0
- package/plugins/kk/skills/content-safety/references/platforms/surfdash.md +36 -0
- package/plugins/kk/skills/content-safety/references/platforms/xhs.md +39 -0
- package/plugins/kk/workflows/kk-collect.yaml +3 -3
- package/plugins/kk/workflows/kk-daily-ai-voice2.yaml +12 -12
- package/plugins/kk/workflows/kk-daily-ai-voice3.yaml +2 -2
- package/plugins/publishings/surfdash-pub/dist/index.js +52 -23
- package/plugins/publishings/surfdash-pub/src/index.ts +69 -24
- package/plugins/publishings/wechatmp-pub/dist/index.js +171 -30
- package/plugins/publishings/wechatmp-pub/src/flow.ts +287 -49
- package/plugins/publishings/weibo-pub/dist/index.js +100 -7
- package/plugins/publishings/weibo-pub/src/index.ts +111 -11
- package/plugins/publishings/xhs-pub/dist/index.js +164 -12
- package/plugins/publishings/xhs-pub/dist/profile-check.js +11 -1
- package/plugins/publishings/xhs-pub/src/flow.ts +194 -23
- package/plugins/publishings/xhs-pub/src/page.ts +26 -0
package/dist/index.js
CHANGED
|
@@ -42458,11 +42458,22 @@ function pluginEnvFromConfig(kind, name, configFile = resolveKkConfigPath()) {
|
|
|
42458
42458
|
if (!isPlainObject5(collectionNode)) {
|
|
42459
42459
|
return {};
|
|
42460
42460
|
}
|
|
42461
|
+
const out = {};
|
|
42462
|
+
if (kind === "publish") {
|
|
42463
|
+
for (const sibling of Object.values(collectionNode)) {
|
|
42464
|
+
if (!isPlainObject5(sibling) || !isPlainObject5(sibling.env))
|
|
42465
|
+
continue;
|
|
42466
|
+
const ch = stringifyEnvValue(sibling.env.KK_BROWSER_CHANNEL);
|
|
42467
|
+
if (ch !== undefined && ch !== "") {
|
|
42468
|
+
out.KK_BROWSER_CHANNEL = ch;
|
|
42469
|
+
break;
|
|
42470
|
+
}
|
|
42471
|
+
}
|
|
42472
|
+
}
|
|
42461
42473
|
const pluginNode = collectionNode[name];
|
|
42462
42474
|
if (!isPlainObject5(pluginNode) || !isPlainObject5(pluginNode.env)) {
|
|
42463
|
-
return
|
|
42475
|
+
return out;
|
|
42464
42476
|
}
|
|
42465
|
-
const out = {};
|
|
42466
42477
|
for (const [key, value] of Object.entries(pluginNode.env)) {
|
|
42467
42478
|
const str = stringifyEnvValue(value);
|
|
42468
42479
|
if (str === undefined || str === "") {
|
|
@@ -53755,11 +53766,12 @@ async function runPublishMatrixWaitUntil(args, deps = {}) {
|
|
|
53755
53766
|
const localHour = part("hour") % 24;
|
|
53756
53767
|
const localMinute = part("minute");
|
|
53757
53768
|
const already = localHour * 60 + localMinute;
|
|
53758
|
-
|
|
53769
|
+
const targetMinutes = hours * 60 + minutes;
|
|
53770
|
+
if (already >= targetMinutes || targetMinutes - already > 180) {
|
|
53759
53771
|
echo(`publish-matrix-wait-until: ${zone} is already past ${target}`);
|
|
53760
53772
|
return 0;
|
|
53761
53773
|
}
|
|
53762
|
-
let wait = (
|
|
53774
|
+
let wait = (targetMinutes - already) * 60000;
|
|
53763
53775
|
const delta = offsetAt(now() + wait) - offsetAt(now());
|
|
53764
53776
|
wait -= delta * 60000;
|
|
53765
53777
|
if (wait <= 0) {
|
package/package.json
CHANGED
package/plugins/kk/plugin.json
CHANGED
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"variant": "episode",
|
|
30
30
|
"tier": 1,
|
|
31
31
|
"window": "after-generate",
|
|
32
|
-
"mode": "
|
|
32
|
+
"mode": "off"
|
|
33
33
|
},
|
|
34
34
|
{
|
|
35
35
|
"id": "xhs.podcast",
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
"variant": "article",
|
|
60
60
|
"tier": 2,
|
|
61
61
|
"window": "after-generate",
|
|
62
|
-
"mode": "
|
|
62
|
+
"mode": "draft"
|
|
63
63
|
},
|
|
64
64
|
{
|
|
65
65
|
"id": "xhs.cards",
|
|
@@ -86,7 +86,7 @@
|
|
|
86
86
|
"variant": "article",
|
|
87
87
|
"tier": 2,
|
|
88
88
|
"window": "cn-peak",
|
|
89
|
-
"mode": "
|
|
89
|
+
"mode": "off"
|
|
90
90
|
},
|
|
91
91
|
{
|
|
92
92
|
"id": "x.post",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"variant": "episode",
|
|
27
27
|
"tier": 1,
|
|
28
28
|
"window": "after-generate",
|
|
29
|
-
"mode": "
|
|
29
|
+
"mode": "off"
|
|
30
30
|
},
|
|
31
31
|
{
|
|
32
32
|
"plugin": "xhs-pub",
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"variant": "article",
|
|
54
54
|
"tier": 2,
|
|
55
55
|
"window": "after-generate",
|
|
56
|
-
"mode": "
|
|
56
|
+
"mode": "draft"
|
|
57
57
|
},
|
|
58
58
|
{
|
|
59
59
|
"plugin": "xhs-pub",
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
"variant": "cards",
|
|
62
62
|
"tier": 3,
|
|
63
63
|
"window": "after-generate",
|
|
64
|
-
"mode": "
|
|
64
|
+
"mode": "draft"
|
|
65
65
|
},
|
|
66
66
|
{
|
|
67
67
|
"plugin": "weibo-pub",
|
|
@@ -77,7 +77,7 @@
|
|
|
77
77
|
"variant": "article",
|
|
78
78
|
"tier": 2,
|
|
79
79
|
"window": "cn-peak",
|
|
80
|
-
"mode": "
|
|
80
|
+
"mode": "off"
|
|
81
81
|
},
|
|
82
82
|
{
|
|
83
83
|
"plugin": "x-pub",
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
#
|
|
8
8
|
# `live_channels` is baked in: this installed line IS the operator's ADR-022 consent for a live
|
|
9
9
|
# send. Nothing in the workflow or the preset can produce `live` on its own. The window is
|
|
10
|
-
# consent-scoped to tokens with a cn-peak row (weibo-pub
|
|
10
|
+
# consent-scoped to tokens with a cn-peak row (weibo-pub). Every live launch
|
|
11
11
|
# stops at the `send-confirm` gate and waits for the operator's manual resume; the entry never
|
|
12
12
|
# delivers unattended.
|
|
13
13
|
#
|
|
@@ -27,4 +27,4 @@
|
|
|
27
27
|
#
|
|
28
28
|
# `run_date` is computed at tick time in America/Los_Angeles by the launcher: a run that starts
|
|
29
29
|
# at 17:00 PDT is still "today" for the day dir the 04:30 generation wrote.
|
|
30
|
-
0 17 * * * cd {{repo}} && mkdir -p .works/kk-daily-publish3 && PATH="{{home}}/.bun/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin" TZ=America/Los_Angeles bun {{repo}}/plugins/kk/scripts/voice3-publish-window.ts --window cn-peak --preset daily-ai-voice --live-channels weibo-pub
|
|
30
|
+
0 17 * * * cd {{repo}} && mkdir -p .works/kk-daily-publish3 && PATH="{{home}}/.bun/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin" TZ=America/Los_Angeles bun {{repo}}/plugins/kk/scripts/voice3-publish-window.ts --window cn-peak --preset daily-ai-voice --live-channels weibo-pub >> {{repo}}/.works/kk-daily-publish3/cn-peak.log 2>&1
|
|
@@ -16,6 +16,9 @@ const { values } = parseArgs({
|
|
|
16
16
|
'retry-agent': { type: 'string', default: 'claude' },
|
|
17
17
|
'required-channels': { type: 'string', default: 'wechat,locales,cms-resync' },
|
|
18
18
|
'wechat-live': { type: 'boolean', default: false },
|
|
19
|
+
workflow: { type: 'string', default: 'kk-daily-ai-voice2' },
|
|
20
|
+
window: { type: 'string' },
|
|
21
|
+
'publish-preset': { type: 'string' },
|
|
19
22
|
},
|
|
20
23
|
});
|
|
21
24
|
const timeoutMs = Number(values['timeout-ms']);
|
|
@@ -31,15 +34,28 @@ const workDir = join(resolve(values['works-dir'] ?? join(repo, '.works')), 'kk-d
|
|
|
31
34
|
const receiptPath = resolve(values.receipt ?? join(workDir, 'schedule-receipt.json'));
|
|
32
35
|
const startedAt = now.toISOString();
|
|
33
36
|
mkdirSync(dirname(receiptPath), { recursive: true });
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
37
|
+
// voice3 shares the voice2 workspace (F08); only the vars and the publish seam differ.
|
|
38
|
+
const isVoice3 = values.workflow === 'kk-daily-ai-voice3';
|
|
39
|
+
const vars = isVoice3
|
|
40
|
+
? {
|
|
41
|
+
auto_publish: 'true',
|
|
42
|
+
...(values['publish-preset'] !== undefined ? { publish_preset: values['publish-preset'] } : {}),
|
|
43
|
+
...(values.window ? { window: values.window } : {}),
|
|
44
|
+
skip_review: 'true',
|
|
45
|
+
agent: values.agent,
|
|
46
|
+
agent_retry: values['retry-agent'],
|
|
47
|
+
work_dir: workDir,
|
|
48
|
+
run_date: stamp,
|
|
49
|
+
}
|
|
50
|
+
: {
|
|
51
|
+
publish_enabled: 'true',
|
|
52
|
+
skip_review: 'true',
|
|
53
|
+
agent: values.agent,
|
|
54
|
+
agent_retry: values['retry-agent'],
|
|
55
|
+
wechat_live: values['wechat-live'] === true ? 'true' : 'false',
|
|
56
|
+
work_dir: workDir,
|
|
57
|
+
run_date: stamp,
|
|
58
|
+
};
|
|
43
59
|
writeFileSync(receiptPath, JSON.stringify({ runId, startedAt, status: 'running', workDir }, null, 2));
|
|
44
60
|
let interrupted = false;
|
|
45
61
|
let timedOut = false;
|
|
@@ -51,7 +67,7 @@ const child = spawn(
|
|
|
51
67
|
'--no-logo',
|
|
52
68
|
'workflow',
|
|
53
69
|
'run',
|
|
54
|
-
|
|
70
|
+
`plugins/kk/workflows/${values.workflow}.yaml`,
|
|
55
71
|
'--run-id',
|
|
56
72
|
runId,
|
|
57
73
|
'--vars',
|
|
@@ -135,7 +151,9 @@ if (child.pid !== undefined && (timedOut || interrupted || exitCode !== 0)) {
|
|
|
135
151
|
await Bun.sleep(100);
|
|
136
152
|
}
|
|
137
153
|
const workflowStatus = timedOut ? 'timed-out' : interrupted ? 'interrupted' : exitCode === 0 ? 'done' : 'failed';
|
|
138
|
-
const primaryPublishStatus =
|
|
154
|
+
const primaryPublishStatus = isVoice3
|
|
155
|
+
? readOptional(join(workDir, '4-publish', 'publish-status.after-generate.txt'))
|
|
156
|
+
: readOptional(join(workDir, '31-publish', `${stamp}_17_publish_publish-status.txt`));
|
|
139
157
|
const runSummary = readOptional(join(workDir, 'run-report.summary.txt'));
|
|
140
158
|
const cleanup: { command: string; exitCode: number | null }[] = [];
|
|
141
159
|
let reservationRecovery: { status: string; reason: string; released?: number } = {
|
|
@@ -168,21 +186,23 @@ if (workflowStatus !== 'done' || runSummary?.startsWith('no-content:')) {
|
|
|
168
186
|
run?: { runId?: string; workflowName?: string; status?: string; startedAt?: string };
|
|
169
187
|
events?: { kind?: string; node?: string; status?: string }[];
|
|
170
188
|
};
|
|
171
|
-
const sendNodes =
|
|
172
|
-
'publish-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
189
|
+
const sendNodes = isVoice3
|
|
190
|
+
? new Set(['publish', 'send', 'send-retry', 'repair', 'send-confirm'])
|
|
191
|
+
: new Set([
|
|
192
|
+
'publish-surfdash',
|
|
193
|
+
'publish-podcast',
|
|
194
|
+
'xhs-publish',
|
|
195
|
+
'wechat-publish',
|
|
196
|
+
'translate-localize',
|
|
197
|
+
'translate',
|
|
198
|
+
'translate-retry',
|
|
199
|
+
'translate-sync',
|
|
200
|
+
'publish-classify',
|
|
201
|
+
]);
|
|
182
202
|
const validTrace =
|
|
183
203
|
trace.status === 0 &&
|
|
184
204
|
parsed.run?.runId === runId &&
|
|
185
|
-
parsed.run?.workflowName ===
|
|
205
|
+
parsed.run?.workflowName === values.workflow &&
|
|
186
206
|
parsed.run?.status === (workflowStatus === 'done' ? 'done' : 'failed') &&
|
|
187
207
|
Date.parse(parsed.run.startedAt ?? '') >= Date.parse(startedAt) - 1000 &&
|
|
188
208
|
Date.parse(parsed.run.startedAt ?? '') <= Date.now() &&
|
|
@@ -227,7 +247,7 @@ if (workflowStatus !== 'done' || runSummary?.startsWith('no-content:')) {
|
|
|
227
247
|
// potentially aired story. Keep the reservation and the original failure visible.
|
|
228
248
|
}
|
|
229
249
|
}
|
|
230
|
-
const ledger = spawnSync('kk', ['ledger', 'sync', '--workflow',
|
|
250
|
+
const ledger = spawnSync('kk', ['ledger', 'sync', '--workflow', values.workflow, '--work-dir', workDir], {
|
|
231
251
|
cwd: repo,
|
|
232
252
|
encoding: 'utf-8',
|
|
233
253
|
timeout: 20000,
|
|
@@ -244,33 +264,63 @@ const requiredChannels = values['required-channels']
|
|
|
244
264
|
.split(',')
|
|
245
265
|
.map((channel) => channel.trim())
|
|
246
266
|
.filter(Boolean);
|
|
247
|
-
let channelCompleteness: { channel: string; state: string; detail?: string }[] =
|
|
267
|
+
let channelCompleteness: { channel: string; state: string; detail?: string; required?: boolean; dummy?: boolean }[] =
|
|
268
|
+
[];
|
|
248
269
|
try {
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
row
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
270
|
+
if (isVoice3) {
|
|
271
|
+
const outcome = JSON.parse(
|
|
272
|
+
readOptional(join(workDir, '4-publish', 'outcome.after-generate.json')) ?? 'null',
|
|
273
|
+
) as { rows?: unknown } | null;
|
|
274
|
+
if (Array.isArray(outcome?.rows)) {
|
|
275
|
+
channelCompleteness = outcome.rows.flatMap(
|
|
276
|
+
(row): { channel: string; state: string; required: boolean; dummy: boolean }[] => {
|
|
277
|
+
if (row === null || typeof row !== 'object') return [];
|
|
278
|
+
const r = row as { id?: unknown; required?: unknown; dummy?: unknown; status?: unknown };
|
|
279
|
+
if (typeof r.id !== 'string' || typeof r.status !== 'string') return [];
|
|
280
|
+
return [
|
|
281
|
+
{
|
|
282
|
+
channel: r.id,
|
|
283
|
+
state: r.status === 'succeeded' ? 'done' : r.status,
|
|
284
|
+
required: r.required === true,
|
|
285
|
+
dummy: r.dummy === true,
|
|
286
|
+
},
|
|
287
|
+
];
|
|
288
|
+
},
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
} else {
|
|
292
|
+
const parsed = JSON.parse(
|
|
293
|
+
readOptional(join(workDir, '31-publish', `${stamp}_17_publish_completeness.json`)) ?? 'null',
|
|
294
|
+
) as { rows?: unknown } | null;
|
|
295
|
+
if (Array.isArray(parsed?.rows)) {
|
|
296
|
+
channelCompleteness = parsed.rows.filter(
|
|
297
|
+
(row): row is { channel: string; state: string; detail?: string } =>
|
|
298
|
+
row !== null &&
|
|
299
|
+
typeof row === 'object' &&
|
|
300
|
+
typeof row.channel === 'string' &&
|
|
301
|
+
typeof row.state === 'string',
|
|
302
|
+
);
|
|
303
|
+
}
|
|
260
304
|
}
|
|
261
305
|
} catch {}
|
|
262
|
-
const missingRequiredChannels =
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
306
|
+
const missingRequiredChannels = isVoice3
|
|
307
|
+
? channelCompleteness
|
|
308
|
+
.filter((row) => row.required === true && row.dummy !== true)
|
|
309
|
+
.filter((row) => row.state !== 'done')
|
|
310
|
+
.map((row) => row.channel)
|
|
311
|
+
: requiredChannels.filter((channel) => {
|
|
312
|
+
const rows = channelCompleteness.filter((row) => row.channel === channel);
|
|
313
|
+
return rows.length !== 1 || rows[0]?.state !== 'done';
|
|
314
|
+
});
|
|
266
315
|
// A terminal green workflow can mean "no content" or failed delivery. Make that observable to
|
|
267
316
|
// the scheduler instead of equating the workflow's process exit with a published edition.
|
|
317
|
+
// A voice3 outcome classified 'full' already proves every non-dummy row succeeded (0326 R2).
|
|
268
318
|
const status =
|
|
269
319
|
workflowStatus !== 'done'
|
|
270
320
|
? workflowStatus
|
|
271
321
|
: runSummary?.startsWith('no-content:')
|
|
272
322
|
? 'no-content'
|
|
273
|
-
: primaryPublishStatus === 'full' && missingRequiredChannels.length
|
|
323
|
+
: primaryPublishStatus === 'full' && !(!isVoice3 && missingRequiredChannels.length > 0)
|
|
274
324
|
? 'done'
|
|
275
325
|
: 'incomplete';
|
|
276
326
|
const receipt = {
|
|
@@ -1726,7 +1726,15 @@ export function buildWechatArticleContent(
|
|
|
1726
1726
|
return {
|
|
1727
1727
|
...article,
|
|
1728
1728
|
body: options.appendOutro === false ? body : `${body}${WECHAT_OUTRO}`,
|
|
1729
|
-
metadata: {
|
|
1729
|
+
metadata: {
|
|
1730
|
+
...metadataOf(article),
|
|
1731
|
+
// wechatmp-pub's 播客 operation resolves the episode mp3 through the generic media key
|
|
1732
|
+
// `metadata.audio` (`modes.ts` resolveMediaPath), while the 文章 audio-embed rail keeps
|
|
1733
|
+
// reading `wechatmp.audioPath` — carry both spellings so one variant serves both rows.
|
|
1734
|
+
...(options.audioPath === undefined ? {} : { audio: options.audioPath }),
|
|
1735
|
+
publishStatus: options.publishStatus,
|
|
1736
|
+
wechatmp,
|
|
1737
|
+
},
|
|
1730
1738
|
};
|
|
1731
1739
|
}
|
|
1732
1740
|
|
|
@@ -1800,9 +1808,24 @@ export function buildPodcastContent(
|
|
|
1800
1808
|
*
|
|
1801
1809
|
* surfdash is the article's own publisher, so it consumes what `22-article` already composed — the
|
|
1802
1810
|
* builder exists so the variant set is uniform, not because it reshapes anything.
|
|
1811
|
+
*
|
|
1812
|
+
* surfdash-pub's `post` operation refuses a digest post without `metadata.coverImage`
|
|
1813
|
+
* (surfdash-pub/src/index.ts), so the run's cover rides along exactly as the episode/wechat
|
|
1814
|
+
* builders stamp it. voice2's prepare mode never exercised this gate; the frozen-bundle matrix
|
|
1815
|
+
* send surfaced it on 2026-11-05 (empty run → 'digest post has no cover image').
|
|
1803
1816
|
*/
|
|
1804
|
-
export function buildSurfdashContent(
|
|
1805
|
-
|
|
1817
|
+
export function buildSurfdashContent(
|
|
1818
|
+
article: Record<string, unknown>,
|
|
1819
|
+
_links: PublishLinks,
|
|
1820
|
+
options: { coverPath?: string } = {},
|
|
1821
|
+
): Record<string, unknown> {
|
|
1822
|
+
return {
|
|
1823
|
+
...article,
|
|
1824
|
+
metadata: {
|
|
1825
|
+
...metadataOf(article),
|
|
1826
|
+
...(options.coverPath === undefined ? {} : { coverImage: options.coverPath }),
|
|
1827
|
+
},
|
|
1828
|
+
};
|
|
1806
1829
|
}
|
|
1807
1830
|
|
|
1808
1831
|
/**
|
|
@@ -1933,7 +1956,7 @@ export function dailyBundleVariants(args: string[]): void {
|
|
|
1933
1956
|
const cardsPath = join(workDir, '25-xhs', 'content.json');
|
|
1934
1957
|
const coverPath = join(workDir, '23-cover', `${runDate}_07_cover_cover.png`);
|
|
1935
1958
|
const socialDir = join(workDir, '26-social');
|
|
1936
|
-
const cover = coverEnabledArg !== 'false' && isFile(coverPath) ? coverPath : undefined;
|
|
1959
|
+
const cover = coverEnabledArg !== 'false' && isFile(coverPath) ? resolve(coverPath) : undefined;
|
|
1937
1960
|
const links = PLACEHOLDER_LINKS;
|
|
1938
1961
|
const written: string[] = [];
|
|
1939
1962
|
const skipped: string[] = [];
|
|
@@ -1950,8 +1973,8 @@ export function dailyBundleVariants(args: string[]): void {
|
|
|
1950
1973
|
// shared copy must exist for the channels the preset does not specialise.
|
|
1951
1974
|
if (article === null) skipped.push('article (no 22-article content)');
|
|
1952
1975
|
else {
|
|
1953
|
-
write('article', buildSurfdashContent(article, links));
|
|
1954
|
-
write('article@surfdash-pub', buildSurfdashContent(article, links));
|
|
1976
|
+
write('article', buildSurfdashContent(article, links, { coverPath: cover }));
|
|
1977
|
+
write('article@surfdash-pub', buildSurfdashContent(article, links, { coverPath: cover }));
|
|
1955
1978
|
write('article@xhs-pub', buildXhsArticleContent(article, { publishStatus: 'draft' }));
|
|
1956
1979
|
write(
|
|
1957
1980
|
'article@wechatmp-pub',
|
|
@@ -3618,9 +3641,13 @@ export function dailySafetyPrep(args: string[]): void {
|
|
|
3618
3641
|
if (isFile(scriptPath))
|
|
3619
3642
|
sted('write', snapshot, () => writeRaw(snapshot, readFileSync(scriptPath, 'utf-8')));
|
|
3620
3643
|
} else {
|
|
3621
|
-
// The remediation budget belongs to this scope's attempt, not to the day.
|
|
3644
|
+
// The remediation budget belongs to this scope's attempt, not to the day. The machine
|
|
3645
|
+
// re-enters this state once per remediation loop, so initializing unconditionally
|
|
3646
|
+
// reset the budget every cycle and the loop could never classify its residue
|
|
3647
|
+
// (20261105 dogfood: the nested variants review revised forever). Like the drafts
|
|
3648
|
+
// scope, only the FIRST entry initializes.
|
|
3622
3649
|
const counter = scopeCounter(workDir, scopeDir);
|
|
3623
|
-
sted('write', counter, () => writeFileSync(counter, '0\n'));
|
|
3650
|
+
if (!isFile(counter)) sted('write', counter, () => writeFileSync(counter, '0\n'));
|
|
3624
3651
|
}
|
|
3625
3652
|
for (const name of [
|
|
3626
3653
|
'safety-review_report.json',
|
|
@@ -3693,7 +3720,14 @@ export function dailySafetyRevise(args: string[]): void {
|
|
|
3693
3720
|
if (!drafts) {
|
|
3694
3721
|
// Variants scope: every manifest-listed file must stay parseable Content with the
|
|
3695
3722
|
// fields the publish half reads; the structural VoiceScript machinery does not apply.
|
|
3696
|
-
|
|
3723
|
+
// Refresh each entry's sha256 to the post-remediation bytes: the manifest is the
|
|
3724
|
+
// contract the request/digest/stamp resolve through, so leaving the pre-remediation
|
|
3725
|
+
// digests in place made every re-review report a request-integrity mismatch and the
|
|
3726
|
+
// loop spin (20261105 dogfood). Structural drift above the hash level stays owned by
|
|
3727
|
+
// daily-bundle-variants (never run here).
|
|
3728
|
+
const manifestPath = join(workDir, SAFETY_VARIANTS_DIR, 'manifest.json');
|
|
3729
|
+
const manifest = readManifest(workDir);
|
|
3730
|
+
for (const [key, entry] of Object.entries(manifest.variants)) {
|
|
3697
3731
|
const path = join(workDir, entry.path);
|
|
3698
3732
|
const value = requireJson(path, 'safety-revise') as {
|
|
3699
3733
|
title?: unknown;
|
|
@@ -3704,7 +3738,16 @@ export function dailySafetyRevise(args: string[]): void {
|
|
|
3704
3738
|
failWith(1, `safety-revise: variant ${path} ${field} must be a non-empty string`);
|
|
3705
3739
|
}
|
|
3706
3740
|
}
|
|
3741
|
+
entry.sha256 = createHash('sha256').update(readFileSync(path)).digest('hex');
|
|
3742
|
+
manifest.variants[key] = entry;
|
|
3707
3743
|
}
|
|
3744
|
+
sted('write', manifestPath, () =>
|
|
3745
|
+
writeJsonText(manifestPath, {
|
|
3746
|
+
schemaVersion: manifest.schemaVersion,
|
|
3747
|
+
runDate: manifest.runDate,
|
|
3748
|
+
variants: manifest.variants,
|
|
3749
|
+
}),
|
|
3750
|
+
);
|
|
3708
3751
|
const counter = scopeCounter(workDir, scopeDir);
|
|
3709
3752
|
const next = readScopeCounter(workDir, scopeDir) + 1;
|
|
3710
3753
|
sted('write', counter, () => writeFileSync(counter, `${next}\n`));
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Salty Podcast (podcast-salty) — platform reference
|
|
2
|
+
|
|
3
|
+
`podcast.salty.vip` 是本项目自有的播客站点(Robin Min 自有域名,microfeed 实例,节目「每日AI乱炖」,RSS `/rss/`)。
|
|
4
|
+
这是**自有渠道**,不是第三方分发平台:没有平台审核队列、字级词表或收听量干预可引用。
|
|
5
|
+
本文件存在的意义是把「自有播客宿主没有平台规则」这件事写清楚,避免审核流程把「无三方平台规则」
|
|
6
|
+
误判成证据缺口并默认 BLOCK。
|
|
7
|
+
|
|
8
|
+
| Field | Value |
|
|
9
|
+
|-------|-------|
|
|
10
|
+
| Official source | 自有站点,无第三方协议页;本文件由站点维护者(Robin Min)本人核定 |
|
|
11
|
+
| Scope | `podcast.salty.vip` 节目标题、音频、show notes、封面(RSS 输出随 item 状态变化) |
|
|
12
|
+
| Effective date | 本文件创建日即生效(自有平台规则由维护者裁定) |
|
|
13
|
+
| Last verified | 2026-11-05(创建) |
|
|
14
|
+
| Evidence status | 维护者本人口径,非三方引用;无字级禁用词表 |
|
|
15
|
+
|
|
16
|
+
## Evidence distinctions
|
|
17
|
+
|
|
18
|
+
- **维护者裁定(本文件的权威来源)**:播客节目是自有域名下的小型日更节目,采用与本项目其他
|
|
19
|
+
渠一致的通用内容纪律;没有平台方提供的可引用条款。
|
|
20
|
+
- **与 apple-podcasts 的边界(AP-06 不在此生效)**:Apple Podcasts 目录收录是第三方目录的准入
|
|
21
|
+
问题,由维护者人工决定,不由本 pipeline 控制;本 host 的 RSS 只是自有分发的出口。对未核实
|
|
22
|
+
的估值/数字,节目文案应保持项目既有的 hedging 与出处注明(这是事实纪律,不是平台准入规则),
|
|
23
|
+
但**不要**因为「聚合页转述未读原文」而 block——那属于 sourcing note,由维护者自行复核。
|
|
24
|
+
- **通用纪律(沿用技能默认)**:AI 声明、来源可靠性、隐私/PII、医疗/投资类内容红线全部沿用
|
|
25
|
+
技能 default;本文件不新增平台规则。
|
|
26
|
+
|
|
27
|
+
## Review items
|
|
28
|
+
|
|
29
|
+
| ID | Item | Grade |
|
|
30
|
+
|----|------|-------|
|
|
31
|
+
| PS-01 | 来源诚实性:show notes 中的对外引用(媒体/论文/Blog)须给出真实出处;转述链保持既有 hedging;未读原文的聚合转述记 note,不 push block。 | note |
|
|
32
|
+
| PS-02 | 数字与事实核对:与源链接一致的数字/日期/机构名冲突时按技能通用规则处理(correctable → revise;冲突无解 → block)。 | note→judge |
|
|
33
|
+
| PS-03 | 可识别个人信息(听众或第三方的私人信息) | desensitize→block if unfixable |
|
|
34
|
+
| PS-04 | 医疗/投资劝诱内容技能默认红线不变(未核实估值→保持 hedging 并注明出处,而非删除条目)。 | default |
|
|
35
|
+
|
|
36
|
+
## Open questions / escalation
|
|
37
|
+
|
|
38
|
+
- 自有播客无第三方审核 → 本文件不应产生 `evidenceGap: true` 类 BLOCK;任何「podcast-salty 无
|
|
39
|
+
reference 文件」的发现都指向本文件缺失,而不是内容风险。
|
|
40
|
+
- 若未来节目上线商业化、赞助或接入第三方目录(Apple/小宇宙实际分配),本文件需要为相应渠道
|
|
41
|
+
追加准入条款。
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Salty Talk / Surfing (surfdash) — platform reference
|
|
2
|
+
|
|
3
|
+
`surfing.salty.vip` 是本项目自有的静态发布站点(Robin Min 自有域名,栏目「AI 乱炖」,zh/en/ja 三语)。
|
|
4
|
+
这是**自有渠道**,不是第三方分发平台:没有平台审核队列、字级词表或 AI 标注接口可引用。
|
|
5
|
+
本文件存在的意义是把「自有站点没有平台规则」这件事写清楚,避免审核流程把「无三方平台规则」
|
|
6
|
+
误判成证据缺口并默认 BLOCK。
|
|
7
|
+
|
|
8
|
+
| Field | Value |
|
|
9
|
+
|-------|-------|
|
|
10
|
+
| Official source | 自有站点,无第三方协议页;本文件由站点维护者(Robin Min)本人核定 |
|
|
11
|
+
| Scope | `surfing.salty.vip` 文章页(正文、frontmatter 的 title/description/key_points/cover) |
|
|
12
|
+
| Effective date | 本文件创建日即生效(自有平台规则由维护者裁定) |
|
|
13
|
+
| Last verified | 2026-11-05(创建) |
|
|
14
|
+
| Evidence status | 维护者本人口径,非三方引用;无字级禁用词表 |
|
|
15
|
+
|
|
16
|
+
## Evidence distinctions
|
|
17
|
+
|
|
18
|
+
- **维护者裁定(本文件的权威来源)**:站点是自有域名下的小型技术/资讯栏目,采用与本项目其他
|
|
19
|
+
台一致的通用内容纪律;没有平台方提供的可引用条款。
|
|
20
|
+
- **通用纪律(沿用技能默认)**:披露/AI 声明、来源可靠性、隐私/PII、医疗/投资类内容红线全部
|
|
21
|
+
沿用技能 default;本文件不新增平台规则。
|
|
22
|
+
|
|
23
|
+
## Review items
|
|
24
|
+
|
|
25
|
+
| ID | Item | Grade |
|
|
26
|
+
|----|------|-------|
|
|
27
|
+
| SD-01 | 来源诚实性:对外引用(媒体/论文/Blog)须给出真实出处链接;「综述性转述未读原文」是 sourcing gap,按 note 记录并在站点正文保持既有 hedging;**不要**因为转述来自聚合页/未读原文而 block——这与 apple-podcasts 的 AP-06 严格度不同,自有站点由维护者自行判断并复核。 | note |
|
|
28
|
+
| SD-02 | 数字与事实核对:与源链接一致的数字/日期/机构名出现冲突时按技能通用规则处理(correctable → revise;冲突无解 → block)。 | note→judge |
|
|
29
|
+
| SD-03 | 可识别个人信息(听众或第三方的私人信息、联系方式、住址等) | desensitize→block if unfixable |
|
|
30
|
+
| SD-04 | roku/医疗/投资劝诱内容技能默认红线不变(本文件不放开任何默认红线)。 | default |
|
|
31
|
+
|
|
32
|
+
## Open questions / escalation
|
|
33
|
+
|
|
34
|
+
- 自有站点无第三方审核 → 本文件不应产生 `evidenceGap: true` 类 BLOCK;任何「surfdash 无 reference
|
|
35
|
+
文件」的发现都指向本文件缺失,而不是内容风险。
|
|
36
|
+
- 若未来站点引入广告或国家安全审查类规则,本文件需要更新。
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# 小红书 (Xiaohongshu / xhs) — platform reference
|
|
2
|
+
|
|
3
|
+
Review-item IDs here are **this skill's bookkeeping numbers**, not official platform rule codes.
|
|
4
|
+
与 xiaoyuzhou.md 同一格式约定:这是本技能基于操作者已核验证据的保守立场,不是平台条文。
|
|
5
|
+
|
|
6
|
+
| Field | Value |
|
|
7
|
+
|-------|-------|
|
|
8
|
+
| Official source | <https://www.xiaohongshu.com/community/>(社区公约/创作细则入口,多级页面) |
|
|
9
|
+
| Scope | 笔记(图文/视频长文)、封面图、话题标签、昵称简介 |
|
|
10
|
+
| Effective date | unknown(社区公约页面未标注可引用的版本生效日期) |
|
|
11
|
+
| Last verified | 2026-11-05(写本文件时按操作者已有账号审查经验整理,未逐字读官方更新后全文) |
|
|
12
|
+
| Evidence status | **未获官方逐字条文**;以下审核项是本技能的保守立场,不是平台条文;与小宇宙 0189 事件类似,任何具体下架理由以官方为主,本技能按保守项判断 |
|
|
13
|
+
|
|
14
|
+
## Evidence distinctions
|
|
15
|
+
|
|
16
|
+
- **官方规则(已知存在,未逐字核对)**:小红书有《社区公约》与《创作规范》,对违法违规、医疗
|
|
17
|
+
健康、财产投资、隐私、低质同质化、AI 生成内容有原则性条款;公开文本不提供逐字词表。
|
|
18
|
+
- **操作者经验(保守立场来源)**:账号为个人技术分享(栏目「AI 乱炖」衍生),非新闻机构、无
|
|
19
|
+
医疗/金融资质、不带货;发帖以图文长文/播客预告为主。
|
|
20
|
+
- **编辑建议(本技能)**:以下审核项是本技能基于上述证据的保守立场,不是平台条文。
|
|
21
|
+
|
|
22
|
+
## Review items
|
|
23
|
+
|
|
24
|
+
| ID | Item | Grade |
|
|
25
|
+
|----|------|-------|
|
|
26
|
+
| XH-01 | 资质口径:不得呈现「新闻资讯播报」形态或暗示持有新闻资质(技术/科普类分享不受影响;同 xiaoyuzhou.md XY-01 的操作者裁定 2026-10-10,按 note 记录,不逐集 escalate)。 | note |
|
|
27
|
+
| XH-02 | 医疗健康/药品/诊疗建议:以医疗结论、疗效、用药指导为主线的内容(本栏目主题为 AI 技术/产业资讯,正常不会触发;一旦触发按 block)。 | block |
|
|
28
|
+
| XH-03 | 投资/理财:以个股买卖建议、收益承诺为主线的内容(对 quadrature/Anthropic 等估值表述属于普通事实转述,不是建议;命中后再 judge)。 | block |
|
|
29
|
+
| XH-04 | AI 生成内容标识:平台对 AI 生成内容有标注要求(** 未逐字核对当前接口规则,按公共常见要求保守处理**)。本栏目发布为「AI 辅助创作」的播客预告/图文 —— 操作者已在账号简介披露 AI 辅助;单条内容不再逐条 escalate。 | note |
|
|
30
|
+
| XH-05 | 可识别个人信息(听众或第三方的私人信息、联系方式、住址等) | desensitize→block if unfixable |
|
|
31
|
+
| XH-06 | 时政/公共事务主线:同 xiaoyuzhou.md XY-02(政策管制、军事、公共政策争议为主线的内容按 block)。 | block |
|
|
32
|
+
|
|
33
|
+
## Open questions / escalation
|
|
34
|
+
|
|
35
|
+
- 无官方字级词表 → 任何「词命中即下架」的断言都不成立;同理,无词表不等于安全。
|
|
36
|
+
- AI 标注、内容审核新规变化频繁 → 每次提审前按 XH-04 复核;`RISK` 或证据缺口一律人工复审
|
|
37
|
+
(`escalate: true`),不得静默 PASS。
|
|
38
|
+
- 资质口径、时政主线与个人信息的红线全部沿用技能默认,本文件只 diminute XH-04 的逐条 escalate
|
|
39
|
+
(业务上固定披露已存在)。
|
|
@@ -48,10 +48,10 @@ states:
|
|
|
48
48
|
onEnter:
|
|
49
49
|
- kind: shell
|
|
50
50
|
options:
|
|
51
|
-
# Reporting only: a ledger failure must never abort a tick before the lock is even taken.
|
|
52
|
-
onError: continue
|
|
53
51
|
command: >-
|
|
54
52
|
kk ledger sync --workflow kk-collect
|
|
53
|
+
# Reporting only: a ledger failure must never abort a tick before the lock is even taken.
|
|
54
|
+
onError: continue
|
|
55
55
|
- kind: shell
|
|
56
56
|
options:
|
|
57
57
|
# stamp: the pinned var, else the UTC minute (R4). work_dir: `.works/kk-collect/<stamp>` (R4),
|
|
@@ -92,13 +92,13 @@ states:
|
|
|
92
92
|
# revocation warns (captured:false) and only disables Board previews.
|
|
93
93
|
# onError: continue keeps the lock-releasing failure path intact instead of a hard abort.
|
|
94
94
|
options:
|
|
95
|
-
onError: continue
|
|
96
95
|
command: >-
|
|
97
96
|
kk board capture --workflow kk-collect --run "${vars.__runId}"
|
|
98
97
|
--work-dir "${vars.work_dir}" --stamp "${vars.stamp}"
|
|
99
98
|
--definition-digest "${vars.__definitionDigest}"
|
|
100
99
|
--setting "limit=${vars.limit}" --setting "horizon_hours=${vars.horizon_hours}"
|
|
101
100
|
> "${vars.work_dir}/board-capture.json"
|
|
101
|
+
onError: continue
|
|
102
102
|
- kind: shell
|
|
103
103
|
# onError: continue on both ingestion actions, with the artifact guard below as the positive
|
|
104
104
|
# success contract: a state-machine aborts the run on a failed action without taking a
|