@gobing-ai/knowledge-kit 0.1.8 → 0.1.10
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/board/collect/index.js +725 -1101
- package/dist/board/daily-voice/index.js +722 -101
- package/dist/board/itc/index.js +13 -0
- package/dist/index.js +1796 -102
- package/package.json +1 -1
- package/plugins/generations/content-gen/dist/index.js +4 -0
- package/plugins/generations/core-facts-gen/dist/index.js +4 -0
- package/plugins/generations/daily-article-gen/dist/index.js +4 -0
- package/plugins/generations/dailynews-gen/dist/index.js +4 -0
- package/plugins/generations/episode-plan-gen/dist/index.js +102 -1
- package/plugins/generations/episode-plan-gen/src/index.ts +2 -4
- package/plugins/generations/gemini-tts/dist/index.js +4 -0
- package/plugins/generations/image-gen/dist/index.js +4 -0
- package/plugins/generations/news-report-gen/dist/index.js +4 -0
- package/plugins/generations/voice-gen/dist/index.js +4 -0
- package/plugins/generations/voxcpm-tts/dist/index.js +4 -0
- package/plugins/ingestions/aihot-ingest/dist/index.js +4 -0
- package/plugins/ingestions/horizon-ingest/dist/index.js +28 -1
- package/plugins/ingestions/horizon-ingest/src/index.ts +34 -0
- package/plugins/ingestions/last30days-ingest/dist/index.js +11 -3
- package/plugins/ingestions/last30days-ingest/src/index.ts +14 -3
- package/plugins/ingestions/web-search/dist/index.js +4 -0
- package/plugins/kk/config.example.yaml +8 -0
- package/plugins/kk/plugin.json +1 -1
- package/plugins/kk/publish-presets/daily-ai-voice.json +123 -0
- package/plugins/kk/schedules/kk-daily-publish3-cn-peak.cron.tmpl +20 -0
- package/plugins/kk/schedules/kk-daily-publish3-overseas.cron.tmpl +17 -0
- package/plugins/kk/scripts/daily-stages.ts +665 -78
- package/plugins/kk/scripts/kk-workflow-stages.ts +11 -1
- package/plugins/kk/scripts/sentences.ts +134 -0
- package/plugins/kk/scripts/series-names.ts +124 -0
- package/plugins/kk/scripts/topic-boundary.json +18 -83
- package/plugins/kk/workflows/kk-collect.yaml +2 -0
- package/plugins/kk/workflows/kk-daily-ai-voice2.yaml +112 -9
- package/plugins/kk/workflows/kk-daily-ai-voice3.yaml +842 -0
- package/plugins/kk/workflows/kk-daily-publish3.yaml +282 -0
- package/plugins/kk/workflows/kk-daily-safety.yaml +50 -5
- package/plugins/publishings/bilibili-pub/dist/index.js +4 -0
- package/plugins/publishings/cnblogs-pub/dist/index.js +4 -0
- package/plugins/publishings/csdn-pub/dist/index.js +4 -0
- package/plugins/publishings/csdn-pub/dist/profile-check.js +4 -0
- package/plugins/publishings/emdash-pub/dist/index.js +4 -0
- package/plugins/publishings/infoq-pub/dist/index.js +4 -0
- package/plugins/publishings/infoq-pub/dist/profile-check.js +4 -0
- package/plugins/publishings/jike-pub/dist/index.js +4 -0
- package/plugins/publishings/jike-pub/dist/profile-check.js +4 -0
- package/plugins/publishings/juejin-pub/dist/index.js +4 -0
- package/plugins/publishings/juejin-pub/dist/profile-check.js +4 -0
- package/plugins/publishings/medium-pub/dist/index.js +4 -0
- package/plugins/publishings/podcast-pub/README.md +65 -0
- package/plugins/publishings/podcast-pub/dist/index.js +1906 -374
- package/plugins/publishings/podcast-pub/plugin.json +1 -1
- package/plugins/publishings/podcast-pub/src/backfill.ts +465 -0
- package/plugins/publishings/podcast-pub/src/index.ts +211 -34
- package/plugins/publishings/podcast-pub/src/microfeed-client.ts +32 -3
- package/plugins/publishings/podcast-pub/src/seo.ts +1079 -0
- package/plugins/publishings/podcast-pub/src/show-notes.ts +74 -14
- package/plugins/publishings/qiita-pub/dist/index.js +4 -0
- package/plugins/publishings/shipinhao-pub/dist/index.js +4 -0
- package/plugins/publishings/sspai-pub/dist/index.js +4 -0
- package/plugins/publishings/substack-pub/dist/index.js +4 -0
- package/plugins/publishings/substack-pub/dist/profile-check.js +4 -0
- package/plugins/publishings/surfdash-pub/dist/index.js +313 -32
- package/plugins/publishings/surfdash-pub/src/digest-check.ts +218 -0
- package/plugins/publishings/surfdash-pub/src/index.ts +170 -39
- package/plugins/publishings/surfdash-pub/src/patch.ts +44 -7
- package/plugins/publishings/threads-pub/dist/index.js +4 -0
- package/plugins/publishings/wechatmp-pub/dist/index.js +4 -0
- package/plugins/publishings/wechatmp-pub/dist/profile-check.js +4 -0
- package/plugins/publishings/weibo-pub/dist/index.js +4 -0
- package/plugins/publishings/weibo-pub/dist/profile-check.js +4 -0
- package/plugins/publishings/x-pub/dist/index.js +4 -0
- package/plugins/publishings/x-pub/dist/profile-check.js +4 -0
- package/plugins/publishings/xhs-pub/dist/index.js +4 -0
- package/plugins/publishings/xhs-pub/dist/profile-check.js +4 -0
- package/plugins/publishings/xiaoyuzhou-pub/dist/index.js +4 -0
- package/plugins/publishings/zenn-pub/dist/index.js +4 -0
|
@@ -1426,7 +1426,12 @@ function dailyPatchSurfdash(args: string[]): void {
|
|
|
1426
1426
|
const pod = targetOf(podcastResult);
|
|
1427
1427
|
const postPath = (sd.metadata as Record<string, unknown> | undefined)?.postPath;
|
|
1428
1428
|
if (typeof postPath !== 'string' || postPath === '') {
|
|
1429
|
-
|
|
1429
|
+
// A surfdash publish that never landed (2026-10-21: its `git push` was rejected with
|
|
1430
|
+
// `fetch first`) leaves no postPath to patch. `publish-classify` has already recorded the
|
|
1431
|
+
// run as `partial`, and failing here turned a published podcast episode into a `failed`
|
|
1432
|
+
// run — the folded tail must not escalate what the classifier already routed.
|
|
1433
|
+
console.error('patch-surfdash: surfdash result carries no postPath — nothing to patch, skip');
|
|
1434
|
+
return;
|
|
1430
1435
|
}
|
|
1431
1436
|
const resultPath = join(workDir, '31-publish', `${runDate}_23_patch-surfdash_sync-result.json`);
|
|
1432
1437
|
if (process.env.KK_SURFDASH_PATCH_OFFLINE === '1') {
|
|
@@ -1742,6 +1747,11 @@ if (import.meta.main)
|
|
|
1742
1747
|
dailyPublishWechat(rest);
|
|
1743
1748
|
break;
|
|
1744
1749
|
}
|
|
1750
|
+
case 'daily-bundle-variants': {
|
|
1751
|
+
const { dailyBundleVariants } = await loadDailyStages();
|
|
1752
|
+
dailyBundleVariants(rest);
|
|
1753
|
+
break;
|
|
1754
|
+
}
|
|
1745
1755
|
case 'daily-publish-classify':
|
|
1746
1756
|
dailyPublishClassify(rest);
|
|
1747
1757
|
break;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quote-aware sentence splitting (2026-10-24).
|
|
3
|
+
*
|
|
4
|
+
* NOTE: This file mirrors `packages/kk-core/src/sentences.ts`. It exists as a separate script file
|
|
5
|
+
* because `plugins/kk/scripts/daily-stages.ts` runs from the installed package or from standalone
|
|
6
|
+
* runner invocations with Node/Bun builtins only and cannot import `@gobing-ai/kk-core`.
|
|
7
|
+
* Keep the algorithm and behaviors in sync between both files.
|
|
8
|
+
*
|
|
9
|
+
* Rules:
|
|
10
|
+
* - splits after 。!? (CJK) always, and after `.`/`!`/`?` only when followed by whitespace or end of text
|
|
11
|
+
* (so Character.AI, llama.cpp, LWN.net, whatmodel.sh, claude.dev, v1.2, 3.6% never split);
|
|
12
|
+
* - keeps any run of closing quotes/brackets ( ” ’ 」 』 ) ) " ' ) attached to the preceding sentence;
|
|
13
|
+
* - never splits while inside an open 「」, 『』, “”, () pair (track depth; unbalanced → treat the rest as one sentence);
|
|
14
|
+
* - trims each sentence, drops empties; joining the results with '' (zh) loses no non-whitespace characters.
|
|
15
|
+
* - `{ cjkOnly: true }` splits only on 。!? — the zh/ja briefing paths, where an English body must
|
|
16
|
+
* stay one unit (its first-two-sentences cut would otherwise glue "vtables.It" and split "U.S.").
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const CJK_TERMINATORS = new Set(['。', '!', '?']);
|
|
20
|
+
const LATIN_TERMINATORS = new Set(['.', '!', '?']);
|
|
21
|
+
const ATTACHED_CLOSERS = new Set(['”', '’', '」', '』', ')', ')', '"', "'"]);
|
|
22
|
+
|
|
23
|
+
const OPEN_PAIRS: Record<string, string> = {
|
|
24
|
+
'「': '」',
|
|
25
|
+
'『': '』',
|
|
26
|
+
'“': '”',
|
|
27
|
+
'(': ')',
|
|
28
|
+
'(': ')',
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/** Splitter options; `cjkOnly` drops the Latin `.!?` terminators (zh/ja split only on 。!?). */
|
|
32
|
+
export interface SplitSentencesOptions {
|
|
33
|
+
cjkOnly?: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const CLOSE_PAIRS: Record<string, string> = {
|
|
37
|
+
'」': '「',
|
|
38
|
+
'』': '『',
|
|
39
|
+
'”': '“',
|
|
40
|
+
')': '(',
|
|
41
|
+
')': '(',
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Splits text into complete sentences while respecting CJK/Latin terminators,
|
|
46
|
+
* attached closing quotes, and nested quote/bracket pairs.
|
|
47
|
+
*/
|
|
48
|
+
export function splitSentences(text: string, options: SplitSentencesOptions = {}): string[] {
|
|
49
|
+
if (!text || typeof text !== 'string') return [];
|
|
50
|
+
const results: string[] = [];
|
|
51
|
+
let sentenceStart = 0;
|
|
52
|
+
let stack: string[] = [];
|
|
53
|
+
let i = 0;
|
|
54
|
+
const isTerminator = (c: string): boolean =>
|
|
55
|
+
CJK_TERMINATORS.has(c) || (options.cjkOnly !== true && LATIN_TERMINATORS.has(c));
|
|
56
|
+
|
|
57
|
+
while (i < text.length) {
|
|
58
|
+
const ch = text[i] as string;
|
|
59
|
+
|
|
60
|
+
if (isTerminator(ch)) {
|
|
61
|
+
let termEnd = i;
|
|
62
|
+
let hasCjkTerm = false;
|
|
63
|
+
let hasLatinTerm = false;
|
|
64
|
+
while (termEnd < text.length) {
|
|
65
|
+
const termChar = text[termEnd];
|
|
66
|
+
if (!termChar || !isTerminator(termChar)) {
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
if (CJK_TERMINATORS.has(termChar)) hasCjkTerm = true;
|
|
70
|
+
if (LATIN_TERMINATORS.has(termChar)) hasLatinTerm = true;
|
|
71
|
+
termEnd++;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
let closerEnd = termEnd;
|
|
75
|
+
const tempStack = [...stack];
|
|
76
|
+
while (closerEnd < text.length) {
|
|
77
|
+
const closer = text[closerEnd];
|
|
78
|
+
if (!closer || !ATTACHED_CLOSERS.has(closer)) {
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
const top = tempStack[tempStack.length - 1];
|
|
82
|
+
if (top !== undefined && top === closer) {
|
|
83
|
+
tempStack.pop();
|
|
84
|
+
} else if (CLOSE_PAIRS[closer]) {
|
|
85
|
+
const idx = tempStack.lastIndexOf(closer);
|
|
86
|
+
if (idx >= 0) tempStack.splice(idx, 1);
|
|
87
|
+
}
|
|
88
|
+
closerEnd++;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
let isBoundary = false;
|
|
92
|
+
if (tempStack.length === 0) {
|
|
93
|
+
if (hasCjkTerm) {
|
|
94
|
+
isBoundary = true;
|
|
95
|
+
} else if (hasLatinTerm) {
|
|
96
|
+
const nextChar = text[closerEnd];
|
|
97
|
+
if (closerEnd === text.length || (nextChar !== undefined && /\s/.test(nextChar))) {
|
|
98
|
+
isBoundary = true;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (isBoundary) {
|
|
104
|
+
const sentence = text.slice(sentenceStart, closerEnd).trim();
|
|
105
|
+
if (sentence.length > 0) {
|
|
106
|
+
results.push(sentence);
|
|
107
|
+
}
|
|
108
|
+
sentenceStart = closerEnd;
|
|
109
|
+
stack = tempStack;
|
|
110
|
+
i = closerEnd;
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const openMatch = OPEN_PAIRS[ch];
|
|
116
|
+
const closeMatch = CLOSE_PAIRS[ch];
|
|
117
|
+
if (openMatch) {
|
|
118
|
+
stack.push(openMatch);
|
|
119
|
+
} else if (closeMatch) {
|
|
120
|
+
const idx = stack.lastIndexOf(ch);
|
|
121
|
+
if (idx >= 0) stack.splice(idx, 1);
|
|
122
|
+
}
|
|
123
|
+
i++;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (sentenceStart < text.length) {
|
|
127
|
+
const tail = text.slice(sentenceStart).trim();
|
|
128
|
+
if (tail.length > 0) {
|
|
129
|
+
results.push(tail);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return results;
|
|
134
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical daily-series naming for the kk-daily-* pipelines (2026-10-23 regression).
|
|
3
|
+
*
|
|
4
|
+
* The 2026-10-23 surfdash run: the translate agent invented locale series names (ja 毎日AIブレンド,
|
|
5
|
+
* en "Daily AI Medley"/"hodgepodge" phrasing, zh-name listen links inside en/ja posts). Series
|
|
6
|
+
* names are product identity, not translation — this module is the pipeline's single definition of
|
|
7
|
+
* the per-locale series/show names plus the deterministic rewrite that repairs the known bad set
|
|
8
|
+
* before the CMS sync (plugins/kk/scripts/daily-stages.ts translate-sync gate).
|
|
9
|
+
*
|
|
10
|
+
* Pure string work only, no I/O. URL segments (http/https) are never touched — a slug or
|
|
11
|
+
* percent-encoded path may legitimately embed any of these tokens. surfdash-pub's cross-link table
|
|
12
|
+
* (plugins/publishings/surfdash-pub/src/patch.ts) cannot import this file (the kk scripts resolve
|
|
13
|
+
* from the installed package with node builtins only), so it repeats the names; tests assert the
|
|
14
|
+
* two tables agree.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** The locales a daily post is published in (mirrors surfdash-pub's PATCH_LOCALES). */
|
|
18
|
+
export type DailyLocale = 'zh' | 'en' | 'ja';
|
|
19
|
+
|
|
20
|
+
/** One locale's daily-series naming: the series (episode-column) name and the show it runs under. */
|
|
21
|
+
export interface DailySeriesNames {
|
|
22
|
+
series: string;
|
|
23
|
+
show: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** The single per-locale table; en/ja names are fixed identity, the zh column name is the source. */
|
|
27
|
+
export const DAILY_SERIES_NAMES: Record<DailyLocale, DailySeriesNames> = {
|
|
28
|
+
zh: { series: '每日AI乱炖', show: '咸话咸说' },
|
|
29
|
+
en: { series: 'Daily AI Stew', show: 'Salty Talk' },
|
|
30
|
+
ja: { series: 'デイリーAIシチュー', show: '咸話咸説' },
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Retired series spellings that must never be emitted in any locale. ごった煮 is deliberately absent:
|
|
35
|
+
* it is ordinary ja vocabulary, so rewriting or failing on it would butcher legitimate prose.
|
|
36
|
+
*/
|
|
37
|
+
const RETIRED_SERIES_NAMES = ['毎日AIブレンド', 'Daily AI Medley', 'Daily AI Briefing'] as const;
|
|
38
|
+
|
|
39
|
+
/** All canonical series tokens — each locale's own name is correct there, wrong-locale elsewhere. */
|
|
40
|
+
const SERIES_TOKENS = ['每日AI乱炖', 'Daily AI Stew', 'デイリーAIシチュー'] as const;
|
|
41
|
+
|
|
42
|
+
const escapeRe = (token: string): string => token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
43
|
+
|
|
44
|
+
/** Latin tokens match case-insensitively with word boundaries; CJK tokens match exactly. */
|
|
45
|
+
const tokenRe = (token: string): RegExp =>
|
|
46
|
+
/^[\x20-\x7E]+$/.test(token) ? new RegExp(`\\b${escapeRe(token)}\\b`, 'gi') : new RegExp(escapeRe(token), 'g');
|
|
47
|
+
|
|
48
|
+
/** Splits markdown into alternating non-URL / URL segments (`split` keeps the capture group). */
|
|
49
|
+
const URL_SPLIT = /(https?:\/\/[^\s<>"')\]]+)/g;
|
|
50
|
+
|
|
51
|
+
function mapOutsideUrls(markdown: string, fn: (text: string) => string): string {
|
|
52
|
+
return markdown
|
|
53
|
+
.split(URL_SPLIT)
|
|
54
|
+
.map((part, index) => (index % 2 === 1 ? part : fn(part)))
|
|
55
|
+
.join('');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** The short/spacing zh form (`AI 乱炖`/`AI乱炖`) as a series name; `每日` already prefixing it is correct zh. */
|
|
59
|
+
const SHORT_ZH_FORM = /(?<!每日)AI ?乱炖/g;
|
|
60
|
+
|
|
61
|
+
/** Every retired or wrong-locale series reference in `text`, as written, deduplicated in order. */
|
|
62
|
+
function retiredIn(text: string, locale: DailyLocale): string[] {
|
|
63
|
+
const canonical = DAILY_SERIES_NAMES[locale].series;
|
|
64
|
+
const patterns: RegExp[] = [
|
|
65
|
+
...RETIRED_SERIES_NAMES.map(tokenRe),
|
|
66
|
+
...SERIES_TOKENS.filter((token) => token !== canonical).map(tokenRe),
|
|
67
|
+
SHORT_ZH_FORM,
|
|
68
|
+
/\bhodgepodge\b/gi,
|
|
69
|
+
];
|
|
70
|
+
const found: string[] = [];
|
|
71
|
+
for (const pattern of patterns) {
|
|
72
|
+
for (const match of text.matchAll(pattern)) {
|
|
73
|
+
if (!found.includes(match[0])) found.push(match[0]);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return found;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Retired / wrong-locale series references in `markdown`, as written (URL segments excluded).
|
|
81
|
+
* Detection only — the repair side is {@link normalizeSeriesNames}.
|
|
82
|
+
*/
|
|
83
|
+
export function findRetiredSeriesNames(markdown: string, locale: DailyLocale): string[] {
|
|
84
|
+
const found: string[] = [];
|
|
85
|
+
mapOutsideUrls(markdown, (text) => {
|
|
86
|
+
for (const hit of retiredIn(text, locale)) {
|
|
87
|
+
if (!found.includes(hit)) found.push(hit);
|
|
88
|
+
}
|
|
89
|
+
return text;
|
|
90
|
+
});
|
|
91
|
+
return found;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Rewrite every known retired / wrong-locale series reference to the locale's canonical series
|
|
96
|
+
* name and list what could not be rewritten safely: a bare `hodgepodge` is vocabulary, not a name
|
|
97
|
+
* — rewriting it would butcher prose, so it is reported for hand repair instead. Idempotent: the
|
|
98
|
+
* rewrite only ever produces the locale's own canonical name, which no rule matches.
|
|
99
|
+
*/
|
|
100
|
+
export function normalizeSeriesNames(
|
|
101
|
+
markdown: string,
|
|
102
|
+
locale: DailyLocale,
|
|
103
|
+
): {
|
|
104
|
+
/** The input with every known bad series token rewritten (URL segments untouched). */
|
|
105
|
+
markdown: string;
|
|
106
|
+
/** Retired series references left over that the caller must report or repair by hand. */
|
|
107
|
+
remaining: string[];
|
|
108
|
+
} {
|
|
109
|
+
const canonical = DAILY_SERIES_NAMES[locale].series;
|
|
110
|
+
const next = mapOutsideUrls(markdown, (text) => {
|
|
111
|
+
let out = text;
|
|
112
|
+
if (locale === 'en') {
|
|
113
|
+
// The observed en intro phrase — rewritten as a phrase first so the bare word never
|
|
114
|
+
// produces "daily AI Daily AI Stew".
|
|
115
|
+
out = out.replace(/\byour daily AI hodgepodge\b/gi, 'Your Daily AI Stew');
|
|
116
|
+
out = out.replace(/\bdaily AI hodgepodge\b/gi, 'Daily AI Stew');
|
|
117
|
+
}
|
|
118
|
+
for (const token of [...RETIRED_SERIES_NAMES, ...SERIES_TOKENS.filter((name) => name !== canonical)]) {
|
|
119
|
+
out = out.replace(tokenRe(token), () => canonical);
|
|
120
|
+
}
|
|
121
|
+
return out.replace(SHORT_ZH_FORM, canonical);
|
|
122
|
+
});
|
|
123
|
+
return { markdown: next, remaining: findRetiredSeriesNames(next, locale) };
|
|
124
|
+
}
|
|
@@ -1,92 +1,27 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version":
|
|
3
|
-
"note": "Task 0189 R2 topic admission for the AI 乱炖 column, with the 2026-10-02 operator scope correction: the political-diplomatic rule is China-only (中国政治/外交主线). Foreign public-affairs items are NOT floor-excluded — they flow to the plan and are handled by the article-write/script-write/cover 选题边界 prompts (skip + note / plan `excluded`) and the content-safety review. Excluded items are recorded with id, rule, reason and the matched keyword. Sources are never deleted or modified. Scan covers the title plus the first scanChars chars of the body. Precision first: these keywords are a floor, not a sieve — a rare false negative is caught by the prompt-layer boundaries and the content-safety skill review, and a suspected false positive is restored by hand with a recorded reason (manual review), never silently.",
|
|
2
|
+
"version": 2,
|
|
4
3
|
"scanChars": 200,
|
|
5
4
|
"rules": [
|
|
6
5
|
{
|
|
7
|
-
"id": "
|
|
8
|
-
"reason": "
|
|
6
|
+
"id": "prc-leadership",
|
|
7
|
+
"reason": "中国最高领导人敏感线:现任及历任国家最高领导人属平台审核红线,不论条目是否与 AI 相关一律排除(2026-10-22 操作者裁定,取代 0189 R2 的五条规则——诉讼、出口管制、监管博弈、军事技术、社会事件本身都是 AI 产业新闻的主线,整类排除过严)。",
|
|
9
8
|
"keywords": [
|
|
10
|
-
"
|
|
11
|
-
"
|
|
9
|
+
"习近平",
|
|
10
|
+
"毛泽东",
|
|
11
|
+
"邓小平",
|
|
12
|
+
"江泽民",
|
|
13
|
+
"胡锦涛",
|
|
14
|
+
"华国锋",
|
|
15
|
+
"赵紫阳",
|
|
16
|
+
"xi jinping",
|
|
17
|
+
"jiang zemin",
|
|
18
|
+
"deng xiaoping",
|
|
19
|
+
"hu jintao",
|
|
20
|
+
"mao zedong",
|
|
21
|
+
"国家主席",
|
|
12
22
|
"总书记",
|
|
13
|
-
"
|
|
14
|
-
"
|
|
15
|
-
"大使馆",
|
|
16
|
-
"国事访问",
|
|
17
|
-
"政治局",
|
|
18
|
-
"中南海",
|
|
19
|
-
"台海",
|
|
20
|
-
"两岸关系",
|
|
21
|
-
"中美关系"
|
|
22
|
-
]
|
|
23
|
-
},
|
|
24
|
-
{
|
|
25
|
-
"id": "trade-controls",
|
|
26
|
-
"reason": "外交/贸易管制主线:制裁、关税、出口管制、实体清单等政府间管制动作属于公共事务,不是技术实践(0189 R2;涉管制的技术影响可在人工复审后改写为纯技术内容)。",
|
|
27
|
-
"keywords": [
|
|
28
|
-
"制裁",
|
|
29
|
-
"关税",
|
|
30
|
-
"出口管制",
|
|
31
|
-
"实体清单",
|
|
32
|
-
"贸易战",
|
|
33
|
-
"禁令",
|
|
34
|
-
"制裁清单",
|
|
35
|
-
"sanctions",
|
|
36
|
-
"tariff",
|
|
37
|
-
"export control",
|
|
38
|
-
"trade war",
|
|
39
|
-
"entity list"
|
|
40
|
-
]
|
|
41
|
-
},
|
|
42
|
-
{
|
|
43
|
-
"id": "military",
|
|
44
|
-
"reason": "军事主线:军队、武器、作战相关的条目一律排除(0189 R2)。",
|
|
45
|
-
"keywords": [
|
|
46
|
-
"军队",
|
|
47
|
-
"军事",
|
|
48
|
-
"解放军",
|
|
49
|
-
"武器",
|
|
50
|
-
"导弹",
|
|
51
|
-
"无人机袭击",
|
|
52
|
-
"战场",
|
|
53
|
-
"military",
|
|
54
|
-
"pentagon",
|
|
55
|
-
"missile"
|
|
56
|
-
]
|
|
57
|
-
},
|
|
58
|
-
{
|
|
59
|
-
"id": "policy-dispute",
|
|
60
|
-
"reason": "公共政策争议主线:立法、听证、反垄断等政策博弈是公共事务主线,不是技术内容(0189 R2;纯技术子话题需人工复审并重写后才可使用)。",
|
|
61
|
-
"keywords": [
|
|
62
|
-
"立法",
|
|
63
|
-
"听证",
|
|
64
|
-
"政策争议",
|
|
65
|
-
"反垄断",
|
|
66
|
-
"监管机构",
|
|
67
|
-
"监管新规",
|
|
68
|
-
"法案",
|
|
69
|
-
"regulation",
|
|
70
|
-
"antitrust",
|
|
71
|
-
"legislation",
|
|
72
|
-
"lawsuit"
|
|
73
|
-
]
|
|
74
|
-
},
|
|
75
|
-
{
|
|
76
|
-
"id": "social-event",
|
|
77
|
-
"reason": "社会公共事件主线:事故、灾害、冲突现场等社会事件的报道不属于技术分享(0189 R2)。",
|
|
78
|
-
"keywords": [
|
|
79
|
-
"社会事件",
|
|
80
|
-
"踩踏",
|
|
81
|
-
"骚乱",
|
|
82
|
-
"抗议",
|
|
83
|
-
"示威",
|
|
84
|
-
"罢工",
|
|
85
|
-
"遇难",
|
|
86
|
-
"伤亡",
|
|
87
|
-
"protest",
|
|
88
|
-
"riot",
|
|
89
|
-
"casualties"
|
|
23
|
+
"政治局常委",
|
|
24
|
+
"中南海"
|
|
90
25
|
]
|
|
91
26
|
}
|
|
92
27
|
],
|
|
@@ -110,6 +110,8 @@ states:
|
|
|
110
110
|
# the same 45m leash the daily workflow pins. Writes <work_dir>/10-ingest/<stamp>_02_collect_blended.json.
|
|
111
111
|
# A single failed source degrades to an error entry in the merge (exit 0, surviving docs kept);
|
|
112
112
|
# only an all-sources failure exits 1 and leaves the file unwritten.
|
|
113
|
+
# Empty 6h windows exit 0 with no digest; the plugin falls back to the newest digest ≤26h old
|
|
114
|
+
# (2026-10-23: Oct 21 contributed zero docs when every tick hit an empty window).
|
|
113
115
|
command: >-
|
|
114
116
|
kk executor fan-in
|
|
115
117
|
--source "aihot-ingest:${vars.work_dir}/10-ingest/source.json"
|
|
@@ -80,7 +80,12 @@ vars:
|
|
|
80
80
|
# siblings declare `role: reviewer` (capable-1), two rungs above the primary's `scribe`, so the
|
|
81
81
|
# default `inline` resolves the retry above any rung the primary reaches by climbing past an
|
|
82
82
|
# unusable cheap executor (ac5-live-1). The operator may still pin a known-good executor here.
|
|
83
|
-
|
|
83
|
+
# 2026-10-24 pin: `inline` resolved to agy-gemini, whose headless write path is dead (terminal
|
|
84
|
+
# sandbox denies workspace writes), so the retry failed the same way twice — the whole day closed
|
|
85
|
+
# with no plan. `claude` carries `--permission-mode acceptEdits --allowedTools Write Edit` and is
|
|
86
|
+
# the tier that actually delivered both producers on this date. Re-point it at whatever
|
|
87
|
+
# `spur agent doctor <name>` reports usable; an empty retry tier fails the run loudly.
|
|
88
|
+
agent_retry: "claude"
|
|
84
89
|
# QC pins — lockstep with episode-plan-gen's DEFAULT_FILTER_CONFIG (env wins), same as voice1.
|
|
85
90
|
qc_categories: "ai-news,ai-technical,ai-research,ai-industry"
|
|
86
91
|
qc_min_quality: "2"
|
|
@@ -91,9 +96,35 @@ vars:
|
|
|
91
96
|
episode_min_minutes: "15"
|
|
92
97
|
# 2026-10-09 operator decision: ingest bodies are spoken in full and today's pools render 22-46
|
|
93
98
|
# minutes at the current item counts, so the 20-minute ceiling refused real episodes before any
|
|
94
|
-
# TTS request. The pair drives
|
|
95
|
-
# gate
|
|
99
|
+
# TTS request. The pair drives the pre-spend estimates (`daily-qc-content`'s pool gate, and
|
|
100
|
+
# `daily-generate`'s script gate, which measures the authored VoiceScript) and the measured gate
|
|
101
|
+
# in `daily-episode-duration`.
|
|
96
102
|
episode_max_minutes: "25"
|
|
103
|
+
# podcast-pub per-episode SEO (D9). Defaults match the plugin. Legacy publish output:
|
|
104
|
+
# podcast_seo=false, podcast_summary_paragraph=false, podcast_timeline_inline_style=before,
|
|
105
|
+
# podcast_auto_episode_number=false, podcast_link_field=url, podcast_slug=auto,
|
|
106
|
+
# podcast_round_duration=false. Title keywords stay in podcast-pub; ID3 keeps the date-only title.
|
|
107
|
+
podcast_seo: "true"
|
|
108
|
+
podcast_episode_title_mode: "as-is"
|
|
109
|
+
podcast_episode_title_pattern: "{series} · {date}:{keywords}"
|
|
110
|
+
podcast_series_name: "每日AI乱炖"
|
|
111
|
+
podcast_show_name: "咸话咸说"
|
|
112
|
+
podcast_title_max: "55"
|
|
113
|
+
podcast_title_max_keywords: "2"
|
|
114
|
+
podcast_seo_title_suffix: "|咸话咸说"
|
|
115
|
+
podcast_seo_title_max: "60"
|
|
116
|
+
podcast_seo_description_min: "120"
|
|
117
|
+
podcast_seo_description_max: "155"
|
|
118
|
+
podcast_seo_description_source: "plan"
|
|
119
|
+
podcast_summary_paragraph: "true"
|
|
120
|
+
podcast_timeline_inline_style: "after"
|
|
121
|
+
podcast_auto_episode_number: "true"
|
|
122
|
+
podcast_title_sanitize: "normalize"
|
|
123
|
+
podcast_link_field: "none"
|
|
124
|
+
podcast_slug: "pattern"
|
|
125
|
+
podcast_slug_pattern: "daily-ai-{date}"
|
|
126
|
+
podcast_social_image: "false"
|
|
127
|
+
podcast_round_duration: "true"
|
|
97
128
|
# Run id, injected by spur at launch (0674 R5/R8) — the selections / ledger / mark-published
|
|
98
129
|
# identity. A shell command may only reference declared vars.
|
|
99
130
|
__runId: ""
|
|
@@ -200,7 +231,8 @@ nodes:
|
|
|
200
231
|
store's `selections` ledger, written by the `select` node below). Writes
|
|
201
232
|
<work_dir>/10-collect/pool.json (the fan-in blended.json shape) and prints {count}. An empty
|
|
202
233
|
pool writes [] and exits 0 — the run's no-content path, not a failure. The `daily-extract`
|
|
203
|
-
stage widens the window (×2, ×3, capped at
|
|
234
|
+
stage widens the window (×2, ×3, ×7, capped at 168h — extended 2026-10-23 when the 72h
|
|
235
|
+
ceiling closed a supply-collapsed day as no-content) only when the configured window's pool cannot
|
|
204
236
|
reach `episode_min_minutes` (2026-10-19): a published episode consumes more stories than a day
|
|
205
237
|
of collection supplies, so the tight window alone closed 2026-10-18/19 as no-content while
|
|
206
238
|
older unselected items sat in the store.
|
|
@@ -580,7 +612,7 @@ nodes:
|
|
|
580
612
|
options:
|
|
581
613
|
onError: continue
|
|
582
614
|
command: >-
|
|
583
|
-
OMNIVOICE_SPEED=1.06 kk stage daily-generate "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}" "${vars.transcode_mp3}" "${vars.vdriver}" "1" "${vars.episode_max_minutes}"
|
|
615
|
+
OMNIVOICE_SPEED=1.06 kk stage daily-generate "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}" "${vars.transcode_mp3}" "${vars.vdriver}" "1" "${vars.episode_max_minutes}" "${vars.episode_min_minutes}"
|
|
584
616
|
|
|
585
617
|
# Driver fallback chain (2026-10-15, operator decision): voxcpm -> gemini -> omni. Each sibling
|
|
586
618
|
# runs the same stage with a pinned driver; its condition re-checks the pool AND that NO upstream
|
|
@@ -601,7 +633,7 @@ nodes:
|
|
|
601
633
|
options:
|
|
602
634
|
onError: continue
|
|
603
635
|
command: >-
|
|
604
|
-
kk stage daily-generate "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}" "${vars.transcode_mp3}" "gemini" "1" "${vars.episode_max_minutes}"
|
|
636
|
+
kk stage daily-generate "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}" "${vars.transcode_mp3}" "gemini" "1" "${vars.episode_max_minutes}" "${vars.episode_min_minutes}"
|
|
605
637
|
|
|
606
638
|
- id: generate-fallback2
|
|
607
639
|
description: "Voice tier fallback 2 (last, loud): omni-voice-gen when neither voxcpm nor gemini delivered"
|
|
@@ -616,7 +648,7 @@ nodes:
|
|
|
616
648
|
kind: shell
|
|
617
649
|
options:
|
|
618
650
|
command: >-
|
|
619
|
-
OMNIVOICE_SPEED=1.06 kk stage daily-generate "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}" "${vars.transcode_mp3}" "ominivoice" "1" "${vars.episode_max_minutes}"
|
|
651
|
+
OMNIVOICE_SPEED=1.06 kk stage daily-generate "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}" "${vars.transcode_mp3}" "ominivoice" "1" "${vars.episode_max_minutes}" "${vars.episode_min_minutes}"
|
|
620
652
|
|
|
621
653
|
- id: intro
|
|
622
654
|
description: "Prepend the 8s intro sting (intro_music var; empty = skip), update duration/audioPath"
|
|
@@ -718,6 +750,27 @@ nodes:
|
|
|
718
750
|
kind: shell
|
|
719
751
|
options:
|
|
720
752
|
command: >-
|
|
753
|
+
PODCAST_PUB_SEO="${vars.podcast_seo}"
|
|
754
|
+
PODCAST_PUB_EPISODE_TITLE_MODE="${vars.podcast_episode_title_mode}"
|
|
755
|
+
PODCAST_PUB_EPISODE_TITLE_PATTERN="${vars.podcast_episode_title_pattern}"
|
|
756
|
+
PODCAST_PUB_SERIES_NAME="${vars.podcast_series_name}"
|
|
757
|
+
PODCAST_PUB_SHOW_NAME="${vars.podcast_show_name}"
|
|
758
|
+
PODCAST_PUB_TITLE_MAX="${vars.podcast_title_max}"
|
|
759
|
+
PODCAST_PUB_TITLE_MAX_KEYWORDS="${vars.podcast_title_max_keywords}"
|
|
760
|
+
PODCAST_PUB_SEO_TITLE_SUFFIX="${vars.podcast_seo_title_suffix}"
|
|
761
|
+
PODCAST_PUB_SEO_TITLE_MAX="${vars.podcast_seo_title_max}"
|
|
762
|
+
PODCAST_PUB_SEO_DESCRIPTION_MIN="${vars.podcast_seo_description_min}"
|
|
763
|
+
PODCAST_PUB_SEO_DESCRIPTION_MAX="${vars.podcast_seo_description_max}"
|
|
764
|
+
PODCAST_PUB_SEO_DESCRIPTION_SOURCE="${vars.podcast_seo_description_source}"
|
|
765
|
+
PODCAST_PUB_SUMMARY_PARAGRAPH="${vars.podcast_summary_paragraph}"
|
|
766
|
+
PODCAST_PUB_TIMELINE_INLINE_STYLE="${vars.podcast_timeline_inline_style}"
|
|
767
|
+
PODCAST_PUB_AUTO_EPISODE_NUMBER="${vars.podcast_auto_episode_number}"
|
|
768
|
+
PODCAST_PUB_TITLE_SANITIZE="${vars.podcast_title_sanitize}"
|
|
769
|
+
PODCAST_PUB_LINK_FIELD="${vars.podcast_link_field}"
|
|
770
|
+
PODCAST_PUB_SLUG="${vars.podcast_slug}"
|
|
771
|
+
PODCAST_PUB_SLUG_PATTERN="${vars.podcast_slug_pattern}"
|
|
772
|
+
PODCAST_PUB_SOCIAL_IMAGE="${vars.podcast_social_image}"
|
|
773
|
+
PODCAST_PUB_ROUND_DURATION="${vars.podcast_round_duration}"
|
|
721
774
|
kk stage daily-show-notes "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}" "${vars.title}"
|
|
722
775
|
|
|
723
776
|
- id: publish-podcast
|
|
@@ -731,6 +784,27 @@ nodes:
|
|
|
731
784
|
kind: shell
|
|
732
785
|
options:
|
|
733
786
|
command: >-
|
|
787
|
+
PODCAST_PUB_SEO="${vars.podcast_seo}"
|
|
788
|
+
PODCAST_PUB_EPISODE_TITLE_MODE="${vars.podcast_episode_title_mode}"
|
|
789
|
+
PODCAST_PUB_EPISODE_TITLE_PATTERN="${vars.podcast_episode_title_pattern}"
|
|
790
|
+
PODCAST_PUB_SERIES_NAME="${vars.podcast_series_name}"
|
|
791
|
+
PODCAST_PUB_SHOW_NAME="${vars.podcast_show_name}"
|
|
792
|
+
PODCAST_PUB_TITLE_MAX="${vars.podcast_title_max}"
|
|
793
|
+
PODCAST_PUB_TITLE_MAX_KEYWORDS="${vars.podcast_title_max_keywords}"
|
|
794
|
+
PODCAST_PUB_SEO_TITLE_SUFFIX="${vars.podcast_seo_title_suffix}"
|
|
795
|
+
PODCAST_PUB_SEO_TITLE_MAX="${vars.podcast_seo_title_max}"
|
|
796
|
+
PODCAST_PUB_SEO_DESCRIPTION_MIN="${vars.podcast_seo_description_min}"
|
|
797
|
+
PODCAST_PUB_SEO_DESCRIPTION_MAX="${vars.podcast_seo_description_max}"
|
|
798
|
+
PODCAST_PUB_SEO_DESCRIPTION_SOURCE="${vars.podcast_seo_description_source}"
|
|
799
|
+
PODCAST_PUB_SUMMARY_PARAGRAPH="${vars.podcast_summary_paragraph}"
|
|
800
|
+
PODCAST_PUB_TIMELINE_INLINE_STYLE="${vars.podcast_timeline_inline_style}"
|
|
801
|
+
PODCAST_PUB_AUTO_EPISODE_NUMBER="${vars.podcast_auto_episode_number}"
|
|
802
|
+
PODCAST_PUB_TITLE_SANITIZE="${vars.podcast_title_sanitize}"
|
|
803
|
+
PODCAST_PUB_LINK_FIELD="${vars.podcast_link_field}"
|
|
804
|
+
PODCAST_PUB_SLUG="${vars.podcast_slug}"
|
|
805
|
+
PODCAST_PUB_SLUG_PATTERN="${vars.podcast_slug_pattern}"
|
|
806
|
+
PODCAST_PUB_SOCIAL_IMAGE="${vars.podcast_social_image}"
|
|
807
|
+
PODCAST_PUB_ROUND_DURATION="${vars.podcast_round_duration}"
|
|
734
808
|
kk stage daily-publish-podcast "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}"
|
|
735
809
|
|
|
736
810
|
# ---- independent soft channels: xhs, wechat, locales (exit 0 on soft failure; fail-fast DAG) ----
|
|
@@ -844,17 +918,46 @@ nodes:
|
|
|
844
918
|
onError: continue
|
|
845
919
|
role: scribe
|
|
846
920
|
agent: ${vars.agent}
|
|
847
|
-
|
|
921
|
+
# 2026-10-24: `onError: continue` documents the locales as non-blocking, but a failure here
|
|
922
|
+
# landed AFTER surfdash/podcast and left `translate-sync` to sync nothing while
|
|
923
|
+
# `publish-classify` still reported the day `full` — the en/ja posts never existed. The
|
|
924
|
+
# retry sibling below carries the capable tier; `daily-publish-completeness` is what makes
|
|
925
|
+
# the residue visible instead of a silent skip.
|
|
926
|
+
input: &translate-input |
|
|
848
927
|
Translate the en/ja draft posts of the zh article. Read the two marker files at these fixed paths — each holds one draft path, or is empty when that locale was not scaffolded:
|
|
849
928
|
English: ${vars.work_dir}/31-publish/${vars.run_date}_20_translate-localize_en-post-path.txt
|
|
850
929
|
Japanese: ${vars.work_dir}/31-publish/${vars.run_date}_20_translate-localize_ja-post-path.txt
|
|
851
930
|
For each: if the marker is empty or unreadable, skip that locale. Otherwise edit the named file in place — translate the frontmatter title and the whole markdown body to natural English/Japanese respectively. Keep frontmatter keys, dates, URLs, code, and the assets/ image reference untouched. Set draft: false (the translated post publishes to the site) and remove any canonical: frontmatter line pointing at a different locale's URL.
|
|
931
|
+
Also translate these frontmatter values into the target locale, keeping all other keys untouched: description, seo_title, seo_description, and each key_points item. Each stays one coherent standalone sentence that summarizes the same story as the zh source — never splice fragments from different items or stories into one, and key_points must keep exactly the same number of items as the zh post. The 2026-10-23 run failed this: spliced description/key_points had to be hand-repaired before sync.
|
|
932
|
+
Series names are fixed product identity, never invented or freely translated: the en series name is "Daily AI Stew" (show "Salty Talk"); the ja series name is "デイリーAIシチュー" (show "咸話咸説"). Never emit 毎日AIブレンド, ごった煮, "Daily AI Medley", "hodgepodge", "Daily AI Briefing", or the zh name 每日AI乱炖 in en/ja prose or link text (a URL keeps whatever it contains). Openers: en "Your Daily AI Stew from Robin, on Salty Talk."; ja "咸話咸説のデイリーAIシチュー、Robin がお届けします。" Listen links: en "[listen to Daily AI Stew · <date>](<url>)", ja "[デイリーAIシチュー · <date> を聴く](<url>)". In each key_points item keep the "Label — sentence" shape: the label before " — " stays a short phrase of at most 14 characters (ja) / 28 (en), never truncated with "…". daily-translate-sync rewrites known retired names deterministically and fails the sync on leftovers.
|
|
852
933
|
Keep frontmatter valid YAML: double-quote any value containing ': '. Read the surfdash translator glossary if present at the surfdash checkout root under .agents/skills/surfdash-translator/glossary.md and apply its terminology.
|
|
853
934
|
Do not wait for operator confirmation. Each edited file must end fully in its target language.
|
|
854
935
|
|
|
936
|
+
# 2026-10-24: the locale translation carried `onError: continue` with NO retry sibling, so a
|
|
937
|
+
# failed executor left the en/ja drafts untranslated and `translate-sync` synced nothing while the
|
|
938
|
+
# day still classified `full`. Same shape as the two hard-fail producers above: the primary
|
|
939
|
+
# tolerates, this sibling is loud-tolerant (a locale is an independent channel, so it continues
|
|
940
|
+
# rather than failing the DAG) and runs only when a scaffolded draft is still untranslated.
|
|
941
|
+
- id: translate-retry
|
|
942
|
+
description: "Retry the en/ja locale translation on the capable tier when the primary left a scaffolded draft untranslated (2026-10-24)"
|
|
943
|
+
dependsOn: [translate]
|
|
944
|
+
condition:
|
|
945
|
+
kind: shell
|
|
946
|
+
options:
|
|
947
|
+
command: >-
|
|
948
|
+
test "$(cat "${vars.work_dir}/31-publish/${vars.run_date}_20_translate-localize_zh-post-path.txt" 2>/dev/null)" != ""
|
|
949
|
+
&& { for L in en ja; do M="${vars.work_dir}/31-publish/${vars.run_date}_20_translate-localize_$L-post-path.txt"; P="$(cat "$M" 2>/dev/null)"; [ -z "$P" ] && continue; grep -q '^draft: *false' "$P" || { echo "untranslated: $L ($P)"; exit 0; }; done; exit 1; }
|
|
950
|
+
action:
|
|
951
|
+
kind: agent.run
|
|
952
|
+
options:
|
|
953
|
+
onError: continue
|
|
954
|
+
role: reviewer
|
|
955
|
+
agent: ${vars.agent_retry}
|
|
956
|
+
input: *translate-input
|
|
957
|
+
|
|
855
958
|
- id: translate-sync
|
|
856
959
|
description: "Sync the translated en/ja drafts into the surfdash CMS (the zh post path rides the marker file)"
|
|
857
|
-
dependsOn: [translate]
|
|
960
|
+
dependsOn: [translate, translate-retry]
|
|
858
961
|
action:
|
|
859
962
|
kind: shell
|
|
860
963
|
options:
|