@gobing-ai/knowledge-kit 0.1.14 → 0.1.15

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.
Files changed (76) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/dist/index.js +796 -176
  3. package/package.json +1 -1
  4. package/plugins/generations/content-gen/dist/index.js +2 -2
  5. package/plugins/generations/core-facts-gen/dist/index.js +2 -2
  6. package/plugins/generations/daily-article-gen/dist/index.js +2 -2
  7. package/plugins/generations/dailynews-gen/dist/index.js +72 -9
  8. package/plugins/generations/dailynews-gen/src/index.ts +23 -1
  9. package/plugins/generations/dailynews-gen/src/script-plan.ts +103 -5
  10. package/plugins/generations/episode-plan-gen/dist/index.js +2 -2
  11. package/plugins/generations/gemini-tts/dist/index.js +2 -2
  12. package/plugins/generations/image-gen/dist/index.js +2 -2
  13. package/plugins/generations/news-report-gen/dist/index.js +2 -2
  14. package/plugins/generations/voice-gen/dist/index.js +2 -2
  15. package/plugins/generations/voxcpm-tts/dist/index.js +2 -2
  16. package/plugins/ingestions/aihot-ingest/dist/index.js +2 -2
  17. package/plugins/ingestions/horizon-ingest/dist/index.js +2 -2
  18. package/plugins/ingestions/last30days-ingest/dist/index.js +2 -2
  19. package/plugins/ingestions/web-search/dist/index.js +2 -2
  20. package/plugins/kk/plugin.json +1 -1
  21. package/plugins/kk/scripts/daily-scheduled-run.ts +4 -0
  22. package/plugins/kk/scripts/daily-stages.ts +476 -12
  23. package/plugins/kk/scripts/kk-workflow-stages.ts +20 -4
  24. package/plugins/kk/skills/content-judge/SKILL.md +5 -4
  25. package/plugins/kk/skills/content-judge/references/rubrics.md +14 -0
  26. package/plugins/kk/skills/news-editorial/SKILL.md +122 -0
  27. package/plugins/kk/skills/news-editorial/references/selection.md +47 -0
  28. package/plugins/kk/skills/news-editorial/references/style-contract.zh.md +99 -0
  29. package/plugins/kk/skills/publish/SKILL.md +3 -3
  30. package/plugins/kk/skills/web-automation/SKILL.md +229 -77
  31. package/plugins/kk/skills/web-automation/references/browser-launch.md +127 -36
  32. package/plugins/kk/workflows/kk-daily-ai-voice2.yaml +4 -6
  33. package/plugins/kk/workflows/kk-daily-ai-voice3.yaml +183 -222
  34. package/plugins/kk/workflows/kk-daily-publish3.yaml +16 -8
  35. package/plugins/publishings/bilibili-pub/dist/index.js +5 -3
  36. package/plugins/publishings/bilibili-pub/dist/profile-check.js +3 -1
  37. package/plugins/publishings/cnblogs-pub/dist/index.js +5 -3
  38. package/plugins/publishings/cnblogs-pub/dist/profile-check.js +3 -1
  39. package/plugins/publishings/csdn-pub/dist/index.js +5 -3
  40. package/plugins/publishings/csdn-pub/dist/profile-check.js +5 -3
  41. package/plugins/publishings/emdash-pub/dist/index.js +2 -2
  42. package/plugins/publishings/infoq-pub/dist/index.js +5 -3
  43. package/plugins/publishings/infoq-pub/dist/profile-check.js +5 -3
  44. package/plugins/publishings/jike-pub/dist/index.js +5 -3
  45. package/plugins/publishings/jike-pub/dist/profile-check.js +5 -3
  46. package/plugins/publishings/juejin-pub/dist/index.js +5 -3
  47. package/plugins/publishings/juejin-pub/dist/profile-check.js +5 -3
  48. package/plugins/publishings/linkedin-pub/dist/index.js +5 -3
  49. package/plugins/publishings/linkedin-pub/dist/profile-check.js +5 -3
  50. package/plugins/publishings/medium-pub/dist/index.js +2 -2
  51. package/plugins/publishings/podcast-pub/dist/index.js +147 -4
  52. package/plugins/publishings/podcast-pub/src/index.ts +71 -2
  53. package/plugins/publishings/podcast-pub/src/map.ts +3 -0
  54. package/plugins/publishings/podcast-pub/src/transcript.ts +195 -0
  55. package/plugins/publishings/qiita-pub/dist/index.js +2 -2
  56. package/plugins/publishings/shipinhao-pub/dist/index.js +5 -3
  57. package/plugins/publishings/shipinhao-pub/dist/profile-check.js +3 -1
  58. package/plugins/publishings/sspai-pub/dist/index.js +5 -3
  59. package/plugins/publishings/sspai-pub/dist/profile-check.js +3 -1
  60. package/plugins/publishings/substack-pub/dist/index.js +5 -3
  61. package/plugins/publishings/substack-pub/dist/profile-check.js +5 -3
  62. package/plugins/publishings/surfdash-pub/dist/index.js +2 -2
  63. package/plugins/publishings/threads-pub/dist/index.js +2 -2
  64. package/plugins/publishings/wechatmp-pub/dist/index.js +52 -33
  65. package/plugins/publishings/wechatmp-pub/dist/profile-check.js +5 -3
  66. package/plugins/publishings/wechatmp-pub/src/flow.ts +70 -33
  67. package/plugins/publishings/weibo-pub/dist/index.js +5 -3
  68. package/plugins/publishings/weibo-pub/dist/profile-check.js +5 -3
  69. package/plugins/publishings/x-pub/dist/index.js +5 -3
  70. package/plugins/publishings/x-pub/dist/profile-check.js +5 -3
  71. package/plugins/publishings/xhs-pub/dist/index.js +5 -3
  72. package/plugins/publishings/xhs-pub/dist/profile-check.js +5 -3
  73. package/plugins/publishings/xiaoyuzhou-pub/dist/index.js +5 -3
  74. package/plugins/publishings/xiaoyuzhou-pub/dist/profile-check.js +3 -1
  75. package/plugins/publishings/zenn-pub/dist/index.js +2 -2
  76. package/plugins/kk/scripts/daily-voice-style.zh.md +0 -70
@@ -55,6 +55,7 @@ import { spawnSync } from 'node:child_process';
55
55
  import { createHash } from 'node:crypto';
56
56
  import {
57
57
  copyFileSync,
58
+ cpSync,
58
59
  existsSync,
59
60
  mkdirSync,
60
61
  readdirSync,
@@ -75,6 +76,7 @@ import {
75
76
  coverBrief,
76
77
  coverNormalizeBody,
77
78
  dedupPlanByTitle,
79
+ EDITORIAL_SKILL_DIR,
78
80
  expandHome,
79
81
  fail,
80
82
  failWith,
@@ -91,12 +93,10 @@ import {
91
93
  readJson,
92
94
  readTopicBoundary,
93
95
  requireJson,
94
- requireSidecar,
95
- resolveSidecar,
96
96
  resultPublishStatus,
97
97
  SAFETY_PLATFORM_DECISIONS,
98
98
  SAFETY_REVISE_COUNT,
99
- STYLE_SIDECAR,
99
+ STYLE_CONTRACT,
100
100
  StageFailure,
101
101
  safetyDigest,
102
102
  safetyDraftPaths,
@@ -511,14 +511,24 @@ export function dailyArticle(args: string[]): void {
511
511
  // half-ran (or the json was pruned), and returning here starves every downstream stage
512
512
  // that reads content.json — publish-prep refuses to merge a draft it cannot read. Requiring
513
513
  // both artifacts makes the pair self-healing instead of permanently stuck.
514
- if (isFile(articleMd) && isFile(articleContent)) {
514
+ // The prose body is the generator's INPUT: when it is newer than content.json the pair
515
+ // is stale (an operator or a retry edited the headings after the render), and serving it
516
+ // ships last round's body to surfdash. Re-derive instead of returning.
517
+ const proseBody = join(workDir, '22-article', `${runDate}_06_article-write_body.md`);
518
+ const proseStale =
519
+ isFile(proseBody) &&
520
+ isFile(articleContent) &&
521
+ statSync(proseBody).mtimeMs > statSync(articleContent).mtimeMs;
522
+ if (isFile(articleMd) && isFile(articleContent) && !proseStale) {
515
523
  console.log(`article cached: ${articleMd}`);
516
524
  return;
517
525
  }
526
+ if (proseStale) {
527
+ console.error(`article: stale pair — ${proseBody} is newer than ${articleContent}, re-deriving`);
528
+ }
518
529
  // Agent-written prose body (workflow `article-write` state) replaces the mechanical
519
530
  // per-item render when present — the 3-format split: surfdash gets an article,
520
531
  // not a news list (2026-09-16 feedback).
521
- const proseBody = join(workDir, '22-article', `${runDate}_06_article-write_body.md`);
522
532
  kk(
523
533
  [
524
534
  'executor',
@@ -3999,7 +4009,8 @@ export function dailyScript(args: string[]): void {
3999
4009
  // cwd = the plugin package dir, so a relative workDir would silently miss the
4000
4010
  // plan; a plan file absent at build time reports missing-plan (template fallback).
4001
4011
  DAILYNEWS_SCRIPT_PLAN: resolve(join(workDir, '24-script', `${runDate}_08_script-write_plan.json`)),
4002
- DAILYNEWS_STYLE_FILE: resolve(resolveSidecar('', STYLE_SIDECAR)),
4012
+ DAILYNEWS_STYLE_FILE: resolve(STYLE_CONTRACT),
4013
+ DAILYNEWS_CURATE_EXCLUDED: resolve(join(workDir, '21-plan', `${runDate}_03_curate_excluded.json`)),
4003
4014
  },
4004
4015
  });
4005
4016
  const body = readJson(contentOut)?.body;
@@ -4021,25 +4032,478 @@ export function dailyScript(args: string[]): void {
4021
4032
 
4022
4033
  /** Task 0189 R10 artifacts of the content-safety verdict gate (24-script, step 09). */
4023
4034
  /**
4024
- * F613 R1: snapshot the shipped style contract into the run's work dir. Spur workflows have no
4025
- * workflow-dir var, so the script-write prompt reads this copy — and the snapshot freezes the
4026
- * exact contract the run's plan was written against. Idempotent (an existing snapshot is kept);
4027
- * a missing sidecar is a broken install, so `requireSidecar` fails loud.
4035
+ * F613 R1 / ADR-045: snapshot the shipped kk:news-editorial skill into the run's work dir. Spur
4036
+ * workflows have no workflow-dir var and kk skills are not installed for every executor, so the
4037
+ * writer prompts read this copy by path — and the snapshot freezes the exact policy the run's plan
4038
+ * was written against. `20-editorial/news-editorial/` holds SKILL.md + references; the
4039
+ * `_08_script-write_style.md` copy of the style contract stays the run's audit record.
4040
+ * Idempotent per artifact (an existing snapshot is kept); a missing skill is a broken install.
4028
4041
  */
4029
4042
  export function dailyScriptStyle(args: string[]): void {
4030
4043
  const [workDir = '', runDate = ''] = args;
4044
+ if (!isFile(STYLE_CONTRACT)) {
4045
+ failWith(
4046
+ 1,
4047
+ `news-editorial skill not found — no ${STYLE_CONTRACT}; install or refresh the kk package (bun link in a checkout)`,
4048
+ );
4049
+ }
4050
+ const skillSnapshot = join(workDir, '20-editorial', 'news-editorial');
4051
+ if (isFile(join(skillSnapshot, 'SKILL.md'))) {
4052
+ console.log(`editorial skill cached: ${skillSnapshot}`);
4053
+ } else {
4054
+ sted('cp', skillSnapshot, () => cpSync(EDITORIAL_SKILL_DIR, skillSnapshot, { recursive: true }));
4055
+ console.log(`editorial skill snapshot: ${skillSnapshot}`);
4056
+ }
4031
4057
  const snapshot = join(workDir, '24-script', `${runDate}_08_script-write_style.md`);
4032
4058
  if (isFile(snapshot)) {
4033
4059
  console.log(`script style cached: ${snapshot}`);
4034
4060
  return;
4035
4061
  }
4036
- const sidecar = requireSidecar('', STYLE_SIDECAR, 'daily-voice-style');
4037
4062
  const scriptDir = join(workDir, '24-script');
4038
4063
  sted('mkdir', scriptDir, () => mkdirSync(scriptDir, { recursive: true }));
4039
- sted('cp', snapshot, () => copyFileSync(sidecar, snapshot));
4064
+ sted('cp', snapshot, () => copyFileSync(STYLE_CONTRACT, snapshot));
4040
4065
  console.log(`script style snapshot: ${snapshot}`);
4041
4066
  }
4042
4067
 
4068
+ /**
4069
+ * 0345 R4: dry-run the dailynews-gen validator on the authored script plan so a rejected plan gets
4070
+ * one targeted `script-write-fix` pass instead of silently shipping the template fallback. Writes
4071
+ * `24-script/<date>_08_script-write_errors.json` ([{itemId, rule}]) when the plan is absent or
4072
+ * rejected, and removes it when the plan is accepted. The trial build goes to a scratch path so the
4073
+ * `script` stage's isFile cache is never primed by a check.
4074
+ */
4075
+ export function dailyScriptCheck(args: string[]): void {
4076
+ const [workDir = '', runDate = '', pluginsPath = ''] = args;
4077
+ const planOut = join(workDir, '24-script', `${runDate}_08_script-write_plan.json`);
4078
+ const errorsOut = join(workDir, '24-script', `${runDate}_08_script-write_errors.json`);
4079
+ const writeErrors = (errors: unknown[]) => {
4080
+ sted('write', errorsOut, () => writeJsonText(errorsOut, errors));
4081
+ console.log(`script plan rejected (${errors.length} error(s)): ${errorsOut}`);
4082
+ };
4083
+ if (!isFile(planOut)) {
4084
+ writeErrors([{ itemId: 'episode', rule: 'missing-plan' }]);
4085
+ return;
4086
+ }
4087
+ const trialOut = join(workDir, '24-script', `.${runDate}_08_script-check_content.json`);
4088
+ const planIn = join(workDir, '21-plan', `${runDate}_03_plan_plan.json`);
4089
+ try {
4090
+ kk(['executor', 'run', 'dailynews-gen', '--in', planIn, '--out', trialOut, ...pluginsArgs(pluginsPath)], {
4091
+ env: {
4092
+ DAILYNEWS_LANGUAGE: 'zh',
4093
+ DAILYNEWS_DATE: runDate,
4094
+ DAILYNEWS_SCRIPT_PLAN: resolve(planOut),
4095
+ DAILYNEWS_STYLE_FILE: resolve(STYLE_CONTRACT),
4096
+ DAILYNEWS_CURATE_EXCLUDED: resolve(join(workDir, '21-plan', `${runDate}_03_curate_excluded.json`)),
4097
+ },
4098
+ });
4099
+ const metadata = readJson(trialOut)?.metadata;
4100
+ const source = metadata?.scriptSource;
4101
+ if (source !== 'plan' && source !== 'template') {
4102
+ failWith(1, `script-check: ${trialOut} carries no scriptSource stamp`);
4103
+ }
4104
+ if (source === 'plan') {
4105
+ if (isFile(errorsOut)) unlinkSync(errorsOut);
4106
+ console.log('script plan accepted by the validator');
4107
+ return;
4108
+ }
4109
+ writeErrors(Array.isArray(metadata.scriptPlanErrors) ? metadata.scriptPlanErrors : []);
4110
+ } finally {
4111
+ if (isFile(trialOut)) unlinkSync(trialOut);
4112
+ }
4113
+ }
4114
+
4115
+ /**
4116
+ * 0345 R8: validate the `curate` agent output (`21-plan/<date>_03_plan_plan.json` +
4117
+ * `21-plan/<date>_03_curate_excluded.json`) against the QC candidates
4118
+ * (`21-plan/<date>_04_quality-control-content_candidates.json`). Unwraps a `Content` envelope and
4119
+ * dedups near-duplicate titles in place before `write` reads the plan. Writes
4120
+ * `21-plan/<date>_03_curate_errors.json` (`[{itemId, rule}]`) on failure and removes it on success.
4121
+ * With `--final`, falls back to the untranslated candidates and `[]` exclusions on failure.
4122
+ */
4123
+ export function dailyCurateCheck(args: string[]): void {
4124
+ const [workDir = '', runDate = '', flag = ''] = args;
4125
+ if (!workDir) fail('missing work_dir');
4126
+ if (!runDate) fail('missing run_date');
4127
+ const isFinal = flag === '--final';
4128
+ const candidatesFile = join(workDir, '21-plan', `${runDate}_04_quality-control-content_candidates.json`);
4129
+ const planFile = join(workDir, '21-plan', `${runDate}_03_plan_plan.json`);
4130
+ const excludedFile = join(workDir, '21-plan', `${runDate}_03_curate_excluded.json`);
4131
+ const errFile = join(workDir, '21-plan', `${runDate}_03_curate_errors.json`);
4132
+
4133
+ const candRaw = readJson(candidatesFile);
4134
+ const candidateDocs: Record<string, unknown>[] = Array.isArray(candRaw)
4135
+ ? (candRaw as Record<string, unknown>[])
4136
+ : Array.isArray(metadataOf(candRaw).docs)
4137
+ ? (metadataOf(candRaw).docs as Record<string, unknown>[])
4138
+ : [];
4139
+ const candidateIds = candidateDocs.map((doc) => (typeof doc?.id === 'string' ? doc.id.trim() : '')).filter(Boolean);
4140
+ const candidateIdSet = new Set(candidateIds);
4141
+
4142
+ const handleErrors = (errors: { itemId: string; rule: string }[]): void => {
4143
+ if (isFinal) {
4144
+ const reason = errors.map((e) => `${e.itemId}:${e.rule}`).join(', ');
4145
+ console.error(
4146
+ `curate-check: final check failed (${reason}) — falling back to ${candidateDocs.length} untranslated candidate(s)`,
4147
+ );
4148
+ sted('write', planFile, () => writeJsonText(planFile, candidateDocs));
4149
+ sted('write', excludedFile, () => writeJsonText(excludedFile, []));
4150
+ if (isFile(errFile)) unlinkSync(errFile);
4151
+ return;
4152
+ }
4153
+ sted('write', errFile, () => writeJsonText(errFile, errors));
4154
+ console.log(`curate-check: rejected (${errors.length} error(s)): ${errFile}`);
4155
+ };
4156
+
4157
+ if (!isFile(planFile)) {
4158
+ handleErrors([{ itemId: 'episode', rule: 'missing-plan' }]);
4159
+ return;
4160
+ }
4161
+
4162
+ let planParsed: unknown;
4163
+ try {
4164
+ planParsed = JSON.parse(readFileSync(planFile, 'utf-8'));
4165
+ } catch {
4166
+ handleErrors([{ itemId: 'episode', rule: 'invalid-json' }]);
4167
+ return;
4168
+ }
4169
+
4170
+ let planDocs: Record<string, unknown>[];
4171
+ if (Array.isArray(planParsed)) {
4172
+ planDocs = planParsed as Record<string, unknown>[];
4173
+ } else if (
4174
+ planParsed !== null &&
4175
+ typeof planParsed === 'object' &&
4176
+ Array.isArray(metadataOf(planParsed as Record<string, unknown>).docs)
4177
+ ) {
4178
+ planDocs = metadataOf(planParsed as Record<string, unknown>).docs as Record<string, unknown>[];
4179
+ sted('write', planFile, () => writeJsonText(planFile, planDocs));
4180
+ console.log(`curate-check: unwrapped Content envelope (${planDocs.length} doc(s)) in ${planFile}`);
4181
+ } else {
4182
+ handleErrors([{ itemId: 'episode', rule: 'invalid-json' }]);
4183
+ return;
4184
+ }
4185
+
4186
+ for (const entry of planDocs) {
4187
+ if (entry === null || typeof entry !== 'object' || typeof entry.id !== 'string' || entry.id.trim() === '') {
4188
+ handleErrors([{ itemId: 'episode', rule: 'invalid-json' }]);
4189
+ return;
4190
+ }
4191
+ }
4192
+
4193
+ const excludedItems: { id: string; reason: string }[] = [];
4194
+ if (isFile(excludedFile)) {
4195
+ let exParsed: unknown;
4196
+ try {
4197
+ exParsed = JSON.parse(readFileSync(excludedFile, 'utf-8'));
4198
+ } catch {
4199
+ handleErrors([{ itemId: 'episode', rule: 'invalid-json' }]);
4200
+ return;
4201
+ }
4202
+ if (!Array.isArray(exParsed)) {
4203
+ handleErrors([{ itemId: 'episode', rule: 'invalid-json' }]);
4204
+ return;
4205
+ }
4206
+ for (const entry of exParsed) {
4207
+ if (
4208
+ entry === null ||
4209
+ typeof entry !== 'object' ||
4210
+ typeof (entry as { id?: unknown }).id !== 'string' ||
4211
+ (entry as { id: string }).id.trim() === '' ||
4212
+ typeof (entry as { reason?: unknown }).reason !== 'string' ||
4213
+ (entry as { reason: string }).reason.trim() === ''
4214
+ ) {
4215
+ handleErrors([{ itemId: 'episode', rule: 'invalid-json' }]);
4216
+ return;
4217
+ }
4218
+ excludedItems.push({
4219
+ id: (entry as { id: string }).id.trim(),
4220
+ reason: (entry as { reason: string }).reason.trim(),
4221
+ });
4222
+ }
4223
+ }
4224
+
4225
+ const errors: { itemId: string; rule: string }[] = [];
4226
+ if (planDocs.length === 0) {
4227
+ errors.push({ itemId: 'episode', rule: 'empty-plan' });
4228
+ }
4229
+
4230
+ const keptIds = new Set<string>();
4231
+ for (const doc of planDocs) {
4232
+ const id = (doc.id as string).trim();
4233
+ keptIds.add(id);
4234
+ if (candidateIdSet.size > 0 && !candidateIdSet.has(id)) {
4235
+ errors.push({ itemId: id, rule: 'unknown-id' });
4236
+ }
4237
+ }
4238
+
4239
+ const excludedIds = new Set<string>();
4240
+ for (const ex of excludedItems) {
4241
+ excludedIds.add(ex.id);
4242
+ if (candidateIdSet.size > 0 && !candidateIdSet.has(ex.id)) {
4243
+ errors.push({ itemId: ex.id, rule: 'unknown-id' });
4244
+ }
4245
+ if (keptIds.has(ex.id)) {
4246
+ errors.push({ itemId: ex.id, rule: 'conflict-id' });
4247
+ }
4248
+ }
4249
+
4250
+ for (const cid of candidateIds) {
4251
+ if (!keptIds.has(cid) && !excludedIds.has(cid)) {
4252
+ errors.push({ itemId: cid, rule: 'missing-item' });
4253
+ }
4254
+ }
4255
+
4256
+ if (errors.length > 0) {
4257
+ handleErrors(errors);
4258
+ return;
4259
+ }
4260
+
4261
+ const { kept, dropped } = dedupPlanByTitle(planDocs);
4262
+ if (dropped.length > 0) {
4263
+ sted('write', planFile, () => writeJsonText(planFile, kept));
4264
+ console.error(`curate-check: dropped ${dropped.length} near-duplicate plan item(s): ${dropped.join(', ')}`);
4265
+ for (const droppedId of dropped) {
4266
+ if (!excludedIds.has(droppedId)) {
4267
+ excludedIds.add(droppedId);
4268
+ excludedItems.push({ id: droppedId, reason: '标题近似重复自动去重' });
4269
+ }
4270
+ }
4271
+ sted('write', excludedFile, () => writeJsonText(excludedFile, excludedItems));
4272
+ } else if (!isFile(excludedFile)) {
4273
+ sted('write', excludedFile, () => writeJsonText(excludedFile, excludedItems));
4274
+ }
4275
+
4276
+ if (isFile(errFile)) unlinkSync(errFile);
4277
+ console.log(`curate-check: accepted (${kept.length} kept, ${excludedItems.length} excluded)`);
4278
+ }
4279
+
4280
+ function sourceLabelForDoc(doc: Record<string, unknown>, sourceUri: string): string {
4281
+ const meta = metadataOf(doc);
4282
+ if (typeof meta.source === 'string' && meta.source.trim() !== '') {
4283
+ return meta.source.trim();
4284
+ }
4285
+ try {
4286
+ return new URL(sourceUri).hostname.replace(/^www\./, '') || '原文';
4287
+ } catch {
4288
+ return '原文';
4289
+ }
4290
+ }
4291
+
4292
+ /** Fixed column opening line of the rendered article body: house copy, not editorial judgement (0345). */
4293
+ const EDITORIAL_ARTICLE_OPENING = '这里是由Robin为你带来的咸话咸说之每日AI乱炖。';
4294
+
4295
+ /**
4296
+ * 0345 R10: deterministically render the five sibling editorial artifacts from the master script
4297
+ * plan (`24-script/<date>_08_script-write_plan.json`) and the curated plan
4298
+ * (`21-plan/<date>_03_plan_plan.json`):
4299
+ * 1. `22-article/<date>_06_article-write_body.md`
4300
+ * 2. `23-cover/<date>_07_cover-brief.json`
4301
+ * 3. `25-xhs/<date>_18_xhs_plan.json` (only when `cards_enabled === 'true'`)
4302
+ * 4. `26-social/{x,threads}.md` (`social.short`)
4303
+ * 5. `26-social/{weibo,linkedin}.md` (`social.long`)
4304
+ * Missing master field -> skip that one artifact with a logged reason, write the others.
4305
+ */
4306
+ export function dailyEditorialRender(args: string[]): void {
4307
+ const [workDir = '', runDate = '', cardsEnabled = 'false', cardsRaw = '6'] = args;
4308
+ if (!workDir) fail('missing work_dir');
4309
+ if (!runDate) fail('missing run_date');
4310
+ const cardsCount = Math.max(2, Number.parseInt(cardsRaw, 10) || 6);
4311
+ const planFile = join(workDir, '21-plan', `${runDate}_03_plan_plan.json`);
4312
+ const masterFile = join(workDir, '24-script', `${runDate}_08_script-write_plan.json`);
4313
+
4314
+ const master = readJson(masterFile);
4315
+ if (master === null) {
4316
+ console.log(`editorial-render: no master script plan at ${masterFile} — skipping all renders`);
4317
+ return;
4318
+ }
4319
+
4320
+ const planRaw = readJson(planFile);
4321
+ const planDocs: Record<string, unknown>[] = Array.isArray(planRaw)
4322
+ ? (planRaw as Record<string, unknown>[])
4323
+ : Array.isArray(metadataOf(planRaw).docs)
4324
+ ? (metadataOf(planRaw).docs as Record<string, unknown>[])
4325
+ : [];
4326
+ const docById = new Map<string, Record<string, unknown>>();
4327
+ for (const doc of planDocs) {
4328
+ if (typeof doc?.id === 'string' && doc.id.trim() !== '') {
4329
+ docById.set(doc.id.trim(), doc);
4330
+ }
4331
+ }
4332
+
4333
+ const rawItems = Array.isArray(master.items) ? (master.items as Record<string, unknown>[]) : [];
4334
+ const itemById = new Map<string, Record<string, unknown>>();
4335
+ for (const item of rawItems) {
4336
+ if (item !== null && typeof item === 'object' && typeof item.id === 'string' && item.id.trim() !== '') {
4337
+ itemById.set(item.id.trim(), item);
4338
+ }
4339
+ }
4340
+ const orderedIds =
4341
+ planDocs.length > 0
4342
+ ? planDocs
4343
+ .map((d) => (typeof d.id === 'string' ? d.id.trim() : ''))
4344
+ .filter((id) => id !== '' && itemById.has(id))
4345
+ : [...itemById.keys()];
4346
+
4347
+ // 1. Article body: 22-article/<date>_06_article-write_body.md
4348
+ const outro = typeof master.outro === 'string' ? master.outro.trim() : '';
4349
+ const articleSections: string[] = [];
4350
+ for (const id of orderedIds) {
4351
+ const doc = docById.get(id);
4352
+ const item = itemById.get(id);
4353
+ const title = typeof doc?.title === 'string' ? doc.title.trim() : '';
4354
+ const body = typeof doc?.body === 'string' ? doc.body.trim() : '';
4355
+ const lead = typeof item?.lead === 'string' ? item.lead.trim() : '';
4356
+ const insight = typeof item?.insight === 'string' ? item.insight.trim() : '';
4357
+ if (!title || !body || !lead || !insight) continue;
4358
+ const sourceUri = typeof doc?.sourceUri === 'string' ? doc.sourceUri.trim() : '';
4359
+ const sourceLine = sourceUri ? `[来源:${sourceLabelForDoc(doc ?? {}, sourceUri)}](${sourceUri})` : '';
4360
+ const parts = [`## ${title}`, lead, body, insight, ...(sourceLine ? [sourceLine] : [])];
4361
+ articleSections.push(parts.join('\n\n'));
4362
+ }
4363
+ if (articleSections.length > 0 && outro !== '') {
4364
+ const articleDir = join(workDir, '22-article');
4365
+ sted('mkdir', articleDir, () => mkdirSync(articleDir, { recursive: true }));
4366
+ const articleOut = join(articleDir, `${runDate}_06_article-write_body.md`);
4367
+ const articleText = `${[EDITORIAL_ARTICLE_OPENING, ...articleSections, outro].join('\n\n')}\n`;
4368
+ sted('write', articleOut, () => writeRaw(articleOut, articleText));
4369
+ console.log(`editorial-render: wrote article body (${articleSections.length} item(s)): ${articleOut}`);
4370
+ } else {
4371
+ console.log('editorial-render: skipping article body (missing items/doc fields or outro)');
4372
+ }
4373
+
4374
+ // 2. Cover brief: 23-cover/<date>_07_cover-brief.json
4375
+ const coverSubject = typeof master.cover === 'string' ? master.cover.trim() : '';
4376
+ if (coverSubject !== '') {
4377
+ const coverDir = join(workDir, '23-cover');
4378
+ sted('mkdir', coverDir, () => mkdirSync(coverDir, { recursive: true }));
4379
+ const coverOut = join(coverDir, `${runDate}_07_cover-brief.json`);
4380
+ sted('write', coverOut, () => writeJsonText(coverOut, { subject: coverSubject }));
4381
+ console.log(`editorial-render: wrote cover brief: ${coverOut}`);
4382
+ } else {
4383
+ console.log('editorial-render: skipping cover brief (missing cover in script plan)');
4384
+ }
4385
+
4386
+ // 3. XHS card plan: 25-xhs/<date>_18_xhs_plan.json (only when cards_enabled === 'true')
4387
+ if (cardsEnabled === 'true') {
4388
+ const cardsMeta =
4389
+ master.cards !== null && typeof master.cards === 'object'
4390
+ ? (master.cards as Record<string, unknown>)
4391
+ : null;
4392
+ const cardTitle = typeof cardsMeta?.title === 'string' ? cardsMeta.title.trim() : '';
4393
+ const cardBody = typeof cardsMeta?.body === 'string' ? cardsMeta.body.trim() : '';
4394
+ const contentItems = orderedIds
4395
+ .map((id) => ({ id, item: itemById.get(id), doc: docById.get(id) }))
4396
+ .filter(({ item }) => {
4397
+ const c = item?.card as Record<string, unknown> | undefined;
4398
+ return (
4399
+ c !== null &&
4400
+ typeof c === 'object' &&
4401
+ typeof c.copy === 'string' &&
4402
+ c.copy.trim() !== '' &&
4403
+ Array.isArray(c.points) &&
4404
+ c.points.length >= 1 &&
4405
+ typeof c.subject === 'string' &&
4406
+ c.subject.trim() !== ''
4407
+ );
4408
+ });
4409
+ const maxContent = Math.max(1, cardsCount - 2);
4410
+ const chosenContent = contentItems.slice(0, maxContent);
4411
+ if (cardTitle !== '' && cardBody !== '' && chosenContent.length > 0) {
4412
+ const firstSubject =
4413
+ coverSubject ||
4414
+ ((chosenContent[0]?.item?.card as { subject?: string } | undefined)?.subject ?? '').trim() ||
4415
+ 'Minimalist geometric light sculpture on a dark studio table';
4416
+ const introText =
4417
+ typeof master.intro === 'string' && master.intro.trim() !== '' ? master.intro.trim() : cardTitle;
4418
+ const coverPoints = chosenContent
4419
+ .slice(0, 4)
4420
+ .map(({ item }) => ((item?.card as { copy?: string } | undefined)?.copy || '').trim())
4421
+ .filter(Boolean);
4422
+ const renderedCards: Record<string, unknown>[] = [
4423
+ {
4424
+ role: 'cover',
4425
+ copy: cardTitle.slice(0, 12),
4426
+ points: coverPoints.length >= 2 ? coverPoints : [cardTitle, introText.slice(0, 36)],
4427
+ layout: 'hero',
4428
+ subject: firstSubject,
4429
+ sourcePoint: introText,
4430
+ },
4431
+ ...chosenContent.map(({ id, item, doc }) => {
4432
+ const card = item?.card as { copy: string; points: string[]; subject: string };
4433
+ const lead = typeof item?.lead === 'string' && item.lead.trim() !== '' ? item.lead.trim() : '';
4434
+ const docTitle = typeof doc?.title === 'string' && doc.title.trim() !== '' ? doc.title.trim() : id;
4435
+ return {
4436
+ role: 'content',
4437
+ copy: card.copy.trim(),
4438
+ points: card.points
4439
+ .map((p) => String(p).trim())
4440
+ .filter(Boolean)
4441
+ .slice(0, 4),
4442
+ layout: 'split',
4443
+ subject: card.subject.trim(),
4444
+ sourcePoint: lead || docTitle,
4445
+ itemId: id,
4446
+ };
4447
+ }),
4448
+ ];
4449
+ // Pad content cards if chosenContent.length < maxContent so total length equals cardsCount
4450
+ while (renderedCards.length < cardsCount - 1) {
4451
+ const seed = renderedCards[renderedCards.length - 1] as Record<string, unknown>;
4452
+ renderedCards.push({ ...seed });
4453
+ }
4454
+ const closingPoints = [outro || introText, cardTitle].filter(Boolean);
4455
+ renderedCards.push({
4456
+ role: 'closing',
4457
+ copy: '本期小结',
4458
+ points: closingPoints.length >= 2 ? closingPoints : [outro || cardTitle, '感谢关注每日AI乱炖'],
4459
+ layout: 'summary',
4460
+ subject: firstSubject,
4461
+ sourcePoint: outro || introText,
4462
+ });
4463
+ const xhsDir = join(workDir, '25-xhs');
4464
+ sted('mkdir', xhsDir, () => mkdirSync(xhsDir, { recursive: true }));
4465
+ const xhsOut = join(xhsDir, `${runDate}_18_xhs_plan.json`);
4466
+ sted('write', xhsOut, () =>
4467
+ writeJsonText(xhsOut, {
4468
+ title: cardTitle,
4469
+ body: cardBody,
4470
+ cards: renderedCards,
4471
+ }),
4472
+ );
4473
+ console.log(`editorial-render: wrote xhs card plan (${renderedCards.length} card(s)): ${xhsOut}`);
4474
+ } else {
4475
+ console.log('editorial-render: skipping xhs card plan (missing cards.title/body or items[].card)');
4476
+ }
4477
+ }
4478
+
4479
+ // 4 & 5. Social posts: 26-social/{x,threads}.md and 26-social/{weibo,linkedin}.md
4480
+ const social =
4481
+ master.social !== null && typeof master.social === 'object' ? (master.social as Record<string, unknown>) : null;
4482
+ const socialShort = typeof social?.short === 'string' ? social.short.trim() : '';
4483
+ const socialDir = join(workDir, '26-social');
4484
+ if (socialShort !== '') {
4485
+ sted('mkdir', socialDir, () => mkdirSync(socialDir, { recursive: true }));
4486
+ for (const name of ['x.md', 'threads.md']) {
4487
+ const out = join(socialDir, name);
4488
+ sted('write', out, () => writeRaw(out, `${socialShort}\n`));
4489
+ }
4490
+ console.log('editorial-render: wrote 26-social/{x,threads}.md');
4491
+ } else {
4492
+ console.log('editorial-render: skipping social short posts (missing social.short in script plan)');
4493
+ }
4494
+ const socialLong = typeof social?.long === 'string' ? social.long.trim() : '';
4495
+ if (socialLong !== '') {
4496
+ sted('mkdir', socialDir, () => mkdirSync(socialDir, { recursive: true }));
4497
+ for (const name of ['weibo.md', 'linkedin.md']) {
4498
+ const out = join(socialDir, name);
4499
+ sted('write', out, () => writeRaw(out, `${socialLong}\n`));
4500
+ }
4501
+ console.log('editorial-render: wrote 26-social/{weibo,linkedin}.md');
4502
+ } else {
4503
+ console.log('editorial-render: skipping social long posts (missing social.long in script plan)');
4504
+ }
4505
+ }
4506
+
4043
4507
  export function dailyShowNotes(args: string[]): void {
4044
4508
  const [workDir = '', runDate = '', pluginsPath = '', title = ''] = args;
4045
4509
  const article = join(workDir, '22-article', `${runDate}_06_article_content.json`);
@@ -61,10 +61,11 @@ export const SCRIPTS_DIR = import.meta.dir;
61
61
  export const VALIDATE_SIDECAR = 'validate-voicescript.ts';
62
62
  const WRAP_SIDECAR = 'wrap-voicescript-doc.ts';
63
63
  const RENDER_SIDECAR = 'render-md.ts';
64
- // F613: the zh style contract the script-write prompt reads (snapshot stage) and dailynews-gen
65
- // consumes (DAILYNEWS_STYLE_FILE); no workflow var carries an override, so the chain is
66
- // $KK_WORKFLOWS_DIR -> package copy -> $HOME.
67
- export const STYLE_SIDECAR = 'daily-voice-style.zh.md';
64
+ // ADR-045: daily-news editorial policy lives in the kk:news-editorial skill, shipped beside this
65
+ // scripts dir in the same package. Its style contract is what the `write` mode reads (via the run
66
+ // snapshot) and dailynews-gen validates against (DAILYNEWS_STYLE_FILE).
67
+ export const EDITORIAL_SKILL_DIR = join(SCRIPTS_DIR, '..', 'skills', 'news-editorial');
68
+ export const STYLE_CONTRACT = join(EDITORIAL_SKILL_DIR, 'references', 'style-contract.zh.md');
68
69
  const REVISE_COUNT = '.revise-count';
69
70
  /** Bounded draft-remediation counter for the daily safety loop (20261005). */
70
71
  export const SAFETY_REVISE_COUNT = '.safety-revise-count';
@@ -1877,6 +1878,21 @@ if (import.meta.main)
1877
1878
  dailyScriptStyle(rest);
1878
1879
  break;
1879
1880
  }
1881
+ case 'daily-script-check': {
1882
+ const { dailyScriptCheck } = await loadDailyStages();
1883
+ dailyScriptCheck(rest);
1884
+ break;
1885
+ }
1886
+ case 'daily-curate-check': {
1887
+ const { dailyCurateCheck } = await loadDailyStages();
1888
+ dailyCurateCheck(rest);
1889
+ break;
1890
+ }
1891
+ case 'daily-editorial-render': {
1892
+ const { dailyEditorialRender } = await loadDailyStages();
1893
+ dailyEditorialRender(rest);
1894
+ break;
1895
+ }
1880
1896
  case 'daily-script': {
1881
1897
  const { dailyScript } = await loadDailyStages();
1882
1898
  dailyScript(rest);
@@ -21,7 +21,7 @@ LLM-as-judge is **not** a knowledge-kit plugin kind (ADR-007 amdt 2026-08-07). I
21
21
 
22
22
  - A spur workflow needs a quality gate between `generate` and `publish`.
23
23
  - A user asks to "judge", "evaluate", "quality-check", or "gate" generated content.
24
- - A workflow or skill names a rubric (`tech-accuracy` / `brand-tone` / `compliance` / `general`).
24
+ - A workflow or skill names a rubric (`tech-accuracy` / `brand-tone` / `compliance` / `news-balance` / `general`).
25
25
 
26
26
  ## Verdict output contract
27
27
 
@@ -99,7 +99,7 @@ and `escalate` flag; treat `LOW` as a signal to demand human review before publi
99
99
  1. **Read the Content** from the path given in the evaluation request (the `content.json` written
100
100
  by the preceding `kk executor run <generator>` step).
101
101
  2. **Select the rubric** — named in the request (`tech-accuracy`, `brand-tone`,
102
- `compliance`, `general`). Default: `general`.
102
+ `compliance`, `news-balance`, `general`). Default: `general`.
103
103
  3. **Evaluate each criterion** in the rubric. For each, produce a `pass` / `fail` / `warn` status
104
104
  backed by a specific, quotable evidence reference (quote, section, claim).
105
105
  4. **Score** — weighted aggregate per the rubric's scoring rules. `score` is 0.0–1.0.
@@ -127,7 +127,7 @@ and `escalate` flag; treat `LOW` as a signal to demand human review before publi
127
127
 
128
128
  ## Rubrics
129
129
 
130
- Four built-in rubrics. Full definitions, weights, and thresholds live in
130
+ Five built-in rubrics. Full definitions, weights, and thresholds live in
131
131
  `references/rubrics.md`. The skill body intentionally stays lean — load the reference when
132
132
  evaluating.
133
133
 
@@ -137,6 +137,7 @@ evaluating.
137
137
  | `tech-accuracy` | Technical content | factual correctness vs. sources, no fabricated citations, code correctness |
138
138
  | `brand-tone` | Published voice | tone consistency, banned-phrase compliance, audience fit |
139
139
  | `compliance` | Sensitive content | no prohibited claims, disclosure presence, jurisdictional rules |
140
+ | `news-balance` | Daily news script plan | news-first, commentary-share, no-restatement |
140
141
 
141
142
  ## Prompt template
142
143
 
@@ -147,7 +148,7 @@ Use this template when the skill is invoked by an `agent.run` step. The workflow
147
148
  You are an LLM-as-judge evaluating generated content for the knowledge-kit pipeline.
148
149
 
149
150
  Content to evaluate: {{content_path}}
150
- Rubric: {{rubric}} # general | tech-accuracy | brand-tone | compliance
151
+ Rubric: {{rubric}} # general | tech-accuracy | brand-tone | compliance | news-balance
151
152
  Output verdict to: {{verdict_path}}
152
153
 
153
154
  Load the rubric definition from plugins/kk/skills/content-judge/references/rubrics.md (section: {{rubric}}).
@@ -100,6 +100,20 @@ Thresholds: `pass_threshold = 0.85`
100
100
  - `data-handling`: scan for PII that may have been carried through from input Docs (emails,
101
101
  phone numbers, national IDs). Leakage is a `fail`.
102
102
 
103
+ ## news-balance
104
+
105
+ For daily news episode script plans (`kk-daily-ai-voice3` advisory `editorial-judge` sensor, task 0345 R11). Evaluates the balance of news reporting, commentary, and technical depth against `kk:news-editorial`.
106
+
107
+ | Criterion | Weight | Must-pass |
108
+ |-----------|--------|-----------|
109
+ | `news-first` — `intro` and every `lead` name concrete actors and actions, not abstract essay themes | 0.30 | yes |
110
+ | `commentary-share` — each `insight` is ≤⅓ of the item's spoken length and contains no generic methodology or shopping-list advice | 0.25 | yes |
111
+ | `no-restatement` — `insight` adds judgement rather than repeating the body's facts or numbers | 0.20 | yes |
112
+ | `tech-depth` — mechanisms are explained in 1–2 sentences without textbook primers | 0.15 | no |
113
+ | `ordering` — items are ordered by news weight (urgency × impact), with the strongest concrete news item leading | 0.10 | no |
114
+
115
+ Thresholds: `pass_threshold = 0.80`
116
+
103
117
  ## Adding a custom rubric
104
118
 
105
119
  1. Add a section to this file with a unique heading and a criteria table matching the schema