@gobing-ai/knowledge-kit 0.1.10 → 0.1.12
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 +2478 -1737
- package/package.json +1 -1
- package/plugins/generations/image-gen/README.md +108 -0
- package/plugins/generations/image-gen/assets/sample/cover__clean-webapp-ui.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/cover__cute.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/cover__daily-news.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/cover__editorial.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/cover__fresh.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/cover__minimalist.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/cover__photorealistic.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/cover__sketch.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/cover__technical-diagram.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/cover__vibrant.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/cover__warm.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/cover__xhs-knowledge.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/daily-news-cover__clean-webapp-ui.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/daily-news-cover__cute.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/daily-news-cover__daily-news.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/daily-news-cover__editorial.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/daily-news-cover__fresh.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/daily-news-cover__minimalist.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/daily-news-cover__photorealistic.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/daily-news-cover__sketch.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/daily-news-cover__technical-diagram.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/daily-news-cover__vibrant.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/daily-news-cover__warm.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/daily-news-cover__xhs-knowledge.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/free__clean-webapp-ui.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/free__cute.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/free__daily-news.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/free__editorial.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/free__fresh.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/free__minimalist.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/free__photorealistic.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/free__sketch.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/free__technical-diagram.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/free__vibrant.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/free__warm.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/free__xhs-knowledge.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/illustration__clean-webapp-ui.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/illustration__cute.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/illustration__daily-news.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/illustration__editorial.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/illustration__fresh.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/illustration__minimalist.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/illustration__photorealistic.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/illustration__sketch.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/illustration__technical-diagram.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/illustration__vibrant.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/illustration__warm.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/illustration__xhs-knowledge.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/xhs-card__clean-webapp-ui.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/xhs-card__cute.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/xhs-card__daily-news.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/xhs-card__editorial.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/xhs-card__fresh.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/xhs-card__minimalist.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/xhs-card__photorealistic.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/xhs-card__sketch.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/xhs-card__technical-diagram.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/xhs-card__vibrant.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/xhs-card__warm.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/xhs-card__xhs-knowledge.jpg +0 -0
- package/plugins/generations/image-gen/dist/index.js +102 -58
- package/plugins/generations/image-gen/scripts/compose-e2e-artifact.ts +5 -21
- package/plugins/generations/image-gen/scripts/gen-samples.ts +208 -0
- package/plugins/generations/image-gen/src/compose/chromium.ts +2 -3
- package/plugins/generations/image-gen/src/presets.ts +6 -3
- package/plugins/generations/image-gen/src/providers/errors.ts +16 -1
- package/plugins/generations/image-gen/src/providers/minimax.ts +46 -17
- package/plugins/ingestions/web-search/src/index.ts +1 -1
- package/plugins/kk/README.md +18 -0
- package/plugins/kk/commands/image-extract.md +1 -0
- package/plugins/kk/commands/image-generate.md +1 -0
- package/plugins/kk/commands/tell-me.md +1 -0
- package/plugins/kk/commands/workflow-run.md +7 -1
- package/plugins/kk/decisions/extract/it-topics.yaml +24 -0
- package/plugins/kk/decisions/kk-itc.yaml +6 -4
- package/plugins/kk/plugin.json +1 -1
- package/plugins/kk/publish-presets/daily-ai-voice-migration.json +146 -0
- package/plugins/kk/publish-presets/daily-ai-voice.json +11 -3
- package/plugins/kk/schedules/kk-collect.grok-bot.md.tmpl +69 -0
- package/plugins/kk/schedules/kk-daily-ai-voice3.cron.tmpl +18 -0
- package/plugins/kk/schedules/kk-daily-publish3-cn-peak.cron.tmpl +15 -5
- package/plugins/kk/schedules/kk-daily-publish3-overseas.cron.tmpl +9 -4
- package/plugins/kk/scripts/daily-scheduled-run.ts +298 -0
- package/plugins/kk/scripts/daily-stages.ts +1403 -156
- package/plugins/kk/scripts/kk-workflow-stages.ts +266 -10
- package/plugins/kk/scripts/safety-artifact-check.ts +277 -0
- package/plugins/kk/scripts/voice3-publish-window.ts +202 -0
- package/plugins/kk/skills/article-adapt/SKILL.md +1 -0
- package/plugins/kk/skills/audio-authoring/SKILL.md +1 -0
- package/plugins/kk/skills/content-judge/SKILL.md +1 -0
- package/plugins/kk/skills/content-safety/SKILL.md +1 -0
- package/plugins/kk/skills/explain-things/SKILL.md +1 -0
- package/plugins/kk/skills/image-authoring/SKILL.md +1 -0
- package/plugins/kk/skills/itc-generating/SKILL.md +1 -0
- package/plugins/kk/skills/publish/SKILL.md +1 -0
- package/plugins/kk/skills/smart-seo/SKILL.md +1 -0
- package/plugins/kk/skills/storm-research/SKILL.md +1 -0
- package/plugins/kk/skills/taste-unslop/SKILL.md +1 -0
- package/plugins/kk/skills/topic/SKILL.md +1 -0
- package/plugins/kk/skills/web-automation/SKILL.md +1 -0
- package/plugins/kk/skills/xhs-images/SKILL.md +1 -0
- package/plugins/kk/workflows/kk-daily-ai-voice2.yaml +177 -128
- package/plugins/kk/workflows/kk-daily-ai-voice3.yaml +161 -61
- package/plugins/kk/workflows/kk-daily-publish3.yaml +82 -26
- package/plugins/kk/workflows/kk-daily-safety.yaml +156 -69
- package/plugins/kk/workflows/kk-itc-author.yaml +75 -0
- package/plugins/kk/workflows/kk-itc-publish-tail.yaml +25 -3
- package/plugins/kk/workflows/kk-itc.yaml +36 -4
- package/plugins/publishings/bilibili-pub/dist/index.js +2 -1
- package/plugins/publishings/bilibili-pub/dist/profile-check.js +2 -1
- package/plugins/publishings/cnblogs-pub/dist/index.js +2 -1
- package/plugins/publishings/cnblogs-pub/dist/profile-check.js +2 -1
- package/plugins/publishings/csdn-pub/dist/index.js +2 -1
- package/plugins/publishings/csdn-pub/dist/profile-check.js +2 -1
- package/plugins/publishings/infoq-pub/dist/index.js +2 -1
- package/plugins/publishings/infoq-pub/dist/profile-check.js +2 -1
- package/plugins/publishings/jike-pub/dist/index.js +2 -1
- package/plugins/publishings/jike-pub/dist/profile-check.js +2 -1
- package/plugins/publishings/juejin-pub/dist/index.js +2 -1
- package/plugins/publishings/juejin-pub/dist/profile-check.js +2 -1
- package/plugins/publishings/linkedin-pub/dist/index.js +21333 -0
- package/plugins/publishings/linkedin-pub/dist/profile-check.js +21005 -0
- package/plugins/publishings/linkedin-pub/package.json +19 -0
- package/plugins/publishings/linkedin-pub/plugin.json +12 -0
- package/plugins/publishings/linkedin-pub/src/index.ts +705 -0
- package/plugins/publishings/linkedin-pub/src/profile-check.ts +17 -0
- package/plugins/publishings/linkedin-pub/tsconfig.json +4 -0
- package/plugins/publishings/podcast-pub/dist/index.js +46 -8
- package/plugins/publishings/podcast-pub/src/index.ts +62 -5
- package/plugins/publishings/shipinhao-pub/dist/index.js +2 -1
- package/plugins/publishings/shipinhao-pub/dist/profile-check.js +2 -1
- package/plugins/publishings/sspai-pub/dist/index.js +2 -1
- package/plugins/publishings/sspai-pub/dist/profile-check.js +2 -1
- package/plugins/publishings/substack-pub/dist/index.js +2 -1
- package/plugins/publishings/substack-pub/dist/profile-check.js +2 -1
- package/plugins/publishings/surfdash-pub/dist/index.js +461 -53
- package/plugins/publishings/surfdash-pub/src/digest-check.ts +85 -19
- package/plugins/publishings/surfdash-pub/src/index.ts +592 -65
- package/plugins/publishings/threads-pub/dist/index.js +7 -0
- package/plugins/publishings/threads-pub/src/index.ts +12 -0
- package/plugins/publishings/wechatmp-pub/dist/index.js +936 -98
- package/plugins/publishings/wechatmp-pub/dist/profile-check.js +60 -3
- package/plugins/publishings/wechatmp-pub/src/flow.ts +1134 -86
- package/plugins/publishings/wechatmp-pub/src/index.ts +112 -30
- package/plugins/publishings/wechatmp-pub/src/page.ts +105 -0
- package/plugins/publishings/weibo-pub/dist/index.js +2 -1
- package/plugins/publishings/weibo-pub/dist/profile-check.js +2 -1
- package/plugins/publishings/x-pub/dist/index.js +9 -1
- package/plugins/publishings/x-pub/dist/profile-check.js +2 -1
- package/plugins/publishings/x-pub/src/index.ts +11 -0
- package/plugins/publishings/xhs-pub/dist/index.js +2 -1
- package/plugins/publishings/xhs-pub/dist/profile-check.js +2 -1
- package/plugins/publishings/xiaoyuzhou-pub/dist/index.js +2 -1
- package/plugins/publishings/xiaoyuzhou-pub/dist/profile-check.js +2 -1
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Preset sample gallery (task 0340): render one sample per shipped format × style
|
|
3
|
+
* pair into assets/sample/ and regenerate README.md as the index over them.
|
|
4
|
+
*
|
|
5
|
+
* Usage (run from the repo root so `kk` resolves this checkout's plugin):
|
|
6
|
+
* bun plugins/generations/image-gen/scripts/gen-samples.ts # render missing pairs + README
|
|
7
|
+
* bun plugins/generations/image-gen/scripts/gen-samples.ts --force # re-render every pair
|
|
8
|
+
* bun plugins/generations/image-gen/scripts/gen-samples.ts --readme-only # README from samples on disk
|
|
9
|
+
* bun plugins/generations/image-gen/scripts/gen-samples.ts --provider gemini --model <m>
|
|
10
|
+
*
|
|
11
|
+
* Only the builtin presets/ root is read — user/project preset roots never leak
|
|
12
|
+
* into the shipped index. A finished sample is never overwritten without --force,
|
|
13
|
+
* so a run that dies on pair 37 resumes from pair 37. Failed pairs are named and
|
|
14
|
+
* the script exits 1.
|
|
15
|
+
*/
|
|
16
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync } from 'node:fs';
|
|
17
|
+
import { join } from 'node:path';
|
|
18
|
+
import { type FormatPreset, FormatPresetSchema, type StylePreset, StylePresetSchema } from '../src/presets';
|
|
19
|
+
|
|
20
|
+
const PLUGIN_DIR = join(import.meta.dir, '..');
|
|
21
|
+
const SAMPLE_DIR = join(PLUGIN_DIR, 'assets', 'sample');
|
|
22
|
+
const README = join(PLUGIN_DIR, 'README.md');
|
|
23
|
+
const WORK_DIR = join(process.cwd(), '.works', 'tmp', 'test-samples');
|
|
24
|
+
const THUMB_PX = 640;
|
|
25
|
+
const CONCURRENCY = 3;
|
|
26
|
+
|
|
27
|
+
/** Fixed per-job inputs so every style is compared on the same subject. */
|
|
28
|
+
const SAMPLE_SUBJECT = 'a request flowing through an API gateway to three microservices';
|
|
29
|
+
const COVER_VARS = {
|
|
30
|
+
title: 'Building Reliable AI Agents',
|
|
31
|
+
subtitle: 'Patterns for production',
|
|
32
|
+
topics: 'AI agents',
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const args = process.argv.slice(2);
|
|
36
|
+
const flag = (name: string): boolean => args.includes(name);
|
|
37
|
+
const option = (name: string, fallback: string): string => {
|
|
38
|
+
const index = args.indexOf(name);
|
|
39
|
+
return index >= 0 && args[index + 1] !== undefined ? args[index + 1] : fallback;
|
|
40
|
+
};
|
|
41
|
+
const provider = option('--provider', 'minimax');
|
|
42
|
+
const model = option('--model', 'image-01');
|
|
43
|
+
|
|
44
|
+
function loadBuiltin<T>(axis: string, schema: { parse(value: unknown): T }): T[] {
|
|
45
|
+
const dir = join(PLUGIN_DIR, 'presets', axis);
|
|
46
|
+
return readdirSync(dir)
|
|
47
|
+
.filter((entry) => entry.endsWith('.yaml'))
|
|
48
|
+
.sort()
|
|
49
|
+
.map((entry) => schema.parse(Bun.YAML.parse(readFileSync(join(dir, entry), 'utf8'))));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const formats = loadBuiltin<FormatPreset>('formats', FormatPresetSchema);
|
|
53
|
+
const styles = loadBuiltin<StylePreset>('styles', StylePresetSchema);
|
|
54
|
+
const sampleName = (format: string, style: string): string => `${format}__${style}.jpg`;
|
|
55
|
+
|
|
56
|
+
/** The Doc envelope that drives one pair through the real executor. */
|
|
57
|
+
function envelope(format: FormatPreset, style: StylePreset): unknown[] {
|
|
58
|
+
const imageGen: Record<string, unknown> = {
|
|
59
|
+
job: format.job,
|
|
60
|
+
format: format.name,
|
|
61
|
+
style: style.name,
|
|
62
|
+
provider,
|
|
63
|
+
model,
|
|
64
|
+
};
|
|
65
|
+
const doc: Record<string, unknown> = { id: `${format.name}__${style.name}`, title: COVER_VARS.title, body: '' };
|
|
66
|
+
if (format.job === 'cover') {
|
|
67
|
+
imageGen.vars = COVER_VARS;
|
|
68
|
+
} else if (format.job === 'illustration') {
|
|
69
|
+
// The anchor must occur exactly once in the body (index.ts assertAnchorPlan).
|
|
70
|
+
doc.body = `Every call enters at the edge: ${SAMPLE_SUBJECT}.`;
|
|
71
|
+
doc.metadata = { positions: [SAMPLE_SUBJECT] };
|
|
72
|
+
} else if (format.compose !== undefined) {
|
|
73
|
+
imageGen.vars = { subject: SAMPLE_SUBJECT, headline: 'API 网关', points: '统一入口\n鉴权限流\n路由转发' };
|
|
74
|
+
} else {
|
|
75
|
+
imageGen.vars = { subject: SAMPLE_SUBJECT };
|
|
76
|
+
}
|
|
77
|
+
doc.metadata = { ...((doc.metadata as object) ?? {}), imageGen: { ...imageGen, maxImages: 1 } };
|
|
78
|
+
return [doc];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function render(format: FormatPreset, style: StylePreset): Promise<void> {
|
|
82
|
+
const stem = `${format.name}__${style.name}`;
|
|
83
|
+
const dir = join(WORK_DIR, stem);
|
|
84
|
+
rmSync(dir, { recursive: true, force: true });
|
|
85
|
+
mkdirSync(dir, { recursive: true });
|
|
86
|
+
const input = join(dir, 'in.json');
|
|
87
|
+
const output = join(dir, 'out.json');
|
|
88
|
+
await Bun.write(input, JSON.stringify(envelope(format, style)));
|
|
89
|
+
const run = Bun.spawn(['kk', 'executor', 'run', 'image-gen', '--in', input, '--out', output], {
|
|
90
|
+
stdout: 'pipe',
|
|
91
|
+
stderr: 'pipe',
|
|
92
|
+
});
|
|
93
|
+
const [code, stderr] = await Promise.all([run.exited, new Response(run.stderr).text()]);
|
|
94
|
+
if (code !== 0) {
|
|
95
|
+
throw new Error(`kk exited ${code}: ${stderr.trim().split('\n').slice(-3).join(' | ')}`);
|
|
96
|
+
}
|
|
97
|
+
const image = (await Bun.file(output).json())?.metadata?.images?.[0]?.path as string | undefined;
|
|
98
|
+
if (image === undefined) {
|
|
99
|
+
throw new Error('executor output carries no metadata.images[0].path');
|
|
100
|
+
}
|
|
101
|
+
const source = image.startsWith('/') ? image : join(dir, image);
|
|
102
|
+
// ponytail: macOS sips does the downscale (no image dep in the repo); add sharp if this must run on Linux.
|
|
103
|
+
const sips = Bun.spawnSync([
|
|
104
|
+
'sips',
|
|
105
|
+
'-Z',
|
|
106
|
+
String(THUMB_PX),
|
|
107
|
+
'-s',
|
|
108
|
+
'format',
|
|
109
|
+
'jpeg',
|
|
110
|
+
'-s',
|
|
111
|
+
'formatOptions',
|
|
112
|
+
'80',
|
|
113
|
+
source,
|
|
114
|
+
'--out',
|
|
115
|
+
join(SAMPLE_DIR, sampleName(format.name, style.name)),
|
|
116
|
+
]);
|
|
117
|
+
if (sips.exitCode !== 0) {
|
|
118
|
+
throw new Error(`sips failed: ${sips.stderr.toString().trim()}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function renderAll(): Promise<string[]> {
|
|
123
|
+
mkdirSync(SAMPLE_DIR, { recursive: true });
|
|
124
|
+
const queue = formats.flatMap((format) =>
|
|
125
|
+
styles
|
|
126
|
+
.filter((style) => flag('--force') || !existsSync(join(SAMPLE_DIR, sampleName(format.name, style.name))))
|
|
127
|
+
.map((style) => ({ format, style })),
|
|
128
|
+
);
|
|
129
|
+
console.log(`${queue.length} of ${formats.length * styles.length} pairs to render via ${provider}/${model}`);
|
|
130
|
+
const failed: string[] = [];
|
|
131
|
+
const worker = async (): Promise<void> => {
|
|
132
|
+
for (let pair = queue.shift(); pair !== undefined; pair = queue.shift()) {
|
|
133
|
+
const name = `${pair.format.name} × ${pair.style.name}`;
|
|
134
|
+
try {
|
|
135
|
+
await render(pair.format, pair.style);
|
|
136
|
+
console.log(`ok ${name}`);
|
|
137
|
+
} catch (error) {
|
|
138
|
+
failed.push(name);
|
|
139
|
+
console.error(`FAIL ${name}: ${error instanceof Error ? error.message : error}`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
await Promise.all(Array.from({ length: CONCURRENCY }, worker));
|
|
144
|
+
return failed;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const oneLine = (text: string): string => text.replace(/\s+/g, ' ').trim().replace(/\|/g, '\\|');
|
|
148
|
+
|
|
149
|
+
function readme(): string {
|
|
150
|
+
const lines = [
|
|
151
|
+
'# image-gen',
|
|
152
|
+
'',
|
|
153
|
+
'Image generation plugin (`kind: generator`): `free`, `cover` and `illustration` jobs over the',
|
|
154
|
+
'`Doc[] → Content` contract. Every image is one **format** (canvas, template, job) composed with one',
|
|
155
|
+
'**style** (visual modifiers); any format pairs with any style.',
|
|
156
|
+
'',
|
|
157
|
+
'This page is the preset index: one sample per shipped format × style pair, so you can pick a',
|
|
158
|
+
'combination by eye. Pass the pair as `metadata.imageGen.format` / `metadata.imageGen.style`, or',
|
|
159
|
+
"`--format` / `--style` to the `kk:image-generate` skill. Omitting the style uses the format's",
|
|
160
|
+
'default (★ below).',
|
|
161
|
+
'',
|
|
162
|
+
`Samples: provider \`${provider}\`, model \`${model}\`, downscaled to ${THUMB_PX}px JPEG. Inputs are fixed per`,
|
|
163
|
+
`job — subject “${SAMPLE_SUBJECT}”; cover title “${COVER_VARS.title}”. Regenerate with`,
|
|
164
|
+
'`bun plugins/generations/image-gen/scripts/gen-samples.ts` (see its header for flags).',
|
|
165
|
+
'',
|
|
166
|
+
'## Formats',
|
|
167
|
+
'',
|
|
168
|
+
'| Format | Job | Aspect | Size | Default style | Description |',
|
|
169
|
+
'| --- | --- | --- | --- | --- | --- |',
|
|
170
|
+
...formats.map(
|
|
171
|
+
(f) =>
|
|
172
|
+
`| [\`${f.name}\`](#format-${f.name}) | ${f.job} | ${f.aspectRatio} | ${f.width ?? '?'}×${f.height ?? '?'} | ${f.defaultStyle} | ${oneLine(f.description)} |`,
|
|
173
|
+
),
|
|
174
|
+
'',
|
|
175
|
+
'## Styles',
|
|
176
|
+
'',
|
|
177
|
+
'| Style | Description | Best for |',
|
|
178
|
+
'| --- | --- | --- |',
|
|
179
|
+
...styles.map((s) => `| \`${s.name}\` | ${oneLine(s.description)} | ${oneLine(s.bestFor ?? '')} |`),
|
|
180
|
+
'',
|
|
181
|
+
'## Gallery',
|
|
182
|
+
];
|
|
183
|
+
const columns = 3;
|
|
184
|
+
for (const format of formats) {
|
|
185
|
+
lines.push('', `<a id="format-${format.name}"></a>`, '', `### ${format.name}`, '');
|
|
186
|
+
lines.push(`${format.job} job · ${format.aspectRatio} · default style \`${format.defaultStyle}\``, '');
|
|
187
|
+
lines.push(`|${' |'.repeat(columns)}`, `|${' --- |'.repeat(columns)}`);
|
|
188
|
+
for (let i = 0; i < styles.length; i += columns) {
|
|
189
|
+
const cells = styles.slice(i, i + columns).map((style) => {
|
|
190
|
+
const file = sampleName(format.name, style.name);
|
|
191
|
+
const label = `\`${style.name}\`${style.name === format.defaultStyle ? ' ★' : ''}`;
|
|
192
|
+
return existsSync(join(SAMPLE_DIR, file))
|
|
193
|
+
? `<img src="assets/sample/${file}" alt="${format.name} × ${style.name}" width="260"><br>${label}`
|
|
194
|
+
: `_not rendered_<br>${label}`;
|
|
195
|
+
});
|
|
196
|
+
lines.push(`| ${cells.join(' | ')} |`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return `${lines.join('\n')}\n`;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const failed = flag('--readme-only') ? [] : await renderAll();
|
|
203
|
+
await Bun.write(README, readme());
|
|
204
|
+
console.log(`wrote ${README}`);
|
|
205
|
+
if (failed.length > 0) {
|
|
206
|
+
console.error(`${failed.length} pair(s) failed — rerun to retry only those:\n ${failed.join('\n ')}`);
|
|
207
|
+
process.exit(1);
|
|
208
|
+
}
|
|
@@ -11,9 +11,8 @@
|
|
|
11
11
|
* Playwright's pipe under Bun 1.3.14 stalls intermittently (fulfill never
|
|
12
12
|
* returns, `document.fonts.ready` never settles — 0212 dogfood, ~1 in 3 runs).
|
|
13
13
|
*
|
|
14
|
-
* The E2E test
|
|
15
|
-
*
|
|
16
|
-
* subprocess. `compose` carries a deadline as the backstop for any other stall.
|
|
14
|
+
* The E2E test runs inside `bun test` on Bun 1.4.3 (the 1.3.14 launch block is
|
|
15
|
+
* gone). `compose` carries a deadline as the backstop for any other stall.
|
|
17
16
|
*
|
|
18
17
|
* Missing Playwright or Chromium surfaces as an actionable error naming the
|
|
19
18
|
* install command.
|
|
@@ -187,7 +187,9 @@ function matchPlaceholder(template: string, start: number): { expr: string; end:
|
|
|
187
187
|
|
|
188
188
|
/**
|
|
189
189
|
* Resolve one expression against the merged vars. An empty value (including
|
|
190
|
-
* a caller-passed '') resolves to the fallback
|
|
190
|
+
* a caller-passed '') resolves to the fallback, except a wrapper fallback
|
|
191
|
+
* (one embedding its own `{{name}}`), which drops out entirely so its literal
|
|
192
|
+
* slot never reaches the provider; an unknown placeholder with
|
|
191
193
|
* no fallback throws, naming the placeholder and the format — wt silently
|
|
192
194
|
* left it in place, and a literal `{{title}}` must not reach a paid provider.
|
|
193
195
|
* Leading trim on the fallback skips the `| ` separator; its trailing space
|
|
@@ -200,13 +202,14 @@ function resolvePlaceholder(expr: string, vars: Record<string, string>, formatNa
|
|
|
200
202
|
const name = expr.slice(0, pipe).trim();
|
|
201
203
|
const fallback = expr.slice(pipe + 1).trimStart();
|
|
202
204
|
const value = vars[name];
|
|
205
|
+
const slot = `{{${name}}}`;
|
|
203
206
|
if (!value) {
|
|
204
|
-
return fallback;
|
|
207
|
+
return fallback.includes(slot) ? '' : fallback;
|
|
205
208
|
}
|
|
206
209
|
// A fallback that embeds its own '{{name}}' slot is a mini-template
|
|
207
210
|
// wrapping the value (the R6 golden's ', subtitled "…"' form); a
|
|
208
211
|
// plain-text fallback is just the default, so the value wins.
|
|
209
|
-
return fallback.includes(
|
|
212
|
+
return fallback.includes(slot) ? fallback.split(slot).join(value) : value;
|
|
210
213
|
}
|
|
211
214
|
const name = expr.trim();
|
|
212
215
|
const value = vars[name];
|
|
@@ -24,6 +24,16 @@
|
|
|
24
24
|
/** The retry decision classes the D6 fallback loop (0166) acts on. */
|
|
25
25
|
export type ProviderErrorClass = 'auth' | 'quota' | 'transient' | 'policy' | 'unknown';
|
|
26
26
|
|
|
27
|
+
/** MiniMax business codes are not HTTP statuses; messages may be only "system error". */
|
|
28
|
+
export function minimaxErrorClass(code: number): ProviderErrorClass {
|
|
29
|
+
// https://platform.minimax.io/docs/api-reference/errorcode
|
|
30
|
+
if ([1026, 1027, 1040, 1046].includes(code)) return 'policy';
|
|
31
|
+
if ([1004, 2049].includes(code)) return 'auth';
|
|
32
|
+
if ([1002, 1008, 1039, 1045, 2056].includes(code)) return 'quota';
|
|
33
|
+
if ([1000, 1001, 1024, 1033].includes(code)) return 'transient';
|
|
34
|
+
return 'unknown';
|
|
35
|
+
}
|
|
36
|
+
|
|
27
37
|
/** Content-policy / safety / moderation rejections — checked first, they often arrive as 400/403. */
|
|
28
38
|
const POLICY_RE = /content[_ -]?policy|moderation|safety|sensitive|inappropriate|violat|blocked/i;
|
|
29
39
|
/** Credential wording; 401/403 statuses are checked separately. */
|
|
@@ -69,7 +79,12 @@ function partsOf(err: unknown): { readonly name: string; readonly message: strin
|
|
|
69
79
|
export function classifyProviderError(err: unknown): ProviderErrorClass {
|
|
70
80
|
const parts = partsOf(err);
|
|
71
81
|
const statuses = statusesIn(parts.message);
|
|
72
|
-
|
|
82
|
+
const miniCode = /^MiniMax API error \(status_code=(\d+)\):/.exec(parts.message)?.[1];
|
|
83
|
+
const miniClass = miniCode === undefined ? 'unknown' : minimaxErrorClass(Number(miniCode));
|
|
84
|
+
if (miniClass === 'policy' || POLICY_RE.test(parts.message)) return 'policy';
|
|
85
|
+
// An explicit business code owns retry semantics; e.g. invalid params (2013) must not
|
|
86
|
+
// become transient merely because its description contains the word "timeout".
|
|
87
|
+
if (miniCode !== undefined) return miniClass;
|
|
73
88
|
if (statuses.some((status) => status === 401 || status === 403) || AUTH_RE.test(parts.message)) return 'auth';
|
|
74
89
|
if (statuses.some((status) => status === 402 || status === 429) || QUOTA_RE.test(parts.message)) return 'quota';
|
|
75
90
|
if (
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { Buffer } from 'node:buffer';
|
|
2
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
3
|
+
import { echoError } from '@gobing-ai/ts-utils';
|
|
2
4
|
import { readBytes } from '../bytes';
|
|
5
|
+
import { classifyProviderError } from './errors';
|
|
3
6
|
import { mimeForPath } from './refs';
|
|
4
7
|
import { PROVIDER_REQUEST_TIMEOUT_MS } from './timeouts';
|
|
5
8
|
import type { ImageProvider, ImageRequest, ImageResult, ProviderFactory, ProviderOptions } from './types';
|
|
@@ -41,6 +44,13 @@ interface MinimaxResponse {
|
|
|
41
44
|
};
|
|
42
45
|
}
|
|
43
46
|
|
|
47
|
+
function responseError(result: MinimaxResponse): Error | null {
|
|
48
|
+
const code = result?.base_resp?.status_code;
|
|
49
|
+
return code !== undefined && code !== 0
|
|
50
|
+
? new Error(`MiniMax API error (status_code=${code}): ${result.base_resp?.status_msg ?? 'unknown error'}`)
|
|
51
|
+
: null;
|
|
52
|
+
}
|
|
53
|
+
|
|
44
54
|
export function parsePixelSize(size: string): { width: number; height: number } | null {
|
|
45
55
|
const match = size.trim().match(/^(\d+)\s*[xX*]\s*(\d+)$/);
|
|
46
56
|
if (match === null) return null;
|
|
@@ -126,10 +136,8 @@ export async function extractImageFromResponse(
|
|
|
126
136
|
fetchFn: typeof fetch = fetch,
|
|
127
137
|
timeoutMs: number = PROVIDER_REQUEST_TIMEOUT_MS,
|
|
128
138
|
): Promise<Uint8Array> {
|
|
129
|
-
const
|
|
130
|
-
if (
|
|
131
|
-
throw new Error(baseResp.status_msg ?? `MiniMax API returned status_code=${baseResp.status_code}`);
|
|
132
|
-
}
|
|
139
|
+
const error = responseError(result);
|
|
140
|
+
if (error !== null) throw error;
|
|
133
141
|
const base64Image = result.data?.image_base64?.[0];
|
|
134
142
|
if (base64Image !== undefined) return Uint8Array.from(Buffer.from(base64Image, 'base64'));
|
|
135
143
|
const url = result.data?.image_urls?.[0];
|
|
@@ -167,20 +175,41 @@ export const createMinimax: ProviderFactory = (options?: ProviderOptions): Image
|
|
|
167
175
|
|
|
168
176
|
const base = (readEnv('MINIMAX_BASE_URL') || 'https://api.minimaxi.com').replace(/\/+$/g, '');
|
|
169
177
|
const url = base.endsWith('/v1') ? `${base}/image_generation` : `${base}/v1/image_generation`;
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
+
// One deadline covers all attempts and backoff. Retry only explicit failed responses:
|
|
179
|
+
// transport errors leave generation outcome ambiguous and must not silently rebill it.
|
|
180
|
+
const signal = AbortSignal.timeout(timeoutMs);
|
|
181
|
+
let result: MinimaxResponse;
|
|
182
|
+
for (let attempt = 1; ; attempt += 1) {
|
|
183
|
+
signal.throwIfAborted();
|
|
184
|
+
const response = await fetchFn(url, {
|
|
185
|
+
method: 'POST',
|
|
186
|
+
headers: { 'content-type': 'application/json', authorization: `Bearer ${apiKey}` },
|
|
187
|
+
body: JSON.stringify(body),
|
|
188
|
+
signal,
|
|
189
|
+
});
|
|
190
|
+
let error: Error | null;
|
|
191
|
+
if (response.ok) {
|
|
192
|
+
result = (await response.json()) as MinimaxResponse;
|
|
193
|
+
error = responseError(result);
|
|
194
|
+
if (error === null) break;
|
|
195
|
+
} else {
|
|
196
|
+
const detail = await response.text();
|
|
197
|
+
let businessError: Error | null = null;
|
|
198
|
+
try {
|
|
199
|
+
businessError = responseError(JSON.parse(detail) as MinimaxResponse);
|
|
200
|
+
} catch {
|
|
201
|
+
// A non-JSON gateway response still carries its HTTP failure class.
|
|
202
|
+
}
|
|
203
|
+
// Business codes own policy/auth semantics even on a generic HTTP 5xx.
|
|
204
|
+
error = businessError ?? new Error(`MiniMax API error (${response.status}): ${detail}`);
|
|
205
|
+
}
|
|
206
|
+
if (attempt >= 3 || classifyProviderError(error) !== 'transient') throw error;
|
|
207
|
+
const delayMs = attempt * 1000;
|
|
208
|
+
echoError(`WARN image-gen: ${error.message}; retrying MiniMax (${attempt + 1}/3) after ${delayMs}ms`);
|
|
209
|
+
await delay(delayMs, undefined, { signal });
|
|
178
210
|
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
fetchFn,
|
|
182
|
-
timeoutMs,
|
|
183
|
-
);
|
|
211
|
+
// Download errors are outside the POST retry loop: the image already exists.
|
|
212
|
+
const bytes = await extractImageFromResponse(result, fetchFn, timeoutMs);
|
|
184
213
|
return { bytes, extension: 'jpg', provider: 'minimax', model: req.model };
|
|
185
214
|
},
|
|
186
215
|
};
|
|
@@ -392,7 +392,7 @@ export async function main(depsOverride?: Partial<SearchDeps>): Promise<number>
|
|
|
392
392
|
...depsOverride,
|
|
393
393
|
};
|
|
394
394
|
// Bun.which with an explicit PATH — without the option it can ignore runtime
|
|
395
|
-
// process.env.PATH changes (observed on Bun 1.3.14).
|
|
395
|
+
// process.env.PATH changes (observed on Bun 1.3.14, still true on 1.4.3).
|
|
396
396
|
if (!deps.markitdown && !Bun.which('markitdown', { PATH: deps.env.PATH })) {
|
|
397
397
|
// R6 — live mode requires the normalizer; fail before any network call.
|
|
398
398
|
throw new Error('markitdown is not on PATH');
|
package/plugins/kk/README.md
CHANGED
|
@@ -28,6 +28,17 @@ Capability files **omit a leading `kk-`**. `superskill install` prefixes the plu
|
|
|
28
28
|
`storm-research`, `itc-generating`, `explain-things` (via `/tell-me`), and `/workflow-run`. Product workflow YAML (`kk-storm-research.yaml`)
|
|
29
29
|
keeps its existing name — that is a workflow stem, not an installable agent capability.
|
|
30
30
|
|
|
31
|
+
### Grok Bot
|
|
32
|
+
|
|
33
|
+
Install: `superskill install kk --targets grok-bot --bot-provides cli:kk --bot-provides local-fs`.
|
|
34
|
+
Add `--bot-provides cli:spur` when the Bot ran the spur bootstrap.
|
|
35
|
+
|
|
36
|
+
Each skill and command declares a `requires:` list (`hooks`, `subagents`, `local-fs`, `cli:<name>`)
|
|
37
|
+
so the install ships only entries the Bot can run; the guard test is
|
|
38
|
+
`plugins/kk/tests/runtime-requires.test.ts`. Which kk work may actually run in the cloud is decided
|
|
39
|
+
by ADR-044 (`docs/00_ADR.md`): cloud Grok Bot routines are store-free and browser-free —
|
|
40
|
+
anything touching `kk.db` or a browser profile stays Mac-side.
|
|
41
|
+
|
|
31
42
|
## Schedule
|
|
32
43
|
|
|
33
44
|
`spur serve` **hosts the scheduler**: the `.spur/config.yaml` `bootstrap.scheduler` jobs (the daily
|
|
@@ -59,6 +70,13 @@ launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/ai.gobing.kk-collect.pli
|
|
|
59
70
|
crontab -l > /tmp/kk-collect.cron 2>/dev/null || :
|
|
60
71
|
kk schedule render kk-collect --platform cron --repo "$PWD" >> /tmp/kk-collect.cron
|
|
61
72
|
crontab /tmp/kk-collect.cron
|
|
73
|
+
|
|
74
|
+
# Grok Bot cloud routine (ADR-044): prints a routine definition for the store-free kk-collect
|
|
75
|
+
# prefix — the Bot owns preparation, the Mac owns the kk store. Paste the printed text into the
|
|
76
|
+
# Bot's routine editor (creating the routine there is the operator action; kk never calls an API).
|
|
77
|
+
# kk-serve and the publish3 windows are refused on grok-bot: they are store- or browser-bound.
|
|
78
|
+
kk schedule render kk-collect --platform grok-bot --bot kk-cloud --tz "$TZ" \
|
|
79
|
+
| pbcopy # then paste into the Bot's routine editor
|
|
62
80
|
```
|
|
63
81
|
|
|
64
82
|
Both rendered artifacts set `PATH` themselves (`{{home}}/.bun/bin`, `/opt/homebrew/bin`,
|
|
@@ -3,6 +3,7 @@ name: image-extract
|
|
|
3
3
|
description: Extract a reusable style preset (and optionally draft a format preset) from 1-5 reference images, saved to ~/.config/kk/image-presets/ after confirmation.
|
|
4
4
|
argument-hint: "--name <style-name> --ref <img> [--ref <img>…] [--emit-format <name> --job free|cover|illustration] (max 5 refs)"
|
|
5
5
|
allowed-tools: ["Skill"]
|
|
6
|
+
requires: [cli:kk, cli:spur, local-fs]
|
|
6
7
|
---
|
|
7
8
|
|
|
8
9
|
# image-extract
|
|
@@ -3,6 +3,7 @@ name: image-generate
|
|
|
3
3
|
description: Generate images from a prompt or an article — covers, illustrations, or free-form — using named format and style presets.
|
|
4
4
|
argument-hint: "[prompt] [--format free|cover|illustration] [--style vibrant|minimalist|photorealistic|technical-diagram|editorial|fresh|warm|cute|sketch|clean-webapp-ui|<custom>] [--content <file>] [--ref <img>…] [--var K=V] [--dry-run] [--list-presets]"
|
|
5
5
|
allowed-tools: ["Skill"]
|
|
6
|
+
requires: [cli:kk, cli:spur, local-fs]
|
|
6
7
|
---
|
|
7
8
|
|
|
8
9
|
# image-generate
|
|
@@ -7,6 +7,7 @@ description: >-
|
|
|
7
7
|
creates the config with defaults if missing; computes the workspace; shells
|
|
8
8
|
spur workflow run against the resolved YAML.
|
|
9
9
|
argument-hint: "[name] <topic|--in file> [--dir <path>] [--playbook generic|english|wechat] [--research] [--judge] [--outline <a|b|c>] [--writer itc-generating|topic] [--duration <min>] [--language <code>] [--script-approved] [--fixture] [--force]"
|
|
10
|
+
requires: [cli:kk, cli:spur, local-fs]
|
|
10
11
|
---
|
|
11
12
|
|
|
12
13
|
Thin runner around the **storm-research**, **itc-generating** / **topic**, and **kk-solo-podcast**
|
|
@@ -223,12 +224,17 @@ stays `false` unless `--live` is passed explicitly (ADR-022). `--playbook wechat
|
|
|
223
224
|
`cover_enabled` / `inline_images` (`false` unless passed); `--max-revisions` binds
|
|
224
225
|
`max_revisions` (default `2`). Each lands in `itc-options.json` under its camelCase key.
|
|
225
226
|
|
|
227
|
+
Two var-only child options skip the options snapshot: `outline_bullets_only=true` keeps the
|
|
228
|
+
outline stage to heading-plus-bullets options (no prose paragraphs), and `plan_only=true`
|
|
229
|
+
stops the parent DAG after the confirmed publish plan (no `send-confirm` pause, no fanout-send,
|
|
230
|
+
no recovery, no settle) — a plan preview, not a send.
|
|
231
|
+
|
|
226
232
|
**Collected topic (`--from-collect <profile>`, 0299).** `itc-prepare` without `--chosen` returns
|
|
227
233
|
an *intake* snapshot (`status: intake`, `workDir: null`). Choose the topic in a run-scoped intake
|
|
228
234
|
dir, then finalize:
|
|
229
235
|
|
|
230
236
|
1. `kk stage run itc-topic-extract --request` `{parentRunId, intakeDir, topicProfile}` → the pool.
|
|
231
|
-
2. The agent writes `$intakeDir/topic-candidates.json` (≤
|
|
237
|
+
2. The agent (or `kk itc candidates`) writes `$intakeDir/topic-candidates.json` (≤5 candidates `a|b|c` up to `d|e` with
|
|
232
238
|
`title`/`reason`/`itemIds` from the pool); `kk stage run itc-topic-validate --request`
|
|
233
239
|
`{parentRunId, intakeDir}` must pass.
|
|
234
240
|
3. `kk stage run itc-decision --topic "$intakeDir" "$RUN_ID"` prints the chosen id or `defer`.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# it-topics topic profile — general IT article pool for kk-itc collected topics.
|
|
2
|
+
#
|
|
3
|
+
# Modeled on ai-news.yaml (task 0210 R1): the pool for `kk itc candidates --profile it-topics`
|
|
4
|
+
# is every labelled item with a substantive body, most time-sensitive first — not AI-scoped,
|
|
5
|
+
# so category is NOT gated and the pool admits the whole `kk-item-category` taxonomy
|
|
6
|
+
# (ai-* and non-ai alike: general IT/tech articles draw from both).
|
|
7
|
+
#
|
|
8
|
+
# Keys are decision ids from the sibling catalog (plugins/kk/decisions/kk-items.yaml);
|
|
9
|
+
# an unknown key or decision id fails loud with `kk.store.profile.invalid`.
|
|
10
|
+
# `limit` is omitted: the compiler's episode-cap default (20) applies unless
|
|
11
|
+
# `kk itc candidates --limit` overrides it.
|
|
12
|
+
topic: it-topics
|
|
13
|
+
where:
|
|
14
|
+
# quality/importance 2, not 3: real ingest bodies (last30days/aihot summaries) are stub-length,
|
|
15
|
+
# so the heuristic labeler tops out at 1-2 and a gte-3 gate empties real pools (task 0212
|
|
16
|
+
# dogfood finding). Pool selection stays lenient here; any stricter cutoff belongs downstream.
|
|
17
|
+
kk-item-quality: { gte: 2 }
|
|
18
|
+
# importance stays in as its own gate with the same 2 floor: an unjudged item stays excluded
|
|
19
|
+
# (fail-closed INNER join on `labels_current`).
|
|
20
|
+
kk-item-importance: { gte: 2 }
|
|
21
|
+
# Same conservative moderation gate as ai-news (task 0229 R5): political-system items never
|
|
22
|
+
# become article topics without explicit operator review.
|
|
23
|
+
kk-item-china-politics: { lte: 1 }
|
|
24
|
+
order: [kk-item-urgency desc, kk-item-impact desc, first_seen_at desc]
|
|
@@ -13,10 +13,12 @@ decisions:
|
|
|
13
13
|
type: string
|
|
14
14
|
description: Formatted candidate options with title, rationale, item IDs and source URLs
|
|
15
15
|
criteria:
|
|
16
|
-
a:
|
|
17
|
-
b:
|
|
18
|
-
c:
|
|
19
|
-
|
|
16
|
+
a: "Slot a is the strongest and most cohesive general IT topic for a technical article — infrastructure, languages, tooling, systems, developer practice or industry signal."
|
|
17
|
+
b: "Slot b is the strongest and most cohesive general IT topic for a technical article — infrastructure, languages, tooling, systems, developer practice or industry signal."
|
|
18
|
+
c: "Slot c is the strongest and most cohesive general IT topic for a technical article — infrastructure, languages, tooling, systems, developer practice or industry signal."
|
|
19
|
+
d: "Slot d is the strongest and most cohesive general IT topic for a technical article — infrastructure, languages, tooling, systems, developer practice or industry signal."
|
|
20
|
+
e: "Slot e is the strongest and most cohesive general IT topic for a technical article — infrastructure, languages, tooling, systems, developer practice or industry signal."
|
|
21
|
+
defer: Candidates are insufficient, contradictory, overlap, or need explicit operator review.
|
|
20
22
|
fallback: defer
|
|
21
23
|
|
|
22
24
|
kk-itc-outline-choice:
|
package/plugins/kk/plugin.json
CHANGED